The Big Cleanup: Refactor BTCPay internals (#5809)

This commit is contained in:
Nicolas Dorier 2024-04-04 16:31:04 +09:00 committed by GitHub
parent 69b589a401
commit 6cc1751924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
265 changed files with 8289 additions and 7673 deletions

View file

@ -1,5 +1,4 @@
using System;
using System.Data.Common;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

View file

@ -1,59 +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<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay",
query), token);
return await HandleResponse<IEnumerable<LNURLPayPaymentMethodData>>(response);
}
public virtual async Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, LNURLPayPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
}
}

View file

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

View file

@ -3,92 +3,47 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
public partial class BTCPayServerClient
{
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, string derivationScheme, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
bodyPayload: new UpdatePaymentMethodRequest() { Config = JValue.CreateString(derivationScheme) },
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain",
query), token);
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodData> GetStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<GenerateOnChainWalletResponse>(response);
}
public virtual async Task RemoveStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<OnChainPaymentMethodData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData>
PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0,
int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
bodyPayload: paymentMethod,
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, int offset = 0, int amount = 10,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);
}
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
@ -7,21 +8,60 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId,
bool? enabled = null,
public virtual async Task<GenericPaymentMethodData> UpdateStorePaymentMethod(
string storeId,
string paymentMethodId,
UpdatePaymentMethodRequest request,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", bodyPayload: request, method: HttpMethod.Put),
token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task RemoveStorePaymentMethod(string storeId, string paymentMethodId)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}", method: HttpMethod.Delete),
CancellationToken.None);
await HandleResponse(response);
}
public virtual async Task<GenericPaymentMethodData> GetStorePaymentMethod(string storeId,
string paymentMethodId, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
if (includeConfig != null)
{
query.Add(nameof(enabled), enabled);
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}",
query), token);
return await HandleResponse<GenericPaymentMethodData>(response);
}
public virtual async Task<GenericPaymentMethodData[]> GetStorePaymentMethods(string storeId,
bool? onlyEnabled = null, bool? includeConfig = null, CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (onlyEnabled != null)
{
query.Add(nameof(onlyEnabled), onlyEnabled);
}
if (includeConfig != null)
{
query.Add(nameof(includeConfig), includeConfig);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods",
query), token);
return await HandleResponse<Dictionary<string, GenericPaymentMethodData>>(response);
return await HandleResponse<GenericPaymentMethodData[]>(response);
}
}
}

View file

@ -0,0 +1,43 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
namespace BTCPayServer.Client.JsonConverters
{
public class SaneOutpointJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(OutPoint).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
try
{
if (!OutPoint.TryParse((string)reader.Value, out var outpoint))
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
return outpoint;
}
catch (EndOfStreamException)
{
}
throw new JsonObjectException("Invalid bitcoin object of type OutPoint", reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is { })
writer.WriteValue(value.ToString());
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -21,7 +22,7 @@ namespace BTCPayServer.Client.Models
public bool ProceedWithPayjoin { get; set; } = true;
public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
[JsonProperty(ItemConverterType = typeof(SaneOutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")]

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
@ -22,4 +25,16 @@ namespace BTCPayServer.Client
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
}
public class GenerateOnChainWalletResponse : GenericPaymentMethodData
{
public class ConfigData
{
public string AccountDerivation { get; set; }
[JsonExtensionData]
IDictionary<string, JToken> AdditionalData { get; set; }
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
public new ConfigData Config { get; set; }
}
}

View file

@ -1,9 +1,22 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class GenericPaymentMethodData
{
public bool Enabled { get; set; }
public object Data { get; set; }
public string CryptoCode { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public JToken Config { get; set; }
public string PaymentMethodId { get; set; }
}
public class UpdatePaymentMethodRequest
{
public UpdatePaymentMethodRequest()
{
}
public bool? Enabled { get; set; }
public JToken Config { get; set; }
}
}

View file

@ -29,13 +29,12 @@ namespace BTCPayServer.Client.Models
public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal NetworkFee { get; set; }
public decimal PaymentMethodFee { get; set; }
public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; }
public string CryptoCode { get; set; }
public JObject AdditionalData { get; set; }
public string PaymentMethodId { get; set; }
public JToken AdditionalData { get; set; }
public string Currency { get; set; }
public class Payment
{

View file

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

View file

@ -1,27 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodData : LNURLPayPaymentMethodBaseData
{
/// <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 LNURLPayPaymentMethodData()
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
LUD12Enabled = lud12Enabled;
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,47 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataPreview : OnChainPaymentMethodBaseData
{
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public OnChainPaymentMethodDataPreview()
{
}
public OnChainPaymentMethodDataPreview(string cryptoCode, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Label = label;
AccountKeyPath = accountKeyPath;
CryptoCode = cryptoCode;
DerivationScheme = derivationScheme;
}
}
public class OnChainPaymentMethodData : OnChainPaymentMethodDataPreview
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public string PaymentMethod { get; set; }
public OnChainPaymentMethodData()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath, string paymentMethod) :
base(cryptoCode, derivationScheme, label, accountKeyPath)
{
Enabled = enabled;
PaymentMethod = paymentMethod;
}
}
}

View file

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

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
@ -12,7 +13,7 @@ namespace BTCPayServer.Client.Models
public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))]
[JsonConverter(typeof(SaneOutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete

View file

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

View file

@ -1,25 +0,0 @@
using NBitcoin;
namespace BTCPayServer.Client.Models
{
public class UpdateOnChainPaymentMethodRequest : OnChainPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
public UpdateOnChainPaymentMethodRequest()
{
}
public UpdateOnChainPaymentMethodRequest(bool enabled, string derivationScheme, string label, RootedKeyPath accountKeyPath)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
DerivationScheme = derivationScheme;
}
}
}

View file

@ -91,7 +91,7 @@ namespace BTCPayServer.Client.Models
}
public bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; }
public string PaymentMethodId { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
}

View file

@ -130,6 +130,11 @@ namespace BTCPayServer
{
return transactionInformationSet;
}
public string GetTrackedDestination(Script scriptPubKey)
{
return scriptPubKey.Hash.ToString() + "#" + CryptoCode.ToUpperInvariant();
}
}
public abstract class BTCPayNetworkBase

View file

@ -17,6 +17,7 @@ namespace BTCPayServer.Data
public override ApplicationDbContext CreateContext()
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
ConfigureBuilder(builder);
return new ApplicationDbContext(builder.Options);
}

View file

@ -8,6 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.23" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

View file

@ -6,11 +6,6 @@ namespace BTCPayServer.Data
{
public class AddressInvoiceData
{
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetHash instead")]
public string Address { get; set; }
public InvoiceData InvoiceData { get; set; }
public string InvoiceDataId { get; set; }

View file

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.IO.Compression;
using System.IO;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Globalization;
using Newtonsoft.Json;
using Microsoft.EntityFrameworkCore.Diagnostics;
using BTCPayServer.Migrations;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public partial class InvoiceData
{
/// <summary>
/// We have a migration running in the background that will migrate the data from the old blob to the new blob
/// Meanwhile, we need to make sure that invoices which haven't been migrated yet are migrated on the fly.
/// </summary>
public class MigrationInterceptor : IMaterializationInterceptor
{
public static readonly MigrationInterceptor Instance = new MigrationInterceptor();
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is InvoiceData invoiceData)
{
invoiceData.Migrate();
}
else if (entity is PaymentData paymentData)
{
paymentData.Migrate();
}
return entity;
}
}
static HashSet<string> superflousProperties = new HashSet<string>()
{
"availableAddressHashes",
"events",
"refunds",
"paidAmount",
"historicalAddresses",
"refundable",
"status",
"exceptionStatus",
"storeId",
"id",
"txFee",
"refundMail",
"rate",
"depositAddress",
"currency",
"price",
"payments",
"orderId",
"buyerInformation",
"productInformation",
"derivationStrategy",
"archived",
"isUnderPaid",
"requiresRefundEmail"
};
#pragma warning disable CS0618 // Type or member is obsolete
public void Migrate()
{
if (Currency is not null)
return;
if (Blob is not null)
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoData"]?["BTC"] is not null)
{
blob.Move(["rate"], ["cryptoData", "BTC", "rate"]);
blob.Move(["txFee"], ["cryptoData", "BTC", "txFee"]);
}
blob.Move(["customerEmail"], ["metadata", "buyerEmail"]);
foreach (var prop in (blob["cryptoData"] as JObject)?.Properties()?.ToList() ?? [])
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
{
if (prop.Value is JObject pm)
{
pm.Remove("depositAddress");
pm.Remove("feeRate");
pm.Remove("txFee");
}
continue;
}
if (prop.Value is JObject o)
{
o.ConvertNumberToString("rate");
if (o["paymentMethod"] is JObject pm)
{
if (pm["networkFeeRate"] is null)
pm["networkFeeRate"] = o["feeRate"] ?? 0.0m;
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
pm.Remove("networkFeeMode");
if (pm["networkFeeMode"] is JValue { Type: JTokenType.Integer, Value: 2 or 2L })
pm["networkFeeRate"] = 0.0m;
}
}
}
var metadata = blob.Property("metadata")?.Value as JObject;
if (metadata is null)
{
metadata = new JObject();
blob.Add("metadata", metadata);
}
foreach (var prop in (blob["buyerInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Value?.Value<string>() is not null)
blob.Move(["buyerInformation", prop.Name], ["metadata", prop.Name]);
}
foreach (var prop in (blob["productInformation"] as JObject)?.Properties()?.ToList() ?? [])
{
if (prop.Name is "price" or "currency")
blob.Move(["productInformation", prop.Name], [prop.Name]);
else if (prop.Value?.Value<string>() is not null)
blob.Move(["productInformation", prop.Name], ["metadata", prop.Name]);
}
blob.Move(["orderId"], ["metadata", "orderId"]);
foreach (string prop in new string[] { "posData", "checkoutType", "defaultLanguage", "notificationEmail", "notificationURL", "storeSupportUrl", "redirectURL" })
{
blob.RemoveIfNull(prop);
}
blob.RemoveIfValue<bool>("fullNotifications", false);
if (blob["receiptOptions"] is JObject receiptOptions)
{
foreach (string prop in new string[] { "showQR", "enabled", "showPayments" })
{
receiptOptions.RemoveIfNull(prop);
}
}
{
if (blob.Property("paymentTolerance") is JProperty { Value: { Type: JTokenType.Float } pv } prop)
{
if (pv.Value<decimal>() == 0.0m)
prop.Remove();
}
}
var posData = blob.Move(["posData"], ["metadata", "posData"]);
if (posData is not null && posData.Value?.Type is JTokenType.String)
{
try
{
posData.Value = JObject.Parse(posData.Value<string>());
}
catch
{
posData.Remove();
}
}
if (posData?.Type is JTokenType.Null)
posData.Remove();
if (blob["derivationStrategies"] is JValue { Type: JTokenType.String } v)
blob["derivationStrategies"] = JObject.Parse(v.Value<string>());
if (blob["derivationStrategies"] is JObject derivations)
{
foreach (var prop in derivations.Properties().ToList())
{
// We should only change data for onchain
if (prop.Name.Contains('_', StringComparison.OrdinalIgnoreCase))
continue;
if (prop.Value is JValue
{
Type: JTokenType.String,
Value: String { Length: > 0 } val
})
{
if (val[0] == '{')
derivations[prop.Name] = JObject.Parse(val);
else
{
if (val.Contains('-', StringComparison.OrdinalIgnoreCase))
derivations[prop.Name] = new JObject() { ["accountDerivation"] = val };
else
derivations[prop.Name] = null;
}
}
if (prop.Value is JObject derivation)
{
derivations[prop.Name] = derivation["accountDerivation"];
}
}
}
if (blob["derivationStrategies"] is null && blob["derivationStrategy"] is not null)
{
// If it's NBX derivation strategy, keep it. Else just give up, it might be Electrum format and we shouldn't support
// that anymore in the backend for long...
if (blob["derivationStrategy"]?.Value<string>().Contains('-', StringComparison.OrdinalIgnoreCase) is true)
blob.Move(["derivationStrategy"], ["derivationStrategies", "BTC"]);
else
{
blob.Remove("derivationStrategy");
blob.Add("derivationStrategies", new JObject() { ["BTC"] = null });
}
}
if (blob["type"]?.Value<string>() is "Standard")
blob.Remove("type");
foreach (var prop in new string[] { "extendedNotifications", "lazyPaymentMethods", "lazyPaymentMethods", "redirectAutomatically" })
{
if (blob[prop]?.Value<bool>() is false)
blob.Remove(prop);
}
blob.ConvertNumberToString("price");
Currency = blob["currency"].Value<string>();
var isTopup = blob["type"]?.Value<string>() is "TopUp";
var amount = decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
Amount = isTopup && amount == 0 ? null : decimal.Parse(blob["price"].Value<string>(), CultureInfo.InvariantCulture);
CustomerEmail = null;
foreach (var prop in superflousProperties)
blob.Property(prop)?.Remove();
if (blob["speedPolicy"] is JValue { Type: JTokenType.Integer, Value: 0 or 0L })
blob.Remove("speedPolicy");
blob.TryAdd("internalTags", new JArray());
blob.TryAdd("receiptOptions", new JObject());
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
if (prop.Name.EndsWith("_LightningLike", StringComparison.OrdinalIgnoreCase) ||
prop.Name.EndsWith("_LNURLPAY", StringComparison.OrdinalIgnoreCase))
{
if (prop.Value["paymentMethod"]?["PaymentHash"] is JObject)
prop.Value["paymentMethod"]["PaymentHash"] = JValue.CreateNull();
if (prop.Value["paymentMethod"]?["Preimage"] is JObject)
prop.Value["paymentMethod"]["Preimage"] = JValue.CreateNull();
}
}
foreach (var prop in ((JObject)blob["cryptoData"]).Properties())
{
var crypto = prop.Name.Split(['_', '-']).First();
if (blob.Move(["cryptoData", prop.Name, "rate"], ["rates", crypto]) is not null)
((JObject)blob["rates"]).ConvertNumberToString(crypto);
}
blob.Move(["cryptoData"], ["prompts"]);
var prompts = ((JObject)blob["prompts"]);
foreach (var prop in prompts.Properties().ToList())
{
((JObject)blob["prompts"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
blob["derivationStrategies"] = blob["derivationStrategies"] ?? new JObject();
foreach (var prop in ((JObject)blob["derivationStrategies"]).Properties().ToList())
{
((JObject)blob["derivationStrategies"]).RenameProperty(prop.Name, MigrationExtensions.MigratePaymentMethodId(prop.Name));
}
foreach (var prop in prompts.Properties())
{
var prompt = prop.Value as JObject;
if (prompt is null)
continue;
prompt["currency"] = prop.Name.Split('-').First();
prompt.RemoveIfNull("depositAddress");
prompt.RemoveIfNull("txFee");
prompt.RemoveIfNull("feeRate");
prompt.RenameProperty("depositAddress", "destination");
prompt.RenameProperty("txFee", "paymentMethodFee");
var divisibility = MigrationExtensions.GetDivisibility(prop.Name);
prompt.Add("divisibility", divisibility);
if (prompt["paymentMethodFee"] is { } paymentMethodFee)
{
prompt["paymentMethodFee"] = ((decimal)paymentMethodFee.Value<long>() / (decimal)Math.Pow(10, divisibility)).ToString(CultureInfo.InvariantCulture);
prompt.RemoveIfValue<string>("paymentMethodFee", "0");
}
prompt.Move(["paymentMethod"], ["details"]);
prompt.Move(["feeRate"], ["details", "recommendedFeeRate"]);
prompt.Move(["details", "networkFeeRate"], ["details", "paymentMethodFeeRate"]);
prompt.Move(["details", "networkFeeMode"], ["details", "feeMode"]);
if ((prompt["details"]?["Activated"])?.Value<bool>() is bool activated)
{
((JObject)prompt["details"]).Remove("Activated");
prompt["inactive"] = !activated;
prompt.RemoveIfValue<bool>("inactive", false);
}
if ((prompt["details"]?["activated"])?.Value<bool>() is bool activated2)
{
((JObject)prompt["details"]).Remove("activated");
prompt["inactive"] = !activated2;
prompt.RemoveIfValue<bool>("inactive", false);
}
var details = prompt["details"] as JObject ?? new JObject();
details.RemoveIfValue<bool>("payjoinEnabled", false);
details.RemoveIfNull("feeMode");
if (details["feeMode"] is not null)
{
details["feeMode"] = details["feeMode"].Value<int>() switch
{
1 => "Always",
2 => "Never",
_ => null
};
details.RemoveIfNull("feeMode");
}
details.RemoveIfNull("BOLT11");
details.RemoveIfNull("address");
details.RemoveIfNull("Address");
prompt.Move(["details", "BOLT11"], ["destination"]);
prompt.Move(["details", "address"], ["destination"]);
prompt.Move(["details", "Address"], ["destination"]);
prompt.RenameProperty("Address", "destination");
prompt.RenameProperty("BOLT11", "destination");
details.Remove("LightningSupportedPaymentMethod");
foreach (var o in detailsRemoveDefault)
details.RemoveIfNull(o);
details.RemoveIfValue<decimal>("recommendedFeeRate", 0.0m);
details.RemoveIfValue<decimal>("paymentMethodFeeRate", 0.0m);
if (prop.Name.EndsWith("-CHAIN"))
blob.Move(["derivationStrategies", prop.Name], ["prompts", prop.Name, "details", "accountDerivation"]);
var camel = new CamelCaseNamingStrategy();
foreach (var p in details.Properties().ToList())
{
var camelName = camel.GetPropertyName(p.Name, false);
if (camelName != p.Name)
details.RenameProperty(p.Name, camelName);
}
}
if (blob["defaultPaymentMethod"] is not null)
blob["defaultPaymentMethod"] = MigrationExtensions.MigratePaymentMethodId(blob["defaultPaymentMethod"].Value<string>());
blob.Remove("derivationStrategies");
blob["version"] = 3;
Blob2 = blob.ToString(Formatting.None);
}
static string[] detailsRemoveDefault =
[
"paymentMethodFeeRate",
"keyPath",
"BOLT11",
"NodeInfo",
"Preimage",
"InvoiceId",
"PaymentHash",
"ProvidedComment",
"GeneratedBoltAmount",
"ConsumedLightningAddress",
"PayRequest"
];
#pragma warning restore CS0618 // Type or member is obsolete
}
}

View file

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class InvoiceData : IHasBlobUntyped
public partial class InvoiceData : IHasBlobUntyped
{
public string Id { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
@ -25,6 +25,7 @@ namespace BTCPayServer.Data
public string OrderId { get; set; }
public string Status { get; set; }
public string ExceptionStatus { get; set; }
[Obsolete("Unused")]
public string CustomerEmail { get; set; }
public List<AddressInvoiceData> AddressInvoices { get; set; }
public bool Archived { get; set; }
@ -43,12 +44,14 @@ namespace BTCPayServer.Data
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql())
{
builder.Entity<InvoiceData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<InvoiceData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View file

@ -0,0 +1,151 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public static class MigrationExtensions
{
public static JProperty? Move(this JObject blob, string[] pathFrom, string[] pathTo)
{
var from = GetProperty(blob, pathFrom, false);
if (from is null)
return null;
var to = GetProperty(blob, pathTo, true);
to!.Value = from.Value;
from.Remove();
return to;
}
public static void RenameProperty(this JObject o, string oldName, string newName)
{
var p = o.Property(oldName);
if (p is null)
return;
RenameProperty(ref p, newName);
}
public static void RenameProperty(ref JProperty ls, string newName)
{
if (ls.Name != newName)
{
var parent = ls.Parent;
ls.Remove();
ls = new JProperty(newName, ls.Value);
parent!.Add(ls);
}
}
public static JProperty? GetProperty(this JObject blob, string[] pathFrom, bool createIfNotExists)
{
var current = blob;
for (int i = 0; i < pathFrom.Length - 1; i++)
{
if (current.TryGetValue(pathFrom[i], out var value) && value is JObject jObject)
{
current = jObject;
}
else
{
if (!createIfNotExists)
return null;
JProperty? prop = null;
for (int ii = i; ii < pathFrom.Length; ii++)
{
var newProp = new JProperty(pathFrom[ii], new JObject());
if (prop is null)
current.Add(newProp);
else
prop.Value = new JObject(newProp);
prop = newProp;
}
return prop;
}
}
var result = current.Property(pathFrom[pathFrom.Length - 1]);
if (result is null && createIfNotExists)
{
result = new JProperty(pathFrom[pathFrom.Length - 1], null as object);
current.Add(result);
}
return result;
}
public static CamelCaseNamingStrategy Camel = new CamelCaseNamingStrategy();
public static void RemoveIfNull(this JObject blob, string propName)
{
if (blob.Property(propName)?.Value.Type is JTokenType.Null)
blob.Remove(propName);
}
public static void RemoveIfValue<T>(this JObject conf, string propName, T v)
{
var p = conf.Property(propName);
if (p is null)
return;
if (p.Value is JValue { Type: JTokenType.Null })
{
if (EqualityComparer<T>.Default.Equals(default, v))
p.Remove();
}
else if (p.Value is JValue jv)
{
if (EqualityComparer<T>.Default.Equals(jv.Value<T>(), v))
{
p.Remove();
}
}
}
public static void ConvertNumberToString(this JObject o, string prop)
{
if (o[prop]?.Type is JTokenType.Float)
o[prop] = o[prop]!.Value<decimal>().ToString(CultureInfo.InvariantCulture);
if (o[prop]?.Type is JTokenType.Integer)
o[prop] = o[prop]!.Value<long>().ToString(CultureInfo.InvariantCulture);
}
public static string Unzip(byte[] bytes)
{
MemoryStream ms = new MemoryStream(bytes);
using GZipStream gzip = new GZipStream(ms, CompressionMode.Decompress);
StreamReader reader = new StreamReader(gzip, Encoding.UTF8);
var unzipped = reader.ReadToEnd();
return unzipped;
}
public static int GetDivisibility(string paymentMethodId)
{
var splitted = paymentMethodId.Split('-');
return (CryptoCode: splitted[0], Type: splitted[1]) switch
{
{ Type: "LN" } or { Type: "LNURL" } => 11,
{ Type: "CHAIN", CryptoCode: var code } when code == "XMR" => 12,
{ Type: "CHAIN" } => 8,
_ => 8
};
}
public static string MigratePaymentMethodId(string paymentMethodId)
{
var splitted = paymentMethodId.Split(new[] { '_', '-' });
if (splitted is [var cryptoCode, var paymentType])
{
return paymentType switch
{
"BTCLike" => $"{cryptoCode}-CHAIN",
"LightningLike" or "LightningNetwork" => $"{cryptoCode}-LN",
"LNURLPAY" => $"{cryptoCode}-LNURL",
_ => throw new NotSupportedException("Unknown payment type " + paymentType)
};
}
if (splitted.Length == 1)
return $"{splitted[0]}-CHAIN";
throw new NotSupportedException("Unknown payment id " + paymentMethodId);
}
}
}

View file

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Migrations;
using NBitcoin;
using NBitcoin.Altcoins;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public partial class PaymentData
{
public void Migrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (Currency is not null)
return;
if (Blob is not null)
{
Blob2 = MigrationExtensions.Unzip(Blob);
Blob = null;
}
var blob = JObject.Parse(Blob2);
if (blob["cryptoPaymentDataType"] is null)
blob["cryptoPaymentDataType"] = "BTCLike";
if (blob["cryptoCode"] is null)
blob["cryptoCode"] = "BTC";
if (blob["receivedTime"] is null)
blob.Move(["receivedTimeMs"], ["receivedTime"]);
else
{
// Convert number of seconds to number of milliseconds
var timeSeconds = (ulong)(long)blob["receivedTime"].Value<long>();
var date = NBitcoin.Utils.UnixTimeToDateTime(timeSeconds);
blob["receivedTime"] = DateTimeToMilliUnixTime(date.UtcDateTime);
}
var cryptoCode = blob["cryptoCode"].Value<string>();
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
Type = MigrationExtensions.MigratePaymentMethodId(Type);
var divisibility = MigrationExtensions.GetDivisibility(Type);
Currency = blob["cryptoCode"].Value<string>();
blob.Remove("cryptoCode");
blob.Remove("cryptoPaymentDataType");
JObject cryptoData;
if (blob["cryptoPaymentData"] is null)
{
cryptoData = new JObject();
blob["cryptoPaymentData"] = cryptoData;
cryptoData["RBF"] = true;
cryptoData["confirmationCount"] = 0;
}
else
{
cryptoData = JObject.Parse(blob["cryptoPaymentData"].Value<string>());
foreach (var prop in cryptoData.Properties().ToList())
{
if (prop.Name is "rbf")
cryptoData.RenameProperty("rbf", "RBF");
else if (prop.Name is "bolT11")
cryptoData.RenameProperty("bolT11", "BOLT11");
else
cryptoData.RenameProperty(prop.Name, MigrationExtensions.Camel.GetPropertyName(prop.Name, false));
}
}
blob.Remove("cryptoPaymentData");
cryptoData["outpoint"] = blob["outpoint"];
if (blob["output"] is not (null or { Type: JTokenType.Null }))
{
// Old versions didn't track addresses, so we take it from output.
// We don't know the network for sure but better having something than nothing in destination.
// If signet/testnet crash we don't really care anyway.
// Also, only LTC was supported at this time.
Network network = (cryptoCode switch { "LTC" => (INetworkSet)Litecoin.Instance, _ => Bitcoin.Instance }).Mainnet;
var txout = network.Consensus.ConsensusFactory.CreateTxOut();
txout.ReadWrite(Encoders.Hex.DecodeData(blob["output"].Value<string>()), network);
cryptoData["value"] = txout.Value.Satoshi;
blob["destination"] = txout.ScriptPubKey.GetDestinationAddress(network)?.ToString();
}
blob.Remove("output");
blob.Remove("outpoint");
// Convert from sats to btc
if (cryptoData["value"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["value"].Value<long>();
Amount = (decimal)v / (decimal)Money.COIN;
cryptoData.Remove("value");
blob["paymentMethodFee"] = blob["networkFee"];
blob.RemoveIfValue<decimal>("paymentMethodFee", 0.0m);
blob.ConvertNumberToString("paymentMethodFee");
blob.Remove("networkFee");
blob.RemoveIfNull("paymentMethodFee");
}
// Convert from millisats to btc
else if (cryptoData["amount"] is not (null or { Type: JTokenType.Null }))
{
var v = cryptoData["amount"].Value<long>();
Amount = (decimal)v / (decimal)Math.Pow(10.0, divisibility);
cryptoData.Remove("amount");
}
if (cryptoData["address"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["address"];
cryptoData.Remove("address");
}
if (cryptoData["BOLT11"] is not (null or { Type: JTokenType.Null }))
{
blob["destination"] = cryptoData["BOLT11"];
cryptoData.Remove("BOLT11");
}
if (cryptoData["outpoint"] is not (null or { Type: JTokenType.Null }))
{
// Convert to format txid-n
cryptoData["outpoint"] = OutPoint.Parse(cryptoData["outpoint"].Value<string>()).ToString();
}
if (Accounted is false)
Status = PaymentStatus.Unaccounted;
else if (cryptoData["confirmationCount"] is { Type: JTokenType.Integer })
{
var confirmationCount = cryptoData["confirmationCount"].Value<int>();
// Technically, we should use the invoice's speed policy, however it's not on our
// scope and is good enough for majority of cases.
Status = confirmationCount > 0 ? PaymentStatus.Settled : PaymentStatus.Processing;
if (cryptoData["LockTime"] is { Type: JTokenType.Integer })
{
var lockTime = cryptoData["LockTime"].Value<int>();
if (confirmationCount < lockTime)
Status = PaymentStatus.Processing;
}
}
else
{
Status = PaymentStatus.Settled;
}
Created = MilliUnixTimeToDateTime(blob["receivedTime"].Value<long>());
cryptoData.RemoveIfValue<bool>("rbf", false);
cryptoData.Remove("legacy");
cryptoData.Remove("networkFee");
cryptoData.Remove("paymentType");
cryptoData.RemoveIfNull("outpoint");
cryptoData.RemoveIfValue<bool>("RBF", false);
blob.Remove("receivedTime");
blob.Remove("accounted");
blob.Remove("networkFee");
blob["details"] = cryptoData;
blob["divisibility"] = divisibility;
blob["version"] = 2;
Blob2 = blob.ToString(Formatting.None);
Accounted = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
static readonly DateTimeOffset unixRef = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public static long DateTimeToMilliUnixTime(in DateTime time)
{
var date = ((DateTimeOffset)time).ToUniversalTime();
long v = (long)(date - unixRef).TotalMilliseconds;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return v;
}
public static DateTimeOffset MilliUnixTimeToDateTime(long value)
{
var v = value;
if (v < 0)
throw new FormatException("Invalid datetime (less than 1/1/1970)");
return unixRef + TimeSpan.FromMilliseconds(v);
}
}
}

View file

@ -4,17 +4,32 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class PaymentData : IHasBlobUntyped
public enum PaymentStatus
{
Processing,
Settled,
Unaccounted
}
public partial class PaymentData : IHasBlobUntyped
{
/// <summary>
/// The date of creation of the payment
/// Note that while it is a nullable field, our migration
/// process ensure it is populated.
/// </summary>
public DateTimeOffset? Created { get; set; }
public string Id { get; set; }
public string InvoiceDataId { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public InvoiceData InvoiceData { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public bool Accounted { get; set; }
[Obsolete("Use Status instead")]
public bool? Accounted { get; set; }
public PaymentStatus? Status { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -23,11 +38,17 @@ namespace BTCPayServer.Data
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<PaymentData>()
.Property(o => o.Status)
.HasConversion<string>();
if (databaseFacade.IsNpgsql())
{
builder.Entity<PaymentData>()
.Property(o => o.Blob2)
.HasColumnType("JSONB");
builder.Entity<PaymentData>()
.Property(o => o.Amount)
.HasColumnType("NUMERIC");
}
}
}

View file

@ -5,6 +5,7 @@ using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{

View file

@ -0,0 +1,43 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
/// <inheritdoc />
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240304003640_addinvoicecolumns")]
public partial class addinvoicecolumns : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Invoices",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Invoices",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Invoices");
migrationBuilder.DropColumn(
name: "Currency",
table: "Invoices");
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240317024757_payments_refactor")]
public partial class payments_refactor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Amount",
table: "Payments",
type: migrationBuilder.IsNpgsql() ? "NUMERIC" : "TEXT",
nullable: true);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Created",
table: "Payments",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Currency",
table: "Payments",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Payments",
type: "TEXT",
nullable: true);
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.AlterColumn<bool?>(
name: "Accounted",
table: "Payments",
nullable: true);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Amount",
table: "Payments");
migrationBuilder.DropColumn(
name: "Created",
table: "Payments");
migrationBuilder.DropColumn(
name: "Currency",
table: "Payments");
migrationBuilder.DropColumn(
name: "Status",
table: "Payments");
}
}
}

View file

@ -247,6 +247,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
@ -259,6 +262,9 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail")
.HasColumnType("TEXT");
@ -505,15 +511,27 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Accounted")
.HasColumnType("INTEGER");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");

View file

@ -17,6 +17,7 @@ using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
@ -172,12 +173,13 @@ namespace BTCPayServer.Tests
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(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);
#pragma warning restore CS0618 // Type or member is obsolete
FastTests.GetParsers().TryParseWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var onchainBTC = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
var network = handlers.GetBitcoinHandler("BTC").Network;
FastTests.GetParsers().TryParseWalletFile(content, network, out var expected, out var error);
var handler = handlers[pmi];
Assert.Equal(JToken.FromObject(expected, handler.Serializer), JToken.FromObject(onchainBTC, handler.Serializer));
Assert.Null(error);
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
@ -302,6 +304,7 @@ namespace BTCPayServer.Tests
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
var firstDue = invoice.CryptoInfo[0].Due;
cashCow.SendToAddress(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
@ -381,7 +384,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
invoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("confirmed", invoice.Status);
Assert.Equal("complete", invoice.Status);
});
// BTC crash by 50%
@ -829,13 +832,13 @@ normal:
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal(PaymentTypes.BTCLike.ToString(),
Assert.Equal("BTC-CHAIN",
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s => "BTC-CHAIN" == s.PaymentType && new[] { "BTC", "LTC" }.Contains(
s.CryptoCode));
//test topup option

View file

@ -19,6 +19,14 @@
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="TestData\OldInvoices.csv" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\OldInvoices.csv" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />

View file

@ -162,9 +162,9 @@ namespace BTCPayServer.Tests
s.AddLightningNode();
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal("Bitcoin (Lightning)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
Assert.Equal("Lightning", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
s.Driver.Quit();
}
@ -210,8 +210,8 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
});
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.Destination, Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
IWebElement closebutton = null;

View file

@ -62,13 +62,13 @@ namespace BTCPayServer.Tests
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text);
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC-CHAIN"));
// Details should show exchange rate
s.Driver.ToggleCollapse("PaymentDetails");
@ -84,13 +84,13 @@ namespace BTCPayServer.Tests
{
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
Assert.StartsWith("lightning:lnurl", payUrl);
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
});
// Default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(21000, "SATS", defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
@ -99,11 +99,11 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-LN .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
s.Driver.ElementDoesNotExist(By.Id("Address_BTC-CHAIN"));
// Lightning amount in sats
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
@ -155,7 +155,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -271,8 +271,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
@ -311,7 +311,7 @@ namespace BTCPayServer.Tests
// BIP21 with LN as default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
@ -340,8 +340,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC-CHAIN .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC-CHAIN .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
@ -414,7 +414,7 @@ namespace BTCPayServer.Tests
// - NFC/LNURL-W available with just Lightning
// - BIP21 works correctly even though Lightning is default payment method
s.GoToHome();
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC-LN");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
@ -462,8 +462,8 @@ namespace BTCPayServer.Tests
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.Destination, Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
TestUtils.Eventually(() =>

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
@ -47,6 +48,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
@ -236,26 +238,24 @@ namespace BTCPayServer.Tests
var id = PaymentMethodId.Parse("BTC");
var id1 = PaymentMethodId.Parse("BTC-OnChain");
var id2 = PaymentMethodId.Parse("BTC-BTCLike");
Assert.Equal("LTC-LN", PaymentMethodId.Parse("LTC-LightningNetwork").ToString());
Assert.Equal(id, id1);
Assert.Equal(id, id2);
Assert.Equal("BTC", id.ToString());
Assert.Equal("BTC", id.ToString());
Assert.Equal("BTC-CHAIN", id.ToString());
Assert.Equal("BTC-CHAIN", id.ToString());
id = PaymentMethodId.Parse("LTC");
Assert.Equal("LTC", id.ToString());
Assert.Equal("LTC", id.ToStringNormalized());
Assert.Equal("LTC-CHAIN", id.ToString());
id = PaymentMethodId.Parse("LTC-offchain");
id1 = PaymentMethodId.Parse("LTC-OffChain");
id2 = PaymentMethodId.Parse("LTC-LightningLike");
Assert.Equal(id, id1);
Assert.Equal(id, id2);
Assert.Equal("LTC_LightningLike", id.ToString());
Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized());
Assert.Equal("LTC-LN", id.ToString());
#if ALTCOINS
id = PaymentMethodId.Parse("XMR");
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
Assert.Equal(id, id1);
Assert.Equal("XMR_MoneroLike", id.ToString());
Assert.Equal("XMR", id.ToStringNormalized());
Assert.Equal("XMR-CHAIN", id.ToString());
#endif
}
@ -439,29 +439,31 @@ namespace BTCPayServer.Tests
}}
}, out items));
}
PaymentMethodId BTC = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
PaymentMethodId LTC = PaymentTypes.CHAIN.GetPaymentMethodId("LTC");
[Fact]
public void CanCalculateDust()
{
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = CreateNetworkProvider(ChainName.Regtest);
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 34_000m;
entity.Rates["LTC"] = 3400m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 34_000m
Divisibility = 8
});
entity.Price = 4000;
entity.UpdateTotals();
var accounting = entity.GetPaymentMethods().First().Calculate();
var accounting = entity.GetPaymentPrompts().First().Calculate();
// Exact price should be 0.117647059..., but the payment method round up to one sat
Assert.Equal(0.11764706m, accounting.Due);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
Accounted = true
Value = 0.11764706m,
Status = PaymentStatus.Settled,
});
entity.UpdateTotals();
Assert.Equal(0.0m, entity.NetDue);
@ -474,13 +476,13 @@ namespace BTCPayServer.Tests
// Now, imagine there is litecoin. It might seem from its
// perspecitve that there has been a slight over payment.
// However, Calculate() should just cap it to 0.0m
entity.SetPaymentMethod(new PaymentMethod()
entity.SetPaymentPrompt(LTC, new PaymentPrompt()
{
Currency = "LTC",
Rate = 3400m
Divisibility = 8
});
entity.UpdateTotals();
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
var method = entity.GetPaymentPrompts().First(p => p.Currency == "LTC");
accounting = method.Calculate();
Assert.Equal(0.0m, accounting.DueUncapped);
@ -492,19 +494,19 @@ namespace BTCPayServer.Tests
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = networkProvider;
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
PaymentMethodFee = 0.1m,
Divisibility = 8
});
entity.Price = 5000;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate();
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(1.1m, accounting.Due);
@ -513,10 +515,10 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()),
Value = 0.5m,
Rate = 5000,
Accounted = true,
NetworkFee = 0.1m
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -527,9 +529,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 0.2m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -539,9 +541,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 0.6m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
@ -549,75 +551,79 @@ namespace BTCPayServer.Tests
Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add(
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
new PaymentEntity() { Currency = "BTC", Value = 0.2m, Status = PaymentStatus.Settled });
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue);
entity = new InvoiceEntity();
entity.Networks = networkProvider;
entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
paymentMethods.Add(
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
entity.SetPaymentMethods(paymentMethods);
entity.Currency = "USD";
entity.Rates["BTC"] = 1000m;
entity.Rates["LTC"] = 500m;
PaymentPromptDictionary paymentMethods =
[
new PaymentPrompt() { PaymentMethodId = BTC, Currency = "BTC", PaymentMethodFee = 0.1m, Divisibility = 8 },
new PaymentPrompt() { PaymentMethodId = LTC, Currency = "LTC", PaymentMethodFee = 0.01m, Divisibility = 8 },
];
entity.SetPaymentPrompts(paymentMethods);
entity.Payments = new List<PaymentEntity>();
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(BTC);
accounting = paymentMethod.Calculate();
Assert.Equal(5.1m, accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(LTC);
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = BTC,
Currency = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = 1.0m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(1.0m, accounting.Paid);
Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
Assert.Equal(0.0m, accounting.CryptoPaid);
Assert.Equal(0.0m, accounting.PaymentMethodPaid);
Assert.Equal(2.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = LTC,
Currency = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.01m
Value = 1.0m,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.01m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(1.5m, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(3.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
@ -625,25 +631,26 @@ namespace BTCPayServer.Tests
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
entity.Payments.Add(new PaymentEntity()
{
PaymentMethodId = BTC,
Currency = "BTC",
Output = new TxOut(Money.Coins(remaining), new Key()),
Accounted = true,
NetworkFee = 0.1m
Value = remaining,
Status = PaymentStatus.Settled,
PaymentMethodFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
Assert.Equal(1.0m + remaining, accounting.PaymentMethodPaid);
Assert.Equal(1.5m + remaining, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.PaymentMethodPaid);
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
@ -679,21 +686,21 @@ namespace BTCPayServer.Tests
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
var entity = new InvoiceEntity() { Currency = "USD" };
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{
Currency = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
PaymentMethodFee = 0.1m,
Divisibility = 8
});
entity.Price = 5000;
entity.PaymentTolerance = 0;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate();
Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
@ -2144,63 +2151,48 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Networks = networkProvider;
invoiceEntity.Currency = "USD";
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00010000m),
DepositAddress = dummy
}));
invoiceEntity.SetPaymentMethods(paymentMethods);
invoiceEntity.Rates.Add("BTC", 10513.44m);
invoiceEntity.Rates.Add("LTC", 216.79m);
PaymentPromptDictionary paymentMethods =
[
new () { PaymentMethodId = BTC, Divisibility = 8, Currency = "BTC", PaymentMethodFee = 0.00000100m, ParentEntity = invoiceEntity },
new () { PaymentMethodId = LTC, Divisibility = 8, Currency = "LTC", PaymentMethodFee = 0.00010000m, ParentEntity = invoiceEntity },
];
invoiceEntity.SetPaymentPrompts(paymentMethods);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var btc = invoiceEntity.GetPaymentPrompt(btcId);
var accounting = btc.Calculate();
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
Status = PaymentStatus.Settled,
Currency = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"),
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
PaymentMethodFee = 0.00000100m,
Value = 0.00151263m,
PaymentMethodId = btcId
});
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
Status = PaymentStatus.Settled,
Currency = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC")
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
}));
Value = accounting.Due,
PaymentMethodFee = 0.00000100m,
PaymentMethodId = btcId
});
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(0.0m, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
var ltc = invoiceEntity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = ltc.Calculate();
Assert.Equal(0.0m, accounting.Due);
@ -2248,42 +2240,172 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Null(metadata.PosData);
}
class CanOldMigrateInvoicesBlobVector
{
public string Type { get; set; }
public JObject Input { get; set; }
public JObject Expected { get; set; }
public bool SkipRountripTest { get; set; }
public Dictionary<string, string> ExpectedProperties { get; set; }
}
[Fact]
public void CanOldMigrateInvoicesBlob()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
int i = 0;
var vectors = JsonConvert.DeserializeObject<CanOldMigrateInvoicesBlobVector[]>(File.ReadAllText(TestUtils.GetTestDataFullPath("InvoiceMigrationTestVectors.json")));
foreach (var v in vectors)
{
TestLogs.LogInformation("Test " + i++);
object obj = null;
if (v.Type == "invoice")
{
Data.InvoiceData data = new Data.InvoiceData();
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
{
// Check that we get the same as when setting blob again
var entity = data.GetBlob();
entity.AdditionalData?.Clear();
entity.SetPaymentPrompts(entity.GetPaymentPrompts()); // Cleanup
data.SetBlob(entity);
actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
}
}
else if (v.Type == "payment")
{
Data.PaymentData data = new Data.PaymentData();
//data.
obj = data;
data.Blob2 = v.Input.ToString();
data.Migrate();
var actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
if (!v.SkipRountripTest)
{
// Check that we get the same as when setting blob again
var entity = data.GetBlob();
data.SetBlob(entity);
actual = JObject.Parse(data.Blob2);
AssertSameJson(v.Expected, actual);
}
}
else
{
Assert.Fail("Unknown vector type");
}
if (v.ExpectedProperties is not null)
{
foreach (var kv in v.ExpectedProperties)
{
if (kv.Key == "CreatedInMs")
{
var actual = PaymentData.DateTimeToMilliUnixTime(((DateTimeOffset)obj.GetType().GetProperty("Created").GetValue(obj)).UtcDateTime);
Assert.Equal(long.Parse(kv.Value), actual);
}
else
{
var actual = obj.GetType().GetProperty(kv.Key).GetValue(obj);
Assert.Equal(kv.Value, actual?.ToString());
}
}
}
}
}
private void AssertSameJson(JToken expected, JToken actual, List<string> path = null)
{
var ok = JToken.DeepEquals(expected, actual);
if (ok)
return;
var e = NormalizeJsonString((JObject)expected);
var a = NormalizeJsonString((JObject)actual);
Assert.Equal(e, a);
}
public static string NormalizeJsonString(JObject parsedObject)
{
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
return JsonConvert.SerializeObject(normalizedObject);
}
private static JObject SortPropertiesAlphabetically(JObject original)
{
var result = new JObject();
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
{
var value = property.Value as JObject;
if (value != null)
{
value = SortPropertiesAlphabetically(value);
result.Add(property.Name, value);
}
else
{
result.Add(property.Name, property.Value);
}
}
return result;
}
[Fact]
public void CanParseInvoiceEntityDerivationStrategies()
{
var serializer = BlobSerializer.CreateSerializer(new NBXplorer.NBXplorerNetworkProvider(ChainName.Regtest).GetBTC()).Serializer;
// We have 3 ways of serializing the derivation strategies:
// through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject
// Let's check that InvoiceEntity is similar in all cases.
var legacy = new JObject()
{
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true });
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is P2SHDerivationStrategy);
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]";
var legacy2 = new JObject()
{
["derivationStrategies"] = scheme.ToJson()
["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
};
var newformat = new JObject()
{
["derivationStrategies"] = JObject.Parse(scheme.ToJson())
["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
};
//new BTCPayNetworkProvider(ChainName.Regtest)
#pragma warning disable CS0618 // Type or member is obsolete
var formats = new[] { legacy, legacy2, newformat }
.Select(o =>
{
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(o.ToString());
entity.Networks = CreateNetworkProvider(ChainName.Regtest);
return entity.DerivationStrategies.ToString();
o.Add("currency", "USD");
o.Add("price", "0.0");
o.Add("cryptoData", new JObject()
{
["BTC"] = new JObject()
});
var data = new Data.InvoiceData();
data.Blob2 = o.ToString();
data.Migrate();
var migrated = JObject.Parse(data.Blob2);
return migrated["prompts"]["BTC-CHAIN"]["details"]["accountDerivation"].Value<string>();
})
.ToHashSet();
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Single(formats);
var v = Assert.Single(formats);
Assert.NotNull(v);
}
[Fact]
@ -2292,25 +2414,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var pmi = "\"BTC_hasjdfhasjkfjlajn\"";
JsonTextReader reader = new(new StringReader(pmi));
reader.Read();
Assert.Null(new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault()));
}
[Fact]
public void CanBeBracefulAfterObsoleteShitcoin()
{
var blob = new StoreBlob();
blob.PaymentMethodCriteria = new List<PaymentMethodCriteria>()
{
new()
{
Above = true,
Value = new CurrencyValue() {Currency = "BTC", Value = 0.1m},
PaymentMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike)
}
};
var newBlob = new Serializer(null).ToString(blob).Replace("paymentMethod\":\"BTC\"", "paymentMethod\":\"ETH_ZYC\"");
Assert.Empty(StoreDataExtensions.GetStoreBlob(new StoreData() { StoreBlob = newBlob }).PaymentMethodCriteria);
Assert.Equal("BTC-hasjdfhasjkfjlajn", new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault()).ToString());
}
}
}

View file

@ -30,6 +30,7 @@ using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using static Org.BouncyCastle.Math.EC.ECCurve;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
namespace BTCPayServer.Tests
@ -228,7 +229,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
// if user is a guest or owner, then it should be ok
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id });
await newUserClient.GetInvoices(store.Id);
}
@ -947,7 +948,7 @@ namespace BTCPayServer.Tests
Assert.Equal(payout.Id, payout2.Id);
Assert.Equal(destination, payout2.Destination);
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
Assert.Equal("BTC", payout2.PaymentMethod);
Assert.Equal("BTC-CHAIN", payout2.PaymentMethod);
Assert.Equal("BTC", payout2.CryptoCode);
Assert.Null(payout.PaymentMethodAmount);
@ -1239,7 +1240,7 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC",
Amount = 0.0001m,
Destination = address.ToString(),
});
await AssertAPIError("invalid-state", async () =>
{
@ -1393,7 +1394,7 @@ namespace BTCPayServer.Tests
//check that pmc equals the one we set
Assert.Equal(10, pmc.Amount);
Assert.True(pmc.Above);
Assert.Equal("BTC", pmc.PaymentMethod);
Assert.Equal("BTC-CHAIN", pmc.PaymentMethod);
Assert.Equal("USD", pmc.CurrencyCode);
updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" });
Assert.Empty(newStore.PaymentMethodCriteria);
@ -1428,7 +1429,7 @@ namespace BTCPayServer.Tests
// We strip the user's Owner right, so the key should not work
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
storeEntity.StoreRoleId = roleId;
await ctx.SaveChangesAsync();
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
@ -1441,7 +1442,7 @@ namespace BTCPayServer.Tests
}
tester.DeleteStore = false;
Assert.Empty(await client.GetStores());
// Archive
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
Assert.False(archivableStore.Archived);
@ -1676,8 +1677,8 @@ namespace BTCPayServer.Tests
Assert.NotNull(serverInfoData.Version);
Assert.NotNull(serverInfoData.Onion);
Assert.True(serverInfoData.FullySynched);
Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC-CHAIN", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC-LN", serverInfoData.SupportedPaymentMethods);
Assert.NotNull(serverInfoData.SyncStatus);
Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC"));
}
@ -1779,7 +1780,7 @@ namespace BTCPayServer.Tests
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
Assert.Equal(Invoice.STATUS_COMPLETE, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
@ -1981,7 +1982,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, "lol fake invoice id", new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
});
});
@ -1989,7 +1990,7 @@ namespace BTCPayServer.Tests
// test validation error for when invoice is not yet in the state in which it can be refunded
var apiError = await AssertAPIError("non-refundable", () => client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
}));
Assert.Equal("Cannot refund this invoice", apiError.Message);
@ -2020,7 +2021,7 @@ namespace BTCPayServer.Tests
// test RefundVariant.RateThen
var pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen
});
Assert.Equal("BTC", pp.Currency);
@ -2031,7 +2032,7 @@ namespace BTCPayServer.Tests
// test RefundVariant.CurrentRate
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.CurrentRate
});
Assert.Equal("BTC", pp.Currency);
@ -2041,7 +2042,7 @@ namespace BTCPayServer.Tests
// test RefundVariant.Fiat
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.Fiat,
Name = "my test name"
});
@ -2055,7 +2056,7 @@ namespace BTCPayServer.Tests
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
});
});
@ -2064,7 +2065,7 @@ namespace BTCPayServer.Tests
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
CustomAmount = 69420,
CustomCurrency = "JPY"
@ -2076,19 +2077,19 @@ namespace BTCPayServer.Tests
// should auto-approve if currencies match
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest()
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.Custom,
CustomAmount = 0.00069420m,
CustomCurrency = "BTC"
});
Assert.True(pp.AutoApproveClaims);
// test subtract percentage
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 101
});
@ -2098,25 +2099,25 @@ namespace BTCPayServer.Tests
// should auto-approve
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.RateThen,
SubtractPercentage = 6.15m
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(0.9385m, pp.Amount);
// test RefundVariant.OverpaidAmount
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
{
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount
});
});
Assert.Contains("Invoice is not overpaid", validationError.Message);
// should auto-approve
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
@ -2134,24 +2135,24 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Settled);
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.True(invoice.Status == InvoiceStatus.Settled);
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
});
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount
});
Assert.Equal("BTC", pp.Currency);
Assert.True(pp.AutoApproveClaims);
Assert.Equal(method.Due, pp.Amount);
// once more with subtract percentage
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
{
PaymentMethod = method.PaymentMethod,
PaymentMethod = method.PaymentMethodId,
RefundVariant = RefundVariant.OverpaidAmount,
SubtractPercentage = 21m
});
@ -2287,8 +2288,8 @@ namespace BTCPayServer.Tests
var paymentMethods = await viewOnly.GetInvoicePaymentMethods(user.StoreId, newInvoice.Id);
Assert.Single(paymentMethods);
var paymentMethod = paymentMethods.First();
Assert.Equal("BTC", paymentMethod.PaymentMethod);
Assert.Equal("BTC", paymentMethod.CryptoCode);
Assert.Equal("BTC-CHAIN", paymentMethod.PaymentMethodId);
Assert.Equal("BTC", paymentMethod.Currency);
Assert.Empty(paymentMethod.Payments);
@ -2455,7 +2456,7 @@ namespace BTCPayServer.Tests
Assert.Single(paymentMethods);
Assert.False(paymentMethods.First().Activated);
await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id,
paymentMethods.First().PaymentMethod);
paymentMethods.First().PaymentMethodId);
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "address");
@ -2474,7 +2475,7 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC_LightningLike"
}
});
Assert.Equal("BTC_LightningLike", invoiceWithDefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
Assert.Equal("BTC-LN", invoiceWithDefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
var invoiceWithDefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
@ -2487,19 +2488,19 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC"
}
});
Assert.Equal("BTC", invoiceWithDefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
Assert.Equal("BTC-CHAIN", invoiceWithDefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
// reset lazy payment methods
store = await client.GetStore(user.StoreId);
store.LazyPaymentMethods = false;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
Assert.False(store.LazyPaymentMethods);
// use store default payment method
store = await client.GetStore(user.StoreId);
Assert.Null(store.DefaultPaymentMethod);
var storeDefaultPaymentMethod = "BTC-LightningNetwork";
var storeDefaultPaymentMethod = "BTC-LN";
store.DefaultPaymentMethod = storeDefaultPaymentMethod;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
@ -2515,7 +2516,7 @@ namespace BTCPayServer.Tests
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
}
});
Assert.Equal(storeDefaultPaymentMethod, invoiceWithStoreDefaultPaymentMethod.Checkout.DefaultPaymentMethod);
Assert.Null(invoiceWithStoreDefaultPaymentMethod.Checkout.DefaultPaymentMethod);
//let's see the overdue amount
invoice = await client.CreateInvoice(user.StoreId,
@ -2724,8 +2725,8 @@ namespace BTCPayServer.Tests
Amount = 100,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LightningNetwork" },
DefaultPaymentMethod = "BTC_LightningLike"
PaymentMethods = new[] { "BTC-LN" },
DefaultPaymentMethod = "BTC-LN"
}
});
}
@ -2751,11 +2752,13 @@ namespace BTCPayServer.Tests
Assert.Equal(PayResult.Ok, resp.Result);
Assert.NotNull(resp.Details.PaymentHash);
Assert.NotNull(resp.Details.Preimage);
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
Assert.True(pm[i].AdditionalData.HasValues);
Assert.Equal(resp.Details.PaymentHash.ToString(), pm[i].AdditionalData.GetValue("paymentHash"));
Assert.Equal(resp.Details.Preimage.ToString(), pm[i].AdditionalData.GetValue("preimage"));
await TestUtils.EventuallyAsync(async () =>
{
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
Assert.True(pm[i].AdditionalData.HasValues);
Assert.Equal(resp.Details.PaymentHash.ToString(), ((JObject)pm[i].AdditionalData).GetValue("paymentHash"));
Assert.Equal(resp.Details.Preimage.ToString(), ((JObject)pm[i].AdditionalData).GetValue("preimage"));
});
}
}
@ -2858,10 +2861,10 @@ namespace BTCPayServer.Tests
var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" });
Assert.Empty(await client.GetStoreOnChainPaymentMethods(store.Id));
Assert.Empty(await client.GetStorePaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new UpdateOnChainPaymentMethodRequest() { });
await viewOnlyClient.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { });
});
var xpriv = new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
@ -2873,36 +2876,19 @@ namespace BTCPayServer.Tests
await client.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC");
});
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC",
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
await AssertValidationError(new[] { "accountKeyPath" }, () => viewOnlyClient.SendHttpRequest<GreenfieldValidationError[]>(path: $"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", method: HttpMethod.Post,
bodyPayload: JObject.Parse("{\"accountKeyPath\": \"0/1\"}")));
var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub });
Assert.Equal(xpub, method.DerivationScheme);
method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") });
method = await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
Assert.Equal("lol", method.Label);
Assert.Equal(RootedKeyPath.Parse("01020304/1/2/3"), method.AccountKeyPath);
Assert.Equal(xpub, method.DerivationScheme);
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC", xpub)).Addresses.First().Address);
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await viewOnlyClient.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
await AssertHttpError(404, async () =>
{
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
await client.GetStorePaymentMethod(store.Id, "BTC-CHAIN");
});
await AssertHttpError(403, async () =>
@ -2923,12 +2909,12 @@ namespace BTCPayServer.Tests
var allMnemonic = new Mnemonic("all all all all all all all all all all all all");
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
var generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, });
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(generateResponse.DerivationScheme, xpub);
Assert.Equal(generateResponse.Config.AccountDerivation, xpub);
await AssertAPIError("already-configured", async () =>
{
@ -2936,22 +2922,22 @@ namespace BTCPayServer.Tests
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, });
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { });
Assert.NotEqual(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(generateResponse.Mnemonic.DeriveExtKey().Derive(KeyPath.Parse("m/84'/1'/0'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme);
Assert.Equal(generateResponse.Mnemonic.DeriveExtKey().Derive(KeyPath.Parse("m/84'/1'/0'")).Neuter().ToString(Network.RegTest), generateResponse.Config.AccountDerivation);
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1 });
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
.Derive(KeyPath.Parse("m/84'/1'/1'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme);
.Derive(KeyPath.Parse("m/84'/1'/1'")).Neuter().ToString(Network.RegTest), generateResponse.Config.AccountDerivation);
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await client.RemoveStorePaymentMethod(store.Id, "BTC-CHAIN");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { WordList = Wordlist.Japanese, WordCount = WordCount.TwentyFour });
@ -2978,26 +2964,29 @@ namespace BTCPayServer.Tests
var viewOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id));
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest() { });
await viewOnlyClient.UpdateStorePaymentMethod(store.Id, "BTC-LN", new UpdatePaymentMethodRequest() { });
});
await AssertHttpError(404, async () =>
{
await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await adminClient.GetStorePaymentMethod(store.Id, "BTC-LN");
});
await admin.RegisterLightningNodeAsync("BTC", false);
var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
var method = await adminClient.GetStorePaymentMethod(store.Id, "BTC-LN");
Assert.Null(method.Config);
method = await adminClient.GetStorePaymentMethod(store.Id, "BTC-LN", includeConfig: true);
Assert.NotNull(method.Config);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await viewOnlyClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");
});
await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");
await AssertHttpError(404, async () =>
{
await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC");
await adminClient.GetStorePaymentMethod(store.Id, "BTC-LN");
});
@ -3016,33 +3005,45 @@ namespace BTCPayServer.Tests
{
var ex = await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await adminClient.UpdateStorePaymentMethod(store.Id, "BTC-LN", new UpdatePaymentMethodRequest()
{
ConnectionString = forbidden,
Config = new JObject()
{
["connectionString"] = forbidden
},
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 UpdateLightningNetworkPaymentMethodRequest()
await admin2Client.UpdateStorePaymentMethod(admin2.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
ConnectionString = forbidden,
Config = new JObject()
{
["connectionString"] = forbidden
},
Enabled = true
});
}
// Allowed ip should be ok
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await adminClient.UpdateStorePaymentMethod(store.Id, "BTC-LN", new UpdatePaymentMethodRequest()
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8",
Config = new JObject()
{
["connectionString"] = "type=clightning;server=tcp://8.8.8.8"
},
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 UpdateLightningNetworkPaymentMethodRequest()
await admin2Client.UpdateStorePaymentMethod(admin2.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1",
Config = new JObject()
{
["connectionString"] = "type=clightning;server=tcp://127.0.0.1"
},
Enabled = true
});
});
@ -3056,52 +3057,72 @@ namespace BTCPayServer.Tests
await AssertHttpError(404, async () =>
{
await nonAdminUserClient.GetStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC");
await nonAdminUserClient.GetStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN");
});
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => nonAdminUserClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = method.Enabled,
ConnectionString = method.ConnectionString
Config = new JObject()
{
["internalNodeRef"] = "Internal Node"
}
}));
settings = await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>();
settings.AllowLightningInternalNodeForAll = true;
await tester.PayTester.GetService<SettingsRepository>().UpdateSetting(settings);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await nonAdminUserClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = method.Enabled,
ConnectionString = method.ConnectionString
Config = new JObject()
{
["internalNodeRef"] = "Internal Node"
}
});
// NonAdmin can't set to internal node in AllowLightningInternalNodeForAll is false, but can do other connection string
settings = (await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
settings.AllowLightningInternalNodeForAll = false;
await tester.PayTester.GetService<SettingsRepository>().UpdateSetting(settings);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await nonAdminUserClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = true,
ConnectionString = "type=clightning;server=tcp://8.8.8.8"
Config = new JObject()
{
["connectionString"] = "type=clightning;server=tcp://8.8.8.8"
}
});
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => nonAdminUserClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = true,
ConnectionString = "Internal Node"
Config = new JObject()
{
["connectionString"] = "Internal Node"
}
}));
// NonAdmin add admin as owner of the store
await nonAdminUser.AddOwner(admin.UserId);
// Admin turn on Internal node
adminClient = await admin.CreateClient(Policies.CanModifyStoreSettings, Policies.CanUseInternalLightningNode);
var data = await adminClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
var data = await adminClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = method.Enabled,
ConnectionString = "Internal Node"
Config = new JObject()
{
["connectionString"] = "Internal Node"
}
});
Assert.NotNull(data);
Assert.NotNull(data.Config["internalNodeRef"]?.Value<string>());
// Make sure that the nonAdmin can toggle enabled, ConnectionString unchanged.
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest()
await nonAdminUserClient.UpdateStorePaymentMethod(nonAdminUser.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Enabled = !data.Enabled,
ConnectionString = "Internal Node"
Config = new JObject()
{
["connectionString"] = "Internal Node"
}
});
}
@ -3385,59 +3406,67 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
await adminClient.UpdateStoreLightningNetworkPaymentMethod(admin.StoreId, "BTC", new UpdateLightningNetworkPaymentMethodRequest("Internal Node", true));
void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary)
await adminClient.UpdateStorePaymentMethod(admin.StoreId, "BTC-LN", new UpdatePaymentMethodRequest()
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString);
Enabled = true,
Config = new JObject()
{
{"connectionString", "Internal Node" }
}
});
void VerifyLightning(GenericPaymentMethodData[] methods)
{
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-LN"));
Assert.Equal("Internal Node", m.Config["internalNodeRef"].Value<string>());
}
var methods = await adminClient.GetStorePaymentMethods(store.Id);
var methods = await adminClient.GetStorePaymentMethods(store.Id, includeConfig: true);
Assert.Single(methods);
VerifyLightning(methods);
var randK = new Mnemonic(Wordlist.English, WordCount.Twelve).DeriveExtKey().Neuter().ToString(Network.RegTest);
await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC",
new UpdateOnChainPaymentMethodRequest(true, randK, "testing", null));
var wallet = await adminClient.GenerateOnChainWallet(store.Id, "BTC", new GenerateOnChainWalletRequest() { });
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
void VerifyOnChain(GenericPaymentMethodData[] dictionary)
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item));
var paymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>();
Assert.Equal(randK, paymentMethodBaseData.DerivationScheme);
var m = Assert.Single(methods.Where(m => m.PaymentMethodId == "BTC-CHAIN"));
var paymentMethodBaseData = Assert.IsType<JObject>(m.Config);
Assert.Equal(wallet.Config.AccountDerivation, paymentMethodBaseData["accountDerivation"].Value<string>());
}
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Equal(2, methods.Count);
methods = await adminClient.GetStorePaymentMethods(store.Id, includeConfig: true);
Assert.Equal(2, methods.Length);
VerifyLightning(methods);
VerifyOnChain(methods);
var connStr = tester.GetLightningConnectionString(LightningConnectionType.CLightning, true);
await adminClient.UpdateStorePaymentMethod(store.Id, "BTC-LN",
new UpdatePaymentMethodRequest()
{
Enabled = true,
Config = new JObject()
{
["connectionString"] = connStr
}
});
await AssertPermissionError("btcpay.store.canmodifystoresettings", () => viewerOnlyClient.GetStorePaymentMethods(store.Id, includeConfig: true));
methods = await adminClient.GetStorePaymentMethods(store.Id, includeConfig: true);
methods = await viewerOnlyClient.GetStorePaymentMethods(store.Id);
VerifyLightning(methods);
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC",
new UpdateLightningNetworkPaymentMethodRequest(
tester.GetLightningConnectionString(LightningConnectionType.CLightning, true), true));
methods = await viewerOnlyClient.GetStorePaymentMethods(store.Id);
Assert.True(methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("*NEED CanModifyStoreSettings PERMISSION TO VIEW*", lightningNetworkPaymentMethodBaseData.ConnectionString);
Assert.Equal(connStr, methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN")?.Config["connectionString"].Value<string>());
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Null(methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN").Config);
await this.AssertValidationError(["paymentMethodId"], () => adminClient.RemoveStorePaymentMethod(store.Id, "LOL"));
await adminClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");
Assert.True(methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out item));
lightningNetworkPaymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.NotEqual("*NEED CanModifyStoreSettings PERMISSION TO VIEW*", lightningNetworkPaymentMethodBaseData.ConnectionString);
// Alternative way of setting the connection string
await adminClient.UpdateStorePaymentMethod(store.Id, "BTC-LN",
new UpdatePaymentMethodRequest()
{
Enabled = true,
Config = JValue.CreateString("Internal Node")
});
methods = await adminClient.GetStorePaymentMethods(store.Id, includeConfig: true);
Assert.Equal("Internal Node", methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN").Config["internalNodeRef"].Value<string>());
}
[Fact(Timeout = TestTimeout)]
@ -3657,11 +3686,11 @@ namespace BTCPayServer.Tests
var adminClient = await admin.CreateClient(Policies.Unrestricted);
Assert.False((await adminClient.GetUserByIdOrEmail(admin.UserId)).RequiresApproval);
Assert.Empty(await adminClient.GetNotifications());
// require approval
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = true });
// new user needs approval
var unapprovedUser = tester.NewAccount();
await unapprovedUser.GrantAccessAsync();
@ -3684,7 +3713,7 @@ namespace BTCPayServer.Tests
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
Assert.True((await unapprovedUserApiKeyClient.GetCurrentUser()).Approved);
Assert.True((await unapprovedUserBasicAuthClient.GetCurrentUser()).Approved);
// un-approve
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None));
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
@ -3696,10 +3725,10 @@ namespace BTCPayServer.Tests
{
await unapprovedUserBasicAuthClient.GetCurrentUser();
});
// reset policies to not require approval
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
// new user does not need approval
var newUser = tester.NewAccount();
await newUser.GrantAccessAsync();
@ -3710,14 +3739,14 @@ namespace BTCPayServer.Tests
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).RequiresApproval);
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).Approved);
Assert.Single(await adminClient.GetNotifications(false));
// try unapproving user which does not have the RequiresApproval flag
await AssertAPIError("invalid-state", async () =>
{
await adminClient.ApproveUser(newUser.UserId, false, CancellationToken.None);
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -3773,7 +3802,7 @@ namespace BTCPayServer.Tests
Amount = 0.0001m,
Metadata = JObject.FromObject(new
{
source ="apitest",
source = "apitest",
sourceLink = "https://chocolate.com"
})
});
@ -3782,16 +3811,16 @@ namespace BTCPayServer.Tests
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
payout =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
{
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
var payout2 = await adminClient.CreatePayout(admin.StoreId,
@ -3885,7 +3914,7 @@ namespace BTCPayServer.Tests
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods));
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PaymentMethods));
//still too poor to process any payouts
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
@ -3921,10 +3950,10 @@ namespace BTCPayServer.Tests
uint256 txid = null;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, PaymentMethodId.Parse("BTC")));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
@ -3935,24 +3964,24 @@ namespace BTCPayServer.Tests
// settings that were added later
var settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.False( settings.ProcessNewPayoutsInstantly);
Assert.False(settings.ProcessNewPayoutsInstantly);
Assert.Equal(0m, settings.Threshold);
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
settings.IntervalSeconds = TimeSpan.FromDays(1);
settings.ProcessNewPayoutsInstantly = true;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.True( settings.ProcessNewPayoutsInstantly);
Assert.True(settings.ProcessNewPayoutsInstantly);
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
var beforeHookTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@ -4008,23 +4037,23 @@ namespace BTCPayServer.Tests
}
throw;
}
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
//let's test the threshold limiter
settings.Threshold = 0.5m;
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
//quick test: when updating processor, it processes instantly
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Equal(0.5m, settings.Threshold);
//create a payout that should not be processed straight away due to threshold
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
@ -4034,13 +4063,13 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
@ -4050,14 +4079,14 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
@ -4070,7 +4099,7 @@ namespace BTCPayServer.Tests
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));

View file

@ -381,6 +381,7 @@ namespace BTCPayServer.Tests
return Task.CompletedTask;
});
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
var handler = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>().GetBitcoinHandler("BTC");
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
@ -389,19 +390,13 @@ namespace BTCPayServer.Tests
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
Assert.Equal(-1,
((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
handler.ParsePaymentDetails(originalPayment.Details).ConfirmationCount);
Assert.Equal(0,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
handler.ParsePaymentDetails(coinjoinPayment.Details).ConfirmationCount);
Assert.False(originalPayment.Accounted);
Assert.True(coinjoinPayment.Accounted);
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
Assert.Equal(originalPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value,
coinjoinPayment.GetCryptoPaymentData()
.AssertType<BitcoinLikePaymentData>()
.Value);
Assert.Equal(originalPayment.Value,
coinjoinPayment.Value);
});
await TestUtils.EventuallyAsync(async () =>
@ -929,10 +924,9 @@ retry:
tester.ExplorerClient.Network.NBitcoinNetwork);
var senderStore = await tester.PayTester.StoreRepository.FindStore(senderUser.StoreId);
var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var derivationSchemeSettings = senderStore.GetSupportedPaymentMethods(tester.NetworkProvider)
.OfType<DerivationSchemeSettings>().SingleOrDefault(settings =>
settings.PaymentId == paymentMethodId);
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var derivationSchemeSettings = senderStore.GetPaymentMethodConfig<DerivationSchemeSettings>(paymentMethodId, handlers);
ReceivedCoin[] senderCoins = null;
ReceivedCoin coin = null;
@ -1138,14 +1132,14 @@ retry:
//broadcast the payjoin
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
Assert.True(res.Success);
var handler = handlers.GetBitcoinHandler("BTC");
// Paid with coinjoin
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(false), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
handler.ParsePaymentDetails(p.Details).PayjoinInformation is null);
});
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
@ -1174,7 +1168,7 @@ 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];
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(handler, false).First().PayjoinInformation.ContributedOutPoints[0];
});
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
// The outpoint should now be available for next pj selection

View file

@ -27,6 +27,7 @@ using BTCPayServer.Views.Wallets;
using ExchangeSharp;
using LNURL;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -1550,17 +1551,16 @@ namespace BTCPayServer.Tests
{
await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.0m));
}
var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
var targetTx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(1.2m));
var tx = await s.Server.ExplorerNode.GetRawTransactionAsync(targetTx);
var spentOutpoint = new OutPoint(targetTx,
tx.Outputs.FindIndex(txout => txout.Value == Money.Coins(1.2m)));
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
await TestUtils.EventuallyAsync(async () =>
{
var store = await s.Server.PayTester.StoreRepository.FindStore(storeId);
var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode);
var x = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
wallet.InvalidateCache(x.AccountDerivation);
Assert.Contains(
@ -1821,7 +1821,8 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var address = invoice.GetPaymentPrompt(btc).Destination;
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result =
@ -1833,7 +1834,7 @@ namespace BTCPayServer.Tests
//lets import and save private keys
invoiceId = s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
address = invoice.GetPaymentPrompt(btc).Destination;
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
@ -1895,8 +1896,7 @@ namespace BTCPayServer.Tests
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21;
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>()).CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
@ -2257,7 +2257,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
@ -2267,7 +2267,7 @@ namespace BTCPayServer.Tests
s.FindAlertMessage();
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
if (!s.Driver.PageSource.Contains(bolt))
@ -2277,7 +2277,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PaymentTypes.LN.GetPaymentMethodId("BTC")}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.Completed}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
@ -2889,6 +2889,7 @@ namespace BTCPayServer.Tests
public async Task CanUseLNURL()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
@ -3028,7 +3029,7 @@ namespace BTCPayServer.Tests
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
Assert.Equal(new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
@ -3053,7 +3054,7 @@ namespace BTCPayServer.Tests
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
s.Driver.FindElement(By.Id("BTC-LN-view")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
@ -3164,14 +3165,14 @@ namespace BTCPayServer.Tests
Assert.Equal(2, invoices.Length);
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
var prompt = i.GetPaymentPrompt(PaymentTypes.LNURL.GetPaymentMethodId("BTC"));
var handlers = s.Server.PayTester.GetService<PaymentMethodHandlerDictionary>();
var details = (LNURLPayPaymentMethodDetails)handlers.ParsePaymentPromptDetails(prompt);
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
details.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
if (details.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}

View file

@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
{
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
return LightningPaymentMethodConfig.InternalNode;
if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
@ -18,9 +19,11 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
@ -29,6 +32,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -148,15 +152,6 @@ namespace BTCPayServer.Tests
await storeController.GeneralSettings(settings);
}
public async Task ModifyWalletSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = await storeController.WalletSettings(StoreId, "BTC");
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
@ -164,6 +159,7 @@ namespace BTCPayServer.Tests
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings);
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -295,7 +291,7 @@ namespace BTCPayServer.Tests
var storeController = GetController<UIStoresController>();
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var nodeType = connectionString == LightningPaymentMethodConfig.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
await storeController.SetupLightningNode(storeId ?? StoreId,
@ -373,8 +369,9 @@ namespace BTCPayServer.Tests
var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>();
var store = await storeRepository.FindStore(StoreId);
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>()
.First();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(psbt.Network.NetworkSet.CryptoCode);
var handlers = parent.PayTester.GetService<PaymentMethodHandlerDictionary>();
var settings = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
TestLogs.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError)
{
@ -491,7 +488,7 @@ namespace BTCPayServer.Tests
public class DummyStoreWebhookEvent : StoreWebhookEvent
{
}
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
@ -576,9 +573,10 @@ retry:
public async Task<uint256> PayOnChain(string invoiceId)
{
var cryptoCode = "BTC";
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == cryptoCode);
var method = methods.First(m => m.PaymentMethodId == pmi.ToString());
var address = method.Destination;
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
{
@ -601,7 +599,7 @@ retry:
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LN");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
@ -615,7 +613,7 @@ retry:
var network = SupportedNetwork.NBitcoinNetwork;
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
var method = methods.First(m => m.PaymentMethodId == $"{cryptoCode}-LNURL");
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
var http = new HttpClient();
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
@ -670,15 +668,36 @@ retry:
var dbContext = this.parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
await db.OpenAsync();
bool isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"CustomerEmail\",\"ExceptionStatus\",\"ItemCode\",\"OrderId\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldInvoices)
{
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
await writer.WriteLineAsync(localInvoice);
if (isHeader)
{
isHeader = false;
await writer.WriteLineAsync(invoice);
}
else
{
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
var fields = localInvoice.Split(',');
var blob1 = ZipUtils.Unzip(Encoders.Hex.DecodeData(fields[1].Substring(2)));
var matched = Regex.Match(blob1, "xpub[^\\\"-]*");
if (matched.Success)
{
var xpub = (BitcoinExtPubKey)Network.Main.Parse(matched.Value);
var xpubTestnet = xpub.ExtPubKey.GetWif(Network.RegTest).ToString();
blob1 = blob1.Replace(xpub.ToString(), xpubTestnet.ToString());
fields[1] = $"\\x{Encoders.Hex.EncodeData(ZipUtils.Zip(blob1))}";
localInvoice = string.Join(',', fields);
}
await writer.WriteLineAsync(localInvoice);
}
}
await writer.FlushAsync();
}
isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldPayments)

View file

@ -0,0 +1,735 @@
[
{
"type": "invoice",
"input": {
"id": "HuzCsv9hghew2FD6zVqUyY",
"storeId": "3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd",
"orderId": "CustomOrderId",
"speedPolicy": 0,
"rate": 5672.82929871779,
"invoiceTime": 1538395793,
"expirationTime": 1538396693,
"depositAddress": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1",
"productInformation": {
"itemDesc": "Masternode Staking",
"itemCode": null,
"physical": false,
"price": 100.0,
"currency": "EUR",
"taxIncluded": 0.0
},
"buyerInformation": {
"buyerName": null,
"buyerEmail": "customer@example.com",
"buyerCountry": null,
"buyerZip": null,
"buyerState": null,
"buyerCity": null,
"buyerAddress2": null,
"buyerAddress1": null,
"buyerPhone": null
},
"posData": null,
"derivationStrategy": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"derivationStrategies": "{\"BTC\":\"xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]\"}",
"status": "new",
"exceptionStatus": null,
"payments": [],
"refundable": false,
"refundMail": "customer@example.com",
"redirectURL": "https://example.com/thanksyou",
"txFee": 100,
"fullNotifications": true,
"notificationURL": "https://example.com/callbacks",
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 5672.82929871779,
"paymentMethod": {},
"feeRate": 1,
"txFee": 100,
"depositAddress": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1"
}
},
"monitoringExpiration": 1538400293,
"historicalAddresses": null,
"availableAddressHashes": null,
"extendedNotifications": false,
"events": null,
"paymentTolerance": 1.0
},
"expected": {
"version": 3,
"metadata": {
"orderId": "CustomOrderId",
"itemDesc": "Masternode Staking",
"physical": false,
"buyerEmail": "customer@example.com",
"taxIncluded": 0.0
},
"rates": { "BTC": "5672.82929871779" },
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"paymentMethodFee": "0.000001",
"details": {
"paymentMethodFeeRate": 1,
"recommendedFeeRate": 1,
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]"
},
"divisibility": 8,
"destination": "39mWwvUDoZ5CxXa6CmgaUda19qYj9LpQD1"
}
},
"invoiceTime": 1538395793,
"redirectURL": "https://example.com/thanksyou",
"receiptOptions": {},
"internalTags": [],
"expirationTime": 1538396693,
"notificationURL": "https://example.com/callbacks",
"paymentTolerance": 1.0,
"fullNotifications": true,
"monitoringExpiration": 1538400293
}
},
{
"type": "invoice",
"input": {
"id": "ANjq7kkV4mqDRPr5F8EmLZ",
"rate": "66823.066",
"price": "30",
"txFee": 0,
"events": null,
"status": "new",
"refunds": null,
"storeId": "8Ja5pfPydLZTt3YLWhZuZ5vfXT2bNfvk1krUhF8ykCyt",
"version": 2,
"customerEmail": "toto@toto.com",
"archived": false,
"currency": "USD",
"metadata": {
"orderId": "CC",
"itemDesc": "CC"
},
"payments": [],
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 66823.066,
"txFee": 0,
"feeRate": 15.13,
"paymentMethod": {
"keyPath": "0/25903",
"activated": true,
"networkFeeRate": 12.319,
"payjoinEnabled": false
},
"depositAddress": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1709806449,
"isUnderPaid": true,
"redirectURL": "https://test/",
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [],
"depositAddress": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g",
"expirationTime": 1709808249,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": true,
"notificationEmail": null,
"lazyPaymentMethods": false,
"requiresRefundEmail": null,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC": {
"label": null,
"source": "ManualDerivationScheme",
"signingKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"isHotWallet": false,
"accountOriginal": null,
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"accountKeySettings": [
{
"accountKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]",
"accountKeyPath": "84/0'/0'",
"rootFingerprint": "312e13db"
}
]
}
},
"monitoringExpiration": 1709894649,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"version": 3,
"metadata": {
"orderId": "CC",
"itemDesc": "CC",
"buyerEmail": "toto@toto.com"
},
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"rates": { "BTC": "66823.066" },
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"destination": "bc1quf9l42tnma7zws9vusrvun4wchau0ue4zj838g",
"details": {
"recommendedFeeRate": 15.13,
"paymentMethodFeeRate": 12.319,
"keyPath": "0/25903",
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR-[p2sh]"
},
"divisibility": 8
}
},
"receiptOptions": {},
"invoiceTime": 1709806449,
"redirectURL": "https://test/",
"speedPolicy": 1,
"internalTags": [],
"expirationTime": 1709808249,
"fullNotifications": true,
"defaultPaymentMethod": "BTC-CHAIN",
"monitoringExpiration": 1709894649
}
},
{
"type": "invoice",
"input": {
"currency": "USD",
"price": 0.0,
"cryptoData": {
"BTC": {
"feeRate": 10.0,
"paymentMethod": {
},
"depositAddress": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
}
},
"expected": {
"internalTags": [],
"metadata": {},
"version": 3,
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"details": {
"paymentMethodFeeRate": 10.0,
"recommendedFeeRate": 10.0
},
"divisibility": 8,
"destination": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"receiptOptions": {}
},
"skipRountripTest" : true
},
{
"type": "invoice",
"input": {
"currency": "USD",
"price": 0.0,
"cryptoData": {
"BTC": {
"feeRate": 4.0,
"paymentMethod": {
"networkFeeMode": 2
},
"depositAddress": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
}
},
"expected": {
"internalTags": [],
"metadata": {},
"version": 3,
"prompts": {
"BTC-CHAIN": {
"currency": "BTC",
"details": {
"feeMode": "Never",
"recommendedFeeRate": 4.0
},
"divisibility": 8,
"destination": "bc1q9l42tnma7zws9vusrvun4wchau0ue4zj838g"
}
},
"receiptOptions": {}
},
"skipRountripTest": true
},
{
"type": "invoice",
"input": {
"id": "AmoigMwzbaBNNCfo21yns1",
"rate": "67056.018",
"price": "13",
"txFee": null,
"events": null,
"status": "new",
"refunds": null,
"storeId": "2b4H99crZ4JuPiRQwUuYpLQsjtHWASLVWHHkQQ2moiED",
"version": 2,
"archived": false,
"currency": "USD",
"metadata": {},
"payments": [],
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"cryptoData": {
"BTC": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"keyPath": null,
"activated": false,
"networkFeeMode": 0,
"networkFeeRate": null,
"payjoinEnabled": false
},
"depositAddress": null
},
"BTC_LNURLPAY": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": null,
"NodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"Preimage": {},
"Activated": true,
"InvoiceId": null,
"Bech32Mode": true,
"PayRequest": null,
"PaymentHash": {},
"ProvidedComment": null,
"GeneratedBoltAmount": null,
"ConsumedLightningAddress": null,
"LightningSupportedPaymentMethod": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"depositAddress": null
},
"BTC_LightningLike": {
"rate": 67056.018,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": null,
"NodeInfo": null,
"Preimage": null,
"Activated": false,
"InvoiceId": null,
"PaymentHash": null
},
"depositAddress": null
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1709864059,
"isUnderPaid": true,
"redirectURL": null,
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [],
"depositAddress": null,
"expirationTime": 1709864959,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": true,
"notificationEmail": null,
"lazyPaymentMethods": true,
"requiresRefundEmail": false,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC": {
"label": null,
"source": "NBXplorerGenerated",
"signingKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"isHotWallet": true,
"accountOriginal": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountKeySettings": [
{
"accountKey": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR",
"accountKeyPath": "84'/0'/0'",
"rootFingerprint": "312e13db"
}
]
},
"BTC_LNURLPAY": {
"CryptoCode": "BTC",
"LUD12Enabled": false,
"UseBech32Scheme": true
},
"BTC_LightningLike": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"monitoringExpiration": 1709951359,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"version": 3,
"metadata": {},
"serverUrl": "https://mainnet.demo.btcpayserver.org",
"rates": { "BTC": "67056.018" },
"prompts": {
"BTC-CHAIN": {
"inactive": true,
"currency": "BTC",
"details": {
"accountDerivation": "xpub6DAbghfuRCW6Qqr3KfSDjik4kEUivAkfhcGHMkVEe4bZq53VQz79k44EoQ5MDpLJKKAPK5vVV6Px7U1R39eQibnbhDsW9uEQ1Kdh9CDbmgR"
},
"divisibility": 8
},
"BTC-LNURL": {
"currency": "BTC",
"divisibility": 11,
"details": {
"nodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"bech32Mode": true
}
},
"BTC-LN": {
"currency": "BTC",
"divisibility": 11,
"inactive": true,
"details": {}
}
},
"invoiceTime": 1709864059,
"speedPolicy": 1,
"expirationTime": 1709864959,
"receiptOptions": {},
"fullNotifications": true,
"lazyPaymentMethods": true,
"defaultPaymentMethod": "BTC-CHAIN",
"internalTags": [],
"monitoringExpiration": 1709951359
}
},
{
"type": "invoice",
"input": {
"id": "ULzMvaSEpvV4XxGb6F78LZ",
"rate": "0",
"type": "TopUp",
"price": "0.0",
"txFee": null,
"events": null,
"status": "new",
"refunds": null,
"storeId": "EBeAyDVUwBSNa6bdZNrytay9ARNL5UB9xnvNPh5xdnhy",
"version": 2,
"archived": false,
"currency": "USD",
"metadata": {
},
"payments": [
],
"serverUrl": "https://donate.nicolas-dorier.com",
"cryptoData": {
"BTC_LNURLPAY": {
"rate": 29501.4,
"txFee": null,
"feeRate": null,
"paymentMethod": {
"BOLT11": "lnbc10n1pjn9nmzpp5a7znt4tv9gy5v6342xrgnntltkljffp255dph40vaf6964j27pvqhp59ly2g7flsy97vqahh9yue8qz7u6tvlpjfh9r0m9nzfezhm6fgmqscqzzsxqzursp55l9zht4zya3jyjdr9khr22z6afvjqdcw06l7vyd6tksdtsc8ezqs9qyyssqwswp9dnz9txv8t8zjrrts9rv4agu40ufqc04434f6lszdwvlhjk45m3pdcpqzghswkrcvgeaztcr6h82xp35suu64hnk4ms929pcahgpfg7sza",
"NodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"Preimage": null,
"Activated": true,
"InvoiceId": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"Bech32Mode": true,
"PayRequest": {
"tag": "payRequest",
"callback": "https://donate.nicolas-dorier.com/BTC/UILNURL/pay/i/ULzMvaSEpvV4XxGb6F78LZ",
"metadata": "[[\"text/identifier\",\"donate@donate.nicolas-dorier.com\"],[\"text/plain\",\"Paid to Nicolas Donation Store (Order ID: )\"]]",
"maxSendable": 612000000000,
"minSendable": 1000,
"commentAllowed": 0
},
"PaymentHash": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"ProvidedComment": null,
"GeneratedBoltAmount": "1000",
"ConsumedLightningAddress": "donate@donate.nicolas-dorier.com",
"LightningSupportedPaymentMethod": {
"CryptoCode": "BTC",
"InternalNodeRef": "Internal Node"
}
},
"depositAddress": null
}
},
"paidAmount": {
"net": 0.0,
"gross": 0.0,
"currency": "USD"
},
"refundMail": null,
"invoiceTime": 1697828706,
"isUnderPaid": false,
"redirectURL": null,
"speedPolicy": 1,
"checkoutType": null,
"internalTags": [
],
"depositAddress": null,
"expirationTime": 1697829606,
"receiptOptions": {
"showQR": null,
"enabled": null,
"showPayments": null
},
"defaultLanguage": null,
"exceptionStatus": "",
"notificationURL": null,
"storeSupportUrl": null,
"paymentTolerance": 0.0,
"fullNotifications": false,
"notificationEmail": null,
"lazyPaymentMethods": false,
"requiresRefundEmail": null,
"defaultPaymentMethod": "BTC",
"derivationStrategies": {
"BTC_LNURLPAY": {
"CryptoCode": "BTC",
"LUD12Enabled": false,
"UseBech32Scheme": true
}
},
"monitoringExpiration": 1697833206,
"extendedNotifications": false,
"redirectAutomatically": false,
"availableAddressHashes": null
},
"expected": {
"type": "TopUp",
"version": 3,
"rates": {
"BTC": "29501.4"
},
"metadata": {
},
"serverUrl": "https://donate.nicolas-dorier.com",
"prompts": {
"BTC-LNURL": {
"currency": "BTC",
"divisibility": 11,
"destination": "lnbc10n1pjn9nmzpp5a7znt4tv9gy5v6342xrgnntltkljffp255dph40vaf6964j27pvqhp59ly2g7flsy97vqahh9yue8qz7u6tvlpjfh9r0m9nzfezhm6fgmqscqzzsxqzursp55l9zht4zya3jyjdr9khr22z6afvjqdcw06l7vyd6tksdtsc8ezqs9qyyssqwswp9dnz9txv8t8zjrrts9rv4agu40ufqc04434f6lszdwvlhjk45m3pdcpqzghswkrcvgeaztcr6h82xp35suu64hnk4ms929pcahgpfg7sza",
"details": {
"nodeInfo": "03d2a44997a0fb6deee0a31c389d9d6bcb6f929f1dd0ba67201d195f2b3c76087c@170.75.160.16:9735",
"invoiceId": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"bech32Mode": true,
"payRequest": {
"tag": "payRequest",
"callback": "https://donate.nicolas-dorier.com/BTC/UILNURL/pay/i/ULzMvaSEpvV4XxGb6F78LZ",
"metadata": "[[\"text/identifier\",\"donate@donate.nicolas-dorier.com\"],[\"text/plain\",\"Paid to Nicolas Donation Store (Order ID: )\"]]",
"maxSendable": 612000000000,
"minSendable": 1000,
"commentAllowed": 0
},
"paymentHash": "ef8535d56c2a09466a35518689cd7f5dbf24a42aa51a1bd5ecea745d564af058",
"generatedBoltAmount": "1000",
"consumedLightningAddress": "donate@donate.nicolas-dorier.com"
}
}
},
"invoiceTime": 1697828706,
"speedPolicy": 1,
"internalTags": [],
"expirationTime": 1697829606,
"receiptOptions": {},
"defaultPaymentMethod": "BTC-CHAIN",
"monitoringExpiration": 1697833206
}
},
{
"type": "payment",
"input": {
"version": 1,
"receivedTime": 1556044076,
"networkFee": 0.000002,
"outpoint": "552b74ff2fc2c8564de40c8cbefd8eb78f1bef5d3010009c643ff889e159663a3d000000",
"output": "7a636f000000000017a914fd2a2d1d15eb6a490512953c12d6db0de662741d87",
"accounted": true,
"cryptoCode": "BTC",
"cryptoPaymentData": "{\"ConfirmationCount\":1414,\"RBF\":false,\"NetworkFee\":0.0,\"Legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"version": 2,
"destination": "3QmdQXq3tSMqiuwNy2ZcbFEHxZ5De4SBYJ",
"paymentMethodFee": "0.000002",
"divisibility": 8,
"details": {
"confirmationCount": 1414,
"outpoint": "3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61"
}
},
"expectedProperties": {
"Created": "04/23/2019 18:27:56 +00:00",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.07299962",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"receivedTime": 1538403202,
"outpoint": "3211391d9dd2d01c8d9f164d0231f72a166c9b27b15fb2603f58a193ca47bea800000000",
"output": "c74500000000000017a9145d741911858531a4e0a8b6a58bf5036d7f68857587",
"accounted": true,
"cryptoCode": "BTC",
"cryptoPaymentData": "{\"ConfirmationCount\":19,\"RBF\":true,\"Legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"destination": "3AD9r1UyXXNFA2o3cucBwqX58xNaXvrdsv",
"divisibility": 8,
"details": {
"confirmationCount": 19,
"rbf": true,
"outpoint": "a8be47ca93a1583f60b25fb1279b6c162af731024d169f8d1cd0d29d1d391132-0"
},
"version": 2
},
"expectedProperties": {
"Created": "10/01/2018 14:13:22 +00:00",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.00017863",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": null,
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1711005875969,
"cryptoPaymentData": "{\"amount\":1000,\"bolT11\":\"lnbc10n1pjlhc90pp5smwdey0c5skr758lpm0w7tf56cafmdd0hey2nylwfl9q398jgf5shp5qra0y2q5w98at2vv0upux3sn2p0efrxs4h2nyzghqcj7009nqpsqcqzzsxqyz5vqsp5y8fvj3dxnwdaavx89nc5vykuywcmzdefh7x7z62q873hmawh7pws9qyyssq8f7ptu037y2vcclwst6nj8cy8ndhrcxj729ea4ntwdmpgtrwfqun5jnh4zul9s7vvtqd58wurdzk9e3xpzs3ykm0umjdwcttfussaxqqayadfs\",\"paymentHash\":\"86dcdc91f8a42c3f50ff0edeef2d34d63a9db5afbe48a993ee4fca0894f24269\",\"paymentType\":\"LNURLPAY\",\"networkFee\":0.0}",
"cryptoPaymentDataType": "LNURLPAY"
},
"expected": {
"version": 2,
"divisibility": 11,
"destination": "lnbc10n1pjlhc90pp5smwdey0c5skr758lpm0w7tf56cafmdd0hey2nylwfl9q398jgf5shp5qra0y2q5w98at2vv0upux3sn2p0efrxs4h2nyzghqcj7009nqpsqcqzzsxqyz5vqsp5y8fvj3dxnwdaavx89nc5vykuywcmzdefh7x7z62q873hmawh7pws9qyyssq8f7ptu037y2vcclwst6nj8cy8ndhrcxj729ea4ntwdmpgtrwfqun5jnh4zul9s7vvtqd58wurdzk9e3xpzs3ykm0umjdwcttfussaxqqayadfs",
"details": {
"paymentHash": "86dcdc91f8a42c3f50ff0edeef2d34d63a9db5afbe48a993ee4fca0894f24269"
}
},
"expectedProperties": {
"Created": "03/21/2024 07:24:35 +00:00",
"CreatedInMs": "1711005875969",
"Amount": "0.00000001",
"Type": "BTC-LNURL",
"Currency": "BTC"
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": "2e0ee2cccec304926677621ab5c1c80723f695740f1aed6915b4e72995d8172016000000",
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1710974348741,
"cryptoPaymentData": "{\"confirmationCount\":6,\"rbf\":false,\"address\":\"bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft\",\"keyPath\":\"0/708\",\"value\":197864,\"legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"destination": "bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft",
"divisibility": 8,
"details": {
"confirmationCount": 6,
"keyPath": "0/708",
"outpoint": "2017d89529e7b41569ed1a0f7495f62307c8c1b51a6277669204c3cecce20e2e-22"
},
"version": 2
},
"expectedProperties": {
"Created": "03/20/2024 22:39:08 +00:00",
"CreatedInMs": "1710974348741",
"Amount": "0.00197864",
"Type": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Accounted": null
}
},
{
"type": "payment",
"input": {
"output": null,
"version": 1,
"outpoint": "2e0ee2cccec304926677621ab5c1c80723f695740f1aed6915b4e72995d8172016000000",
"accounted": true,
"cryptoCode": "BTC",
"networkFee": 0.0,
"receivedTimeMs": 1710974348741,
"cryptoPaymentData": "{\"confirmationCount\":6,\"rbf\":true,\"address\":\"bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft\",\"keyPath\":\"0/708\",\"value\":197864,\"legacy\":false}",
"cryptoPaymentDataType": "BTCLike"
},
"expected": {
"divisibility": 8,
"destination": "bc1qdamnd0fjegj4a5efrwx4gvjc69zufmu7ntf5ft",
"details": {
"confirmationCount": 6,
"keyPath": "0/708",
"outpoint": "2017d89529e7b41569ed1a0f7495f62307c8c1b51a6277669204c3cecce20e2e-22",
"RBF": true
},
"version": 2
}
}
]

View file

@ -40,6 +40,7 @@ using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Plugins.PayButton;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
@ -69,6 +70,7 @@ using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Npgsql;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
@ -377,14 +379,16 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m));
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance);
await tester.ExplorerNode.GenerateAsync(1);
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m), new NBitcoin.RPC.SendToAddressParameters()
{
Replaceable = false
});
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId == PaymentTypes.LN.GetPaymentMethodId("BTC"));
Invoice newInvoice = null;
await TestUtils.EventuallyAsync(async () =>
{
@ -560,7 +564,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
await acc.ModifyWalletSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice
{
Price = 5.0m,
@ -1043,16 +1047,17 @@ namespace BTCPayServer.Tests
tx1.ToString(),
}).Result["txid"].Value<string>());
TestLogs.LogInformation($"Bumped with {tx1Bump}");
var handler = tester.PayTester.GetService<PaymentMethodHandlerDictionary>().GetBitcoinHandler("BTC");
await TestUtils.EventuallyAsync(async () =>
{
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray();
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(handler, false).ToArray();
var payments = invoiceEntity.GetPayments(false).ToArray();
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
Assert.False(payments[0].Accounted);
Assert.Equal(tx1Bump, payments[1].Outpoint.Hash);
Assert.Equal(tx1Bump, btcPayments[1].Outpoint.Hash);
Assert.True(payments[1].Accounted);
Assert.Equal(0.0m, payments[1].NetworkFee);
Assert.Equal(0.0m, payments[1].PaymentMethodFee);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
Assert.Equal("paid", invoice.Status);
@ -1085,8 +1090,8 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await user.GetController<UIInvoiceController>().Invoice(invoice.Id)).Model)
.Payments;
Assert.Single(payments);
var paymentData = payments.First().GetCryptoPaymentData() as BitcoinLikePaymentData;
Assert.NotNull(paymentData.KeyPath);
var paymentData = payments.First().Details;
Assert.NotNull(paymentData["keyPath"]);
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -1339,12 +1344,10 @@ namespace BTCPayServer.Tests
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
var paid = btcSent;
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network);
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc)
.PaymentMethodFee;
if (networkFeeMode != NetworkFeeMode.Always)
{
networkFee = 0.0m;
@ -1364,7 +1367,7 @@ namespace BTCPayServer.Tests
Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString());
// Check if we index by price correctly once we know it
var invoices = await client.GetInvoices(user.StoreId, textSearch: $"{bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture)}");
var invoices = await client.GetInvoices(user.StoreId, textSearch: bitpayinvoice.Price.ToString(CultureInfo.InvariantCulture).Split('.')[0]);
Assert.Contains(invoices, inv => inv.Id == bitpayinvoice.Id);
}
catch (JsonSerializationException)
@ -1492,15 +1495,15 @@ namespace BTCPayServer.Tests
await user.RegisterLightningNodeAsync("BTC");
var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString();
var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToString();
var lnMethod = PaymentTypes.LN.GetPaymentMethodId("BTC").ToString();
var btcMethod = PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString();
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1518,8 +1521,8 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
// LN and LNURL
Assert.Equal(2, invoice.CryptoInfo.Length);
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LNURLPay.ToString());
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LightningLike.ToString());
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LNURL");
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LN");
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
@ -1639,7 +1642,7 @@ namespace BTCPayServer.Tests
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1652,14 +1655,14 @@ namespace BTCPayServer.Tests
Currency = "USD"
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
Assert.Equal("BTC-LN", invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm).Result);
// However, creating an invoice should show LNURL
@ -1713,7 +1716,7 @@ namespace BTCPayServer.Tests
public async Task CanChangeNetworkFeeMode()
{
using var tester = CreateServerTester();
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
@ -1733,10 +1736,7 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc).PaymentMethodFee;
var firstPaymentFee = nextNetworkFee;
switch (networkFeeMode)
{
@ -1768,10 +1768,8 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation($"Remaining due after first payment: {due}");
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc]
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
.GetPaymentPrompt(btc)
.PaymentMethodFee;
switch (networkFeeMode)
{
case NetworkFeeMode.Never:
@ -1942,10 +1940,10 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>();
var entity = (await repo.GetInvoice(invoice6.Id));
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
entity.GetPaymentMethods().First().Calculate();
entity.GetPaymentPrompts().First().Calculate();
// Shouldn't be possible as we clamp the value, but existing invoice may have that
entity.Price = decimal.MaxValue;
entity.GetPaymentMethods().First().Calculate();
entity.GetPaymentPrompts().First().Calculate();
}
@ -1979,14 +1977,14 @@ namespace BTCPayServer.Tests
});
var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
@ -2028,8 +2026,8 @@ namespace BTCPayServer.Tests
Currency = "BTC",
});
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
PaymentMethodId.Parse(model.PaymentMethodId) ==
PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network);
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
@ -2140,7 +2138,7 @@ namespace BTCPayServer.Tests
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Assert.Contains(Math.Round(invoice.MinerFees["BTC"].SatoshiPerBytes), new[] { 100.0m, 20.0m });
TestUtils.Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -2218,14 +2216,6 @@ namespace BTCPayServer.Tests
await cashCow.GenerateAsync(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
await cashCow.GenerateAsync(5); //Now should be complete
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
@ -2268,7 +2258,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
@ -2289,7 +2279,7 @@ namespace BTCPayServer.Tests
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), c.PaymentMethodId);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2299,7 +2289,7 @@ namespace BTCPayServer.Tests
c =>
{
Assert.False(c.AfterExpiration);
Assert.Equal(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), c.PaymentMethod);
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), c.PaymentMethodId);
Assert.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2539,7 +2529,9 @@ namespace BTCPayServer.Tests
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();
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var v = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
Assert.Equal(derivation, v.AccountDerivation.ToString());
Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString());
@ -2547,13 +2539,26 @@ namespace BTCPayServer.Tests
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
pmi = PaymentTypes.LN.GetPaymentMethodId("BTC");
var lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var conf = store.GetPaymentMethodConfig(pmi);
conf["LightningConnectionString"] = conf["connectionString"].Value<string>();
conf["DisableBOLT11PaymentOption"] = true;
((JObject)conf).Remove("connectionString");
store.SetPaymentMethodConfig(pmi, conf);
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();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.Null(lnMethod.GetExternalLightningUrl());
Assert.True(lnMethod.IsInternalNode);
conf = store.GetPaymentMethodConfig(pmi);
Assert.Null(conf["CryptoCode"]); // Osolete
Assert.Null(conf["connectionString"]); // Null, so should be stripped
Assert.Null(conf["DisableBOLT11PaymentOption"]); // Old garbage cleaned
// Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject()
@ -2569,9 +2574,8 @@ namespace BTCPayServer.Tests
}.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();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
@ -2596,8 +2600,23 @@ namespace BTCPayServer.Tests
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();
lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, handlers);
Assert.True(lnMethod.IsInternalNode);
store.SetPaymentMethodConfig(PaymentMethodId.Parse("BTC-LNURL"),
new JObject()
{
["CryptoCode"] = "BTC",
["LUD12Enabled"] = true,
["UseBech32Scheme"] = false,
});
await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
conf = store.GetPaymentMethodConfig(PaymentMethodId.Parse("BTC-LNURL"));
Assert.Null(conf["CryptoCode"]);
Assert.True(conf["lud12Enabled"].Value<bool>());
Assert.Null(conf["useBech32Scheme"]); // default stripped
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -2725,7 +2744,7 @@ namespace BTCPayServer.Tests
serializer.ToString(new Dictionary<string, string>()
{
{
new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(),
PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(),
new KeyPath("44'/0'/0'").ToString()
}
})));
@ -2756,13 +2775,33 @@ namespace BTCPayServer.Tests
Assert.Empty(blob.AdditionalData);
Assert.Single(blob.PaymentMethodCriteria);
Assert.Contains(blob.PaymentMethodCriteria,
criteria => criteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) &&
criteria => criteria.PaymentMethod == PaymentTypes.CHAIN.GetPaymentMethodId("BTC") &&
criteria.Above && criteria.Value.Value == 5m && criteria.Value.Currency == "USD");
Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode);
Assert.Contains(store.GetSupportedPaymentMethods(tester.NetworkProvider), method =>
method is DerivationSchemeSettings dss &&
method.PaymentId == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) &&
dss.AccountKeyPath == new KeyPath("44'/0'/0'"));
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
Assert.Contains(store.GetPaymentMethodConfigs(handlers), method =>
method.Value is DerivationSchemeSettings dss &&
method.Key == PaymentTypes.CHAIN.GetPaymentMethodId("BTC") &&
dss.AccountKeySettings[0].AccountKeyPath == new KeyPath("44'/0'/0'"));
await acc.ImportOldInvoices();
var dbContext = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var invoiceMigrator = tester.PayTester.GetService<InvoiceBlobMigratorHostedService>();
invoiceMigrator.BatchSize = 2;
await invoiceMigrator.ResetMigration();
await invoiceMigrator.StartAsync(default);
tester.DeleteStore = false;
await TestUtils.EventuallyAsync(async () =>
{
var invoices = await dbContext.Invoices.AsNoTracking().ToListAsync();
foreach (var invoice in invoices)
{
Assert.NotNull(invoice.Currency);
Assert.NotNull(invoice.Amount);
Assert.NotNull(invoice.Blob2);
}
Assert.True(await invoiceMigrator.IsComplete());
});
}
private static async Task RestartMigration(ServerTester tester)
@ -3062,7 +3101,7 @@ namespace BTCPayServer.Tests
// 1 payment on chain
Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress");
var paymentTypeIndex = report.GetIndex("PaymentType");
var paymentTypeIndex = report.GetIndex("Category");
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>())

View file

@ -216,6 +216,5 @@
</Content>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores-payment-methods_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
</Project>

View file

@ -1,7 +1,8 @@
@using BTCPayServer.Payments
@using BTCPayServer.Services.Invoices
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
@{
var state = Model.State.ToString();
@ -41,16 +42,17 @@
</div>
@if (Model.Payments != null)
{
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.PaymentMethodId).Distinct())
{
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
var badge = paymentMethodId.PaymentType.GetBadge();
var extension = Extensions.TryGetValue(paymentMethodId, out var e) ? e : null;
var image = extension?.Image;
var badge = extension?.Badge;
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
{
<span class="d-inline-flex align-items-center gap-1">
@if (!string.IsNullOrEmpty(image))
{
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.ToString()" style="height:1.5em" />
}
@if (!string.IsNullOrEmpty(badge))
{

View file

@ -16,6 +16,7 @@
@inject PoliciesSettings PoliciesSettings
@inject ThemeSettings Theme
@inject PluginService PluginService
@inject PrettyNameProvider PrettyName
@model BTCPayServer.Components.MainNav.MainNavViewModel
@ -60,14 +61,14 @@
{
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
else
{
<a asp-area="" asp-controller="UIStores" asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
</li>
@ -80,14 +81,14 @@
{
<a asp-area="" asp-controller="UIStores" asp-action="Lightning" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Lightning) @ViewData.IsActivePage(StoreNavPages.LightningSettings)" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}
else
{
<a asp-area="" asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.LightningSettings)" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span>@(Model.AltcoinsBuild ? $"{scheme.CryptoCode} " : "")Lightning</span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}

View file

@ -12,6 +12,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -30,6 +31,7 @@ public class StoreLightningBalance : ViewComponent
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly IAuthorizationService _authorizationService;
private readonly PaymentMethodHandlerDictionary _handlers;
public StoreLightningBalance(
StoreRepository storeRepo,
@ -38,8 +40,9 @@ public class StoreLightningBalance : ViewComponent
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService)
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers)
{
_storeRepo = storeRepo;
_currencies = currencies;
@ -47,6 +50,7 @@ public class StoreLightningBalance : ViewComponent
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_authorizationService = authorizationService;
_handlers = handlers;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
}
@ -101,10 +105,8 @@ public class StoreLightningBalance : ViewComponent
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_networkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
if (existing == null)
return null;

View file

@ -8,7 +8,9 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
@ -27,21 +29,20 @@ public class StoreRecentTransactions : ViewComponent
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletRepository _walletRepository;
private readonly LabelService _labelService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly TransactionLinkProviders _transactionLinkProviders;
public BTCPayNetworkProvider NetworkProvider { get; }
public StoreRecentTransactions(
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
WalletRepository walletRepository,
LabelService labelService,
PaymentMethodHandlerDictionary handlers,
TransactionLinkProviders transactionLinkProviders)
{
NetworkProvider = networkProvider;
_walletProvider = walletProvider;
_walletRepository = walletRepository;
_labelService = labelService;
_handlers = handlers;
_transactionLinkProviders = transactionLinkProviders;
}
@ -57,15 +58,16 @@ public class StoreRecentTransactions : ViewComponent
if (vm.InitialRendering)
return View(vm);
var derivationSettings = vm.Store.GetDerivationSchemeSettings(NetworkProvider, vm.CryptoCode);
var derivationSettings = vm.Store.GetDerivationSchemeSettings(_handlers, vm.CryptoCode);
var transactions = new List<StoreRecentTransactionViewModel>();
if (derivationSettings?.AccountDerivation is not null)
{
var network = derivationSettings.Network;
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(vm.CryptoCode);
var network = ((IHasNetwork)_handlers[pmi]).Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
var pmi = new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike);
transactions = allTransactions
.Select(tx =>
{
@ -78,7 +80,7 @@ public class StoreRecentTransactions : ViewComponent
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = _transactionLinkProviders.GetTransactionLink(pmi, tx.TransactionId.ToString()),
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, tx.TransactionId.ToString()),
Timestamp = tx.SeenAt,
Labels = labels
};

View file

@ -2,6 +2,8 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -12,17 +14,14 @@ namespace BTCPayServer.Components.StoreSelector
public class StoreSelector : ViewComponent
{
private readonly StoreRepository _storeRepo;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly UserManager<ApplicationUser> _userManager;
public StoreSelector(
StoreRepository storeRepo,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager)
{
_storeRepo = storeRepo;
_userManager = userManager;
_networkProvider = networkProvider;
}
public async Task<IViewComponentResult> InvokeAsync()
@ -34,21 +33,12 @@ namespace BTCPayServer.Components.StoreSelector
var options = stores
.Where(store => !store.Archived)
.Select(store =>
new StoreSelectorOption
{
var cryptoCode = store
.GetSupportedPaymentMethods(_networkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault()?
.Network.CryptoCode;
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
return new StoreSelectorOption
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
WalletId = walletId,
Store = store
};
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
Store = store
})
.OrderBy(s => s.Text)
.ToList();

View file

@ -17,7 +17,6 @@ namespace BTCPayServer.Components.StoreSelector
public bool Selected { get; set; }
public string Text { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public StoreData Store { get; set; }
}
}

View file

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

View file

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
@ -29,19 +30,22 @@ public class StoreWalletBalance : ViewComponent
private readonly WalletHistogramService _walletHistogramService;
private readonly BTCPayWalletProvider _walletProvider;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
public StoreWalletBalance(
StoreRepository storeRepo,
CurrencyNameTable currencies,
WalletHistogramService walletHistogramService,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider)
BTCPayNetworkProvider networkProvider,
PaymentMethodHandlerDictionary handlers)
{
_storeRepo = storeRepo;
_currencies = currencies;
_walletProvider = walletProvider;
_walletHistogramService = walletHistogramService;
_networkProvider = networkProvider;
_walletHistogramService = walletHistogramService;
_handlers = handlers;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
@ -71,11 +75,12 @@ public class StoreWalletBalance : ViewComponent
{
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(3));
var wallet = _walletProvider.GetWallet(_networkProvider.DefaultNetwork);
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
var derivation = store.GetDerivationSchemeSettings(_handlers, walletId.CryptoCode);
var network = _handlers.GetBitcoinHandler(walletId.CryptoCode).Network;
if (derivation is not null)
{
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
vm.Balance = balance.Available.GetValue(derivation.Network);
vm.Balance = balance.Available.GetValue(network);
}
}

View file

@ -26,6 +26,7 @@ namespace BTCPayServer.Components.WalletNav
public class WalletNav : ViewComponent
{
private readonly BTCPayWalletProvider _walletProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly UIWalletsController _walletsController;
private readonly CurrencyNameTable _currencies;
private readonly BTCPayNetworkProvider _networkProvider;
@ -33,12 +34,14 @@ namespace BTCPayServer.Components.WalletNav
public WalletNav(
BTCPayWalletProvider walletProvider,
PaymentMethodHandlerDictionary handlers,
BTCPayNetworkProvider networkProvider,
UIWalletsController walletsController,
CurrencyNameTable currencies,
RateFetcher rateFetcher)
{
_walletProvider = walletProvider;
_handlers = handlers;
_networkProvider = networkProvider;
_walletsController = walletsController;
_currencies = currencies;
@ -51,7 +54,7 @@ namespace BTCPayServer.Components.WalletNav
var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var wallet = _walletProvider.GetWallet(network);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
var derivation = store.GetDerivationSchemeSettings(_handlers, walletId.CryptoCode);
var balance = await wallet.GetBalance(derivation?.AccountDerivation) switch
{
{ Available: null, Total: var total } => total,

View file

@ -26,12 +26,15 @@ namespace BTCPayServer.Controllers
public class BitpayInvoiceController : Controller
{
private readonly UIInvoiceController _InvoiceController;
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
private readonly InvoiceRepository _InvoiceRepository;
public BitpayInvoiceController(UIInvoiceController invoiceController,
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
InvoiceRepository invoiceRepository)
{
_InvoiceController = invoiceController;
_bitpayExtensions = bitpayExtensions;
_InvoiceRepository = invoiceRepository;
}
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
})).FirstOrDefault();
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO());
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO(_bitpayExtensions, Url));
}
[HttpGet]
[Route("invoices")]
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
};
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
.Select((o) => o.EntityToDTO(_bitpayExtensions, Url)).ToArray();
return Json(DataWrapper.Create(entities));
}
@ -100,7 +103,7 @@ namespace BTCPayServer.Controllers
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
{
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator);
var resp = entity.EntityToDTO();
var resp = entity.EntityToDTO(_bitpayExtensions, Url);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -178,7 +181,10 @@ namespace BTCPayServer.Controllers
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
}
entity.PaymentTolerance = storeBlob.PaymentTolerance;
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
if (invoice.DefaultPaymentMethod is not null && PaymentMethodId.TryParse(invoice.DefaultPaymentMethod, out var defaultPaymentMethod))
{
entity.DefaultPaymentMethod = defaultPaymentMethod;
}
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
return await _InvoiceController.CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);

View file

@ -9,9 +9,12 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -29,7 +32,9 @@ namespace BTCPayServer.Controllers
readonly RateFetcher _rateProviderFactory;
readonly BTCPayNetworkProvider _networkProvider;
readonly CurrencyNameTable _currencyNameTable;
private readonly PaymentMethodHandlerDictionary _handlers;
readonly StoreRepository _storeRepo;
private readonly InvoiceRepository _invoiceRepository;
private StoreData CurrentStore => HttpContext.GetStoreData();
@ -37,12 +42,16 @@ namespace BTCPayServer.Controllers
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
PaymentMethodHandlerDictionary handlers)
{
_rateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_networkProvider = networkProvider;
_storeRepo = storeRepo;
_invoiceRepository = invoiceRepository;
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_handlers = handlers;
}
[Route("rates/{baseCurrency}")]
@ -50,11 +59,17 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var supportedMethods = CurrentStore.GetSupportedPaymentMethods(_networkProvider);
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
.Select(method => method.PaymentId.CryptoCode).Distinct();
var inv = _invoiceRepository.CreateNewInvoice(CurrentStore.Id);
inv.Currency = baseCurrency;
var ctx = new InvoiceCreationContext(CurrentStore, CurrentStore.GetStoreBlob(), inv, new Logging.InvoiceLogs(), _handlers, null);
ctx.SetLazyActivation(true);
await ctx.BeforeFetchingRates();
var currencyCodes = ctx
.PaymentMethodContexts
.SelectMany(c => c.Value.RequiredRates)
.Where(c => c.Left.Equals(baseCurrency, StringComparison.OrdinalIgnoreCase))
.Select(c => c.Right)
.ToHashSet();
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, null, cryptoCode, cancellationToken);

View file

@ -11,6 +11,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
@ -21,7 +22,9 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NBitcoin;
using Newtonsoft.Json.Linq;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
@ -42,6 +45,8 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly InvoiceActivator _invoiceActivator;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService;
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PaymentMethodHandlerDictionary _handlers;
public LanguageService LanguageService { get; }
@ -51,7 +56,9 @@ namespace BTCPayServer.Controllers.Greenfield
InvoiceActivator invoiceActivator,
PullPaymentHostedService pullPaymentService,
ApplicationDbContextFactory dbContextFactory,
IAuthorizationService authorizationService)
IAuthorizationService authorizationService,
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PaymentMethodHandlerDictionary handlers)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
@ -63,6 +70,8 @@ namespace BTCPayServer.Controllers.Greenfield
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
_authorizationService = authorizationService;
_paymentLinkExtensions = paymentLinkExtensions;
_handlers = handlers;
LanguageService = languageService;
}
@ -347,7 +356,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
{
await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethodId, invoice, store);
await _invoiceActivator.ActivateInvoicePaymentMethod(invoiceId, paymentMethodId);
return Ok();
}
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");
@ -384,33 +393,31 @@ namespace BTCPayServer.Controllers.Greenfield
{
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
}
PaymentMethod? invoicePaymentMethod = null;
PaymentPrompt? paymentPrompt = null;
PaymentMethodId? paymentMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
{
invoicePaymentMethod = invoice.GetPaymentMethods().SingleOrDefault(method => method.GetId() == paymentMethodId);
paymentPrompt = invoice.GetPaymentPrompt(paymentMethodId);
}
if (invoicePaymentMethod is null)
if (paymentPrompt is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
if (!ModelState.IsValid || paymentPrompt is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var accounting = invoicePaymentMethod.Calculate();
var accounting = paymentPrompt.Calculate();
var cryptoPaid = accounting.Paid;
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency),
new CurrencyPair(paymentPrompt.Currency, invoice.Currency),
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var cryptoCode = invoicePaymentMethod.GetId().CryptoCode;
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var paidAmount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility);
var createPullPayment = new CreatePullPayment
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
@ -436,17 +443,17 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
var appliedDivisibility = paymentMethodDivisibility;
var appliedDivisibility = paymentPrompt.Divisibility;
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = cryptoCode;
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = paidAmount;
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = cryptoCode;
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
@ -469,7 +476,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
var dueAmount = accounting.TotalDue;
createPullPayment.Currency = cryptoCode;
createPullPayment.Currency = paymentPrompt.Currency;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
@ -501,7 +508,7 @@ namespace BTCPayServer.Controllers.Greenfield
createPullPayment.Currency = request.CustomCurrency;
createPullPayment.Amount = request.CustomAmount.Value;
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == request.CustomCurrency;
createPullPayment.AutoApproveClaims = paymentPrompt.Currency == request.CustomCurrency;
break;
default:
@ -569,49 +576,52 @@ namespace BTCPayServer.Controllers.Greenfield
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
{
return entity.GetPaymentMethods().Select(
method =>
return entity.GetPaymentPrompts().Select(
prompt =>
{
var accounting = method.Calculate();
var details = method.GetPaymentMethodDetails();
var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
paymentEntity.GetPaymentMethodId() == method.GetId());
_handlers.TryGetValue(prompt.PaymentMethodId, out var handler);
var accounting = prompt.Currency is not null ? prompt.Calculate() : null;
var payments = prompt.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
paymentEntity.PaymentMethodId == prompt.PaymentMethodId);
_paymentLinkExtensions.TryGetValue(prompt.PaymentMethodId, out var paymentLinkExtension);
var details = prompt.Details;
if (handler is not null && prompt.Activated)
details = JToken.FromObject(handler.ParsePaymentPromptDetails(details), handler.Serializer.ForAPI());
return new InvoicePaymentMethodDataModel
{
Activated = details.Activated,
PaymentMethod = method.GetId().ToStringNormalized(),
CryptoCode = method.GetId().CryptoCode,
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
Due = accounting.DueUncapped,
TotalPaid = accounting.Paid,
PaymentMethodPaid = accounting.CryptoPaid,
Amount = accounting.TotalDue,
NetworkFee = accounting.NetworkFee,
PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due,
Request.GetAbsoluteRoot()),
Activated = prompt.Activated,
PaymentMethodId = prompt.PaymentMethodId.ToString(),
Currency = prompt.Currency,
Destination = prompt.Destination,
Rate = prompt.Currency is not null ? prompt.Rate : 0m,
Due = accounting?.DueUncapped ?? 0m,
TotalPaid = accounting?.Paid ?? 0m,
PaymentMethodPaid = accounting?.PaymentMethodPaid ?? 0m,
Amount = accounting?.TotalDue ?? 0m,
PaymentMethodFee = accounting?.PaymentMethodFee ?? 0m,
PaymentLink = (prompt.Activated ? paymentLinkExtension?.GetPaymentLink(prompt, Url) : null) ?? string.Empty,
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
AdditionalData = details.GetAdditionalData()
AdditionalData = prompt.Details
};
}).ToArray();
}
public static InvoicePaymentMethodDataModel.Payment ToPaymentModel(InvoiceEntity entity, PaymentEntity paymentEntity)
{
var data = paymentEntity.GetCryptoPaymentData();
return new InvoicePaymentMethodDataModel.Payment()
{
Destination = data.GetDestination(),
Id = data.GetPaymentId(),
Status = !paymentEntity.Accounted
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) || data.PaymentCompleted(paymentEntity)
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled
: InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
Fee = paymentEntity.NetworkFee,
Value = data.GetValue(),
Destination = paymentEntity.Destination,
Id = paymentEntity.Id,
Status = paymentEntity.Status switch
{
PaymentStatus.Processing => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
PaymentStatus.Settled => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled,
PaymentStatus.Unaccounted => InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid,
_ => throw new NotSupportedException(paymentEntity.Status.ToString())
},
Fee = paymentEntity.PaymentMethodFee,
Value = paymentEntity.Value,
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}
@ -656,8 +666,8 @@ namespace BTCPayServer.Controllers.Greenfield
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
PaymentTolerance = entity.PaymentTolerance,
PaymentMethods =
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
DefaultPaymentMethod = entity.DefaultPaymentMethod,
entity.GetPaymentPrompts().Select(method => method.PaymentMethodId.ToString()).ToArray(),
DefaultPaymentMethod = entity.DefaultPaymentMethod?.ToString(),
SpeedPolicy = entity.SpeedPolicy,
DefaultLanguage = entity.DefaultLanguage,
RedirectAutomatically = entity.RedirectAutomatically,

View file

@ -9,6 +9,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
@ -22,19 +23,20 @@ namespace BTCPayServer.Controllers.Greenfield
[EnableCors(CorsPolicies.All)]
public class GreenfieldInternalLightningNodeApiController : GreenfieldLightningNodeApiController
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly PaymentMethodHandlerDictionary _handlers;
public GreenfieldInternalLightningNodeApiController(
BTCPayNetworkProvider btcPayNetworkProvider, PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, policiesSettings, authorizationService)
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers
) : base(policiesSettings, authorizationService, handlers)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
_handlers = handlers;
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
@ -135,7 +137,7 @@ namespace BTCPayServer.Controllers.Greenfield
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
if (network is null)
throw ErrorCryptoCodeNotFound();
if (!_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,

View file

@ -12,6 +12,8 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using ExchangeSharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
@ -27,18 +29,17 @@ namespace BTCPayServer.Controllers.Greenfield
{
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
public GreenfieldStoreLightningNodeApiController(
IOptions<LightningNetworkOptions> lightningNetworkOptions,
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers,
PoliciesSettings policiesSettings,
IAuthorizationService authorizationService) : base(
btcPayNetworkProvider, policiesSettings, authorizationService)
IAuthorizationService authorizationService) : base(policiesSettings, authorizationService, handlers)
{
_lightningNetworkOptions = lightningNetworkOptions;
_lightningClientFactory = lightningClientFactory;
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
}
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
@ -138,22 +139,20 @@ namespace BTCPayServer.Controllers.Greenfield
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
bool doingAdminThings)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
if (!_handlers.TryGetValue(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), out var o) ||
o is not LightningLikePaymentHandler handler)
{
throw ErrorCryptoCodeNotFound();
}
var network = handler.Network;
var store = HttpContext.GetStoreData();
if (store == null)
{
throw new JsonHttpException(StoreNotFound());
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
if (existing == null)
throw ErrorLightningNodeNotConfiguredForStore();
if (existing.GetExternalLightningUrl() is {} connectionString)

View file

@ -6,8 +6,11 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
@ -26,16 +29,18 @@ namespace BTCPayServer.Controllers.Greenfield
public abstract class GreenfieldLightningNodeApiController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService;
protected GreenfieldLightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
private readonly PaymentMethodHandlerDictionary _handlers;
protected GreenfieldLightningNodeApiController(
PoliciesSettings policiesSettings,
IAuthorizationService authorizationService)
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_policiesSettings = policiesSettings;
_authorizationService = authorizationService;
_handlers = handlers;
}
public virtual async Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
@ -207,7 +212,7 @@ namespace BTCPayServer.Controllers.Greenfield
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice, CancellationToken cancellationToken = default)
{
var lightningClient = await GetLightningClient(cryptoCode, true);
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
BOLT11PaymentRequest bolt11 = null;
if (string.IsNullOrEmpty(lightningInvoice.BOLT11) ||
@ -336,7 +341,11 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateAPIError("generic-error", ex.Message);
}
}
protected BTCPayNetwork GetNetwork(string cryptoCode)
=> _handlers.TryGetValue(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), out var h)
&& h is IHasNetwork { Network: var network }
? network
: null;
protected JsonHttpException ErrorLightningNodeNotConfiguredForStore()
{
return new JsonHttpException(this.CreateAPIError(404, "lightning-not-configured", "The lightning node is not set up"));

View file

@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
Name = factory.Processor,
FriendlyName = factory.FriendlyName,
PaymentMethods = factory.GetSupportedPaymentMethods().Select(id => id.ToStringNormalized())
PaymentMethods = factory.GetSupportedPaymentMethods().Select(id => id.ToString())
.ToArray()
}));
}

View file

@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers.Greenfield
public ActionResult ServerInfo()
{
var supportedPaymentMethods = _paymentMethodHandlerDictionary
.SelectMany(handler => handler.GetSupportedPaymentMethods().Select(id => id.ToString()))
.Select(handler => handler.PaymentMethodId.ToString())
.Distinct();
ServerInfoData model = new ServerInfoData2

View file

@ -38,14 +38,14 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
string storeId, string? paymentMethod)
{
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod) : null;
var configured =
await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] { storeId },
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = paymentMethod is null ? null : new[] { paymentMethod }
PaymentMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
});
return Ok(configured.Select(ToModel).ToArray());
@ -80,20 +80,20 @@ namespace BTCPayServer.Controllers.Greenfield
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var paymentMethodId = PaymentMethodId.Parse(paymentMethod);
var activeProcessor =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] { storeId },
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] { paymentMethod }
PaymentMethods = new[] { paymentMethodId }
}))
.FirstOrDefault();
activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(FromModel(request));
activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethod;
activeProcessor.PaymentMethod = paymentMethodId.ToString();
activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated()

View file

@ -39,14 +39,14 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
string storeId, string? paymentMethod)
{
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null;
var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod) : null;
var configured =
await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = paymentMethod is null ? null : new[] { paymentMethod }
PaymentMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
});
return Ok(configured.Select(ToModel).ToArray());
@ -86,20 +86,20 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var paymentMethodId = PaymentMethodId.Parse(paymentMethod);
var activeProcessor =
(await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery()
{
Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] { paymentMethod }
PaymentMethods = new[] { paymentMethodId }
}))
.FirstOrDefault();
activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(FromModel(request));
activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethod;
activeProcessor.PaymentMethod = paymentMethodId.ToString();
activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated()

View file

@ -1,170 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
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.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldStoreLNURLPayPaymentMethodsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public GreenfieldStoreLNURLPayPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
public static IEnumerable<LNURLPayPaymentMethodData> GetLNURLPayPaymentMethods(StoreData store,
BTCPayNetworkProvider networkProvider, bool? enabled)
{
var blob = store.GetStoreBlob();
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(networkProvider)
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.LNURLPay)
.OfType<LNURLPaySupportedPaymentMethod>()
.Select(paymentMethod =>
new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.UseBech32Scheme,
paymentMethod.LUD12Enabled
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay")]
public ActionResult<IEnumerable<LNURLPayPaymentMethodData>> GetLNURLPayPaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
return Ok(GetLNURLPayPaymentMethods(Store, _btcPayNetworkProvider, enabled));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public IActionResult GetLNURLPayPaymentMethod(string storeId, string cryptoCode)
{
AssertCryptoCodeWallet(cryptoCode, out _);
var method = GetExistingLNURLPayPaymentMethod(cryptoCode);
if (method is null)
{
return this.CreateAPIError(404, "paymentmethod-not-found", "The LNURL Payment Method isn't activated");
}
return Ok(method);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> RemoveLNURLPayPaymentMethod(
string storeId,
string cryptoCode)
{
AssertCryptoCodeWallet(cryptoCode, out _);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
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/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> UpdateLNURLPayPaymentMethod(string storeId, string cryptoCode,
[FromBody] LNURLPayPaymentMethodData paymentMethodData)
{
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
AssertCryptoCodeWallet(cryptoCode, out _);
var lnMethod = GreenfieldStoreLightningNetworkPaymentMethodsController.GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider,
cryptoCode, Store);
if ((lnMethod is null || lnMethod.Enabled is false) && paymentMethodData.Enabled)
{
ModelState.AddModelError(nameof(LNURLPayPaymentMethodData.Enabled),
"LNURL Pay cannot be enabled unless the lightning payment method is configured and enabled on this store");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var paymentMethod = new LNURLPaySupportedPaymentMethod
{
CryptoCode = cryptoCode,
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
LUD12Enabled = paymentMethodData.LUD12Enabled
};
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(GetExistingLNURLPayPaymentMethod(cryptoCode, store));
}
private LNURLPayPaymentMethodData? GetExistingLNURLPayPaymentMethod(string cryptoCode,
StoreData? store = null)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LNURLPaySupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id);
var excluded = storeBlob.IsExcluded(id);
return paymentMethod is null
? null
: new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excluded,
paymentMethod.UseBech32Scheme,
paymentMethod.LUD12Enabled
);
}
private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network)
{
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null)
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance"));
}
}
}

View file

@ -1,244 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
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.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldStoreLightningNetworkPaymentMethodsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
public PoliciesSettings PoliciesSettings { get; }
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly LightningClientFactoryService _lightningClientFactoryService;
public GreenfieldStoreLightningNetworkPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
IAuthorizationService authorizationService,
LightningClientFactoryService lightningClientFactoryService,
PoliciesSettings policiesSettings)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_authorizationService = authorizationService;
_lightningClientFactoryService = lightningClientFactoryService;
PoliciesSettings = policiesSettings;
}
public static IEnumerable<LightningNetworkPaymentMethodData> GetLightningPaymentMethods(StoreData store,
BTCPayNetworkProvider networkProvider, bool? enabled)
{
var blob = store.GetStoreBlob();
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(networkProvider)
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.LightningLike)
.OfType<LightningSupportedPaymentMethod>()
.Select(paymentMethod =>
new LightningNetworkPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetExternalLightningUrl()?.ToString() ??
paymentMethod.GetDisplayableConnectionString(),
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.PaymentId.ToStringNormalized()
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork")]
public ActionResult<IEnumerable<LightningNetworkPaymentMethodData>> GetLightningPaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
return Ok(GetLightningPaymentMethods(Store, _btcPayNetworkProvider, enabled));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public ActionResult<LightningNetworkPaymentMethodData> GetLightningNetworkPaymentMethod(string storeId, string cryptoCode)
{
AssertSupportLightning(cryptoCode);
var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store);
if (method is null)
{
throw ErrorPaymentMethodNotConfigured();
}
return Ok(method);
}
protected JsonHttpException ErrorPaymentMethodNotConfigured()
{
return new JsonHttpException(this.CreateAPIError(404, "paymentmethod-not-configured", "The lightning payment method is not set up"));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
string storeId,
string cryptoCode)
{
AssertSupportLightning(cryptoCode);
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 storeId, string cryptoCode,
[FromBody] UpdateLightningNetworkPaymentMethodRequest request)
{
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
AssertSupportLightning(cryptoCode);
if (string.IsNullOrEmpty(request.ConnectionString))
{
ModelState.AddModelError(nameof(LightningNetworkPaymentMethodData.ConnectionString),
"Missing connectionString");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
LightningSupportedPaymentMethod? paymentMethod = null;
var store = Store;
var storeBlob = store.GetStoreBlob();
var existing = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store);
if (existing == null || existing.ConnectionString != request.ConnectionString)
{
if (request.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
{
if (!await CanUseInternalLightning())
{
return this.CreateAPIPermissionError(Policies.CanUseInternalLightningNode, $"You are not authorized to use the internal lightning node. Either add '{Policies.CanUseInternalLightningNode}' to an API Key, or allow non-admin users to use the internal lightning node in the server settings.");
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode();
}
else
{
ILightningClient? lightningClient;
try
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
lightningClient = _lightningClientFactoryService.Create(request.ConnectionString, network);
}
catch (Exception e)
{
ModelState.AddModelError(nameof(request.ConnectionString), $"Invalid URL ({e.Message})");
return this.CreateValidationError(ModelState);
}
// if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
// {
// ModelState.AddModelError(nameof(request.ConnectionString),
// $"BTCPay does not support gRPC connections");
// return this.CreateValidationError(ModelState);
// }
if (!await CanManageServer() && !lightningClient.IsSafe())
{
ModelState.AddModelError(nameof(request.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(lightningClient);
}
}
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
storeBlob.SetExcluded(paymentMethodId, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store));
}
public static LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(BTCPayNetworkProvider btcPayNetworkProvider, string cryptoCode,
StoreData 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 is null
? null
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetDisplayableConnectionString(), !excluded,
paymentMethod.PaymentId.ToStringNormalized());
}
private BTCPayNetwork AssertSupportLightning(string cryptoCode)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null)
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance"));
if (!(network.SupportLightning is true))
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code doesn't support lightning"));
return network;
}
private async Task<bool> CanUseInternalLightning()
{
return PoliciesSettings.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;
}
}
}

View file

@ -6,36 +6,39 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers.Greenfield
{
public partial class GreenfieldStoreOnChainPaymentMethodsController
{
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate")]
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate")]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> GenerateOnChainWallet(string storeId, string cryptoCode,
public async Task<IActionResult> GenerateOnChainWallet(string storeId,
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId,
GenerateWalletRequest request)
{
AssertCryptoCodeWallet(cryptoCode, out var network, out _);
AssertCryptoCodeWallet(paymentMethodId, out var network, out _);
if (!_walletProvider.IsAvailable(network))
{
return this.CreateAPIError(503, "not-available",
$"{cryptoCode} services are not currently available");
$"{paymentMethodId} services are not currently available");
}
var method = GetExistingBtcLikePaymentMethod(cryptoCode);
if (method != null)
if (IsConfigured(paymentMethodId, out _))
{
return this.CreateAPIError("already-configured",
$"{cryptoCode} wallet is already configured for this store");
$"{paymentMethodId} wallet is already configured for this store");
}
var canUseHotWallet = await CanUseHotWallet();
@ -64,13 +67,13 @@ namespace BTCPayServer.Controllers.Greenfield
if (response == null)
{
return this.CreateAPIError(503, "not-available",
$"{cryptoCode} services are not currently available");
$"{paymentMethodId} services are not currently available");
}
}
catch (Exception e)
{
return this.CreateAPIError(503, "not-available",
$"{cryptoCode} error: {e.Message}");
$"{paymentMethodId} error: {e.Message}");
}
var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network);
@ -86,16 +89,22 @@ namespace BTCPayServer.Controllers.Greenfield
var store = Store;
var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike),
var handler = _handlers[paymentMethodId];
store.SetPaymentMethodConfig(_handlers[paymentMethodId],
derivationSchemeSettings);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
var rawResult = GetExistingBtcLikePaymentMethod(cryptoCode, store);
var result = new OnChainPaymentMethodDataWithSensitiveData(rawResult.CryptoCode, rawResult.DerivationScheme,
rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic(), derivationSchemeSettings.PaymentId.ToStringNormalized());
var result = new GenerateOnChainWalletResponse()
{
Enabled = !storeBlob.IsExcluded(paymentMethodId),
PaymentMethodId = paymentMethodId.ToString(),
Config = ((JObject)JToken.FromObject(derivationSchemeSettings, handler.Serializer.ForAPI())).ToObject<GenerateOnChainWalletResponse.ConfigData>(handler.Serializer.ForAPI())
};
result.Mnemonic = response.GetMnemonic();
_eventAggregator.Publish(new WalletChangedEvent()
{
WalletId = new WalletId(storeId, cryptoCode)
WalletId = new WalletId(storeId, network.CryptoCode)
});
return Ok(result);
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@ -9,8 +10,11 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
@ -19,6 +23,9 @@ using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Asn1.X509.Qualified;
using static Org.BouncyCastle.Math.EC.ECCurve;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield
@ -33,71 +40,28 @@ namespace BTCPayServer.Controllers.Greenfield
public PoliciesSettings PoliciesSettings { get; }
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayWalletProvider _walletProvider;
private readonly IAuthorizationService _authorizationService;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly EventAggregator _eventAggregator;
public GreenfieldStoreOnChainPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayWalletProvider walletProvider,
IAuthorizationService authorizationService,
ExplorerClientProvider explorerClientProvider,
PoliciesSettings policiesSettings,
PaymentMethodHandlerDictionary handlers,
EventAggregator eventAggregator)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_walletProvider = walletProvider;
_authorizationService = authorizationService;
_explorerClientProvider = explorerClientProvider;
_eventAggregator = eventAggregator;
PoliciesSettings = policiesSettings;
}
public static IEnumerable<OnChainPaymentMethodData> GetOnChainPaymentMethods(StoreData store,
BTCPayNetworkProvider networkProvider, bool? enabled)
{
var blob = store.GetStoreBlob();
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(networkProvider)
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.BTCLike)
.OfType<DerivationSchemeSettings>()
.Select(strategy =>
new OnChainPaymentMethodData(strategy.PaymentId.CryptoCode,
strategy.AccountDerivation.ToString(), !excludedPaymentMethods.Match(strategy.PaymentId),
strategy.Label, strategy.GetSigningAccountKeySettings().GetRootedKeyPath(),
strategy.PaymentId.ToStringNormalized()))
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain")]
public ActionResult<IEnumerable<OnChainPaymentMethodData>> GetOnChainPaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
return Ok(GetOnChainPaymentMethods(Store, _btcPayNetworkProvider, enabled));
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}")]
public ActionResult<OnChainPaymentMethodData> GetOnChainPaymentMethod(
string storeId,
string cryptoCode)
{
AssertCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _);
var method = GetExistingBtcLikePaymentMethod(cryptoCode);
if (method is null)
{
throw ErrorPaymentMethodNotConfigured();
}
return Ok(method);
_handlers = handlers;
}
protected JsonHttpException ErrorPaymentMethodNotConfigured()
@ -106,86 +70,64 @@ namespace BTCPayServer.Controllers.Greenfield
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview")]
public IActionResult GetOnChainPaymentMethodPreview(
string storeId,
string cryptoCode,
int offset = 0, int amount = 10)
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId,
int offset = 0, int count = 10)
{
AssertCryptoCodeWallet(cryptoCode, out var network, out _);
var paymentMethod = GetExistingBtcLikePaymentMethod(cryptoCode);
if (string.IsNullOrEmpty(paymentMethod?.DerivationScheme))
AssertCryptoCodeWallet(paymentMethodId, out var network, out _);
if (!IsConfigured(paymentMethodId, out var settings))
{
throw ErrorPaymentMethodNotConfigured();
}
try
{
var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
var address = line.Derive((uint)i);
result.Addresses.Add(
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), address.ScriptPubKey)
.ToString()
});
}
return Ok(result);
}
catch
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Invalid Derivation Scheme");
return this.CreateValidationError(ModelState);
}
return Ok(GetPreviewResultData(offset, count, network, settings.AccountDerivation));
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")]
public IActionResult GetProposedOnChainPaymentMethodPreview(
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview")]
public async Task<IActionResult> GetProposedOnChainPaymentMethodPreview(
string storeId,
string cryptoCode,
[FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData,
int offset = 0, int amount = 10)
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId,
[FromBody] UpdatePaymentMethodRequest request = null,
int offset = 0, int count = 10)
{
AssertCryptoCodeWallet(cryptoCode, out var network, out _);
if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme))
if (request is null)
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Missing derivationScheme");
ModelState.AddModelError(nameof(request), "Missing body");
return this.CreateValidationError(ModelState);
}
if (request.Config is null)
{
ModelState.AddModelError(nameof(request.Config), "Missing config");
return this.CreateValidationError(ModelState);
}
AssertCryptoCodeWallet(paymentMethodId, out var network, out _);
var handler = _handlers.GetBitcoinHandler(network);
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState, request.Config, User, Store.GetPaymentMethodConfig(paymentMethodId));
await handler.ValidatePaymentMethodConfig(ctx);
if (ctx.MissingPermission is not null)
{
return this.CreateAPIPermissionError(ctx.MissingPermission.Permission, ctx.MissingPermission.Message);
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
DerivationStrategyBase strategy;
try
{
strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Invalid Derivation Scheme");
return this.CreateValidationError(ModelState);
}
var settings = handler.ParsePaymentMethodConfig(ctx.Config);
var result = GetPreviewResultData(offset, count, network, settings.AccountDerivation);
return Ok(result);
}
private static OnChainPaymentMethodPreviewResultData GetPreviewResultData(int offset, int count, BTCPayNetwork network, DerivationStrategyBase strategy)
{
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
for (var i = offset; i < count; i++)
{
var derivation = line.Derive((uint)i);
result.Addresses.Add(
@ -195,120 +137,32 @@ namespace BTCPayServer.Controllers.Greenfield
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
network.NBXplorerNetwork.CreateAddress(strategy, deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
.ToString()
});
}
return Ok(result);
return result;
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}")]
public async Task<IActionResult> RemoveOnChainPaymentMethod(
string storeId,
string cryptoCode,
int offset = 0, int amount = 10)
private void AssertCryptoCodeWallet(PaymentMethodId paymentMethodId, out BTCPayNetwork network, out BTCPayWallet wallet)
{
AssertCryptoCodeWallet(cryptoCode, out _, out _);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var store = Store;
store.SetSupportedPaymentMethod(id, null);
await _storeRepository.UpdateStore(store);
_eventAggregator.Publish(new WalletChangedEvent()
{
WalletId = new WalletId(storeId, cryptoCode)
});
return Ok();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}")]
public async Task<IActionResult> UpdateOnChainPaymentMethod(
string storeId,
string cryptoCode,
[FromBody] UpdateOnChainPaymentMethodRequest request)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
AssertCryptoCodeWallet(cryptoCode, out var network, out var wallet);
if (string.IsNullOrEmpty(request?.DerivationScheme))
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Missing derivationScheme");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var store = Store;
var storeBlob = store.GetStoreBlob();
var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme);
if (strategy != null)
await wallet.TrackAsync(strategy);
var dss = new DerivationSchemeSettings(strategy, network) {Label = request.Label,};
var signing = dss.GetSigningAccountKeySettings();
if (request.AccountKeyPath is { } r)
{
signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint;
}
else
{
signing.AccountKeyPath = null;
signing.RootFingerprint = null;
}
store.SetSupportedPaymentMethod(id, dss);
storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
_eventAggregator.Publish(new WalletChangedEvent()
{
WalletId = new WalletId(storeId, cryptoCode)
});
return Ok(GetExistingBtcLikePaymentMethod(cryptoCode, store));
}
catch
{
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
"Invalid Derivation Scheme");
return this.CreateValidationError(ModelState);
}
}
private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network, out BTCPayWallet wallet)
{
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null)
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance"));
if (!_handlers.TryGetValue(paymentMethodId, out var h) || h is not BitcoinLikePaymentHandler handler)
throw new JsonHttpException(this.CreateAPIError(404, "unknown-paymentMethodId", "This payment method id isn't set up in this BTCPay Server instance"));
network = handler.Network;
wallet = _walletProvider.GetWallet(network);
if (wallet is null)
throw ErrorPaymentMethodNotConfigured();
}
private OnChainPaymentMethodData GetExistingBtcLikePaymentMethod(string cryptoCode, StoreData store = null)
bool IsConfigured(PaymentMethodId paymentMethodId, [MaybeNullWhen(false)] out DerivationSchemeSettings settings)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(method => method.PaymentId == id);
var excluded = storeBlob.IsExcluded(id);
return paymentMethod == null
? null
: new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.AccountDerivation.ToString(), !excluded, paymentMethod.Label,
paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath(),
paymentMethod.PaymentId.ToStringNormalized());
var store = Store;
var conf = store.GetPaymentMethodConfig(paymentMethodId);
settings = null;
if (conf is (null or { Type: JTokenType.Null }))
return false;
settings = ((BitcoinLikePaymentHandler)_handlers[paymentMethodId]).ParsePaymentMethodConfig(conf);
return settings?.AccountDerivation is not null;
}
}
}

View file

@ -15,9 +15,11 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models.WalletViewModels;
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.Labels;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
@ -44,7 +46,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly IAuthorizationService _authorizationService;
private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly WalletRepository _walletRepository;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly NBXplorerDashboard _nbXplorerDashboard;
@ -60,7 +62,7 @@ namespace BTCPayServer.Controllers.Greenfield
public GreenfieldStoreOnChainWalletsController(
IAuthorizationService authorizationService,
BTCPayWalletProvider btcPayWalletProvider,
BTCPayNetworkProvider btcPayNetworkProvider,
PaymentMethodHandlerDictionary handlers,
WalletRepository walletRepository,
ExplorerClientProvider explorerClientProvider,
NBXplorerDashboard nbXplorerDashboard,
@ -77,7 +79,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
_authorizationService = authorizationService;
_btcPayWalletProvider = btcPayWalletProvider;
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_walletRepository = walletRepository;
_explorerClientProvider = explorerClientProvider;
PoliciesSettings = policiesSettings;
@ -316,7 +318,7 @@ namespace BTCPayServer.Controllers.Greenfield
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId,
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
return Ok(utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
@ -332,7 +334,7 @@ namespace BTCPayServer.Controllers.Greenfield
#pragma warning disable CS0612 // Type or member is obsolete
Labels = info?.LegacyLabels ?? new Dictionary<string, LabelData>(),
#pragma warning restore CS0612 // Type or member is obsolete
Link = _transactionLinkProviders.GetTransactionLink(pmi, coin.OutPoint.ToString()),
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, coin.OutPoint.ToString()),
Timestamp = coin.Timestamp,
KeyPath = coin.KeyPath,
Confirmations = coin.Confirmations,
@ -771,14 +773,14 @@ namespace BTCPayServer.Controllers.Greenfield
[MaybeNullWhen(true)] out DerivationSchemeSettings derivationScheme,
[MaybeNullWhen(false)] out IActionResult actionResult)
{
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
derivationScheme = null;
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null)
if (!_handlers.TryGetValue(pmi, out var handler))
{
throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode",
"This crypto code isn't set up in this BTCPay Server instance"));
}
network = ((IHasNetwork)handler).Network;
if (!network.WalletSupported || !_btcPayWalletProvider.IsAvailable(network))
{
@ -801,13 +803,7 @@ namespace BTCPayServer.Controllers.Greenfield
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;
return Store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers);
}
private OnChainWalletTransactionData ToModel(WalletTransactionInfo? walletTransactionsInfoAsync,

View file

@ -6,11 +6,18 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using BTCPayServer.Abstractions.Extensions;
using StoreData = BTCPayServer.Data.StoreData;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers.Greenfield
{
@ -20,37 +27,138 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldStorePaymentMethodsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly StoreRepository _storeRepository;
private readonly IAuthorizationService _authorizationService;
public GreenfieldStorePaymentMethodsController(BTCPayNetworkProvider btcPayNetworkProvider, IAuthorizationService authorizationService)
public GreenfieldStorePaymentMethodsController(
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository,
IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_storeRepository = storeRepository;
_authorizationService = authorizationService;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods")]
public async Task<ActionResult<Dictionary<string, GenericPaymentMethodData>>> GetStorePaymentMethods(
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}")]
public async Task<IActionResult> GetStorePaymentMethod(
string storeId,
[FromQuery] bool? enabled)
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId,
[FromQuery] bool? includeConfig)
{
var result = await GetStorePaymentMethods(storeId, onlyEnabled: false, includeConfig);
if (result is OkObjectResult { Value: GenericPaymentMethodData[] methods })
{
var m = methods.FirstOrDefault(m => m.PaymentMethodId == paymentMethodId.ToString());
return m is { } ? Ok(m) : PaymentMethodNotFound();
}
return result;
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}")]
public async Task<IActionResult> RemoveStorePaymentMethod(
string storeId,
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId)
{
AssertHasHandler(paymentMethodId);
if (Store.GetPaymentMethodConfig(paymentMethodId) is null)
return Ok();
Store.SetPaymentMethodConfig(paymentMethodId, null);
await _storeRepository.UpdateStore(Store);
return Ok();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}")]
public async Task<IActionResult> UpdateStorePaymentMethod(
string storeId,
[ModelBinder(typeof(PaymentMethodIdModelBinder))]
PaymentMethodId paymentMethodId,
[FromBody] UpdatePaymentMethodRequest? request = null)
{
if (request is null)
{
ModelState.AddModelError(nameof(request), "Missing body");
return this.CreateValidationError(ModelState);
}
var handler = AssertHasHandler(paymentMethodId);
if (request?.Config is { } config)
{
try
{
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState, config, User, Store.GetPaymentMethodConfig(paymentMethodId));
await handler.ValidatePaymentMethodConfig(ctx);
config = ctx.Config;
if (ctx.MissingPermission is not null)
{
return this.CreateAPIPermissionError(ctx.MissingPermission.Permission, ctx.MissingPermission.Message);
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (ctx.StripUnknownProperties)
config = JToken.FromObject(handler.ParsePaymentMethodConfig(config), handler.Serializer);
}
catch
{
ModelState.AddModelError(nameof(config), "Invalid configuration");
return this.CreateValidationError(ModelState);
}
Store.SetPaymentMethodConfig(paymentMethodId, config);
}
if (request?.Enabled is { } enabled)
{
var storeBlob = Store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, enabled);
Store.SetStoreBlob(storeBlob);
}
await _storeRepository.UpdateStore(Store);
return await GetStorePaymentMethod(storeId, paymentMethodId, request?.Config is not null);
}
private IPaymentMethodHandler AssertHasHandler(PaymentMethodId paymentMethodId)
{
if (!_handlers.TryGetValue(paymentMethodId, out var handler))
throw new JsonHttpException(PaymentMethodNotFound());
return handler;
}
private IActionResult PaymentMethodNotFound()
{
return this.CreateAPIError(404, "paymentmethod-not-found", "The payment method is not found");
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods")]
public async Task<IActionResult> GetStorePaymentMethods(
string storeId,
[FromQuery] bool? onlyEnabled, [FromQuery] bool? includeConfig)
{
var storeBlob = Store.GetStoreBlob();
var excludedPaymentMethods = storeBlob.GetExcludedPaymentMethods();
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
;
return Ok(Store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.Where(method =>
enabled is null || (enabled is false && excludedPaymentMethods.Match(method.PaymentId)))
.ToDictionary(
method => method.PaymentId.ToStringNormalized(),
if (includeConfig is true)
{
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
if (!canModifyStore)
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
}
return Ok(Store.GetPaymentMethodConfigs(_handlers, onlyEnabled is true)
.Select(
method => new GenericPaymentMethodData()
{
CryptoCode = method.PaymentId.CryptoCode,
Enabled = enabled.GetValueOrDefault(!excludedPaymentMethods.Match(method.PaymentId)),
Data = method.PaymentId.PaymentType.GetGreenfieldData(method, canModifyStore)
}));
PaymentMethodId = method.Key.ToString(),
Enabled = onlyEnabled.GetValueOrDefault(!excludedPaymentMethods.Match(method.Key)),
Config = includeConfig is true ? JToken.FromObject(method.Value, _handlers[method.Key].Serializer.ForAPI()) : null
}).ToArray());
}
}
}

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
Stores = new[] { storeId },
Processors = new[] { processor },
PaymentMethods = new[] { paymentMethod }
PaymentMethods = new[] { PaymentMethodId.Parse(paymentMethod) }
})).FirstOrDefault();
if (matched is null)
{

View file

@ -124,7 +124,7 @@ namespace BTCPayServer.Controllers.Greenfield
Archived = data.Archived,
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToString(),
//blob
//we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
@ -163,7 +163,7 @@ namespace BTCPayServer.Controllers.Greenfield
Above = criteria.Above,
Amount = criteria.Value.Value,
CurrencyCode = criteria.Value.Currency,
PaymentMethod = criteria.PaymentMethod.ToStringNormalized()
PaymentMethod = criteria.PaymentMethod.ToString()
}).ToList() ?? new List<PaymentMethodCriteriaData>()
};
}

View file

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
@ -559,54 +560,21 @@ namespace BTCPayServer.Controllers.Greenfield
return result.Value ?? GetFromActionResult<T>(result.Result);
}
public override Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled, CancellationToken token = default)
{
return Task.FromResult(
GetFromActionResult(GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethods(storeId, enabled)));
}
public override Task<OnChainPaymentMethodData> GetStoreOnChainPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethod(storeId, cryptoCode)));
}
public override async Task RemoveStoreOnChainPaymentMethod(string storeId, string cryptoCode,
public override async Task<OnChainPaymentMethodPreviewResultData> PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string paymentMethodId,
string derivationScheme, int offset = 0, int count = 10,
CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldStoreOnChainPaymentMethodsController>().RemoveOnChainPaymentMethod(storeId, cryptoCode));
}
public override async Task<OnChainPaymentMethodData> UpdateStoreOnChainPaymentMethod(string storeId,
string cryptoCode, UpdateOnChainPaymentMethodRequest paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<OnChainPaymentMethodData>(
await GetController<GreenfieldStoreOnChainPaymentMethodsController>().UpdateOnChainPaymentMethod(storeId, cryptoCode,
new UpdateOnChainPaymentMethodRequest(
enabled: paymentMethod.Enabled,
label: paymentMethod.Label,
accountKeyPath: paymentMethod.AccountKeyPath,
derivationScheme: paymentMethod.DerivationScheme
)));
}
public override Task<OnChainPaymentMethodPreviewResultData> PreviewProposedStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode,
UpdateOnChainPaymentMethodRequest paymentMethod, int offset = 0, int amount = 10,
CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetProposedOnChainPaymentMethodPreview(storeId, cryptoCode,
paymentMethod, offset, amount)));
return GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetProposedOnChainPaymentMethodPreview(storeId, Payments.PaymentMethodId.Parse(paymentMethodId),
new UpdatePaymentMethodRequest() { Config = JValue.CreateString(derivationScheme) }, offset, count));
}
public override Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
string storeId, string cryptoCode, int offset = 0, int amount = 10, CancellationToken token = default)
string storeId, string paymentMethodId, int offset = 0, int amount = 10, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethodPreview(storeId, cryptoCode, offset,
GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethodPreview(storeId, Payments.PaymentMethodId.Parse(paymentMethodId), offset,
amount)));
}
@ -814,71 +782,6 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult<StoreData>(await GetController<GreenfieldStoresController>().UpdateStore(storeId, request));
}
public override Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
GetController<GreenfieldStoreLNURLPayPaymentMethodsController>().GetLNURLPayPaymentMethods(storeId, enabled)));
}
public override Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<LNURLPayPaymentMethodData>(
GetController<GreenfieldStoreLNURLPayPaymentMethodsController>().GetLNURLPayPaymentMethod(storeId, cryptoCode)));
}
public override async Task RemoveStoreLNURLPayPaymentMethod(string storeId, string cryptoCode,
CancellationToken token = default)
{
HandleActionResult(
await GetController<GreenfieldStoreLNURLPayPaymentMethodsController>().RemoveLNURLPayPaymentMethod(storeId,
cryptoCode));
}
public override async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode,
LNURLPayPaymentMethodData paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<LNURLPayPaymentMethodData>(await
GetController<GreenfieldStoreLNURLPayPaymentMethodsController>().UpdateLNURLPayPaymentMethod(storeId, cryptoCode,
paymentMethod));
}
public override Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
GetController<GreenfieldStoreLightningNetworkPaymentMethodsController>().GetLightningPaymentMethods(storeId, enabled)));
}
public override Task<LightningNetworkPaymentMethodData> GetStoreLightningNetworkPaymentMethod(
string storeId, string cryptoCode, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
GetController<GreenfieldStoreLightningNetworkPaymentMethodsController>().GetLightningNetworkPaymentMethod(storeId, cryptoCode)));
}
public override async Task RemoveStoreLightningNetworkPaymentMethod(string storeId, string cryptoCode,
CancellationToken token = default)
{
HandleActionResult(
await GetController<GreenfieldStoreLightningNetworkPaymentMethodsController>().RemoveLightningNetworkPaymentMethod(storeId,
cryptoCode));
}
public override async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId, string cryptoCode,
UpdateLightningNetworkPaymentMethodRequest paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<LightningNetworkPaymentMethodData>(await
GetController<GreenfieldStoreLightningNetworkPaymentMethodsController>().UpdateLightningNetworkPaymentMethod(storeId, cryptoCode,
new UpdateLightningNetworkPaymentMethodRequest(paymentMethod.ConnectionString,
paymentMethod.Enabled)));
}
public override async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string[] orderId = null,
InvoiceStatus[] status = null,
DateTimeOffset? startDate = null,
@ -982,18 +885,18 @@ namespace BTCPayServer.Controllers.Greenfield
return Task.FromResult(GetFromActionResult<PermissionMetadata[]>(GetController<UIHomeController>().Permissions()));
}
public override async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId,
bool? enabled = null, CancellationToken token = default)
public override async Task<GenericPaymentMethodData[]> GetStorePaymentMethods(string storeId,
bool? onlyEnabled = null, bool? includeConfig = null, CancellationToken token = default)
{
return GetFromActionResult(await GetController<GreenfieldStorePaymentMethodsController>().GetStorePaymentMethods(storeId, enabled));
return GetFromActionResult<GenericPaymentMethodData[]>(await GetController<GreenfieldStorePaymentMethodsController>().GetStorePaymentMethods(storeId, onlyEnabled, includeConfig));
}
public override async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request,
public override async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
return GetFromActionResult<OnChainPaymentMethodDataWithSensitiveData>(
await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GenerateOnChainWallet(storeId, cryptoCode,
return GetFromActionResult<GenerateOnChainWalletResponse>(
await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GenerateOnChainWallet(storeId, Payments.PaymentMethodId.Parse(paymentMethodId),
new GenerateWalletRequest()
{
Passphrase = request.Passphrase,

View file

@ -6,6 +6,8 @@ using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
@ -41,60 +43,61 @@ namespace BTCPayServer.Controllers
var btcpayNetwork = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = btcpayNetwork.NBitcoinNetwork;
var paymentMethodId = new[] { store.GetDefaultPaymentId() }
.Concat(store.GetEnabledPaymentIds(_NetworkProvider))
.Concat(store.GetEnabledPaymentIds())
.FirstOrDefault(p => p?.ToString() == request.PaymentMethodId);
try
{
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var destination = paymentMethod?.GetPaymentMethodDetails().GetPaymentDestination();
var paymentMethod = invoice.GetPaymentPrompt(paymentMethodId);
var details = _handlers.ParsePaymentPromptDetails(paymentMethod);
var destination = paymentMethod?.Destination;
switch (paymentMethod?.GetId().PaymentType)
if (details is BitcoinPaymentPromptDetails)
{
case BitcoinPaymentType:
var address = BitcoinAddress.Create(destination, network);
var txid = (await cheater.CashCow.SendToAddressAsync(address, amount)).ToString();
var address = BitcoinAddress.Create(destination, network);
var txid = (await cheater.GetCashCow(cryptoCode).SendToAddressAsync(address, amount)).ToString();
return Ok(new
{
Txid = txid,
AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC),
SuccessMessage = $"Created transaction {txid}"
});
}
else if (details is LigthningPaymentPromptDetails)
{
// requires the channels to be set up using the BTCPayServer.Tests/docker-lightning-channel-setup.sh script
var lnClient = lightningClientFactoryService.Create(
Environment.GetEnvironmentVariable("BTCPAY_BTCEXTERNALLNDREST"),
btcpayNetwork);
var lnAmount = new LightMoney(amount.Satoshi, LightMoneyUnit.Satoshi);
var response = await lnClient.Pay(destination, new PayInvoiceParams { Amount = lnAmount });
if (response.Result == PayResult.Ok)
{
var bolt11 = BOLT11PaymentRequest.Parse(destination, network);
var paymentHash = bolt11.PaymentHash?.ToString();
var paid = response.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC);
return Ok(new
{
Txid = txid,
AmountRemaining = paymentMethod.Calculate().Due - amount.ToDecimal(MoneyUnit.BTC),
SuccessMessage = $"Created transaction {txid}"
});
case LightningPaymentType:
// requires the channels to be set up using the BTCPayServer.Tests/docker-lightning-channel-setup.sh script
var lnClient = lightningClientFactoryService.Create(
Environment.GetEnvironmentVariable("BTCPAY_BTCEXTERNALLNDREST"),
btcpayNetwork);
var lnAmount = new LightMoney(amount.Satoshi, LightMoneyUnit.Satoshi);
var response = await lnClient.Pay(destination, new PayInvoiceParams { Amount = lnAmount });
if (response.Result == PayResult.Ok)
{
var bolt11 = BOLT11PaymentRequest.Parse(destination, network);
var paymentHash = bolt11.PaymentHash?.ToString();
var paid = response.Details.TotalAmount.ToDecimal(LightMoneyUnit.BTC);
return Ok(new
{
Txid = paymentHash,
AmountRemaining = paymentMethod.Calculate().TotalDue - paid,
SuccessMessage = $"Sent payment {paymentHash}"
});
}
return UnprocessableEntity(new
{
ErrorMessage = response.ErrorDetail
});
default:
return UnprocessableEntity(new
{
ErrorMessage = $"Payment method {paymentMethodId} is not supported"
Txid = paymentHash,
AmountRemaining = paymentMethod.Calculate().TotalDue - paid,
SuccessMessage = $"Sent payment {paymentHash}"
});
}
return UnprocessableEntity(new
{
ErrorMessage = response.ErrorDetail
});
}
else
{
return UnprocessableEntity(new
{
ErrorMessage = $"Payment method {paymentMethodId} is not supported"
});
}
}
catch (Exception e)
{
@ -109,12 +112,12 @@ namespace BTCPayServer.Controllers
[CheatModeRoute]
public IActionResult MineBlock(string invoiceId, MineBlocksRequest request, [FromServices] Cheater cheater)
{
var blockRewardBitcoinAddress = cheater.CashCow.GetNewAddress();
var blockRewardBitcoinAddress = cheater.GetCashCow(request.CryptoCode).GetNewAddress();
try
{
if (request.BlockCount > 0)
{
cheater.CashCow.GenerateToAddress(request.BlockCount, blockRewardBitcoinAddress);
cheater.GetCashCow(request.CryptoCode).GenerateToAddress(request.BlockCount, blockRewardBitcoinAddress);
return Ok(new { SuccessMessage = $"Mined {request.BlockCount} block{(request.BlockCount == 1 ? "" : "s")} " });
}
return BadRequest(new { ErrorMessage = "Number of blocks should be at least 1" });

View file

@ -18,6 +18,8 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
@ -118,7 +120,7 @@ namespace BTCPayServer.Controllers
var additionalData = metaData
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
.ToDictionary(dict => dict.Key, dict => dict.Value);
var model = new InvoiceDetailsModel
{
StoreId = store.Id,
@ -131,7 +133,6 @@ namespace BTCPayServer.Controllers
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
"low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
@ -162,13 +163,13 @@ namespace BTCPayServer.Controllers
model.Overpaid = details.Overpaid;
model.StillDue = details.StillDue;
model.HasRates = details.HasRates;
if (additionalData.ContainsKey("receiptData"))
{
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
additionalData.Remove("receiptData");
}
if (additionalData.ContainsKey("posData") && additionalData["posData"] is string posData)
{
// overwrite with parsed JSON if possible
@ -181,7 +182,7 @@ namespace BTCPayServer.Controllers
additionalData["posData"] = posData;
}
}
model.AdditionalData = additionalData;
return View(model);
@ -204,7 +205,7 @@ namespace BTCPayServer.Controllers
if (i.RedirectURL is not null)
{
return Redirect(i.RedirectURL.ToString());
}
}
return NotFound();
}
@ -230,7 +231,7 @@ namespace BTCPayServer.Controllers
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders);
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders, _handlers);
vm.Amount = i.PaidAmount.Net;
vm.Payments = receipt.ShowPayments is false ? null : payments;
@ -266,12 +267,23 @@ namespace BTCPayServer.Controllers
new { pullPaymentId = ppId });
}
var paymentMethods = invoice.GetBlob(_NetworkProvider).GetPaymentMethods();
var pmis = paymentMethods.Select(method => method.GetId()).ToList();
pmis = pmis.Concat(pmis.Where(id => id.PaymentType == LNURLPayPaymentType.Instance)
.Select(id => new PaymentMethodId(id.CryptoCode, LightningPaymentType.Instance))).ToList();
var paymentMethods = invoice.GetBlob().GetPaymentPrompts();
var pmis = paymentMethods.Select(method => method.PaymentMethodId).ToHashSet();
// If LNURL is contained, add the LN too as a possible option
foreach (var pmi in pmis.ToList())
{
if (!_handlers.TryGetValue(pmi, out var h))
{
pmis.Remove(pmi);
continue;
}
if (h is LNURLPayPaymentHandler lh)
{
pmis.Add(PaymentTypes.LN.GetPaymentMethodId(lh.Network.CryptoCode));
}
}
var relevant = payoutHandlers.Where(handler => pmis.Any(handler.CanHandle));
var options = (await relevant.GetSupportedPaymentMethods(invoice.StoreData)).Where(id => pmis.Contains(id)).ToList();
var options = (await relevant.GetSupportedPaymentMethods(invoice.StoreData)).Where(id => pmis.Contains(id)).ToHashSet();
if (!options.Any())
{
var vm = new RefundModel { Title = "No matching payment method" };
@ -281,15 +293,15 @@ namespace BTCPayServer.Controllers
}
var defaultRefund = invoice.Payments
.Select(p => p.GetBlob(_NetworkProvider))
.Select(p => p?.GetPaymentMethodId())
.Select(p => p.GetBlob())
.Select(p => p.PaymentMethodId)
.FirstOrDefault(p => p != null && options.Contains(p));
var refund = new RefundModel
{
Title = "Payment method",
AvailablePaymentMethods =
new SelectList(options.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString())),
new SelectList(options.Select(id => new SelectListItem(id.ToString(), id.ToString())),
"Value", "Text"),
SelectedPaymentMethod = defaultRefund?.ToString() ?? options.First().ToString()
};
@ -318,71 +330,67 @@ namespace BTCPayServer.Controllers
var store = GetCurrentStore();
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
RateRules rules;
RateResult rateResult;
CreatePullPayment createPullPayment;
PaymentMethodAccounting accounting;
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
var appliedDivisibility = paymentMethodDivisibility;
decimal dueAmount = default;
decimal paidAmount = default;
decimal cryptoPaid = default;
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
var pms = invoice.GetPaymentPrompts();
if (!pms.TryGetValue(paymentMethodId, out var paymentMethod))
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
// We included Lightning if only LNURL was set, so this must be LNURL
if (_handlers.TryGetValue(paymentMethodId, out var h) && h is LightningLikePaymentHandler lnh)
{
pms.TryGetValue(PaymentTypes.LNURL.GetPaymentMethodId(lnh.Network.CryptoCode), out paymentMethod);
}
}
if (paymentMethod is null || paymentMethod.Currency is null)
{
ModelState.AddModelError(nameof(model.SelectedPaymentMethod), $"Invalid payment method");
return View("_RefundModal", model);
}
if (paymentMethod != null)
{
accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid;
dueAmount = accounting.TotalDue;
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
}
var accounting = paymentMethod.Calculate();
decimal cryptoPaid = accounting.Paid;
decimal dueAmount = accounting.TotalDue;
var paymentMethodCurrency = paymentMethodId.CryptoCode;
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
decimal? overpaidAmount = isPaidOver ? Math.Round(paidAmount - dueAmount, appliedDivisibility) : null;
decimal? overpaidAmount = isPaidOver ? Math.Round(cryptoPaid - dueAmount, paymentMethod.Divisibility) : null;
int ppDivisibility = paymentMethod.Divisibility;
switch (model.RefundStep)
{
case RefundSteps.SelectPaymentMethod:
model.RefundStep = RefundSteps.SelectRate;
model.Title = "How much to refund?";
if (paymentMethod != null && cryptoPaid != default)
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethod.Divisibility);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency);
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules,
cancellationToken);
//TODO: What if fetching rate failed?
if (rateResult.BidAsk is null)
{
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
cancellationToken);
//TODO: What if fetching rate failed?
if (rateResult.BidAsk is null)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption),
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
return View("_RefundModal", model);
}
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.FiatAmount = paidCurrency;
ModelState.AddModelError(nameof(model.SelectedRefundOption),
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
return View("_RefundModal", model);
}
model.CryptoCode = paymentMethodId.CryptoCode;
model.CryptoDivisibility = paymentMethodDivisibility;
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethod.Divisibility);
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodCurrency);
model.FiatAmount = paidCurrency;
model.CryptoCode = paymentMethodCurrency;
model.CryptoDivisibility = paymentMethod.Divisibility;
model.InvoiceDivisibility = cdCurrency.Divisibility;
model.InvoiceCurrency = invoice.Currency;
model.CustomAmount = model.FiatAmount;
model.CustomCurrency = invoice.Currency;
model.SubtractPercentage = 0;
model.OverpaidAmount = overpaidAmount;
model.OverpaidAmountText = overpaidAmount != null ? _displayFormatter.Currency(overpaidAmount.Value, paymentMethodId.CryptoCode) : null;
model.OverpaidAmountText = overpaidAmount != null ? _displayFormatter.Currency(overpaidAmount.Value, paymentMethodCurrency) : null;
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
return View("_RefundModal", model);
@ -405,32 +413,32 @@ namespace BTCPayServer.Controllers
{
return View("_RefundModal", model);
}
switch (model.SelectedRefundOption)
{
case "RateThen":
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Currency = paymentMethodCurrency;
createPullPayment.Amount = model.CryptoAmountThen;
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
break;
case "CurrentRate":
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Currency = paymentMethodCurrency;
createPullPayment.Amount = model.CryptoAmountNow;
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
break;
case "Fiat":
appliedDivisibility = cdCurrency.Divisibility;
ppDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = model.FiatAmount;
createPullPayment.AutoApproveClaims = false;
break;
case "OverpaidAmount":
model.Title = "How much to refund?";
model.RefundStep = RefundSteps.SelectRate;
if (!isPaidOver)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
@ -443,8 +451,8 @@ namespace BTCPayServer.Controllers
{
return View("_RefundModal", model);
}
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Currency = paymentMethodCurrency;
createPullPayment.Amount = overpaidAmount!.Value;
createPullPayment.AutoApproveClaims = true;
break;
@ -469,7 +477,7 @@ namespace BTCPayServer.Controllers
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodId.CryptoCode, model.CustomCurrency), rules,
new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules,
cancellationToken);
//TODO: What if fetching rate failed?
@ -482,7 +490,7 @@ namespace BTCPayServer.Controllers
createPullPayment.Currency = model.CustomCurrency;
createPullPayment.Amount = model.CustomAmount;
createPullPayment.AutoApproveClaims = authorizedForAutoApprove && paymentMethodId.CryptoCode == model.CustomCurrency;
createPullPayment.AutoApproveClaims = authorizedForAutoApprove && paymentMethodCurrency == model.CustomCurrency;
break;
default:
@ -499,7 +507,7 @@ namespace BTCPayServer.Controllers
if (model.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (model.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, ppDivisibility);
}
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
@ -531,30 +539,33 @@ namespace BTCPayServer.Controllers
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false),
CryptoPayments = invoice.GetPaymentMethods().Select(
CryptoPayments = invoice.GetPaymentPrompts().Select(
data =>
{
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var hasPayment = accounting.CryptoPaid > 0;
var paymentMethodId = data.PaymentMethodId;
var hasPayment = accounting.PaymentMethodPaid > 0;
var overpaidAmount = accounting.OverpaidHelper;
var rate = ExchangeRate(data.GetId().CryptoCode, data);
if (rate is not null) hasRates = true;
if (hasPayment && overpaidAmount > 0) overpaid = true;
if (hasPayment && accounting.Due > 0) stillDue = true;
var rate = ExchangeRate(data.Currency, data);
if (rate is not null)
hasRates = true;
if (hasPayment && overpaidAmount > 0)
overpaid = true;
if (hasPayment && accounting.Due > 0)
stillDue = true;
return new InvoiceDetailsModel.CryptoPayment
{
Rate = rate,
PaymentMethodRaw = data,
PaymentMethodId = paymentMethodId,
PaymentMethod = paymentMethodId.ToPrettyString(),
TotalDue = _displayFormatter.Currency(accounting.TotalDue, paymentMethodId.CryptoCode),
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode) : null,
Paid = hasPayment ? _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode) : null,
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode) : null,
Address = data.GetPaymentMethodDetails().GetPaymentDestination()
PaymentMethod = paymentMethodId.ToString(),
TotalDue = _displayFormatter.Currency(accounting.TotalDue, data.Currency),
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, data.Currency) : null,
Paid = hasPayment ? _displayFormatter.Currency(accounting.PaymentMethodPaid, data.Currency) : null,
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, data.Currency) : null,
Address = data.Destination
};
}).ToList(),
Overpaid = overpaid,
@ -620,11 +631,12 @@ namespace BTCPayServer.Controllers
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
return Forbid();
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode))?.AccountDerivation;
if (derivationScheme is null)
return NotSupported("This feature is only available to BTC wallets");
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var bumpableAddresses = (await GetAddresses(selectedItems))
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
.Where(p => p.GetPaymentMethodId() == btc)
.Select(p => p.GetAddress()).ToHashSet();
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
@ -706,12 +718,13 @@ namespace BTCPayServer.Controllers
bool isDefaultPaymentId = false;
var storeBlob = store.GetStoreBlob();
var btcId = PaymentMethodId.Parse("BTC");
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
var lnurlId = PaymentMethodId.Parse("BTC_LNURLPAY");
var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToList();
var displayedPaymentMethods = invoice.GetPaymentMethods().Select(p => p.GetId()).ToList();
var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId("BTC");
var lnId = PaymentTypes.LN.GetPaymentMethodId("BTC");
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob is { OnChainWithLnInvoiceFallback: true } &&
@ -729,11 +742,12 @@ namespace BTCPayServer.Controllers
if (displayedPaymentMethods.Contains(lnId) && displayedPaymentMethods.Contains(lnurlId))
displayedPaymentMethods.Remove(lnurlId);
if (paymentMethodId is not null && !displayedPaymentMethods.Contains(paymentMethodId))
paymentMethodId = null;
if (paymentMethodId is null)
{
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
PaymentMethodId? invoicePaymentId = invoice.DefaultPaymentMethod;
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
if (invoicePaymentId is not null)
{
@ -755,56 +769,55 @@ namespace BTCPayServer.Controllers
}
if (paymentMethodId is null)
{
paymentMethodId = displayedPaymentMethods.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
displayedPaymentMethods.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType != PaymentTypes.LNURLPay) ??
var defaultBTC = PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
var defaultLNURLPay = PaymentTypes.LNURL.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode);
paymentMethodId = displayedPaymentMethods.FirstOrDefault(e => e == defaultBTC) ??
displayedPaymentMethods.FirstOrDefault(e => e == defaultLNURLPay) ??
displayedPaymentMethods.FirstOrDefault();
}
isDefaultPaymentId = true;
}
if (paymentMethodId is null)
return null;
BTCPayNetworkBase network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network is null || !invoice.Support(paymentMethodId))
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice
.GetPaymentMethods()
.Where(p => displayedPaymentMethods.Contains(p.GetId()))
.GetPaymentPrompts()
.Where(p => displayedPaymentMethods.Contains(p.PaymentMethodId))
.FirstOrDefault();
if (paymentMethodTemp is null)
return null;
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodId = paymentMethodTemp.PaymentMethodId;
}
if (!_handlers.TryGetValue(paymentMethodId, out var handler))
return null;
// We activate the default payment method, and also those which aren't displayed (as they can't be set as default)
bool activated = false;
foreach (var pm in invoice.GetPaymentMethods())
PaymentPrompt? prompt = null;
foreach (var pm in invoice.GetPaymentPrompts())
{
var pmi = pm.GetId();
var pmi = pm.PaymentMethodId;
if (pmi == paymentMethodId)
prompt = pm;
if (pmi != paymentMethodId || !displayedPaymentMethods.Contains(pmi))
continue;
var pmd = pm.GetPaymentMethodDetails();
if (!pmd.Activated)
if (!pm.Activated)
{
if (await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, invoice, store))
if (await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, pmi))
{
activated = true;
}
}
}
if (prompt is null)
return null;
if (activated)
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO();
var accounting = paymentMethod.Calculate();
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var accounting = prompt.Calculate();
switch (lang?.ToLowerInvariant())
{
@ -841,10 +854,21 @@ namespace BTCPayServer.Controllers
.Replace("{InvoiceId}", Uri.EscapeDataString(invoice.Id))
: null;
string GetPaymentMethodName(PaymentMethodId paymentMethodId)
{
_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension);
return extension?.DisplayName ?? paymentMethodId.ToString();
}
string GetPaymentMethodImage(PaymentMethodId paymentMethodId)
{
_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension);
return extension?.Image ?? "";
}
var model = new PaymentModel
{
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
Activated = prompt.Activated,
PaymentMethodName = GetPaymentMethodName(paymentMethodId),
CryptoCode = prompt.Currency,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
OrderId = orderId,
InvoiceId = invoiceId,
@ -858,21 +882,21 @@ namespace BTCPayServer.Controllers
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CelebratePayment = storeBlob.CelebratePayment,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(paymentMethodId)),
BtcAddress = prompt.Destination,
BtcDue = accounting.ShowMoney(accounting.Due),
BtcPaid = accounting.ShowMoney(accounting.Paid),
InvoiceCurrency = invoice.Currency,
OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.NetworkFee),
OrderAmount = accounting.ShowMoney(accounting.TotalDue - accounting.PaymentMethodFee),
IsUnsetTopUp = invoice.IsUnsetTopUp(),
CustomerEmail = invoice.RefundMail,
CustomerEmail = invoice.Metadata.BuyerEmail,
RequiresRefundEmail = invoice.RequiresRefundEmail ?? storeBlob.RequiresRefundEmail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalSeconds,
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.Metadata.ItemDesc,
Rate = ExchangeRate(network.CryptoCode, paymentMethod, DisplayFormatter.CurrencyFormat.Symbol),
Rate = ExchangeRate(prompt.Currency, prompt, DisplayFormatter.CurrencyFormat.Symbol),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
@ -894,48 +918,54 @@ namespace BTCPayServer.Controllers
SpeedPolicy.LowSpeed => 6,
_ => null
},
ReceivedConfirmations = invoice.GetAllBitcoinPaymentData(false).FirstOrDefault()?.ConfirmationCount,
ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice.GetAllBitcoinPaymentData(bh, false).FirstOrDefault()?.ConfirmationCount : null,
#pragma warning disable CS0618 // Type or member is obsolete
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,
NetworkFee = prompt.PaymentMethodFee,
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.PaymentMethodId).Concat(new[] { prompt.PaymentMethodId }).Distinct().Count() > 1,
StoreId = store.Id,
AvailableCryptos = invoice.GetPaymentMethods()
AvailableCryptos = invoice.GetPaymentPrompts()
.Select(kv =>
{
var availableCryptoPaymentMethodId = kv.GetId();
var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId];
var pmName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId);
var handler = _handlers[kv.PaymentMethodId];
var pmName = GetPaymentMethodName(kv.PaymentMethodId);
return new PaymentModel.AvailableCrypto
{
Displayed = displayedPaymentMethods.Contains(kv.GetId()),
PaymentMethodId = kv.GetId().ToString(),
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
Displayed = displayedPaymentMethods.Contains(kv.PaymentMethodId),
PaymentMethodId = kv.PaymentMethodId.ToString(),
CryptoCode = kv.Currency,
PaymentMethodName = isAltcoinsBuild
? pmName
: pmName.Replace("Bitcoin (", "").Replace(")", "").Replace("Lightning ", ""),
IsLightning =
kv.GetId().PaymentType == PaymentTypes.LightningLike,
CryptoImage = Request.GetRelativePathOrAbsolute(availableCryptoHandler.GetCryptoImage(availableCryptoPaymentMethodId)),
IsLightning = handler is ILightningPaymentHandler,
CryptoImage = Request.GetRelativePathOrAbsolute(GetPaymentMethodImage(kv.PaymentMethodId)),
Link = Url.Action(nameof(Checkout),
new
{
invoiceId,
paymentMethodId = kv.GetId().ToString()
paymentMethodId = kv.PaymentMethodId.ToString()
})
};
}).Where(c => c.CryptoImage != "/")
.OrderByDescending(a => a.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode).ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
.ToList()
};
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var extension))
extension.ModifyPaymentModel(new PaymentModelContext(model, store, storeBlob, invoice, Url, prompt, handler));
model.UISettings = _viewProvider.TryGetViewViewModel(prompt, "CheckoutUI")?.View as CheckoutUIPaymentMethodSettings;
model.PaymentMethodId = paymentMethodId.ToString();
model.PaymentType = paymentMethodId.PaymentType.ToString();
model.OrderAmountFiat = OrderAmountFromInvoice(model.CryptoCode, invoice, DisplayFormatter.CurrencyFormat.Symbol);
foreach (var paymentPrompt in invoice.GetPaymentPrompts())
{
var vvm = _viewProvider.TryGetViewViewModel(paymentPrompt, "CheckoutUI");
if (vvm?.View is CheckoutUIPaymentMethodSettings { ExtensionPartial: { } partial })
{
model.ExtensionPartials.Add(partial);
}
}
if (storeBlob.PlaySoundOnPayment)
{
model.PaymentSoundUrl = string.IsNullOrEmpty(storeBlob.SoundFileId)
@ -944,7 +974,7 @@ namespace BTCPayServer.Controllers
model.ErrorSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout-v2/error.mp3");
model.NfcReadSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout-v2/nfcread.mp3");
}
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint();
return model;
@ -962,7 +992,7 @@ namespace BTCPayServer.Controllers
return _displayFormatter.Currency(invoiceEntity.Price, currency, format);
}
private string? ExchangeRate(string cryptoCode, PaymentMethod paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
private string? ExchangeRate(string cryptoCode, PaymentPrompt paymentMethod, DisplayFormatter.CurrencyFormat format = DisplayFormatter.CurrencyFormat.Code)
{
var currency = paymentMethod.ParentEntity.Currency;
var crypto = cryptoCode.ToUpperInvariant(); // uppercase to make comparison easier, might be "sats"
@ -1081,7 +1111,7 @@ namespace BTCPayServer.Controllers
invoiceQuery.Take = model.Count;
invoiceQuery.Skip = model.Skip;
invoiceQuery.IncludeRefunds = true;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
// Apps
@ -1157,12 +1187,12 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
return NotFound();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
if (!store.AnyPaymentMethodAvailable())
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
var vm = new CreateInvoiceModel
{
@ -1182,11 +1212,11 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
var store = HttpContext.GetStoreData();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
if (!store.AnyPaymentMethodAvailable())
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
model.CheckoutType = storeBlob.CheckoutType;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList(store);
@ -1203,7 +1233,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
}
}
if (!ModelState.IsValid)
{
return View(model);
@ -1231,7 +1261,7 @@ namespace BTCPayServer.Controllers
Amount = model.Amount,
Currency = model.Currency,
Metadata = metadata.ToJObject(),
Checkout = new ()
Checkout = new()
{
RedirectURL = store.StoreWebsite,
DefaultPaymentMethod = model.DefaultPaymentMethod,
@ -1338,7 +1368,7 @@ namespace BTCPayServer.Controllers
? ParsePosData(items[i])
: items[i].ToString());
}
result.TryAdd(item.Key, arrayResult);
break;
@ -1357,9 +1387,9 @@ namespace BTCPayServer.Controllers
private SelectList GetPaymentMethodsSelectList(StoreData store)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
return new SelectList(store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(method => new SelectListItem(method.PaymentId.ToPrettyString(), method.PaymentId.ToString())),
return new SelectList(store.GetPaymentMethodConfigs()
.Where(s => !excludeFilter.Match(s.Key))
.Select(method => new SelectListItem(method.Key.ToString(), method.Key.ToString())),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}

View file

@ -32,6 +32,8 @@ using Microsoft.AspNetCore.Routing;
using NBitcoin;
using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
using Serilog.Filters;
using PeterO.Numbers;
namespace BTCPayServer.Controllers
{
@ -47,7 +49,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter;
readonly EventAggregator _EventAggregator;
readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService;
private readonly LanguageService _languageService;
@ -57,6 +59,8 @@ namespace BTCPayServer.Controllers
private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly PaymentMethodViewProvider _viewProvider;
private readonly AppService _appService;
private readonly IFileService _fileService;
@ -85,7 +89,9 @@ namespace BTCPayServer.Controllers
AppService appService,
IFileService fileService,
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders)
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
PaymentMethodViewProvider viewProvider)
{
_displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
@ -96,7 +102,7 @@ namespace BTCPayServer.Controllers
_UserManager = userManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_handlers = paymentMethodHandlerDictionary;
_dbContextFactory = dbContextFactory;
_paymentHostedService = paymentHostedService;
WebhookNotificationManager = webhookNotificationManager;
@ -107,6 +113,8 @@ namespace BTCPayServer.Controllers
_linkGenerator = linkGenerator;
_authorizationService = authorizationService;
_transactionLinkProviders = transactionLinkProviders;
_paymentModelExtensions = paymentModelExtensions;
_viewProvider = viewProvider;
_fileService = fileService;
_appService = appService;
}
@ -126,7 +134,7 @@ namespace BTCPayServer.Controllers
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id),
PaymentRequestId = id,
BuyerEmail = invoiceMetadata.TryGetValue("buyerEmail", out var formEmail) && formEmail.Type == JTokenType.String ? formEmail.Value<string>():
BuyerEmail = invoiceMetadata.TryGetValue("buyerEmail", out var formEmail) && formEmail.Type == JTokenType.String ? formEmail.Value<string>() :
string.IsNullOrEmpty(prBlob.Email) ? null : prBlob.Email
}.ToJObject(), new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore });
@ -169,7 +177,10 @@ namespace BTCPayServer.Controllers
}
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod ?? store.GetDefaultPaymentId()?.ToStringNormalized() ?? new PaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode, PaymentTypes.BTCLike).ToStringNormalized();
if (invoice.Checkout.DefaultPaymentMethod is not null && PaymentMethodId.TryParse(invoice.Checkout.DefaultPaymentMethod, out var paymentMethodId))
{
entity.DefaultPaymentMethod = paymentMethodId;
}
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
entity.CheckoutType = invoice.Checkout.CheckoutType;
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
@ -217,77 +228,29 @@ namespace BTCPayServer.Controllers
}
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
if (entity.Metadata.BuyerEmail != null)
{
if (!MailboxAddressValidator.IsMailboxAddress(entity.Metadata.BuyerEmail))
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.Metadata.BuyerEmail;
}
entity.Status = InvoiceStatusLegacy.New;
entity.UpdateTotals();
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoicePaymentMethodFilter != null)
var creationContext = new InvoiceCreationContext(store, storeBlob, entity, logs, _handlers, invoicePaymentMethodFilter);
creationContext.SetLazyActivation(entity.LazyPaymentMethods);
foreach (var term in additionalSearchTerms ?? Array.Empty<string>())
creationContext.AdditionalSearchTerms.Add(term);
if (entity.Type == InvoiceType.TopUp || entity.Price != 0m)
{
excludeFilter = PaymentFilter.Or(excludeFilter,
invoicePaymentMethodFilter);
}
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency));
foreach (var paymentMethodCriteria in storeBlob.PaymentMethodCriteria)
{
if (paymentMethodCriteria.Value != null)
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency));
}
}
}
await creationContext.BeforeFetchingRates();
await FetchRates(creationContext, cancellationToken);
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
bool noNeedForMethods = entity.Type != InvoiceType.TopUp && entity.Price == 0m;
if (!noNeedForMethods)
{
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
var x1 = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) &&
_paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null).ToList();
var pmis = x1.Select(tuple => tuple.SupportedPaymentMethod.PaymentId).ToHashSet();
foreach (var o in x1
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler,
o.SupportedPaymentMethod, o.Network, entity, store, logs, pmis)))
.ToList())
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
if (supported.Count == 0)
await creationContext.CreatePaymentPrompts();
var contexts = creationContext.PaymentMethodContexts
.Where(s => s.Value.Status is PaymentMethodContext.ContextStatus.WaitingForActivation or PaymentMethodContext.ContextStatus.Created)
.Select(s => s.Value)
.ToList();
if (contexts.Count == 0)
{
StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
if (!store.GetPaymentMethodConfigs(_handlers).Any())
errors.AppendLine(
"Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
else
@ -299,9 +262,13 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, errors.ToString());
}
entity.SetPaymentPrompts(new PaymentPromptDictionary(contexts.Select(c => c.Prompt)));
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
else
{
entity.SetPaymentPrompts(new PaymentPromptDictionary());
}
foreach (var app in await getAppsTaggingStore)
{
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
@ -313,151 +280,18 @@ namespace BTCPayServer.Controllers
}
using (logs.Measure("Saving invoice"))
{
await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms);
var links = new List<WalletObjectLinkData>();
foreach (var method in paymentMethods)
{
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
{
var walletId = new WalletId(store.Id, method.GetId().CryptoCode);
await _walletRepository.EnsureWalletObject(new WalletObjectId(
walletId,
WalletObjectData.Types.Invoice,
entity.Id
));
if (bp.GetDepositAddress(((BTCPayNetwork)method.Network).NBitcoinNetwork) is BitcoinAddress address)
{
links.Add(WalletRepository.NewWalletObjectLinkData(new WalletObjectId(
walletId,
WalletObjectData.Types.Address,
address.ToString()),
new WalletObjectId(
walletId,
WalletObjectData.Types.Invoice,
entity.Id)));
}
}
}
await _walletRepository.EnsureCreated(null,links);
await _InvoiceRepository.CreateInvoiceAsync(creationContext);
await creationContext.ActivatingPaymentPrompt();
}
_ = Task.Run(async () =>
{
try
{
await fetchingAll;
}
catch (AggregateException ex)
{
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}", InvoiceEventData.EventSeverity.Error); return true; });
}
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
});
_ = _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created));
return entity;
}
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
private async Task FetchRates(InvoiceCreationContext context, CancellationToken cancellationToken)
{
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
{
var rateResult = await pair.Value;
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}", InvoiceEventData.EventSeverity.Info);
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}", InvoiceEventData.EventSeverity.Info);
if (rateResult.Errors.Count != 0)
{
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})", InvoiceEventData.EventSeverity.Error);
}
foreach (var ex in rateResult.ExchangeExceptions)
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})", InvoiceEventData.EventSeverity.Error);
}
}).ToArray());
}
private async Task<PaymentMethod?> CreatePaymentMethodAsync(
Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair,
IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network,
InvoiceEntity entity,
StoreData store, InvoiceLogs logs,
HashSet<PaymentMethodId> invoicePaymentMethods)
{
try
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
var storeBlob = store.GetStoreBlob();
// Checkout v2 does not show a payment method switch for Bitcoin-only + BIP21, so exclude that case
var preparePayment = entity.LazyPaymentMethods && !storeBlob.OnChainWithLnInvoiceFallback
? null
: handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
if (rate.BidAsk == null)
{
return null;
}
var paymentMethod = new PaymentMethod
{
ParentEntity = entity,
Network = network,
Rate = rate.BidAsk.Bid,
PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase)
};
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
var paymentDetails = await handler.CreatePaymentMethodDetails(logs, supportedPaymentMethod, paymentMethod, store, network, preparePayment, invoicePaymentMethods);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}
var criteria = storeBlob.PaymentMethodCriteria?.Find(methodCriteria => methodCriteria.PaymentMethod == supportedPaymentMethod.PaymentId);
if (criteria?.Value != null && entity.Type != InvoiceType.TopUp)
{
var currentRateToCrypto =
await fetchingByCurrencyPair[new CurrencyPair(supportedPaymentMethod.PaymentId.CryptoCode, criteria.Value.Currency)];
if (currentRateToCrypto?.BidAsk != null)
{
var amount = paymentMethod.Calculate().Due;
var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
if (amount < limitValueCrypto && criteria.Above)
{
logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error);
return null;
}
if (amount > limitValueCrypto && !criteria.Above)
{
logs.Write($"{logPrefix} invoice amount above accepted value for payment method", InvoiceEventData.EventSeverity.Error);
return null;
}
}
else
{
var suffix = currentRateToCrypto?.EvaluatedRule is string s ? $" ({s})" : string.Empty;
logs.Write($"{logPrefix} This payment method should be created only if the amount of this invoice is in proper range. However, we are unable to fetch the rate of those limits. {suffix}", InvoiceEventData.EventSeverity.Warning);
}
}
#pragma warning disable CS0618
if (paymentMethod.GetId().IsBTCOnChain)
{
entity.TxFee = paymentMethod.NextNetworkFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
}
catch (PaymentMethodUnavailableException ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error);
}
catch (Exception ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error);
}
return null;
var rateRules = context.StoreBlob.GetRateRules(_NetworkProvider);
await context.FetchingRates(_RateProvider, rateRules, cancellationToken);
}
}
}

View file

@ -34,6 +34,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
@ -47,8 +48,6 @@ namespace BTCPayServer
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
@ -59,11 +58,11 @@ namespace BTCPayServer
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IPluginHookService _pluginHookService;
private readonly InvoiceActivator _invoiceActivator;
private readonly PaymentMethodHandlerDictionary _handlers;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayNetworkProvider btcPayNetworkProvider,
LightningLikePaymentHandler lightningLikePaymentHandler,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository,
AppService appService,
UIInvoiceController invoiceController,
@ -77,8 +76,7 @@ namespace BTCPayServer
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningLikePaymentHandler = lightningLikePaymentHandler;
_handlers = handlers;
_storeRepository = storeRepository;
_appService = appService;
_invoiceController = invoiceController;
@ -98,13 +96,13 @@ namespace BTCPayServer
[NonAction]
internal async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, string k1, CancellationToken cancellationToken)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
if (network is null || !network.SupportLightning)
{
return NotFound();
}
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, true);
if (!pp.IsRunning() || !pp.IsSupported(pmi))
{
@ -149,9 +147,7 @@ namespace BTCPayServer
if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" });
var store = await _storeRepository.FindStore(pp.StoreId);
var pm = store!.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == pmi);
var pm = store!.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers);
if (pm is null)
{
return NotFound();
@ -169,12 +165,13 @@ namespace BTCPayServer
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" });
var lightningHandler = _handlers.GetLightningHandler(network);
switch (claimResponse.PayoutData.State)
{
case PayoutState.AwaitingPayment:
{
var client =
_lightningLikePaymentHandler.CreateLightningClient(pm, network);
lightningHandler.CreateLightningClient(pm);
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, pmi, cancellationToken);
@ -226,10 +223,18 @@ namespace BTCPayServer
return Ok(request);
}
private BTCPayNetwork GetNetwork(string cryptoCode)
{
if (!_handlers.TryGetValue(PaymentTypes.LNURL.GetPaymentMethodId(cryptoCode), out var o) ||
o is not LNURLPayPaymentHandler { Network: var network })
return null;
return network;
}
[HttpGet("pay/app/{appId}/{itemCode}")]
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
if (network is null || !network.SupportLightning)
{
return NotFound();
@ -514,7 +519,7 @@ namespace BTCPayServer
{
createInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions();
createInvoice.Checkout.LazyPaymentMethods = false;
createInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() };
createInvoice.Checkout.PaymentMethods = new[] { pmi.ToString() };
i = await _invoiceController.CreateInvoiceCoreRaw(createInvoice, store, Request.GetAbsoluteRoot(), additionalTags);
}
catch (Exception e)
@ -540,14 +545,17 @@ namespace BTCPayServer
lnurlRequest ??= new LNURLPayRequest();
lnUrlMetadata ??= new Dictionary<string, string>();
var pm = i.GetPaymentMethod(pmi);
var pm = i.GetPaymentPrompt(pmi);
if (pm is null)
return null;
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
var handler = ((LNURLPayPaymentHandler)_handlers[pmi]);
var paymentMethodDetails = handler.ParsePaymentPromptDetails(pm.Details);
bool updatePaymentMethodDetails = false;
List<string> searchTerms = new List<string>();
if (lnUrlMetadata?.TryGetValue("text/identifier", out var lnAddress) is true && lnAddress is not null)
{
paymentMethodDetails.ConsumedLightningAddress = lnAddress;
searchTerms.Add(lnAddress);
updatePaymentMethodDetails = true;
}
@ -561,7 +569,7 @@ namespace BTCPayServer
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
action: nameof(GetLNURLForInvoice),
controller: "UILNURL",
values: new { pmi.CryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase));
values: new { cryptoCode, invoiceId = i.Id }, Request.Scheme, Request.Host, Request.PathBase));
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
if (i.Type != InvoiceType.TopUp)
{
@ -580,8 +588,10 @@ namespace BTCPayServer
}
if (updatePaymentMethodDetails)
{
pm.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
pm.Details = JToken.FromObject(paymentMethodDetails, handler.Serializer);
await _invoiceRepository.UpdatePaymentDetails(i.Id, handler, paymentMethodDetails);
await _invoiceRepository.AddSearchTerms(i.Id, searchTerms);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(i.Id, paymentMethodDetails, pmi));
}
return lnurlRequest;
}
@ -605,18 +615,16 @@ namespace BTCPayServer
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
}
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaymentMethodConfig lnUrlSettings)
{
lnUrlSettings = null;
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
if (network is null || !network.SupportLightning)
return null;
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
var lnUrlMethod =
methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod;
var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi);
var pmi = PaymentTypes.LNURL.GetPaymentMethodId(cryptoCode);
var lnpmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var lnUrlMethod = store.GetPaymentMethodConfig<LNURLPaymentMethodConfig>(pmi, _handlers);
var lnMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(lnpmi, _handlers);
if (lnUrlMethod is null || lnMethod is null)
return null;
var blob = store.GetStoreBlob();
@ -632,7 +640,7 @@ namespace BTCPayServer
public async Task<IActionResult> GetLNURLForInvoice(string invoiceId, string cryptoCode,
[FromQuery] long? amount = null, string comment = null)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var network = GetNetwork(cryptoCode);
if (network is null || !network.SupportLightning)
{
return NotFound();
@ -651,25 +659,25 @@ namespace BTCPayServer
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnurlSupportedPaymentMethod);
if (pmi is null)
return NotFound();
var lightningPaymentMethod = i.GetPaymentMethod(pmi);
var paymentMethodDetails =
lightningPaymentMethod?.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
if (paymentMethodDetails is not null && !paymentMethodDetails.Activated)
var handler = ((LNURLPayPaymentHandler)_handlers[pmi]);
var lightningPaymentMethod = i.GetPaymentPrompt(pmi);
var promptDetails = handler.ParsePaymentPromptDetails(lightningPaymentMethod.Details);
if (promptDetails is null)
{
if (!await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, i, store))
if (!await _invoiceActivator.ActivateInvoicePaymentMethod(i.Id, pmi))
return NotFound();
i = await _invoiceRepository.GetInvoice(invoiceId, true);
lightningPaymentMethod = i.GetPaymentMethod(pmi);
paymentMethodDetails = lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
lightningPaymentMethod = i.GetPaymentPrompt(pmi);
promptDetails = handler.ParsePaymentPromptDetails(lightningPaymentMethod.Details);
}
if (paymentMethodDetails?.LightningSupportedPaymentMethod is null)
var lnConfig = _handlers.GetLightningConfig(store, network);
if (lnConfig is null)
return NotFound();
LNURLPayRequest lnurlPayRequest = paymentMethodDetails.PayRequest;
LNURLPayRequest lnurlPayRequest = promptDetails.PayRequest;
var blob = store.GetStoreBlob();
if (paymentMethodDetails.PayRequest is null)
if (promptDetails.PayRequest is null)
{
lnurlPayRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, allowOverpay: false);
if (lnurlPayRequest is null)
@ -705,23 +713,20 @@ namespace BTCPayServer
if (lnurlSupportedPaymentMethod.LUD12Enabled)
{
comment = comment?.Truncate(2000);
if (paymentMethodDetails.ProvidedComment != comment)
if (promptDetails.ProvidedComment != comment)
{
paymentMethodDetails.ProvidedComment = comment;
promptDetails.ProvidedComment = comment;
updatePaymentMethod = true;
}
}
if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt)
if (string.IsNullOrEmpty(lightningPaymentMethod.Destination) || promptDetails.GeneratedBoltAmount != amt)
{
var client =
_lightningLikePaymentHandler.CreateLightningClient(
paymentMethodDetails.LightningSupportedPaymentMethod, network);
if (!string.IsNullOrEmpty(paymentMethodDetails.BOLT11))
var client = _handlers.GetLightningHandler(network).CreateLightningClient(lnConfig);
if (!string.IsNullOrEmpty(lightningPaymentMethod.Destination))
{
try
{
await client.CancelInvoice(paymentMethodDetails.InvoiceId);
await client.CancelInvoice(promptDetails.InvoiceId);
}
catch (Exception)
{
@ -764,26 +769,26 @@ namespace BTCPayServer
});
}
paymentMethodDetails.BOLT11 = invoice.BOLT11;
paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash);
paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage);
paymentMethodDetails.InvoiceId = invoice.Id;
paymentMethodDetails.GeneratedBoltAmount = amt;
lightningPaymentMethod.Destination = invoice.BOLT11;
promptDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash);
promptDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage);
promptDetails.InvoiceId = invoice.Id;
promptDetails.GeneratedBoltAmount = amt;
lightningPaymentMethod.Details = JToken.FromObject(promptDetails, handler.Serializer);
updatePaymentMethod = true;
}
if (updatePaymentMethod)
{
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, paymentMethodDetails, pmi));
await _invoiceRepository.UpdatePrompt(invoiceId, lightningPaymentMethod);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, promptDetails, pmi));
}
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true,
Routes = Array.Empty<string>(),
Pr = paymentMethodDetails.BOLT11,
Pr = lightningPaymentMethod.Destination,
SuccessAction = successAction
});
}
@ -800,8 +805,8 @@ namespace BTCPayServer
[HttpGet("~/stores/{storeId}/plugins/lightning-address")]
public async Task<IActionResult> EditLightningAddress(string storeId)
{
if (ControllerContext.HttpContext.GetStoreData().GetEnabledPaymentIds(_btcPayNetworkProvider)
.All(id => id.PaymentType != LNURLPayPaymentType.Instance))
if (ControllerContext.HttpContext.GetStoreData().GetEnabledPaymentIds()
.All(id => _handlers.TryGet(id) is not LNURLPayPaymentHandler))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{

View file

@ -121,7 +121,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
if (!store.AnyPaymentMethodAvailable())
{
return NoPaymentMethodResult(storeId);
}
@ -156,7 +156,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
if (!store.AnyPaymentMethodAvailable())
{
return NoPaymentMethodResult(store.Id);
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
@ -8,6 +9,7 @@ using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -19,13 +21,19 @@ namespace BTCPayServer.Controllers
public class UIPublicLightningNodeInfoController : Controller
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
private readonly StoreRepository _StoreRepository;
public UIPublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
PaymentMethodHandlerDictionary handlers,
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
{
_BtcPayNetworkProvider = btcPayNetworkProvider;
_paymentModelExtensions = paymentModelExtensions;
_handlers = handlers;
_LightningLikePaymentHandler = lightningLikePaymentHandler;
_StoreRepository = storeRepository;
}
@ -47,13 +55,13 @@ namespace BTCPayServer.Controllers
};
try
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers);
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var nodeInfo = await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network,
new InvoiceLogs(), throws: true);
var nodeInfo = await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, null, throws: true);
vm.Available = true;
vm.CryptoImage = GetImage(paymentMethodDetails.PaymentId, network);
vm.CryptoImage = GetImage(pmi);
vm.NodeInfo = nodeInfo.Select(n => new ShowLightningNodeInfoViewModel.NodeData(n)).ToArray();
}
catch (Exception)
@ -64,21 +72,13 @@ namespace BTCPayServer.Controllers
return View(vm);
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
private string GetImage(PaymentMethodId paymentMethodId)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
if (_paymentModelExtensions.TryGetValue(paymentMethodId, out var paymentModelExtension))
{
return "/" + Url.Content(paymentModelExtension.Image);
}
return null;
}
}

View file

@ -72,7 +72,7 @@ public partial class UIReportsController : Controller
var vm = new StoreReportsViewModel
{
InvoiceTemplateUrl = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }),
ExplorerTemplateUrls = TransactionLinkProviders.ToDictionary(p => p.Key.CryptoCode, p => p.Value.BlockExplorerLink?.Replace("{0}", "TX_ID")),
ExplorerTemplateUrls = TransactionLinkProviders.ToDictionary(p => p.Key, p => p.Value.BlockExplorerLink?.Replace("{0}", "TX_ID")),
Request = new StoreReportRequest { ViewName = viewName ?? "Payments" },
AvailableViews = ReportService.ReportProviders
.Values

View file

@ -348,7 +348,7 @@ namespace BTCPayServer.Controllers
return View(settings);
}
settings.BlockExplorerLinks = settings.BlockExplorerLinks
.Where(tuple => _transactionLinkProviders.GetDefaultBlockExplorerLink(PaymentMethodId.Parse(tuple.CryptoCode)) != tuple.Link)
.Where(tuple => _transactionLinkProviders.GetDefaultBlockExplorerLink(tuple.CryptoCode) != tuple.Link)
.Where(tuple => tuple.Link is not null)
.ToList();

View file

@ -93,7 +93,7 @@ namespace BTCPayServer.Controllers
CustomCSSLink = "",
EmbeddedCSS = "",
PaymentMethodItems =
paymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true))
paymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString(), true))
});
}
@ -106,7 +106,7 @@ namespace BTCPayServer.Controllers
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
model.PaymentMethodItems =
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), true));
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), true));
model.Name ??= string.Empty;
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
model.PaymentMethods ??= new List<string>();
@ -115,7 +115,7 @@ namespace BTCPayServer.Controllers
// Since we assign all payment methods to be selected by default above we need to update
// them here to reflect user's selection so that they can correct their mistake
model.PaymentMethodItems =
paymentMethodOptions.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString(), false));
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), false));
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
}
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
@ -386,7 +386,7 @@ namespace BTCPayServer.Controllers
case "pay":
{
if (handler is { })
return await handler?.InitiatePayment(paymentMethodId, payoutIds);
return await handler.InitiatePayment(paymentMethodId, payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Paying via this payment method is not supported",

View file

@ -14,9 +14,12 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -110,6 +113,9 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var network = _ExplorerProvider.GetNetwork(vm.CryptoCode);
var oldConf = _handlers.GetLightningConfig(store, network);
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
if (vm.CryptoCode == null)
@ -118,21 +124,13 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var network = _ExplorerProvider.GetNetwork(vm.CryptoCode);
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
LightningSupportedPaymentMethod? paymentMethod = null;
LightningPaymentMethodConfig? paymentMethod = null;
if (vm.LightningNodeType == LightningNodeType.Internal)
{
if (!CanUseInternalLightning(network.CryptoCode))
{
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 = new LightningPaymentMethodConfig();
paymentMethod.SetInternalNode();
}
else
@ -142,47 +140,26 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
return View(vm);
}
ILightningClient? lightningClient = null;
try
{
lightningClient = _lightningClientFactoryService.Create(vm.ConnectionString, network);
}
catch (Exception e)
{
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({e.Message})");
return View(vm);
}
if (!User.IsInRole(Roles.ServerAdmin) && !lightningClient.IsSafe())
{
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 '.'.");
return View(vm);
}
paymentMethod = new LightningSupportedPaymentMethod
{
CryptoCode = paymentMethodId.CryptoCode
};
try
{
paymentMethod.SetLightningUrl(lightningClient);
}
catch (Exception ex)
{
ModelState.AddModelError(nameof(vm.ConnectionString), ex.Message);
return View(vm);
}
paymentMethod = new LightningPaymentMethodConfig();
paymentMethod.ConnectionString = vm.ConnectionString;
}
var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId];
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState,
JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer));
await handler.ValidatePaymentMethodConfig(ctx);
if (ctx.MissingPermission is not null)
ModelState.AddModelError(nameof(vm.ConnectionString), "You do not have the permissions to change this settings");
if (!ModelState.IsValid)
return View(vm);
switch (command)
{
case "save":
var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
store.SetSupportedPaymentMethod(lnurl, new LNURLPaySupportedPaymentMethod()
var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod);
store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig()
{
CryptoCode = vm.CryptoCode,
UseBech32Scheme = true,
LUD12Enabled = false
});
@ -192,10 +169,9 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
case "test":
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try
{
var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion(), true);
var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true);
var hasPublicAddress = info.Any();
if (!vm.SkipPortTest && hasPublicAddress)
{
@ -228,7 +204,8 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
if (lightning == null)
{
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
@ -240,7 +217,7 @@ namespace BTCPayServer.Controllers
{
CryptoCode = cryptoCode,
StoreId = storeId,
Enabled = !excludeFilters.Match(lightning.PaymentId),
Enabled = !excludeFilters.Match(lnId),
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
@ -248,10 +225,11 @@ namespace BTCPayServer.Controllers
};
SetExistingValues(store, vm);
var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store);
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
var lnurl = GetConfig<LNURLPaymentMethodConfig>(lnurlId, store);
if (lnurl != null)
{
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurl.PaymentId);
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId);
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
vm.LUD12Enabled = lnurl.LUD12Enabled;
}
@ -280,10 +258,10 @@ namespace BTCPayServer.Controllers
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
var lnurlId = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
blob.SetExcluded(lnurlId, !vm.LNURLEnabled);
var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store);
var lnurl = GetConfig<LNURLPaymentMethodConfig>(PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode), store);
if (lnurl is null || (
lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
lnurl.LUD12Enabled != vm.LUD12Enabled))
@ -291,9 +269,8 @@ namespace BTCPayServer.Controllers
needUpdate = true;
}
store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod
store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig
{
CryptoCode = vm.CryptoCode,
UseBech32Scheme = vm.LNURLBech32Mode,
LUD12Enabled = vm.LUD12Enabled
});
@ -325,16 +302,16 @@ namespace BTCPayServer.Controllers
return NotFound();
var network = _ExplorerProvider.GetNetwork(cryptoCode);
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store);
if (lightning == null)
return NotFound();
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !enabled);
if (!enabled)
{
storeBlob.SetExcluded(new PaymentMethodId(network.CryptoCode, PaymentTypes.LNURLPay), true);
storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true);
}
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
@ -351,7 +328,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store);
if (lightning != null)
{
@ -364,22 +341,9 @@ namespace BTCPayServer.Controllers
}
}
private LightningSupportedPaymentMethod? GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
private T? GetConfig<T>(PaymentMethodId paymentMethodId, StoreData store) where T: class
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LNURLPaySupportedPaymentMethod? GetExistingLNURLSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LNURLPaySupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
}
}
}

View file

@ -12,7 +12,10 @@ using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
@ -20,6 +23,7 @@ using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -83,7 +87,8 @@ namespace BTCPayServer.Controllers
vm.Network = network;
DerivationSchemeSettings strategy = null;
PaymentMethodId paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
BitcoinLikePaymentHandler handler = (BitcoinLikePaymentHandler)_handlers[paymentMethodId];
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -145,7 +150,11 @@ namespace BTCPayServer.Controllers
}
else if (!string.IsNullOrEmpty(vm.Config))
{
if (!DerivationSchemeSettings.TryParseFromJson(UnprotectString(vm.Config), network, out strategy))
try
{
strategy = handler.ParsePaymentMethodConfig(JToken.Parse(UnprotectString(vm.Config)));
}
catch
{
ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format");
return View(vm.ViewName, vm);
@ -158,17 +167,16 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm);
}
vm.Config = ProtectString(strategy.ToJson());
vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString());
ModelState.Remove(nameof(vm.Config));
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var storeBlob = store.GetStoreBlob();
if (vm.Confirmation)
{
try
{
await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
store.SetPaymentMethodConfig(_handlers[paymentMethodId], strategy);
storeBlob.SetExcluded(paymentMethodId, false);
storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false);
store.SetStoreBlob(storeBlob);
@ -186,7 +194,7 @@ namespace BTCPayServer.Controllers
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(WalletSettings), new { storeId = vm.StoreId, cryptoCode = vm.CryptoCode });
}
return ConfirmAddresses(vm, strategy);
return ConfirmAddresses(vm, strategy, network.NBXplorerNetwork);
}
private string ProtectString(string str)
@ -256,7 +264,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
var handler = _handlers.GetBitcoinHandler(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var isImport = method == WalletSetupMethod.Seed;
var vm = new WalletSetupViewModel
@ -322,7 +330,7 @@ namespace BTCPayServer.Controllers
vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString();
vm.AccountKey = response.AccountHDKey.Neuter().ToWif();
vm.KeyPath = response.AccountKeyPath.KeyPath.ToString();
vm.Config = ProtectString(derivationSchemeSettings.ToJson());
vm.Config = ProtectString(JToken.FromObject(derivationSchemeSettings, handler.Serializer).ToString());
var result = await UpdateWallet(vm);
@ -398,12 +406,13 @@ namespace BTCPayServer.Controllers
(bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet();
var client = _ExplorerProvider.GetExplorerClient(network);
var handler = _handlers.GetBitcoinHandler(cryptoCode);
var vm = new WalletSettingsViewModel
{
StoreId = storeId,
CryptoCode = cryptoCode,
WalletId = new WalletId(storeId, cryptoCode),
Enabled = !excludeFilters.Match(derivation.PaymentId),
Enabled = !excludeFilters.Match(handler.PaymentMethodId),
Network = network,
IsHotWallet = derivation.IsHotWallet,
Source = derivation.Source,
@ -411,7 +420,7 @@ namespace BTCPayServer.Controllers
DerivationScheme = derivation.AccountDerivation.ToString(),
DerivationSchemeInput = derivation.AccountOriginal,
KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(),
UriScheme = derivation.Network.NBitcoinNetwork.UriScheme,
UriScheme = network.NBitcoinNetwork.UriScheme,
Label = derivation.Label,
SelectedSigningKey = derivation.SigningKey.ToString(),
NBXSeedAvailable = derivation.IsHotWallet &&
@ -425,7 +434,7 @@ namespace BTCPayServer.Controllers
MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null,
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
}).ToList(),
Config = ProtectString(derivation.ToJson()),
Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
PayJoinEnabled = storeBlob.PayJoinEnabled,
MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes,
SpeedPolicy = store.SpeedPolicy,
@ -433,10 +442,7 @@ namespace BTCPayServer.Controllers
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
CanUseHotWallet = canUseHotWallet,
CanUseRPCImport = rpcImport,
CanUsePayJoin = canUseHotWallet && store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet),
CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
StoreName = store.StoreName,
};
@ -451,7 +457,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> UpdateWalletSettings(WalletSettingsViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
@ -462,17 +468,17 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
var handler = _handlers.GetBitcoinHandler(vm.CryptoCode);
var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var currentlyEnabled = !excludeFilters.Match(derivation.PaymentId);
var currentlyEnabled = !excludeFilters.Match(handler.PaymentMethodId);
bool enabledChanged = currentlyEnabled != vm.Enabled;
bool needUpdate = enabledChanged;
string errorMessage = null;
if (enabledChanged)
{
storeBlob.SetExcluded(derivation.PaymentId, !vm.Enabled);
storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled);
store.SetStoreBlob(storeBlob);
}
@ -484,7 +490,7 @@ namespace BTCPayServer.Controllers
var signingKey = string.IsNullOrEmpty(vm.SelectedSigningKey)
? null
: new BitcoinExtPubKey(vm.SelectedSigningKey, derivation.Network.NBitcoinNetwork);
: new BitcoinExtPubKey(vm.SelectedSigningKey, network.NBitcoinNetwork);
if (derivation.SigningKey != signingKey && signingKey != null)
{
needUpdate = true;
@ -531,9 +537,15 @@ namespace BTCPayServer.Controllers
}
}
if (store.SpeedPolicy != vm.SpeedPolicy)
{
store.SpeedPolicy = vm.SpeedPolicy;
needUpdate = true;
}
if (needUpdate)
{
store.SetSupportedPaymentMethod(derivation);
store.SetPaymentMethodConfig(handler, derivation);
await _Repo.UpdateStore(store);
@ -561,7 +573,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> UpdatePaymentSettings(WalletSettingsViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out _);
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
@ -574,12 +586,6 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
if (store.SpeedPolicy != vm.SpeedPolicy)
{
needUpdate = true;
store.SpeedPolicy = vm.SpeedPolicy;
}
var blob = store.GetStoreBlob();
var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled;
blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration);
@ -598,21 +604,17 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
if (payjoinChanged && blob.PayJoinEnabled)
{
var problematicPayjoinEnabledMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Where(settings => settings.Network.SupportPayJoin && !settings.IsHotWallet)
.Select(settings => settings.PaymentId.CryptoCode)
.ToArray();
if (problematicPayjoinEnabledMethods.Any())
if (payjoinChanged && blob.PayJoinEnabled && network.SupportPayJoin)
{
var config = store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers);
if (!config.IsHotWallet)
{
TempData.Remove(WellKnownTempData.SuccessMessage);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = $"The payment settings were updated successfully. However, PayJoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
Html = $"The payment settings were updated successfully. However, PayJoin will not work, as this isn't a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
});
}
}
@ -743,8 +745,7 @@ namespace BTCPayServer.Controllers
return NotFound();
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
store.SetSupportedPaymentMethod(paymentMethodId, null);
store.SetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), null);
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) });
@ -755,7 +756,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(GeneralSettings), new { storeId });
}
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy, NBXplorerNetwork network)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
@ -769,7 +770,7 @@ namespace BTCPayServer.Controllers
var keyPath = deposit.GetKeyPath(i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive(i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
var address = network.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath(i),
derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
@ -792,11 +793,7 @@ namespace BTCPayServer.Controllers
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;
return store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers);
}
private async Task<string> GetSeed(ExplorerClient client, DerivationSchemeSettings derivation)

View file

@ -18,6 +18,7 @@ using BTCPayServer.HostedServices.Webhooks;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
@ -82,7 +83,7 @@ namespace BTCPayServer.Controllers
_LangService = langService;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_handlers = paymentMethodHandlerDictionary;
_policiesSettings = policiesSettings;
_authorizationService = authorizationService;
_appService = appService;
@ -118,7 +119,7 @@ namespace BTCPayServer.Controllers
readonly SettingsRepository _settingsRepository;
private readonly ExplorerClientProvider _ExplorerProvider;
private readonly LanguageService _LangService;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService;
@ -337,16 +338,15 @@ namespace BTCPayServer.Controllers
var storeBlob = CurrentStore.GetStoreBlob();
var vm = new CheckoutAppearanceViewModel();
SetCryptoCurrencies(vm, CurrentStore);
vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.PaymentId))
.Where(s => _NetworkProvider.GetNetwork(s.PaymentId.CryptoCode) != null)
.Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay)
.Select(method =>
vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers)
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig)
.Select(c =>
{
var pmi = c.Key;
var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
criteria.PaymentMethod == method.PaymentId);
criteria.PaymentMethod == pmi);
return existing is null
? new PaymentMethodCriteriaViewModel { PaymentMethod = method.PaymentId.ToString(), Value = "" }
? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" }
: new PaymentMethodCriteriaViewModel
{
PaymentMethod = existing.PaymentMethod.ToString(),
@ -391,13 +391,13 @@ namespace BTCPayServer.Controllers
public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData)
{
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider);
var enabled = storeData.GetEnabledPaymentIds();
return enabled
.Select(o =>
new PaymentMethodOptionViewModel.Format()
{
Name = o.ToPrettyString(),
Name = o.ToString(),
Value = o.ToString(),
PaymentId = o
}).ToArray();
@ -405,13 +405,13 @@ namespace BTCPayServer.Controllers
PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData)
{
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider);
var enabled = storeData.GetEnabledPaymentIds();
var defaultPaymentId = storeData.GetDefaultPaymentId();
var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null;
if (defaultChoice is null)
{
defaultChoice = enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.LightningLike) ??
defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ??
enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ??
enabled.FirstOrDefault();
}
var choices = GetEnabledPaymentMethodChoices(storeData);
@ -507,15 +507,15 @@ namespace BTCPayServer.Controllers
foreach (var newCriteria in model.PaymentMethodCriteria.ToList())
{
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
if (paymentMethodId.PaymentType == PaymentTypes.LightningLike)
if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h)
model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel()
{
PaymentMethod = new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay).ToString(),
PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(),
Type = newCriteria.Type,
Value = newCriteria.Value
});
// Should not be able to set LNUrlPay criteria directly in UI
if (paymentMethodId.PaymentType == PaymentTypes.LNURLPay)
if (_handlers.TryGet(paymentMethodId) is LNURLPayPaymentHandler)
model.PaymentMethodCriteria.Remove(newCriteria);
}
blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>();
@ -575,54 +575,49 @@ namespace BTCPayServer.Controllers
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.ToDictionary(c => c.Network.CryptoCode.ToUpperInvariant());
.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers)
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (DerivationSchemeSettings)c.Value);
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance)
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
.GetPaymentMethodConfigs(_handlers)
.Where(c => c.Value is LightningPaymentMethodConfig)
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value);
derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>();
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
foreach (var handler in _handlers)
{
switch (paymentMethodId.PaymentType)
if (handler is BitcoinLikePaymentHandler { Network: var network })
{
case BitcoinPaymentType _:
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
derivationSchemes.Add(new StoreDerivationScheme
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
Value = value,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null,
derivationSchemes.Add(new StoreDerivationScheme
{
Crypto = network.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
WalletSupported = network.WalletSupported,
Value = value,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,
#if ALTCOINS
Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
#endif
});
break;
case LNURLPayPaymentType:
break;
case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
lightningNodes.Add(new StoreLightningNode
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(),
Enabled = isEnabled
});
break;
});
}
else if (handler is LightningLikePaymentHandler)
{
var lnNetwork = ((IHasNetwork)handler).Network;
var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode);
var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null;
lightningNodes.Add(new StoreLightningNode
{
CryptoCode = lnNetwork.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
Address = lightning?.GetDisplayableConnectionString(),
Enabled = isEnabled
});
}
}
}
@ -843,7 +838,7 @@ namespace BTCPayServer.Controllers
var isOD = Regex.Match(derivationScheme, @"\(.*?\)");
if (isOD.Success)
{
var derivationSchemeSettings = new DerivationSchemeSettings { Network = network };
var derivationSchemeSettings = new DerivationSchemeSettings();
var result = parser.ParseOutputDescriptor(derivationScheme);
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
@ -1087,8 +1082,8 @@ namespace BTCPayServer.Controllers
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = !store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
StoreNotConfigured = !store.GetPaymentMethodConfigs(_handlers)
.Where(p => !excludeFilter.Match(p.Key))
.Any();
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
if (pairingResult == PairingResult.Partial)

View file

@ -9,6 +9,9 @@ using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -22,15 +25,15 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
public class UIVaultController : Controller
{
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IAuthorizationService _authorizationService;
public UIVaultController(BTCPayNetworkProvider networks, IAuthorizationService authorizationService)
public UIVaultController(PaymentMethodHandlerDictionary handlers, IAuthorizationService authorizationService)
{
Networks = networks;
_handlers = handlers;
_authorizationService = authorizationService;
}
public BTCPayNetworkProvider Networks { get; }
[HttpGet]
[Route("{cryptoCode}/xpub")]
@ -46,8 +49,7 @@ namespace BTCPayServer.Controllers
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)))
{
var cancellationToken = cts.Token;
var network = Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null)
if (!_handlers.TryGetValue(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), out var h) || h is not IHasNetwork { Network: var network })
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var vaultClient = new VaultClient(websocket);
@ -397,11 +399,8 @@ askdevice:
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
{
var paymentMethod = CurrentStore
.GetSupportedPaymentMethods(Networks)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
return CurrentStore.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, _handlers);
}
}
}

View file

@ -79,7 +79,7 @@ namespace BTCPayServer.Controllers
// we just assume that it is 20 blocks
var assumedFeeRate = await fr.GetFeeRateAsync(20);
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(NetworkProvider, network.CryptoCode))?.AccountDerivation;
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode))?.AccountDerivation;
if (derivationScheme is null)
return NotFound();

View file

@ -19,8 +19,10 @@ using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -69,6 +71,8 @@ namespace BTCPayServer.Controllers
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
private readonly LabelService _labelService;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly WalletHistogramService _walletHistogramService;
@ -94,10 +98,14 @@ namespace BTCPayServer.Controllers
IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService,
LabelService labelService,
PaymentMethodHandlerDictionary handlers,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
TransactionLinkProviders transactionLinkProviders)
{
_currencyTable = currencyTable;
_labelService = labelService;
_handlers = handlers;
_paymentModelExtensions = paymentModelExtensions;
_transactionLinkProviders = transactionLinkProviders;
Repository = repo;
WalletRepository = walletRepository;
@ -176,11 +184,11 @@ namespace BTCPayServer.Controllers
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
Network: d.Network)))
.SelectMany(s => s.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers)
.Select(d => (
Wallet: _walletProvider.GetWallet(((IHasNetwork)_handlers[d.Key]).Network),
DerivationStrategy: d.Value.AccountDerivation,
Network: ((IHasNetwork)_handlers[d.Key]).Network))
.Where(_ => _.Wallet != null && _.Network.WalletSupported)
.Select(_ => (Wallet: _.Wallet,
Store: s,
@ -225,8 +233,8 @@ namespace BTCPayServer.Controllers
var paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var network = _handlers.GetBitcoinHandler(walletId.CryptoCode).Network;
var wallet = _walletProvider.GetWallet(network);
// We can't filter at the database level if we need to apply label filter
var preFiltering = string.IsNullOrEmpty(labelFilter);
@ -253,12 +261,12 @@ namespace BTCPayServer.Controllers
}
else
{
var pmi = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
foreach (var tx in transactions)
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
vm.Id = tx.TransactionId.ToString();
vm.Link = _transactionLinkProviders.GetTransactionLink(pmi, vm.Id);
vm.Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, vm.Id);
vm.Timestamp = tx.SeenAt;
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
vm.Balance = tx.BalanceChange.ShowMoney(wallet.Network);
@ -325,7 +333,7 @@ namespace BTCPayServer.Controllers
{
bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey,
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
new { walletId.CryptoCode })));
new { cryptoCode = walletId.CryptoCode })));
}
string[]? labels = null;
@ -340,7 +348,7 @@ namespace BTCPayServer.Controllers
{
CryptoCode = walletId.CryptoCode,
Address = address?.ToString(),
CryptoImage = GetImage(paymentMethod.PaymentId, network),
CryptoImage = GetImage(network),
PaymentLink = bip21.ToString(),
ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath,
SelectedLabels = labels ?? Array.Empty<string>()
@ -449,7 +457,7 @@ namespace BTCPayServer.Controllers
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, storeData.DefaultCurrency);
var currencyPair = new Rating.CurrencyPair(walletId.CryptoCode, storeData.DefaultCurrency);
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel
@ -591,7 +599,7 @@ namespace BTCPayServer.Controllers
var utxos = await _walletProvider.GetWallet(network)
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
var pmi = new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(vm.CryptoCode);
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
vm.InputsAvailable = utxos.Select(coin =>
@ -606,7 +614,7 @@ namespace BTCPayServer.Controllers
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = _labelService.CreateTransactionTagModels(info, Request),
Link = _transactionLinkProviders.GetTransactionLink(pmi, coin.OutPoint.ToString()),
Link = _transactionLinkProviders.GetTransactionLink(network.CryptoCode, coin.OutPoint.ToString()),
Confirmations = coin.Confirmations
};
}).ToArray();
@ -753,7 +761,7 @@ namespace BTCPayServer.Controllers
CreatePSBTResponse psbtResponse;
if (command == "schedule")
{
var pmi = new PaymentMethodId(walletId.CryptoCode, BitcoinPaymentType.Instance);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
var claims =
vm.Outputs.Where(output => string.IsNullOrEmpty(output.PayoutId)).Select(output => new ClaimRequest()
{
@ -1211,7 +1219,7 @@ namespace BTCPayServer.Controllers
internal DerivationSchemeSettings? GetDerivationSchemeSettings(WalletId walletId)
{
return GetCurrentStore().GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
return GetCurrentStore().GetDerivationSchemeSettings(_handlers, walletId.CryptoCode);
}
private static async Task<IMoney> GetBalanceAsMoney(BTCPayWallet wallet,
@ -1251,7 +1259,8 @@ namespace BTCPayServer.Controllers
CancellationToken cancellationToken = default)
{
var derivationScheme = GetDerivationSchemeSettings(walletId);
if (derivationScheme == null || derivationScheme.Network.ReadonlyWallet)
var network = _handlers.GetBitcoinHandler(walletId.CryptoCode).Network;
if (derivationScheme == null || network.ReadonlyWallet)
return NotFound();
switch (command)
@ -1335,7 +1344,8 @@ namespace BTCPayServer.Controllers
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var network = _handlers.GetBitcoinHandler(walletId.CryptoCode).Network;
var wallet = _walletProvider.GetWallet(network);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, cancellationToken: cancellationToken);
var walletTransactionsInfo = await walletTransactionsInfoAsync;
@ -1469,12 +1479,14 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(WalletLabels), new { walletId });
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
private string? GetImage(BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return Request.GetRelativePathOrAbsolute(res);
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
if (_paymentModelExtensions.TryGetValue(pmi, out var extension))
{
return Request.GetRelativePathOrAbsolute(Url.Content(extension.Image));
}
return null;
}
private string GetUserId() => _userManager.GetUserId(User)!;

View file

@ -15,11 +15,6 @@ namespace BTCPayServer.Data
return addressInvoiceData.Address;
return addressInvoiceData.Address.Substring(0, index);
}
public static AddressInvoiceData Set(this AddressInvoiceData addressInvoiceData, string address, PaymentMethodId paymentMethodId)
{
addressInvoiceData.Address = address + "#" + paymentMethodId.ToString();
return addressInvoiceData;
}
public static PaymentMethodId GetPaymentMethodId(this AddressInvoiceData addressInvoiceData)
{
if (addressInvoiceData.Address == null)

View file

@ -0,0 +1,54 @@
#nullable enable
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public static class BlobSerializer
{
public static (JsonSerializerSettings SerializerSettings, JsonSerializer Serializer) CreateSerializer()
{
JsonSerializerSettings settings = CreateSettings();
return (settings, JsonSerializer.CreateDefault(settings));
}
public static (JsonSerializerSettings SerializerSettings, JsonSerializer Serializer) CreateSerializer(Network? network)
{
JsonSerializerSettings settings = CreateSettings();
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(settings, network);
settings.ContractResolver = CreateResolver();
return (settings, JsonSerializer.CreateDefault(settings));
}
public static (JsonSerializerSettings SerializerSettings, JsonSerializer Serializer) CreateSerializer(NBXplorerNetwork network)
{
JsonSerializerSettings settings = CreateSettings();
network.Serializer.ConfigureSerializer(settings);
settings.ContractResolver = CreateResolver();
return (settings, JsonSerializer.CreateDefault(settings));
}
private static JsonSerializerSettings CreateSettings()
{
var settings = new JsonSerializerSettings()
{
ContractResolver = CreateResolver(),
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
};
return settings;
}
private static CamelCasePropertyNamesContractResolver CreateResolver()
{
return new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = false
}
};
}
}
}

View file

@ -12,15 +12,17 @@ namespace BTCPayServer.Data
{
public static class IHasBlobExtensions
{
static readonly JsonSerializerSettings DefaultSerializer;
static readonly JsonSerializerSettings DefaultSerializerSettings;
static readonly JsonSerializer DefaultSerializer;
static IHasBlobExtensions()
{
DefaultSerializer = new JsonSerializerSettings()
DefaultSerializerSettings = new JsonSerializerSettings()
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.None
};
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializer);
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
DefaultSerializer = JsonSerializer.CreateDefault(DefaultSerializerSettings);
}
class HasBlobWrapper<B> : IHasBlob<B>
{
@ -60,7 +62,7 @@ namespace BTCPayServer.Data
public static B? GetBlob<B>(this IHasBlob<B> data, JsonSerializerSettings? settings = null)
{
if (data.Blob2 is not null)
return JObject.Parse(data.Blob2).ToObject<B>(JsonSerializer.CreateDefault(settings ?? DefaultSerializer));
return JObject.Parse(data.Blob2).ToObject<B>(JsonSerializer.CreateDefault(settings ?? DefaultSerializerSettings));
#pragma warning disable CS0618 // Type or member is obsolete
if (data.Blob is not null && data.Blob.Length != 0)
{
@ -69,7 +71,7 @@ namespace BTCPayServer.Data
str = Encoding.UTF8.GetString(data.Blob);
else
str = ZipUtils.Unzip(data.Blob);
return JObject.Parse(str).ToObject<B>(JsonSerializer.CreateDefault(settings ?? DefaultSerializer));
return JObject.Parse(str).ToObject<B>(JsonSerializer.CreateDefault(settings ?? DefaultSerializerSettings));
}
#pragma warning restore CS0618 // Type or member is obsolete
return default;
@ -78,19 +80,28 @@ namespace BTCPayServer.Data
public static object? GetBlob(this IHasBlob data, JsonSerializerSettings? settings = null)
{
if (data.Blob2 is not null)
return JObject.Parse(data.Blob2).ToObject(data.Type, JsonSerializer.CreateDefault(settings ?? DefaultSerializer));
return JObject.Parse(data.Blob2).ToObject(data.Type, JsonSerializer.CreateDefault(settings ?? DefaultSerializerSettings));
#pragma warning disable CS0618 // Type or member is obsolete
if (data.Blob is not null && data.Blob.Length != 0)
return JObject.Parse(ZipUtils.Unzip(data.Blob)).ToObject(data.Type, JsonSerializer.CreateDefault(settings ?? DefaultSerializer));
return JObject.Parse(ZipUtils.Unzip(data.Blob)).ToObject(data.Type, JsonSerializer.CreateDefault(settings ?? DefaultSerializerSettings));
#pragma warning restore CS0618 // Type or member is obsolete
return default;
}
public static T SetBlob<T, B>(this T data, B blob, JsonSerializerSettings? settings = null) where T : IHasBlob<B>
public static T SetBlob<T, B>(this T data, B blob) where T : IHasBlob<B>
{
return SetBlob(data, blob, (JsonSerializer?)null);
}
public static T SetBlob<T, B>(this T data, B blob, JsonSerializerSettings? settings) where T : IHasBlob<B>
{
return SetBlob(data, blob, settings is null ? null : JsonSerializer.CreateDefault(settings));
}
public static T SetBlob<T, B>(this T data, B blob, JsonSerializer? settings) where T : IHasBlob<B>
{
if (blob is null)
data.Blob2 = null;
else
data.Blob2 = JObject.FromObject(blob, JsonSerializer.CreateDefault(settings ?? DefaultSerializer)).ToString(Formatting.None);
data.Blob2 = JObject.FromObject(blob, settings ?? DefaultSerializer).ToString(Formatting.None);
#pragma warning disable CS0618 // Type or member is obsolete
data.Blob = new byte[0];
#pragma warning restore CS0618 // Type or member is obsolete

View file

@ -1,48 +1,69 @@
using System.Globalization;
using System.Linq;
using System.Reflection.Metadata;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data
{
public static class InvoiceDataExtensions
{
public static readonly JsonSerializerSettings DefaultSerializerSettings;
public static readonly JsonSerializer DefaultSerializer;
static InvoiceDataExtensions()
{
(DefaultSerializerSettings, DefaultSerializer) = BlobSerializer.CreateSerializer(null as NBitcoin.Network);
}
public static void SetBlob(this InvoiceData invoiceData, InvoiceEntity blob)
{
if (blob.Metadata is null)
blob.Metadata = new InvoiceMetadata();
invoiceData.HasTypedBlob<InvoiceEntity>().SetBlob(blob);
invoiceData.Currency = blob.Currency;
invoiceData.Amount = blob.Price;
invoiceData.HasTypedBlob<InvoiceEntity>().SetBlob(blob, DefaultSerializer);
}
public static InvoiceEntity GetBlob(this InvoiceData invoiceData, BTCPayNetworkProvider networks)
public static InvoiceEntity GetBlob(this InvoiceData invoiceData)
{
#pragma warning disable CS0618 // Type or member is obsolete
if (invoiceData.Blob is not null && invoiceData.Blob.Length != 0)
var entity = invoiceData.HasTypedBlob<InvoiceEntity>().GetBlob(DefaultSerializerSettings);
entity.Payments = invoiceData.Payments?
.Select(p => p.GetBlob())
.OrderBy(a => a.ReceivedTime)
.ToList();
#pragma warning restore CS0618
var state = invoiceData.GetInvoiceState();
entity.Id = invoiceData.Id;
entity.Currency = invoiceData.Currency;
if (invoiceData.Amount is decimal price)
{
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(ZipUtils.Unzip(invoiceData.Blob), InvoiceRepository.DefaultSerializerSettings);
entity.Networks = networks;
if (entity.Metadata is null)
{
if (entity.Version < InvoiceEntity.GreenfieldInvoices_Version)
{
entity.MigrateLegacyInvoice();
}
else
{
entity.Metadata = new InvoiceMetadata();
}
}
return entity;
entity.Price = price;
}
else
entity.StoreId = invoiceData.StoreDataId;
entity.ExceptionStatus = state.ExceptionStatus;
entity.Status = state.Status;
if (invoiceData.AddressInvoices != null)
{
var entity = invoiceData.HasTypedBlob<InvoiceEntity>().GetBlob();
entity.Networks = networks;
return entity;
entity.AvailableAddressHashes = invoiceData.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId()).ToHashSet();
}
#pragma warning restore CS0618 // Type or member is obsolete
if (invoiceData.Events != null)
{
entity.Events = invoiceData.Events.OrderBy(c => c.Timestamp).ToList();
}
if (invoiceData.Refunds != null)
{
entity.Refunds = invoiceData.Refunds.OrderBy(c => c.PullPaymentData.StartDate).ToList();
}
entity.Archived = invoiceData.Archived;
entity.UpdateTotals();
return entity;
}
public static InvoiceState GetInvoiceState(this InvoiceData invoiceData)
{
return new InvoiceState(invoiceData.Status, invoiceData.ExceptionStatus);
return new InvoiceState(invoiceData.Status ?? "new", invoiceData.ExceptionStatus);
}
}
}

View file

@ -1,54 +1,54 @@
#nullable enable
using System;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
public static class PaymentDataExtensions
{
public static void SetBlob(this PaymentData paymentData, PaymentEntity entity)
public static PaymentData Set(this PaymentData paymentData, InvoiceEntity invoiceEntity, IPaymentMethodHandler handler, object details)
{
paymentData.Type = entity.GetPaymentMethodId().ToStringNormalized();
paymentData.Blob2 = entity.Network.ToString(entity);
var prompt = invoiceEntity.GetPaymentPrompt(handler.PaymentMethodId) ?? throw new InvalidOperationException($"Payment prompt for {handler.PaymentMethodId} is not found");
var paymentBlob = new PaymentBlob()
{
Destination = prompt.Destination,
PaymentMethodFee = prompt.PaymentMethodFee,
Divisibility = prompt.Divisibility
}.SetDetails(handler, details);
paymentData.InvoiceDataId = invoiceEntity.Id;
paymentData.SetBlob(handler.PaymentMethodId, paymentBlob);
return paymentData;
}
public static PaymentEntity GetBlob(this PaymentData paymentData, BTCPayNetworkProvider networks)
public static PaymentEntity SetBlob(this PaymentData paymentData, PaymentEntity entity)
{
#pragma warning disable CS0618 // Type or member is obsolete
if (paymentData.Blob is not null && paymentData.Blob.Length != 0)
{
var unziped = ZipUtils.Unzip(paymentData.Blob);
var cryptoCode = "BTC";
if (JObject.Parse(unziped).TryGetValue("cryptoCode", out var v) && v.Type == JTokenType.String)
cryptoCode = v.Value<string>();
var network = networks.GetNetwork<BTCPayNetworkBase>(cryptoCode);
PaymentEntity paymentEntity = null;
if (network == null)
{
return null;
}
else
{
paymentEntity = network.ToObject<PaymentEntity>(unziped);
}
paymentEntity.Network = network;
paymentEntity.Accounted = paymentData.Accounted;
return paymentEntity;
}
#pragma warning restore CS0618 // Type or member is obsolete
if (paymentData.Blob2 is not null)
{
if (!PaymentMethodId.TryParse(paymentData.Type, out var pmi))
return null;
var network = networks.GetNetwork<BTCPayNetworkBase>(pmi.CryptoCode);
if (network is null)
return null;
var entity = network.ToObject<PaymentEntity>(paymentData.Blob2);
entity.Network = network;
entity.Accounted = paymentData.Accounted;
return entity;
}
return null;
paymentData.Amount = entity.Value;
paymentData.Currency = entity.Currency;
paymentData.Status = entity.Status;
paymentData.SetBlob(entity.PaymentMethodId, (PaymentBlob)entity);
return entity;
}
public static PaymentData SetBlob(this PaymentData paymentData, PaymentMethodId paymentMethodId, PaymentBlob blob)
{
paymentData.Type = paymentMethodId.ToString();
paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
return paymentData;
}
public static PaymentEntity GetBlob(this PaymentData paymentData)
{
var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}");
entity.Status = paymentData.Status!.Value;
entity.Currency = paymentData.Currency;
entity.PaymentMethodId = PaymentMethodId.Parse(paymentData.Type);
entity.Value = paymentData.Amount!.Value;
entity.Id = paymentData.Id;
entity.ReceivedTime = paymentData.Created!.Value;
return entity;
}
}
}

View file

@ -13,7 +13,9 @@ using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Mvc;
@ -32,6 +34,7 @@ using StoreData = BTCPayServer.Data.StoreData;
public class BitcoinLikePayoutHandler : IPayoutHandler
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly ApplicationDbContextFactory _dbContextFactory;
@ -43,6 +46,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public WalletRepository WalletRepository { get; }
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
PaymentMethodHandlerDictionary handlers,
WalletRepository walletRepository,
ExplorerClientProvider explorerClientProvider,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
@ -53,6 +57,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
TransactionLinkProviders transactionLinkProviders)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
WalletRepository = walletRepository;
_explorerClientProvider = explorerClientProvider;
_jsonSerializerSettings = jsonSerializerSettings;
@ -66,20 +71,20 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public bool CanHandle(PaymentMethodId paymentMethod)
{
return paymentMethod?.PaymentType == BitcoinPaymentType.Instance &&
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
return _handlers.TryGetValue(paymentMethod, out var h) &&
h is BitcoinLikePaymentHandler { Network: { ReadonlyWallet: false } };
}
public async Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(claimRequest.PaymentMethodId.CryptoCode);
var network = _handlers.GetNetwork(claimRequest.PaymentMethodId);
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
if (claimRequest.Destination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
{
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
await WalletRepository.AddWalletTransactionAttachment(
new WalletId(claimRequest.StoreId, claimRequest.PaymentMethodId.CryptoCode),
new WalletId(claimRequest.StoreId, network.CryptoCode),
bitcoinLikeClaimDestination.Address.ToString(),
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id), WalletObjectData.Types.Address);
}
@ -87,7 +92,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var network = _handlers.GetNetwork(paymentMethodId);
destination = destination.Trim();
try
{
@ -116,19 +121,19 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return null;
var paymentMethodId = payout.GetPaymentMethodId();
if (paymentMethodId is null)
{
return null;
}
var cryptoCode = _handlers.TryGetNetwork(paymentMethodId)?.CryptoCode;
if (cryptoCode is null)
return null;
ParseProofType(payout.Proof, out var raw, out var proofType);
if (proofType == PayoutTransactionOnChainBlob.Type)
{
var res = raw.ToObject<PayoutTransactionOnChainBlob>(
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(paymentMethodId.CryptoCode)));
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(cryptoCode)));
if (res == null)
return null;
res.LinkTemplate = _transactionLinkProviders.GetBlockExplorerLink(paymentMethodId);
res.LinkTemplate = _transactionLinkProviders.GetBlockExplorerLink(cryptoCode);
return res;
}
return raw.ToObject<ManualPayoutProof>();
@ -181,7 +186,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
{
if (_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode)?
var network = _handlers.TryGetNetwork(paymentMethodId);
if (network?
.NBitcoinNetwork?
.Consensus?
.ConsensusFactory?
@ -269,25 +275,23 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData)
{
return Task.FromResult(storeData.GetEnabledPaymentIds(_btcPayNetworkProvider)
.Where(id => id.PaymentType == BitcoinPaymentType.Instance));
return Task.FromResult(storeData.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers, true).Select(c => c.Key));
}
public async Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds)
{
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var pmi = paymentMethodId.ToString();
var pmi = paymentMethodId;
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
.Where(data => payoutIds.Contains(data.Id)
&& pmi == data.PaymentMethodId
&& pmi.ToString() == data.PaymentMethodId
&& data.State == PayoutState.AwaitingPayment)
.ToListAsync();
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().Where(s => s != null).ToArray();
var storeId = payouts.First().StoreDataId;
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var network = _handlers.GetNetwork(paymentMethodId);
List<string> bip21 = new List<string>();
foreach (var payout in payouts)
{
@ -315,10 +319,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
}
}
if (bip21.Any())
return new RedirectToActionResult("WalletSend", "UIWallets", new { walletId = new WalletId(storeId, paymentMethodId.CryptoCode).ToString(), bip21 });
return new RedirectToActionResult("WalletSend", "UIWallets", new { walletId = new WalletId(storeId, network.CryptoCode).ToString(), bip21 });
return new RedirectToActionResult("Payouts", "UIWallets", new
{
walletId = new WalletId(storeId, paymentMethodId.CryptoCode).ToString(),
walletId = new WalletId(storeId, network.CryptoCode).ToString(),
pullPaymentId = pullPaymentIds.Length == 1 ? pullPaymentIds.First() : null
});
}
@ -418,7 +422,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
var destinationSum =
newTransaction.NewTransactionEvent.Outputs.Sum(output => output.Value.GetValue(network));
var destination = addressTrackedSource.Address.ToString();
var paymentMethodId = new PaymentMethodId(newTransaction.CryptoCode, BitcoinPaymentType.Instance);
var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(newTransaction.CryptoCode);
await using var ctx = _dbContextFactory.CreateContext();
var payouts = await ctx.Payouts
@ -443,7 +447,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return;
var derivationSchemeSettings = payout.StoreData
.GetDerivationSchemeSettings(_btcPayNetworkProvider, newTransaction.CryptoCode)?.AccountDerivation;
.GetDerivationSchemeSettings(_handlers, newTransaction.CryptoCode)?.AccountDerivation;
if (derivationSchemeSettings is null)
return;

View file

@ -9,8 +9,10 @@ using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using LNURL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -25,16 +27,15 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService;
private readonly IAuthorizationService _authorizationService;
public LightningLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
public LightningLikePayoutHandler(PaymentMethodHandlerDictionary handlers,
IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_httpClientFactory = httpClientFactory;
_userService = userService;
_authorizationService = authorizationService;
@ -42,8 +43,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public bool CanHandle(PaymentMethodId paymentMethod)
{
return (paymentMethod.PaymentType == LightningPaymentType.Instance || paymentMethod.PaymentType == LNURLPayPaymentType.Instance) &&
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.SupportLightning is true;
return _handlers.TryGetValue(paymentMethod, out var h) && h is ILightningPaymentHandler;
}
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
@ -61,7 +62,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public async Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken)
{
destination = destination.Trim();
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var network = ((IHasNetwork)_handlers[paymentMethodId]).Network;
try
{
string lnurlTag = null;
@ -161,12 +162,12 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public async Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData)
{
var result = new List<PaymentMethodId>();
var methods = storeData.GetEnabledPaymentMethods(_btcPayNetworkProvider).Where(id => id.PaymentId.PaymentType == LightningPaymentType.Instance).OfType<LightningSupportedPaymentMethod>();
foreach (LightningSupportedPaymentMethod supportedPaymentMethod in methods)
var methods = storeData.GetPaymentMethodConfigs<LightningPaymentMethodConfig>(_handlers, true);
foreach (var m in methods)
{
if (!supportedPaymentMethod.IsInternalNode)
if (!m.Value.IsInternalNode)
{
result.Add(supportedPaymentMethod.PaymentId);
result.Add(m.Key);
continue;
}
@ -174,7 +175,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
if (!await _userService.IsAdminUser(storeDataUserStore.ApplicationUserId))
continue;
result.Add(supportedPaymentMethod.PaymentId);
result.Add(m.Key);
break;
}
@ -185,8 +186,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds)
{
var cryptoCode = _handlers.GetNetwork(paymentMethodId).CryptoCode;
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout",
"UILightningLikePayout", new { cryptoCode = paymentMethodId.CryptoCode, payoutIds }));
"UILightningLikePayout", new { cryptoCode, payoutIds }));
}
}

View file

@ -10,9 +10,11 @@ using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using LNURL;
using Microsoft.AspNetCore.Authorization;
@ -32,7 +34,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly IOptions<LightningNetworkOptions> _options;
private readonly IAuthorizationService _authorizationService;
@ -43,7 +45,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
UserManager<ApplicationUser> userManager,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers,
BTCPayNetworkProvider btcPayNetworkProvider,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository,
LightningClientFactoryService lightningClientFactoryService,
IOptions<LightningNetworkOptions> options,
@ -54,7 +56,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_userManager = userManager;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_payoutHandlers = payoutHandlers;
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_lightningClientFactoryService = lightningClientFactoryService;
_options = options;
_storeRepository = storeRepository;
@ -101,7 +103,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
await SetStoreContext();
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = await GetPayouts(ctx, pmi, payoutIds);
@ -125,14 +127,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
await SetStoreContext();
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.FindPayoutHandler(pmi);
await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
var results = new List<ResultVM>();
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(pmi.CryptoCode);
var network = ((IHasNetwork)_handlers[pmi]).Network;
//we group per store and init the transfers by each
@ -141,9 +143,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
var store = payoutDatas.First().StoreData;
var lightningSupportedPaymentMethod = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == pmi);
var lightningSupportedPaymentMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers);
if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode)
{

View file

@ -5,12 +5,15 @@ using System.Data;
using System.Linq;
using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace BTCPayServer.Data
{
@ -24,21 +27,19 @@ namespace BTCPayServer.Data
return defaultPaymentId;
}
public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks)
public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData)
{
return GetEnabledPaymentMethods(storeData, networks).Select(method => method.PaymentId).ToArray();
return storeData.GetPaymentMethodConfigs(true).Select(c => c.Key).ToArray();
}
public static ISupportedPaymentMethod[] GetEnabledPaymentMethods(this StoreData storeData, BTCPayNetworkProvider networks)
public static Dictionary<PaymentMethodId, object> GetEnabledPaymentMethods(this StoreData storeData, PaymentMethodHandlerDictionary handlers)
{
var excludeFilter = storeData.GetStoreBlob().GetExcludedPaymentMethods();
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks)
.Where(a => !excludeFilter.Match(a.PaymentId))
.OrderByDescending(a => a.PaymentId.CryptoCode == "BTC")
.ThenBy(a => a.PaymentId.CryptoCode)
.ThenBy(a => a.PaymentId.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
.ToArray();
return paymentMethodIds;
return storeData.GetPaymentMethodConfigs(true)
.Where(m => handlers.Support(m.Key))
.OrderByDescending(a => a.Key.ToString() == "BTC")
.ThenBy(a => a.Key.ToString())
.ThenBy(a => handlers[a.Key].ParsePaymentMethodConfig(a.Value) is LightningPaymentMethodConfig ? 1 : 0)
.ToDictionary(a => a.Key, a => handlers[a.Key].ParsePaymentMethodConfig(a.Value));
}
public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId? defaultPaymentId)
@ -60,12 +61,9 @@ namespace BTCPayServer.Data
return result;
}
public static bool AnyPaymentMethodAvailable(this StoreData storeData, BTCPayNetworkProvider networkProvider)
public static bool AnyPaymentMethodAvailable(this StoreData storeData)
{
var storeBlob = GetStoreBlob(storeData);
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return GetSupportedPaymentMethods(storeData, networkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
return storeData.GetPaymentMethodConfigs(true).Any();
}
public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob)
@ -78,106 +76,94 @@ namespace BTCPayServer.Data
return true;
}
public static IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(this StoreData storeData, BTCPayNetworkProvider networks)
public static object? GetPaymentMethodConfig(this StoreData storeData, Payments.PaymentMethodId paymentMethodId, PaymentMethodHandlerDictionary handlers, bool onlyEnabled = false)
{
ArgumentNullException.ThrowIfNull(storeData);
#pragma warning disable CS0618
bool btcReturned = false;
if (!string.IsNullOrEmpty(storeData.DerivationStrategies))
var config = GetPaymentMethodConfig(storeData, paymentMethodId, onlyEnabled);
if (config is null || !handlers.Support(paymentMethodId))
return null;
return handlers[paymentMethodId].ParsePaymentMethodConfig(config);
}
public static JToken? GetPaymentMethodConfig(this StoreData storeData, Payments.PaymentMethodId paymentMethodId, bool onlyEnabled = false)
{
if (string.IsNullOrEmpty(storeData.DerivationStrategies))
return null;
if (!onlyEnabled)
{
JObject strategies = JObject.Parse(storeData.DerivationStrategies);
foreach (var strat in strategies.Properties())
{
if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId))
{
continue;
}
var network = networks.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike && btcReturned)
continue;
if (strat.Value.Type == JTokenType.Null)
continue;
yield return
paymentMethodId.PaymentType.DeserializeSupportedPaymentMethod(network, strat.Value);
}
}
return strategies[paymentMethodId.ToString()];
}
#pragma warning restore CS0618
else
{
var excludeFilter = storeData.GetStoreBlob().GetExcludedPaymentMethods();
JObject strategies = JObject.Parse(storeData.DerivationStrategies);
return excludeFilter.Match(paymentMethodId) ? null : strategies[paymentMethodId.ToString()];
}
}
public static T? GetPaymentMethodConfig<T>(this StoreData storeData, Payments.PaymentMethodId paymentMethodId, PaymentMethodHandlerDictionary handlers, bool onlyEnabled = false) where T : class
{
var conf = storeData.GetPaymentMethodConfig(paymentMethodId, onlyEnabled);
if (conf is null)
return default;
return handlers[paymentMethodId].ParsePaymentMethodConfig(conf) as T;
}
public static void SetSupportedPaymentMethod(this StoreData storeData, ISupportedPaymentMethod supportedPaymentMethod)
public static void SetPaymentMethodConfig(this StoreData storeData, IPaymentMethodHandler handler, object? config)
{
storeData.SetSupportedPaymentMethod(null, supportedPaymentMethod);
storeData.SetPaymentMethodConfig(handler.PaymentMethodId, config is null ? null : JToken.FromObject(config, handler.Serializer));
}
/// <summary>
/// Set or remove a new supported payment method for the store
/// </summary>
/// <param name="paymentMethodId">The paymentMethodId</param>
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
public static void SetSupportedPaymentMethod(this StoreData storeData, PaymentMethodId? paymentMethodId, ISupportedPaymentMethod? supportedPaymentMethod)
public static void SetPaymentMethodConfig(this StoreData storeData, PaymentMethodId paymentMethodId, JToken? config)
{
if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
{
throw new InvalidOperationException("Incoherent arguments, this should never happen");
}
if (supportedPaymentMethod == null && paymentMethodId == null)
throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified");
if (supportedPaymentMethod != null && paymentMethodId == null)
{
paymentMethodId = supportedPaymentMethod.PaymentId;
}
#pragma warning disable CS0618
JObject strategies = string.IsNullOrEmpty(storeData.DerivationStrategies) ? new JObject() : JObject.Parse(storeData.DerivationStrategies);
bool existing = false;
foreach (var strat in strategies.Properties().ToList())
if (config is null)
strategies.Remove(paymentMethodId.ToString());
else
strategies[paymentMethodId.ToString()] = config;
storeData.DerivationStrategies = strategies.ToString(Newtonsoft.Json.Formatting.None);
}
public static Dictionary<PaymentMethodId, object> GetPaymentMethodConfigs(this StoreData storeData, PaymentMethodHandlerDictionary handlers, bool onlyEnabled = false)
{
return storeData.GetPaymentMethodConfigs(onlyEnabled)
.Where(h => handlers.Support(h.Key))
.ToDictionary(c => c.Key, c => handlers[c.Key].ParsePaymentMethodConfig(c.Value));
}
public static Dictionary<PaymentMethodId, T> GetPaymentMethodConfigs<T>(this StoreData storeData, PaymentMethodHandlerDictionary handlers, bool onlyEnabled = false) where T : class
{
return storeData.GetPaymentMethodConfigs(onlyEnabled)
.Select(h => (h.Key, Config: handlers.TryGetValue(h.Key, out var handler) ? handler.ParsePaymentMethodConfig(h.Value) as T : null))
.Where(h => h.Config is not null)
.ToDictionary(c => c.Key, c => c.Config!);
}
public static Dictionary<PaymentMethodId, JToken> GetPaymentMethodConfigs(this StoreData storeData, bool onlyEnabled = false)
{
if (string.IsNullOrEmpty(storeData.DerivationStrategies))
return new Dictionary<PaymentMethodId, JToken>();
var excludeFilter = onlyEnabled ? storeData.GetStoreBlob().GetExcludedPaymentMethods() : null;
var paymentMethodConfigurations = new Dictionary<PaymentMethodId, JToken>();
JObject strategies = JObject.Parse(storeData.DerivationStrategies);
foreach (var strat in strategies.Properties())
{
if (!PaymentMethodId.TryParse(strat.Name, out var stratId))
{
if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId))
continue;
}
if (stratId == paymentMethodId)
{
if (supportedPaymentMethod == null)
{
strat.Remove();
}
else
{
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
}
existing = true;
break;
}
if (excludeFilter?.Match(paymentMethodId) is true)
continue;
paymentMethodConfigurations.Add(paymentMethodId, strat.Value);
}
if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
storeData.DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
return paymentMethodConfigurations;
}
public static bool IsLightningEnabled(this StoreData storeData, BTCPayNetworkProvider networks, string cryptoCode)
public static bool IsLightningEnabled(this StoreData storeData, string cryptoCode)
{
return IsPaymentTypeEnabled(storeData, networks, cryptoCode, LightningPaymentType.Instance);
return IsPaymentMethodEnabled(storeData, PaymentTypes.LN.GetPaymentMethodId(cryptoCode));
}
public static bool IsLNUrlEnabled(this StoreData storeData, BTCPayNetworkProvider networks, string cryptoCode)
public static bool IsLNUrlEnabled(this StoreData storeData, string cryptoCode)
{
return IsPaymentTypeEnabled(storeData, networks, cryptoCode, LNURLPayPaymentType.Instance);
return IsPaymentMethodEnabled(storeData, PaymentTypes.LNURL.GetPaymentMethodId(cryptoCode));
}
private static bool IsPaymentTypeEnabled(this StoreData storeData, BTCPayNetworkProvider networks, string cryptoCode, PaymentType paymentType)
private static bool IsPaymentMethodEnabled(this StoreData storeData, PaymentMethodId paymentMethodId)
{
var paymentMethods = storeData.GetSupportedPaymentMethods(networks);
var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods();
return paymentMethods.Any(method =>
method.PaymentId.CryptoCode == cryptoCode &&
method.PaymentId.PaymentType == paymentType &&
!excludeFilters.Match(method.PaymentId));
return storeData.GetPaymentMethodConfig(paymentMethodId, true) is not null;
}
}
}

View file

@ -9,13 +9,13 @@ using Newtonsoft.Json;
namespace BTCPayServer
{
public class DerivationSchemeSettings : ISupportedPaymentMethod
public class DerivationSchemeSettings
{
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
{
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings { Network = network };
var result = new DerivationSchemeSettings();
var parser = network.GetDerivationSchemeParser();
if (parser.TryParseXpub(derivationStrategy, ref result) ||
parser.TryParseXpub(derivationStrategy, ref result, electrum: true))
@ -26,23 +26,9 @@ namespace BTCPayServer
throw new FormatException($"Invalid Derivation Scheme");
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
public string GetNBXWalletId(Network network)
{
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(config);
strategy = null;
try
{
strategy = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(config);
strategy.Network = network;
}
catch { }
return strategy != null;
}
public string GetNBXWalletId()
{
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(network.NetworkSet.CryptoCode, AccountDerivation.ToString());
}
public DerivationSchemeSettings()
@ -55,7 +41,6 @@ namespace BTCPayServer
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(derivationStrategy);
AccountDerivation = derivationStrategy;
Network = network;
AccountKeySettings = derivationStrategy.GetExtPubKeys().Select(c => new AccountKeySettings()
{
AccountKey = c.GetWif(network.NBitcoinNetwork)
@ -75,9 +60,6 @@ namespace BTCPayServer
_SigningKey = value;
}
}
[JsonIgnore]
public BTCPayNetwork Network { get; set; }
public string Source { get; set; }
public bool IsHotWallet { get; set; }
@ -97,49 +79,15 @@ namespace BTCPayServer
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
[JsonIgnore]
[Obsolete("Use GetSigningAccountKeySettings().AccountKey instead")]
public BitcoinExtPubKey AccountKey
{
get
{
return ExplicitAccountKey ?? new BitcoinExtPubKey(AccountDerivation.GetExtPubKeys().First(), Network.NBitcoinNetwork);
}
}
public AccountKeySettings GetSigningAccountKeySettings()
{
return AccountKeySettings.Single(a => a.AccountKey == SigningKey);
}
AccountKeySettings[] _AccountKeySettings;
public AccountKeySettings[] AccountKeySettings
{
get
{
// Legacy
if (_AccountKeySettings == null)
{
if (this.Network == null)
return null;
_AccountKeySettings = AccountDerivation.GetExtPubKeys().Select(e => new AccountKeySettings()
{
AccountKey = e.GetWif(this.Network.NBitcoinNetwork),
}).ToArray();
#pragma warning disable CS0618 // Type or member is obsolete
_AccountKeySettings[0].AccountKeyPath = AccountKeyPath;
_AccountKeySettings[0].RootFingerprint = RootFingerprint;
ExplicitAccountKey = null;
AccountKeyPath = null;
RootFingerprint = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
return _AccountKeySettings;
}
set
{
_AccountKeySettings = value;
}
get;
set;
}
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
@ -159,9 +107,6 @@ namespace BTCPayServer
public string Label { get; set; }
[JsonIgnore]
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return AccountDerivation.ToString();
@ -173,11 +118,6 @@ namespace BTCPayServer
ToString();
}
public string ToJson()
{
return Network.NBXplorerNetwork.Serializer.ToString(this);
}
public void RebaseKeyPaths(PSBT psbt)
{
foreach (var rebase in GetPSBTRebaseKeyRules())

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