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;
using System.Data.Common;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata; 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client namespace BTCPayServer.Client
{ {
public partial class BTCPayServerClient public partial class BTCPayServerClient
{ {
public virtual async Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId, public virtual async Task<OnChainPaymentMethodPreviewResultData>
bool? enabled = null, PreviewProposedStoreOnChainPaymentMethodAddresses(
CancellationToken token = default) string storeId, string paymentMethodId, string derivationScheme, int offset = 0,
{ int amount = 10,
var query = new Dictionary<string, object>(); CancellationToken token = default)
if (enabled != null) {
{ var response = await _httpClient.SendAsync(
query.Add(nameof(enabled), enabled); 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 = public virtual async Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses(
await _httpClient.SendAsync( string storeId, string paymentMethodId, int offset = 0, int amount = 10,
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain", CancellationToken token = default)
query), token); {
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response); 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, public virtual async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string cryptoCode, CancellationToken token = default) string paymentMethodId, GenerateOnChainWalletRequest request,
{ CancellationToken token = default)
var response = {
await _httpClient.SendAsync( var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token); CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/generate",
return await HandleResponse<OnChainPaymentMethodData>(response); 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.Collections.Generic;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
@ -7,21 +8,60 @@ namespace BTCPayServer.Client
{ {
public partial class BTCPayServerClient public partial class BTCPayServerClient
{ {
public virtual async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId, public virtual async Task<GenericPaymentMethodData> UpdateStorePaymentMethod(
bool? enabled = null, string storeId,
string paymentMethodId,
UpdatePaymentMethodRequest request,
CancellationToken token = default) 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>(); 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 = var response =
await _httpClient.SendAsync( await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods", CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods",
query), token); 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 System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using NBitcoin; using NBitcoin;
using NBitcoin.JsonConverters; using NBitcoin.JsonConverters;
@ -21,7 +22,7 @@ namespace BTCPayServer.Client.Models
public bool ProceedWithPayjoin { get; set; } = true; public bool ProceedWithPayjoin { get; set; } = true;
public bool ProceedWithBroadcast { get; set; } = true; public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false; public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))] [JsonProperty(ItemConverterType = typeof(SaneOutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null; public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; } public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")] [JsonProperty("rbf")]

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters; using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client namespace BTCPayServer.Client
{ {
@ -22,4 +25,16 @@ namespace BTCPayServer.Client
public bool ImportKeysToRPC { get; set; } public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { 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 namespace BTCPayServer.Client.Models
{ {
public class GenericPaymentMethodData public class GenericPaymentMethodData
{ {
public bool Enabled { get; set; } public bool Enabled { get; set; }
public object Data { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string CryptoCode { get; set; } 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; } public decimal Amount { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))] [JsonConverter(typeof(NumericStringJsonConverter))]
public decimal NetworkFee { get; set; } public decimal PaymentMethodFee { get; set; }
public List<Payment> Payments { get; set; } public List<Payment> Payments { get; set; }
public string PaymentMethod { get; set; } public string PaymentMethodId { get; set; }
public JToken AdditionalData { get; set; }
public string CryptoCode { get; set; } public string Currency { get; set; }
public JObject AdditionalData { get; set; }
public class Payment 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;
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using NBitcoin; using NBitcoin;
using NBitcoin.JsonConverters; using NBitcoin.JsonConverters;
@ -12,7 +13,7 @@ namespace BTCPayServer.Client.Models
public string Comment { get; set; } public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))] [JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; } public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))] [JsonConverter(typeof(SaneOutpointJsonConverter))]
public OutPoint Outpoint { get; set; } public OutPoint Outpoint { get; set; }
public string Link { get; set; } public string Link { get; set; }
#pragma warning disable CS0612 // Type or member is obsolete #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 bool AfterExpiration { get; set; }
public string PaymentMethod { get; set; } public string PaymentMethodId { get; set; }
public InvoicePaymentMethodDataModel.Payment Payment { get; set; } public InvoicePaymentMethodDataModel.Payment Payment { get; set; }
} }

View file

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

View file

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

View file

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

View file

@ -6,11 +6,6 @@ namespace BTCPayServer.Data
{ {
public class AddressInvoiceData 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 string Address { get; set; }
public InvoiceData InvoiceData { get; set; } public InvoiceData InvoiceData { get; set; }
public string InvoiceDataId { 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class InvoiceData : IHasBlobUntyped public partial class InvoiceData : IHasBlobUntyped
{ {
public string Id { get; set; } public string Id { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public string StoreDataId { get; set; } public string StoreDataId { get; set; }
public StoreData StoreData { get; set; } public StoreData StoreData { get; set; }
@ -25,6 +25,7 @@ namespace BTCPayServer.Data
public string OrderId { get; set; } public string OrderId { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string ExceptionStatus { get; set; } public string ExceptionStatus { get; set; }
[Obsolete("Unused")]
public string CustomerEmail { get; set; } public string CustomerEmail { get; set; }
public List<AddressInvoiceData> AddressInvoices { get; set; } public List<AddressInvoiceData> AddressInvoices { get; set; }
public bool Archived { 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.StoreDataId);
builder.Entity<InvoiceData>().HasIndex(o => o.OrderId); builder.Entity<InvoiceData>().HasIndex(o => o.OrderId);
builder.Entity<InvoiceData>().HasIndex(o => o.Created); builder.Entity<InvoiceData>().HasIndex(o => o.Created);
if (databaseFacade.IsNpgsql()) if (databaseFacade.IsNpgsql())
{ {
builder.Entity<InvoiceData>() builder.Entity<InvoiceData>()
.Property(o => o.Blob2) .Property(o => o.Blob2)
.HasColumnType("JSONB"); .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 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 Id { get; set; }
public string InvoiceDataId { get; set; } public string InvoiceDataId { get; set; }
public string Currency { get; set; }
public decimal? Amount { get; set; }
public InvoiceData InvoiceData { get; set; } public InvoiceData InvoiceData { get; set; }
[Obsolete("Use Blob2 instead")] [Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; } public byte[] Blob { get; set; }
public string Blob2 { get; set; } public string Blob2 { get; set; }
public string Type { 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) internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{ {
@ -23,11 +38,17 @@ namespace BTCPayServer.Data
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade); .WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>() builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId); .HasIndex(o => o.InvoiceDataId);
builder.Entity<PaymentData>()
.Property(o => o.Status)
.HasConversion<string>();
if (databaseFacade.IsNpgsql()) if (databaseFacade.IsNpgsql())
{ {
builder.Entity<PaymentData>() builder.Entity<PaymentData>()
.Property(o => o.Blob2) .Property(o => o.Blob2)
.HasColumnType("JSONB"); .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;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data 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") b.Property<string>("Id")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<bool>("Archived") b.Property<bool>("Archived")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -259,6 +262,9 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Created") b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail") b.Property<string>("CustomerEmail")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -505,15 +511,27 @@ namespace BTCPayServer.Migrations
b.Property<bool>("Accounted") b.Property<bool>("Accounted")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<decimal?>("Amount")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob") b.Property<byte[]>("Blob")
.HasColumnType("BLOB"); .HasColumnType("BLOB");
b.Property<string>("Blob2") b.Property<string>("Blob2")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<DateTimeOffset?>("Created")
.HasColumnType("TEXT");
b.Property<string>("Currency")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId") b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Status")
.HasColumnType("TEXT");
b.Property<string>("Type") b.Property<string>("Type")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security; using System.Security;
using System.Text; using System.Text;
@ -47,6 +48,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot; using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -236,26 +238,24 @@ namespace BTCPayServer.Tests
var id = PaymentMethodId.Parse("BTC"); var id = PaymentMethodId.Parse("BTC");
var id1 = PaymentMethodId.Parse("BTC-OnChain"); var id1 = PaymentMethodId.Parse("BTC-OnChain");
var id2 = PaymentMethodId.Parse("BTC-BTCLike"); var id2 = PaymentMethodId.Parse("BTC-BTCLike");
Assert.Equal("LTC-LN", PaymentMethodId.Parse("LTC-LightningNetwork").ToString());
Assert.Equal(id, id1); Assert.Equal(id, id1);
Assert.Equal(id, id2); Assert.Equal(id, id2);
Assert.Equal("BTC", id.ToString()); Assert.Equal("BTC-CHAIN", id.ToString());
Assert.Equal("BTC", id.ToString()); Assert.Equal("BTC-CHAIN", id.ToString());
id = PaymentMethodId.Parse("LTC"); id = PaymentMethodId.Parse("LTC");
Assert.Equal("LTC", id.ToString()); Assert.Equal("LTC-CHAIN", id.ToString());
Assert.Equal("LTC", id.ToStringNormalized());
id = PaymentMethodId.Parse("LTC-offchain"); id = PaymentMethodId.Parse("LTC-offchain");
id1 = PaymentMethodId.Parse("LTC-OffChain"); id1 = PaymentMethodId.Parse("LTC-OffChain");
id2 = PaymentMethodId.Parse("LTC-LightningLike"); id2 = PaymentMethodId.Parse("LTC-LightningLike");
Assert.Equal(id, id1); Assert.Equal(id, id1);
Assert.Equal(id, id2); Assert.Equal(id, id2);
Assert.Equal("LTC_LightningLike", id.ToString()); Assert.Equal("LTC-LN", id.ToString());
Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized());
#if ALTCOINS #if ALTCOINS
id = PaymentMethodId.Parse("XMR"); id = PaymentMethodId.Parse("XMR");
id1 = PaymentMethodId.Parse("XMR-MoneroLike"); id1 = PaymentMethodId.Parse("XMR-MoneroLike");
Assert.Equal(id, id1); Assert.Equal(id, id1);
Assert.Equal("XMR_MoneroLike", id.ToString()); Assert.Equal("XMR-CHAIN", id.ToString());
Assert.Equal("XMR", id.ToStringNormalized());
#endif #endif
} }
@ -439,29 +439,31 @@ namespace BTCPayServer.Tests
}} }}
}, out items)); }, out items));
} }
PaymentMethodId BTC = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
PaymentMethodId LTC = PaymentTypes.CHAIN.GetPaymentMethodId("LTC");
[Fact] [Fact]
public void CanCalculateDust() public void CanCalculateDust()
{ {
var entity = new InvoiceEntity() { Currency = "USD" }; var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = CreateNetworkProvider(ChainName.Regtest);
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); 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", Currency = "BTC",
Rate = 34_000m Divisibility = 8
}); });
entity.Price = 4000; entity.Price = 4000;
entity.UpdateTotals(); 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 // Exact price should be 0.117647059..., but the payment method round up to one sat
Assert.Equal(0.11764706m, accounting.Due); Assert.Equal(0.11764706m, accounting.Due);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(0.11764706m), new Key()), Value = 0.11764706m,
Accounted = true Status = PaymentStatus.Settled,
}); });
entity.UpdateTotals(); entity.UpdateTotals();
Assert.Equal(0.0m, entity.NetDue); Assert.Equal(0.0m, entity.NetDue);
@ -474,13 +476,13 @@ namespace BTCPayServer.Tests
// Now, imagine there is litecoin. It might seem from its // Now, imagine there is litecoin. It might seem from its
// perspecitve that there has been a slight over payment. // perspecitve that there has been a slight over payment.
// However, Calculate() should just cap it to 0.0m // However, Calculate() should just cap it to 0.0m
entity.SetPaymentMethod(new PaymentMethod() entity.SetPaymentPrompt(LTC, new PaymentPrompt()
{ {
Currency = "LTC", Currency = "LTC",
Rate = 3400m Divisibility = 8
}); });
entity.UpdateTotals(); entity.UpdateTotals();
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC"); var method = entity.GetPaymentPrompts().First(p => p.Currency == "LTC");
accounting = method.Calculate(); accounting = method.Calculate();
Assert.Equal(0.0m, accounting.DueUncapped); Assert.Equal(0.0m, accounting.DueUncapped);
@ -492,19 +494,19 @@ namespace BTCPayServer.Tests
{ {
var networkProvider = CreateNetworkProvider(ChainName.Regtest); var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" }; var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = networkProvider;
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>(); entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{ {
Currency = "BTC", Currency = "BTC",
Rate = 5000, PaymentMethodFee = 0.1m,
NextNetworkFee = Money.Coins(0.1m) Divisibility = 8
}); });
entity.Price = 5000; entity.Price = 5000;
entity.UpdateTotals(); entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC))); Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(1.1m, accounting.Due); Assert.Equal(1.1m, accounting.Due);
@ -513,10 +515,10 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()), Value = 0.5m,
Rate = 5000, Rate = 5000,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.1m PaymentMethodFee = 0.1m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
@ -527,9 +529,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()), Value = 0.2m,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.1m PaymentMethodFee = 0.1m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
@ -539,9 +541,9 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()), Value = 0.6m,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.1m PaymentMethodFee = 0.1m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
@ -549,75 +551,79 @@ namespace BTCPayServer.Tests
Assert.Equal(1.3m, accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity.Payments.Add( 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(); entity.UpdateTotals();
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue); Assert.Equal(1.3m, accounting.TotalDue);
entity = new InvoiceEntity(); entity = new InvoiceEntity();
entity.Networks = networkProvider;
entity.Price = 5000; entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); entity.Currency = "USD";
paymentMethods.Add( entity.Rates["BTC"] = 1000m;
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); entity.Rates["LTC"] = 500m;
paymentMethods.Add( PaymentPromptDictionary paymentMethods =
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) }); [
entity.SetPaymentMethods(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.Payments = new List<PaymentEntity>();
entity.UpdateTotals(); entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(BTC);
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(5.1m, accounting.Due); Assert.Equal(5.1m, accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(LTC);
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(10.01m, accounting.TotalDue); Assert.Equal(10.01m, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
PaymentMethodId = BTC,
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Value = 1.0m,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.1m PaymentMethodFee = 0.1m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(4.2m, accounting.Due); 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(1.0m, accounting.Paid);
Assert.Equal(5.2m, accounting.TotalDue); Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due); 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(2.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
PaymentMethodId = LTC,
Currency = "LTC", Currency = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()), Value = 1.0m,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.01m PaymentMethodFee = 0.01m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due); 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(1.5m, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due); 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(3.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue); Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); 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); var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
entity.Payments.Add(new PaymentEntity() entity.Payments.Add(new PaymentEntity()
{ {
PaymentMethodId = BTC,
Currency = "BTC", Currency = "BTC",
Output = new TxOut(Money.Coins(remaining), new Key()), Value = remaining,
Accounted = true, Status = PaymentStatus.Settled,
NetworkFee = 0.1m PaymentMethodFee = 0.1m
}); });
entity.UpdateTotals(); entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due); 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(1.5m + remaining, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired); Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); paymentMethod = entity.GetPaymentPrompt(PaymentTypes.CHAIN.GetPaymentMethodId("LTC"));
accounting = paymentMethod.Calculate(); accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due); 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); Assert.Equal(3.0m + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid // Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */, Assert.Equal(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() public void CanAcceptInvoiceWithTolerance()
{ {
var networkProvider = CreateNetworkProvider(ChainName.Regtest); var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity(); var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = networkProvider;
#pragma warning disable CS0618 #pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>(); entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod() entity.Rates["BTC"] = 5000m;
entity.SetPaymentPrompt(BTC, new PaymentPrompt()
{ {
Currency = "BTC", Currency = "BTC",
Rate = 5000, PaymentMethodFee = 0.1m,
NextNetworkFee = Money.Coins(0.1m) Divisibility = 8
}); });
entity.Price = 5000; entity.Price = 5000;
entity.PaymentTolerance = 0; entity.PaymentTolerance = 0;
entity.UpdateTotals(); entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike); var paymentMethod = entity.GetPaymentPrompts().TryGet(PaymentTypes.CHAIN.GetPaymentMethodId("BTC"));
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
Assert.Equal(1.1m, accounting.Due); Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue); Assert.Equal(1.1m, accounting.TotalDue);
@ -2144,63 +2151,48 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var networkBTC = networkProvider.GetNetwork("BTC"); var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC"); var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity(); InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Networks = networkProvider; invoiceEntity.Currency = "USD";
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>(); invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100; invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); invoiceEntity.Rates.Add("BTC", 10513.44m);
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, } invoiceEntity.Rates.Add("LTC", 216.79m);
.SetPaymentMethodDetails( PaymentPromptDictionary paymentMethods =
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() [
{ new () { PaymentMethodId = BTC, Divisibility = 8, Currency = "BTC", PaymentMethodFee = 0.00000100m, ParentEntity = invoiceEntity },
NextNetworkFee = Money.Coins(0.00000100m), new () { PaymentMethodId = LTC, Divisibility = 8, Currency = "LTC", PaymentMethodFee = 0.00010000m, ParentEntity = invoiceEntity },
DepositAddress = dummy ];
})); invoiceEntity.SetPaymentPrompts(paymentMethods);
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);
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var btc = invoiceEntity.GetPaymentPrompt(btcId);
var accounting = btc.Calculate(); var accounting = btc.Calculate();
invoiceEntity.Payments.Add( invoiceEntity.Payments.Add(
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Status = PaymentStatus.Settled,
Currency = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, PaymentMethodFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"), Value = 0.00151263m,
} PaymentMethodId = btcId
.SetCryptoPaymentData(new BitcoinLikePaymentData() });
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
invoiceEntity.UpdateTotals(); invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
invoiceEntity.Payments.Add( invoiceEntity.Payments.Add(
new PaymentEntity() new PaymentEntity()
{ {
Accounted = true, Status = PaymentStatus.Settled,
Currency = "BTC", Currency = "BTC",
NetworkFee = 0.00000100m, Value = accounting.Due,
Network = networkProvider.GetNetwork("BTC") PaymentMethodFee = 0.00000100m,
} PaymentMethodId = btcId
.SetCryptoPaymentData(new BitcoinLikePaymentData() });
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
}));
invoiceEntity.UpdateTotals(); invoiceEntity.UpdateTotals();
accounting = btc.Calculate(); accounting = btc.Calculate();
Assert.Equal(0.0m, accounting.Due); Assert.Equal(0.0m, accounting.Due);
Assert.Equal(0.0m, accounting.DueUncapped); 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(); accounting = ltc.Calculate();
Assert.Equal(0.0m, accounting.Due); Assert.Equal(0.0m, accounting.Due);
@ -2248,42 +2240,172 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Null(metadata.PosData); 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] [Fact]
public void CanParseInvoiceEntityDerivationStrategies() public void CanParseInvoiceEntityDerivationStrategies()
{ {
var serializer = BlobSerializer.CreateSerializer(new NBXplorer.NBXplorerNetworkProvider(ChainName.Regtest).GetBTC()).Serializer;
// We have 3 ways of serializing the derivation strategies: // We have 3 ways of serializing the derivation strategies:
// through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject // through "derivationStrategy", through "derivationStrategies" as a string, through "derivationStrategies" as JObject
// Let's check that InvoiceEntity is similar in all cases. // Let's check that InvoiceEntity is similar in all cases.
var legacy = new JObject() var legacy = new JObject()
{ {
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf" ["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]"
}; };
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC); var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true }); Assert.True(scheme.AccountDerivation is P2SHDerivationStrategy);
scheme.Source = "ManualDerivationScheme"; scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"; scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf-[p2sh]";
var legacy2 = new JObject() var legacy2 = new JObject()
{ {
["derivationStrategies"] = scheme.ToJson() ["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
}; };
var newformat = new JObject() var newformat = new JObject()
{ {
["derivationStrategies"] = JObject.Parse(scheme.ToJson()) ["derivationStrategies"] = new JObject()
{
["BTC"] = JToken.FromObject(scheme, serializer)
}
}; };
//new BTCPayNetworkProvider(ChainName.Regtest) //new BTCPayNetworkProvider(ChainName.Regtest)
#pragma warning disable CS0618 // Type or member is obsolete
var formats = new[] { legacy, legacy2, newformat } var formats = new[] { legacy, legacy2, newformat }
.Select(o => .Select(o =>
{ {
var entity = JsonConvert.DeserializeObject<InvoiceEntity>(o.ToString()); o.Add("currency", "USD");
entity.Networks = CreateNetworkProvider(ChainName.Regtest); o.Add("price", "0.0");
return entity.DerivationStrategies.ToString(); 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(); .ToHashSet();
#pragma warning restore CS0618 // Type or member is obsolete var v = Assert.Single(formats);
Assert.Single(formats); Assert.NotNull(v);
} }
[Fact] [Fact]
@ -2292,25 +2414,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var pmi = "\"BTC_hasjdfhasjkfjlajn\""; var pmi = "\"BTC_hasjdfhasjkfjlajn\"";
JsonTextReader reader = new(new StringReader(pmi)); JsonTextReader reader = new(new StringReader(pmi));
reader.Read(); reader.Read();
Assert.Null(new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null, Assert.Equal("BTC-hasjdfhasjkfjlajn", new PaymentMethodIdJsonConverter().ReadJson(reader, typeof(PaymentMethodId), null,
JsonSerializer.CreateDefault())); JsonSerializer.CreateDefault()).ToString());
}
[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);
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
@ -18,9 +19,11 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.PayJoin.Sender; using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
@ -29,6 +32,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Operations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient; using NBitpayClient;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
@ -148,15 +152,6 @@ namespace BTCPayServer.Tests
await storeController.GeneralSettings(settings); 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) public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
{ {
var storeController = GetController<UIStoresController>(); var storeController = GetController<UIStoresController>();
@ -164,6 +159,7 @@ namespace BTCPayServer.Tests
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model; WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
modify(walletSettings); modify(walletSettings);
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult(); storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
} }
public T GetController<T>(bool setImplicitStore = true) where T : Controller public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -295,7 +291,7 @@ namespace BTCPayServer.Tests
var storeController = GetController<UIStoresController>(); var storeController = GetController<UIStoresController>();
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant); 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 }; var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
await storeController.SetupLightningNode(storeId ?? StoreId, await storeController.SetupLightningNode(storeId ?? StoreId,
@ -373,8 +369,9 @@ namespace BTCPayServer.Tests
var pjClient = parent.PayTester.GetService<PayjoinClient>(); var pjClient = parent.PayTester.GetService<PayjoinClient>();
var storeRepository = parent.PayTester.GetService<StoreRepository>(); var storeRepository = parent.PayTester.GetService<StoreRepository>();
var store = await storeRepository.FindStore(StoreId); var store = await storeRepository.FindStore(StoreId);
var settings = store.GetSupportedPaymentMethods(parent.NetworkProvider).OfType<DerivationSchemeSettings>() var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(psbt.Network.NetworkSet.CryptoCode);
.First(); var handlers = parent.PayTester.GetService<PaymentMethodHandlerDictionary>();
var settings = store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers);
TestLogs.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}"); TestLogs.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError) if (expectedError is null && !senderError)
{ {
@ -491,7 +488,7 @@ namespace BTCPayServer.Tests
public class DummyStoreWebhookEvent : StoreWebhookEvent public class DummyStoreWebhookEvent : StoreWebhookEvent
{ {
} }
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>(); public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
@ -576,9 +573,10 @@ retry:
public async Task<uint256> PayOnChain(string invoiceId) public async Task<uint256> PayOnChain(string invoiceId)
{ {
var cryptoCode = "BTC"; var cryptoCode = "BTC";
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
var client = await CreateClient(); var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId); 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 address = method.Destination;
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest() var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
{ {
@ -601,7 +599,7 @@ retry:
var cryptoCode = "BTC"; var cryptoCode = "BTC";
var client = await CreateClient(); var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId); 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; var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING"); TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11); await parent.CustomerLightningD.Pay(bolt11);
@ -615,7 +613,7 @@ retry:
var network = SupportedNetwork.NBitcoinNetwork; var network = SupportedNetwork.NBitcoinNetwork;
var client = await CreateClient(); var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId); 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 lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
var http = new HttpClient(); var http = new HttpClient();
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http); 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 dbContext = this.parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection(); var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
await db.OpenAsync(); 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")) 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) foreach (var invoice in oldInvoices)
{ {
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId); if (isHeader)
await writer.WriteLineAsync(localInvoice); {
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(); await writer.FlushAsync();
} }
isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER")) using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
{ {
foreach (var invoice in oldPayments) 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.PayButton;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers; using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Security.Greenfield; using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services; using BTCPayServer.Services;
@ -69,6 +70,7 @@ using NBXplorer.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema;
using Npgsql;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
@ -377,14 +379,16 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC"); await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning); await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never); 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")); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () => await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{ {
await tester.ExplorerNode.SendToAddressAsync( await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m)); BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m), new NBitcoin.RPC.SendToAddressParameters()
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance); {
await tester.ExplorerNode.GenerateAsync(1); Replaceable = false
});
}, e => e.InvoiceId == invoice.Id && e.PaymentMethodId == PaymentTypes.LN.GetPaymentMethodId("BTC"));
Invoice newInvoice = null; Invoice newInvoice = null;
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
@ -560,7 +564,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.GrantAccess(); acc.GrantAccess();
acc.RegisterDerivationScheme("BTC"); 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 var invoice = acc.BitPay.CreateInvoice(new Invoice
{ {
Price = 5.0m, Price = 5.0m,
@ -1043,16 +1047,17 @@ namespace BTCPayServer.Tests
tx1.ToString(), tx1.ToString(),
}).Result["txid"].Value<string>()); }).Result["txid"].Value<string>());
TestLogs.LogInformation($"Bumped with {tx1Bump}"); TestLogs.LogInformation($"Bumped with {tx1Bump}");
var handler = tester.PayTester.GetService<PaymentMethodHandlerDictionary>().GetBitcoinHandler("BTC");
await TestUtils.EventuallyAsync(async () => await TestUtils.EventuallyAsync(async () =>
{ {
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id); 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(); var payments = invoiceEntity.GetPayments(false).ToArray();
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash); Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
Assert.False(payments[0].Accounted); 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.True(payments[1].Accounted);
Assert.Equal(0.0m, payments[1].NetworkFee); Assert.Equal(0.0m, payments[1].PaymentMethodFee);
invoice = user.BitPay.GetInvoice(invoice.Id); invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid); Assert.Equal(payment1, invoice.BtcPaid);
Assert.Equal("paid", invoice.Status); Assert.Equal("paid", invoice.Status);
@ -1085,8 +1090,8 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await user.GetController<UIInvoiceController>().Invoice(invoice.Id)).Model) Assert.IsType<ViewResult>(await user.GetController<UIInvoiceController>().Invoice(invoice.Id)).Model)
.Payments; .Payments;
Assert.Single(payments); Assert.Single(payments);
var paymentData = payments.First().GetCryptoPaymentData() as BitcoinLikePaymentData; var paymentData = payments.First().Details;
Assert.NotNull(paymentData.KeyPath); Assert.NotNull(paymentData["keyPath"]);
} }
[Fact(Timeout = LongRunningTestTimeout)] [Fact(Timeout = LongRunningTestTimeout)]
@ -1339,12 +1344,10 @@ namespace BTCPayServer.Tests
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0]; var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
var paid = btcSent; var paid = btcSent;
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network); 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)) var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc] .GetPaymentPrompt(btc)
.GetPaymentMethodDetails() .PaymentMethodFee;
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
if (networkFeeMode != NetworkFeeMode.Always) if (networkFeeMode != NetworkFeeMode.Always)
{ {
networkFee = 0.0m; networkFee = 0.0m;
@ -1364,7 +1367,7 @@ namespace BTCPayServer.Tests
Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString()); Assert.Equal("False", bitpayinvoice.ExceptionStatus.ToString());
// Check if we index by price correctly once we know it // 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); Assert.Contains(invoices, inv => inv.Id == bitpayinvoice.Id);
} }
catch (JsonSerializationException) catch (JsonSerializationException)
@ -1492,15 +1495,15 @@ namespace BTCPayServer.Tests
await user.RegisterLightningNodeAsync("BTC"); await user.RegisterLightningNodeAsync("BTC");
var lnMethod = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(); var lnMethod = PaymentTypes.LN.GetPaymentMethodId("BTC").ToString();
var btcMethod = new PaymentMethodId("BTC", PaymentTypes.BTCLike).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 // 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 var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model); .IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
Assert.Equal(2, vm.PaymentMethodCriteria.Count); Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); 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.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm) Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1518,8 +1521,8 @@ namespace BTCPayServer.Tests
}, Facade.Merchant); }, Facade.Merchant);
// LN and LNURL // LN and LNURL
Assert.Equal(2, invoice.CryptoInfo.Length); Assert.Equal(2, invoice.CryptoInfo.Length);
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LNURLPay.ToString()); Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LNURL");
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LightningLike.ToString()); Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == "BTC-LN");
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963 // 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 // 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); user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>(); var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria); 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.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm) Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)
@ -1652,14 +1655,14 @@ namespace BTCPayServer.Tests
Currency = "USD" Currency = "USD"
}, Facade.Merchant); }, Facade.Merchant);
Assert.Single(invoice.CryptoInfo); 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. // Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode); user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true); user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>(); vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria); 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); Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm).Result);
// However, creating an invoice should show LNURL // However, creating an invoice should show LNURL
@ -1713,7 +1716,7 @@ namespace BTCPayServer.Tests
public async Task CanChangeNetworkFeeMode() public async Task CanChangeNetworkFeeMode()
{ {
using var tester = CreateServerTester(); using var tester = CreateServerTester();
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike); var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
await tester.StartAsync(); await tester.StartAsync();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
@ -1733,10 +1736,7 @@ namespace BTCPayServer.Tests
FullNotifications = true FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) var nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc] .GetPaymentPrompt(btc).PaymentMethodFee;
.GetPaymentMethodDetails()
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
var firstPaymentFee = nextNetworkFee; var firstPaymentFee = nextNetworkFee;
switch (networkFeeMode) switch (networkFeeMode)
{ {
@ -1768,10 +1768,8 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation($"Remaining due after first payment: {due}"); TestLogs.LogInformation($"Remaining due after first payment: {due}");
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid)); Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id)) nextNetworkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
.GetPaymentMethods()[btc] .GetPaymentPrompt(btc)
.GetPaymentMethodDetails() .PaymentMethodFee;
.AssertType<BitcoinLikeOnChainPaymentMethod>()
.GetNextNetworkFee();
switch (networkFeeMode) switch (networkFeeMode)
{ {
case NetworkFeeMode.Never: case NetworkFeeMode.Never:
@ -1942,10 +1940,10 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>(); var repo = tester.PayTester.GetService<InvoiceRepository>();
var entity = (await repo.GetInvoice(invoice6.Id)); var entity = (await repo.GetInvoice(invoice6.Id));
Assert.Equal((decimal)ulong.MaxValue, entity.Price); 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 // Shouldn't be possible as we clamp the value, but existing invoice may have that
entity.Price = decimal.MaxValue; 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 => var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) == PaymentMethodId.Parse(model.PaymentMethodId) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance)) PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network); .PaymentLink, tester.ExplorerNode.Network);
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m)); 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 => invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) == PaymentMethodId.Parse(model.PaymentMethodId) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance)) PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network); .PaymentLink, tester.ExplorerNode.Network);
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC))); var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
@ -2028,8 +2026,8 @@ namespace BTCPayServer.Tests
Currency = "BTC", Currency = "BTC",
}); });
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model => invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
PaymentMethodId.Parse(model.PaymentMethod) == PaymentMethodId.Parse(model.PaymentMethodId) ==
new PaymentMethodId("BTC", BitcoinPaymentType.Instance)) PaymentTypes.CHAIN.GetPaymentMethodId("BTC"))
.PaymentLink, tester.ExplorerNode.Network); .PaymentLink, tester.ExplorerNode.Network);
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m)); 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(); var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount); Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC")); 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(() => TestUtils.Eventually(() =>
{ {
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() 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 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(() => TestUtils.Eventually(() =>
{ {
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
@ -2268,7 +2258,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() => TestUtils.Eventually(() =>
{ {
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); 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(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value); Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
}); });
@ -2289,7 +2279,7 @@ namespace BTCPayServer.Tests
c => c =>
{ {
Assert.False(c.AfterExpiration); 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.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id); Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2299,7 +2289,7 @@ namespace BTCPayServer.Tests
c => c =>
{ {
Assert.False(c.AfterExpiration); 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.NotNull(c.Payment);
Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination); Assert.Equal(invoice.BitcoinAddress, c.Payment.Destination);
Assert.StartsWith(txId.ToString(), c.Payment.Id); Assert.StartsWith(txId.ToString(), c.Payment.Id);
@ -2539,7 +2529,9 @@ namespace BTCPayServer.Tests
await RestartMigration(tester); await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
Assert.True(string.IsNullOrEmpty(store.DerivationStrategy)); 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.AccountDerivation.ToString());
Assert.Equal(derivation, v.AccountOriginal.ToString()); Assert.Equal(derivation, v.AccountOriginal.ToString());
Assert.Equal(xpub, v.SigningKey.ToString()); Assert.Equal(xpub, v.SigningKey.ToString());
@ -2547,13 +2539,26 @@ namespace BTCPayServer.Tests
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true); await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); 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()); 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); await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); 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.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 // Test if legacy lightning charge settings are converted to LightningConnectionString
store.DerivationStrategies = new JObject() store.DerivationStrategies = new JObject()
@ -2569,9 +2574,8 @@ namespace BTCPayServer.Tests
}.ToString(); }.ToString();
await tester.PayTester.StoreRepository.UpdateStore(store); await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester); await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); 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()); Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl(); var url = lnMethod.GetExternalLightningUrl();
@ -2596,8 +2600,23 @@ namespace BTCPayServer.Tests
await tester.PayTester.StoreRepository.UpdateStore(store); await tester.PayTester.StoreRepository.UpdateStore(store);
await RestartMigration(tester); await RestartMigration(tester);
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId); 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); 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)] [Fact(Timeout = LongRunningTestTimeout)]
@ -2725,7 +2744,7 @@ namespace BTCPayServer.Tests
serializer.ToString(new Dictionary<string, string>() serializer.ToString(new Dictionary<string, string>()
{ {
{ {
new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(),
new KeyPath("44'/0'/0'").ToString() new KeyPath("44'/0'/0'").ToString()
} }
}))); })));
@ -2756,13 +2775,33 @@ namespace BTCPayServer.Tests
Assert.Empty(blob.AdditionalData); Assert.Empty(blob.AdditionalData);
Assert.Single(blob.PaymentMethodCriteria); Assert.Single(blob.PaymentMethodCriteria);
Assert.Contains(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"); criteria.Above && criteria.Value.Value == 5m && criteria.Value.Currency == "USD");
Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode); Assert.Equal(NetworkFeeMode.Never, blob.NetworkFeeMode);
Assert.Contains(store.GetSupportedPaymentMethods(tester.NetworkProvider), method => var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
method is DerivationSchemeSettings dss && Assert.Contains(store.GetPaymentMethodConfigs(handlers), method =>
method.PaymentId == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) && method.Value is DerivationSchemeSettings dss &&
dss.AccountKeyPath == new KeyPath("44'/0'/0'")); 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) private static async Task RestartMigration(ServerTester tester)
@ -3062,7 +3101,7 @@ namespace BTCPayServer.Tests
// 1 payment on chain // 1 payment on chain
Assert.Equal(4, report.Data.Count); Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress"); 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); Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>()) .GroupBy(d => d[paymentTypeIndex].Value<string>())

View file

@ -216,6 +216,5 @@
</Content> </Content>
</ItemGroup> </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_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>
<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>
</Project> </Project>

View file

@ -1,7 +1,8 @@
@using BTCPayServer.Payments
@using BTCPayServer.Services.Invoices @using BTCPayServer.Services.Invoices
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel @model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary @inject Dictionary<PaymentMethodId, IPaymentModelExtension> Extensions
@{ @{
var state = Model.State.ToString(); var state = Model.State.ToString();
@ -41,16 +42,17 @@
</div> </div>
@if (Model.Payments != null) @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 extension = Extensions.TryGetValue(paymentMethodId, out var e) ? e : null;
var badge = paymentMethodId.PaymentType.GetBadge(); var image = extension?.Image;
var badge = extension?.Badge;
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge)) if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
{ {
<span class="d-inline-flex align-items-center gap-1"> <span class="d-inline-flex align-items-center gap-1">
@if (!string.IsNullOrEmpty(image)) @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)) @if (!string.IsNullOrEmpty(badge))
{ {

View file

@ -16,6 +16,7 @@
@inject PoliciesSettings PoliciesSettings @inject PoliciesSettings PoliciesSettings
@inject ThemeSettings Theme @inject ThemeSettings Theme
@inject PluginService PluginService @inject PluginService PluginService
@inject PrettyNameProvider PrettyName
@model BTCPayServer.Components.MainNav.MainNavViewModel @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}")"> <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 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> </a>
} }
else 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}")"> <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 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> </a>
} }
</li> </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}")"> <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 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> </a>
} }
else 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}")"> <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 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> </a>
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@
{ {
<div class="ct-chart"></div> <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> <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>. 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 System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
@ -29,19 +30,22 @@ public class StoreWalletBalance : ViewComponent
private readonly WalletHistogramService _walletHistogramService; private readonly WalletHistogramService _walletHistogramService;
private readonly BTCPayWalletProvider _walletProvider; private readonly BTCPayWalletProvider _walletProvider;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
public StoreWalletBalance( public StoreWalletBalance(
StoreRepository storeRepo, StoreRepository storeRepo,
CurrencyNameTable currencies, CurrencyNameTable currencies,
WalletHistogramService walletHistogramService, WalletHistogramService walletHistogramService,
BTCPayWalletProvider walletProvider, BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider) BTCPayNetworkProvider networkProvider,
PaymentMethodHandlerDictionary handlers)
{ {
_storeRepo = storeRepo; _storeRepo = storeRepo;
_currencies = currencies; _currencies = currencies;
_walletProvider = walletProvider; _walletProvider = walletProvider;
_walletHistogramService = walletHistogramService;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_walletHistogramService = walletHistogramService;
_handlers = handlers;
} }
public async Task<IViewComponentResult> InvokeAsync(StoreData store) public async Task<IViewComponentResult> InvokeAsync(StoreData store)
@ -71,11 +75,12 @@ public class StoreWalletBalance : ViewComponent
{ {
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(3)); using CancellationTokenSource cts = new(TimeSpan.FromSeconds(3));
var wallet = _walletProvider.GetWallet(_networkProvider.DefaultNetwork); 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) if (derivation is not null)
{ {
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token); 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 public class WalletNav : ViewComponent
{ {
private readonly BTCPayWalletProvider _walletProvider; private readonly BTCPayWalletProvider _walletProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly UIWalletsController _walletsController; private readonly UIWalletsController _walletsController;
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
@ -33,12 +34,14 @@ namespace BTCPayServer.Components.WalletNav
public WalletNav( public WalletNav(
BTCPayWalletProvider walletProvider, BTCPayWalletProvider walletProvider,
PaymentMethodHandlerDictionary handlers,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
UIWalletsController walletsController, UIWalletsController walletsController,
CurrencyNameTable currencies, CurrencyNameTable currencies,
RateFetcher rateFetcher) RateFetcher rateFetcher)
{ {
_walletProvider = walletProvider; _walletProvider = walletProvider;
_handlers = handlers;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_walletsController = walletsController; _walletsController = walletsController;
_currencies = currencies; _currencies = currencies;
@ -51,7 +54,7 @@ namespace BTCPayServer.Components.WalletNav
var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode); var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var wallet = _walletProvider.GetWallet(network); var wallet = _walletProvider.GetWallet(network);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency; 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 var balance = await wallet.GetBalance(derivation?.AccountDerivation) switch
{ {
{ Available: null, Total: var total } => total, { Available: null, Total: var total } => total,

View file

@ -26,12 +26,15 @@ namespace BTCPayServer.Controllers
public class BitpayInvoiceController : Controller public class BitpayInvoiceController : Controller
{ {
private readonly UIInvoiceController _InvoiceController; private readonly UIInvoiceController _InvoiceController;
private readonly Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> _bitpayExtensions;
private readonly InvoiceRepository _InvoiceRepository; private readonly InvoiceRepository _InvoiceRepository;
public BitpayInvoiceController(UIInvoiceController invoiceController, public BitpayInvoiceController(UIInvoiceController invoiceController,
Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension> bitpayExtensions,
InvoiceRepository invoiceRepository) InvoiceRepository invoiceRepository)
{ {
_InvoiceController = invoiceController; _InvoiceController = invoiceController;
_bitpayExtensions = bitpayExtensions;
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
} }
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
})).FirstOrDefault(); })).FirstOrDefault();
if (invoice == null) if (invoice == null)
throw new BitpayHttpException(404, "Object not found"); throw new BitpayHttpException(404, "Object not found");
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO()); return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO(_bitpayExtensions, Url));
} }
[HttpGet] [HttpGet]
[Route("invoices")] [Route("invoices")]
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
}; };
var entities = (await _InvoiceRepository.GetInvoices(query)) var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray(); .Select((o) => o.EntityToDTO(_bitpayExtensions, Url)).ToArray();
return Json(DataWrapper.Create(entities)); return Json(DataWrapper.Create(entities));
} }
@ -100,7 +103,7 @@ namespace BTCPayServer.Controllers
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null) CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
{ {
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator); 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" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
@ -178,7 +181,10 @@ namespace BTCPayServer.Controllers
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
} }
entity.PaymentTolerance = storeBlob.PaymentTolerance; 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; entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
return await _InvoiceController.CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator); 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.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -29,7 +32,9 @@ namespace BTCPayServer.Controllers
readonly RateFetcher _rateProviderFactory; readonly RateFetcher _rateProviderFactory;
readonly BTCPayNetworkProvider _networkProvider; readonly BTCPayNetworkProvider _networkProvider;
readonly CurrencyNameTable _currencyNameTable; readonly CurrencyNameTable _currencyNameTable;
private readonly PaymentMethodHandlerDictionary _handlers;
readonly StoreRepository _storeRepo; readonly StoreRepository _storeRepo;
private readonly InvoiceRepository _invoiceRepository;
private StoreData CurrentStore => HttpContext.GetStoreData(); private StoreData CurrentStore => HttpContext.GetStoreData();
@ -37,12 +42,16 @@ namespace BTCPayServer.Controllers
RateFetcher rateProviderFactory, RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo, StoreRepository storeRepo,
CurrencyNameTable currencyNameTable) InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
PaymentMethodHandlerDictionary handlers)
{ {
_rateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory)); _rateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_networkProvider = networkProvider; _networkProvider = networkProvider;
_storeRepo = storeRepo; _storeRepo = storeRepo;
_invoiceRepository = invoiceRepository;
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_handlers = handlers;
} }
[Route("rates/{baseCurrency}")] [Route("rates/{baseCurrency}")]
@ -50,11 +59,17 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint] [BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string cryptoCode = null, CancellationToken cancellationToken = default) public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string cryptoCode = null, CancellationToken cancellationToken = default)
{ {
var supportedMethods = CurrentStore.GetSupportedPaymentMethods(_networkProvider); var inv = _invoiceRepository.CreateNewInvoice(CurrentStore.Id);
inv.Currency = baseCurrency;
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode)) var ctx = new InvoiceCreationContext(CurrentStore, CurrentStore.GetStoreBlob(), inv, new Logging.InvoiceLogs(), _handlers, null);
.Select(method => method.PaymentId.CryptoCode).Distinct(); 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 currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, null, cryptoCode, cancellationToken); var result = await GetRates2(currencypairs, null, cryptoCode, cancellationToken);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,14 +39,14 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors( public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
string storeId, string? paymentMethod) string storeId, string? paymentMethod)
{ {
paymentMethod = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod).ToString() : null; var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod) : null;
var configured = var configured =
await _payoutProcessorService.GetProcessors( await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = paymentMethod is null ? null : new[] { paymentMethod } PaymentMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
}); });
return Ok(configured.Select(ToModel).ToArray()); 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"); ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000");
if (!ModelState.IsValid) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString(); var paymentMethodId = PaymentMethodId.Parse(paymentMethod);
var activeProcessor = var activeProcessor =
(await _payoutProcessorService.GetProcessors( (await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] { paymentMethod } PaymentMethods = new[] { paymentMethodId }
})) }))
.FirstOrDefault(); .FirstOrDefault();
activeProcessor ??= new PayoutProcessorData(); activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(FromModel(request)); activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(FromModel(request));
activeProcessor.StoreId = storeId; activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethod; activeProcessor.PaymentMethod = paymentMethodId.ToString();
activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName; activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated() _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.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers.Greenfield namespace BTCPayServer.Controllers.Greenfield
{ {
public partial class GreenfieldStoreOnChainPaymentMethodsController public partial class GreenfieldStoreOnChainPaymentMethodsController
{ {
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [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)] [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) GenerateWalletRequest request)
{ {
AssertCryptoCodeWallet(cryptoCode, out var network, out _); AssertCryptoCodeWallet(paymentMethodId, out var network, out _);
if (!_walletProvider.IsAvailable(network)) if (!_walletProvider.IsAvailable(network))
{ {
return this.CreateAPIError(503, "not-available", return this.CreateAPIError(503, "not-available",
$"{cryptoCode} services are not currently available"); $"{paymentMethodId} services are not currently available");
} }
var method = GetExistingBtcLikePaymentMethod(cryptoCode); if (IsConfigured(paymentMethodId, out _))
if (method != null)
{ {
return this.CreateAPIError("already-configured", 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(); var canUseHotWallet = await CanUseHotWallet();
@ -64,13 +67,13 @@ namespace BTCPayServer.Controllers.Greenfield
if (response == null) if (response == null)
{ {
return this.CreateAPIError(503, "not-available", return this.CreateAPIError(503, "not-available",
$"{cryptoCode} services are not currently available"); $"{paymentMethodId} services are not currently available");
} }
} }
catch (Exception e) catch (Exception e)
{ {
return this.CreateAPIError(503, "not-available", return this.CreateAPIError(503, "not-available",
$"{cryptoCode} error: {e.Message}"); $"{paymentMethodId} error: {e.Message}");
} }
var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network); var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network);
@ -86,16 +89,22 @@ namespace BTCPayServer.Controllers.Greenfield
var store = Store; var store = Store;
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike), var handler = _handlers[paymentMethodId];
store.SetPaymentMethodConfig(_handlers[paymentMethodId],
derivationSchemeSettings); derivationSchemeSettings);
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store); await _storeRepository.UpdateStore(store);
var rawResult = GetExistingBtcLikePaymentMethod(cryptoCode, store);
var result = new OnChainPaymentMethodDataWithSensitiveData(rawResult.CryptoCode, rawResult.DerivationScheme, var result = new GenerateOnChainWalletResponse()
rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic(), derivationSchemeSettings.PaymentId.ToStringNormalized()); {
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() _eventAggregator.Publish(new WalletChangedEvent()
{ {
WalletId = new WalletId(storeId, cryptoCode) WalletId = new WalletId(storeId, network.CryptoCode)
}); });
return Ok(result); return Ok(result);
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
@ -9,8 +10,11 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -19,6 +23,9 @@ using Microsoft.AspNetCore.Mvc;
using NBitcoin; using NBitcoin;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer.Models; 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; using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.Greenfield namespace BTCPayServer.Controllers.Greenfield
@ -33,71 +40,28 @@ namespace BTCPayServer.Controllers.Greenfield
public PoliciesSettings PoliciesSettings { get; } public PoliciesSettings PoliciesSettings { get; }
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly BTCPayWalletProvider _walletProvider; private readonly BTCPayWalletProvider _walletProvider;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly ExplorerClientProvider _explorerClientProvider; private readonly ExplorerClientProvider _explorerClientProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
public GreenfieldStoreOnChainPaymentMethodsController( public GreenfieldStoreOnChainPaymentMethodsController(
StoreRepository storeRepository, StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
BTCPayWalletProvider walletProvider, BTCPayWalletProvider walletProvider,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ExplorerClientProvider explorerClientProvider, ExplorerClientProvider explorerClientProvider,
PoliciesSettings policiesSettings, PoliciesSettings policiesSettings,
PaymentMethodHandlerDictionary handlers,
EventAggregator eventAggregator) EventAggregator eventAggregator)
{ {
_storeRepository = storeRepository; _storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_walletProvider = walletProvider; _walletProvider = walletProvider;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
PoliciesSettings = policiesSettings; PoliciesSettings = policiesSettings;
} _handlers = handlers;
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);
} }
protected JsonHttpException ErrorPaymentMethodNotConfigured() protected JsonHttpException ErrorPaymentMethodNotConfigured()
@ -106,86 +70,64 @@ namespace BTCPayServer.Controllers.Greenfield
} }
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.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( public IActionResult GetOnChainPaymentMethodPreview(
string storeId, string storeId,
string cryptoCode, [ModelBinder(typeof(PaymentMethodIdModelBinder))]
int offset = 0, int amount = 10) PaymentMethodId paymentMethodId,
int offset = 0, int count = 10)
{ {
AssertCryptoCodeWallet(cryptoCode, out var network, out _); AssertCryptoCodeWallet(paymentMethodId, out var network, out _);
if (!IsConfigured(paymentMethodId, out var settings))
var paymentMethod = GetExistingBtcLikePaymentMethod(cryptoCode);
if (string.IsNullOrEmpty(paymentMethod?.DerivationScheme))
{ {
throw ErrorPaymentMethodNotConfigured(); throw ErrorPaymentMethodNotConfigured();
} }
return Ok(GetPreviewResultData(offset, count, network, settings.AccountDerivation));
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);
}
} }
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")] [HttpPost("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/preview")]
public IActionResult GetProposedOnChainPaymentMethodPreview( public async Task<IActionResult> GetProposedOnChainPaymentMethodPreview(
string storeId, string storeId,
string cryptoCode, [ModelBinder(typeof(PaymentMethodIdModelBinder))]
[FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData, PaymentMethodId paymentMethodId,
int offset = 0, int amount = 10) [FromBody] UpdatePaymentMethodRequest request = null,
int offset = 0, int count = 10)
{ {
AssertCryptoCodeWallet(cryptoCode, out var network, out _); if (request is null)
if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme))
{ {
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme), ModelState.AddModelError(nameof(request), "Missing body");
"Missing derivationScheme"); 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) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); 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 deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.GetLineFor(deposit); var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData(); var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++) for (var i = offset; i < count; i++)
{ {
var derivation = line.Derive((uint)i); var derivation = line.Derive((uint)i);
result.Addresses.Add( result.Addresses.Add(
@ -195,120 +137,32 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
KeyPath = deposit.GetKeyPath((uint)i).ToString(), KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey) network.NBXplorerNetwork.CreateAddress(strategy, deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
.ToString() .ToString()
}); });
} }
return result;
return Ok(result);
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] private void AssertCryptoCodeWallet(PaymentMethodId paymentMethodId, out BTCPayNetwork network, out BTCPayWallet wallet)
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}")]
public async Task<IActionResult> RemoveOnChainPaymentMethod(
string storeId,
string cryptoCode,
int offset = 0, int amount = 10)
{ {
AssertCryptoCodeWallet(cryptoCode, out _, out _); 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"));
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); network = handler.Network;
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"));
wallet = _walletProvider.GetWallet(network); wallet = _walletProvider.GetWallet(network);
if (wallet is null) if (wallet is null)
throw ErrorPaymentMethodNotConfigured(); throw ErrorPaymentMethodNotConfigured();
} }
private OnChainPaymentMethodData GetExistingBtcLikePaymentMethod(string cryptoCode, StoreData store = null) bool IsConfigured(PaymentMethodId paymentMethodId, [MaybeNullWhen(false)] out DerivationSchemeSettings settings)
{ {
store ??= Store; var store = Store;
var storeBlob = store.GetStoreBlob(); var conf = store.GetPaymentMethodConfig(paymentMethodId);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); settings = null;
var paymentMethod = store if (conf is (null or { Type: JTokenType.Null }))
.GetSupportedPaymentMethods(_btcPayNetworkProvider) return false;
.OfType<DerivationSchemeSettings>() settings = ((BitcoinLikePaymentHandler)_handlers[paymentMethodId]).ParsePaymentMethodConfig(conf);
.FirstOrDefault(method => method.PaymentId == id); return settings?.AccountDerivation is not null;
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());
} }
} }
} }

View file

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

View file

@ -6,11 +6,18 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using BTCPayServer.Abstractions.Extensions;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
using BTCPayServer.ModelBinders;
using BTCPayServer.Payments;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers.Greenfield namespace BTCPayServer.Controllers.Greenfield
{ {
@ -20,37 +27,138 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldStorePaymentMethodsController : ControllerBase public class GreenfieldStorePaymentMethodsController : ControllerBase
{ {
private StoreData Store => HttpContext.GetStoreData(); private StoreData Store => HttpContext.GetStoreData();
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly StoreRepository _storeRepository;
private readonly IAuthorizationService _authorizationService; 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; _authorizationService = authorizationService;
} }
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods")] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}")]
public async Task<ActionResult<Dictionary<string, GenericPaymentMethodData>>> GetStorePaymentMethods( public async Task<IActionResult> GetStorePaymentMethod(
string storeId, 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 storeBlob = Store.GetStoreBlob();
var excludedPaymentMethods = storeBlob.GetExcludedPaymentMethods(); var excludedPaymentMethods = storeBlob.GetExcludedPaymentMethods();
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded; if (includeConfig is true)
; {
return Ok(Store.GetSupportedPaymentMethods(_btcPayNetworkProvider) var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
.Where(method => new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
enabled is null || (enabled is false && excludedPaymentMethods.Match(method.PaymentId))) if (!canModifyStore)
.ToDictionary( return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
method => method.PaymentId.ToStringNormalized(), }
return Ok(Store.GetPaymentMethodConfigs(_handlers, onlyEnabled is true)
.Select(
method => new GenericPaymentMethodData() method => new GenericPaymentMethodData()
{ {
CryptoCode = method.PaymentId.CryptoCode, PaymentMethodId = method.Key.ToString(),
Enabled = enabled.GetValueOrDefault(!excludedPaymentMethods.Match(method.PaymentId)), Enabled = onlyEnabled.GetValueOrDefault(!excludedPaymentMethods.Match(method.Key)),
Data = method.PaymentId.PaymentType.GetGreenfieldData(method, canModifyStore) 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.Abstractions.Constants;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { processor }, Processors = new[] { processor },
PaymentMethods = new[] { paymentMethod } PaymentMethods = new[] { PaymentMethodId.Parse(paymentMethod) }
})).FirstOrDefault(); })).FirstOrDefault();
if (matched is null) if (matched is null)
{ {

View file

@ -124,7 +124,7 @@ namespace BTCPayServer.Controllers.Greenfield
Archived = data.Archived, Archived = data.Archived,
SupportUrl = storeBlob.StoreSupportUrl, SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy, SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(), DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToString(),
//blob //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 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 //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, Above = criteria.Above,
Amount = criteria.Value.Value, Amount = criteria.Value.Value,
CurrencyCode = criteria.Value.Currency, CurrencyCode = criteria.Value.Currency,
PaymentMethod = criteria.PaymentMethod.ToStringNormalized() PaymentMethod = criteria.PaymentMethod.ToString()
}).ToList() ?? new List<PaymentMethodCriteriaData>() }).ToList() ?? new List<PaymentMethodCriteriaData>()
}; };
} }

View file

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData; using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
@ -559,54 +560,21 @@ namespace BTCPayServer.Controllers.Greenfield
return result.Value ?? GetFromActionResult<T>(result.Result); return result.Value ?? GetFromActionResult<T>(result.Result);
} }
public override Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId, public override async Task<OnChainPaymentMethodPreviewResultData> PreviewProposedStoreOnChainPaymentMethodAddresses(
bool? enabled, CancellationToken token = default) string storeId, string paymentMethodId,
{ string derivationScheme, int offset = 0, int count = 10,
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,
CancellationToken token = default) CancellationToken token = default)
{ {
HandleActionResult(await GetController<GreenfieldStoreOnChainPaymentMethodsController>().RemoveOnChainPaymentMethod(storeId, cryptoCode)); return GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
} await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetProposedOnChainPaymentMethodPreview(storeId, Payments.PaymentMethodId.Parse(paymentMethodId),
new UpdatePaymentMethodRequest() { Config = JValue.CreateString(derivationScheme) }, offset, count));
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)));
} }
public override Task<OnChainPaymentMethodPreviewResultData> PreviewStoreOnChainPaymentMethodAddresses( 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>( return Task.FromResult(GetFromActionResult<OnChainPaymentMethodPreviewResultData>(
GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethodPreview(storeId, cryptoCode, offset, GetController<GreenfieldStoreOnChainPaymentMethodsController>().GetOnChainPaymentMethodPreview(storeId, Payments.PaymentMethodId.Parse(paymentMethodId), offset,
amount))); amount)));
} }
@ -814,71 +782,6 @@ namespace BTCPayServer.Controllers.Greenfield
return GetFromActionResult<StoreData>(await GetController<GreenfieldStoresController>().UpdateStore(storeId, request)); 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, public override async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string[] orderId = null,
InvoiceStatus[] status = null, InvoiceStatus[] status = null,
DateTimeOffset? startDate = null, DateTimeOffset? startDate = null,
@ -982,18 +885,18 @@ namespace BTCPayServer.Controllers.Greenfield
return Task.FromResult(GetFromActionResult<PermissionMetadata[]>(GetController<UIHomeController>().Permissions())); return Task.FromResult(GetFromActionResult<PermissionMetadata[]>(GetController<UIHomeController>().Permissions()));
} }
public override async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId, public override async Task<GenericPaymentMethodData[]> GetStorePaymentMethods(string storeId,
bool? enabled = null, CancellationToken token = default) 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, public override async Task<GenerateOnChainWalletResponse> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request, string paymentMethodId, GenerateOnChainWalletRequest request,
CancellationToken token = default) CancellationToken token = default)
{ {
return GetFromActionResult<OnChainPaymentMethodDataWithSensitiveData>( return GetFromActionResult<GenerateOnChainWalletResponse>(
await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GenerateOnChainWallet(storeId, cryptoCode, await GetController<GreenfieldStoreOnChainPaymentMethodsController>().GenerateOnChainWallet(storeId, Payments.PaymentMethodId.Parse(paymentMethodId),
new GenerateWalletRequest() new GenerateWalletRequest()
{ {
Passphrase = request.Passphrase, Passphrase = request.Passphrase,

View file

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

View file

@ -32,6 +32,8 @@ using Microsoft.AspNetCore.Routing;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
using Serilog.Filters;
using PeterO.Numbers;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -47,7 +49,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
readonly EventAggregator _EventAggregator; readonly EventAggregator _EventAggregator;
readonly BTCPayNetworkProvider _NetworkProvider; readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService; private readonly PullPaymentHostedService _paymentHostedService;
private readonly LanguageService _languageService; private readonly LanguageService _languageService;
@ -57,6 +59,8 @@ namespace BTCPayServer.Controllers
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly TransactionLinkProviders _transactionLinkProviders; private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly PaymentMethodViewProvider _viewProvider;
private readonly AppService _appService; private readonly AppService _appService;
private readonly IFileService _fileService; private readonly IFileService _fileService;
@ -85,7 +89,9 @@ namespace BTCPayServer.Controllers
AppService appService, AppService appService,
IFileService fileService, IFileService fileService,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders) TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
PaymentMethodViewProvider viewProvider)
{ {
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
@ -96,7 +102,7 @@ namespace BTCPayServer.Controllers
_UserManager = userManager; _UserManager = userManager;
_EventAggregator = eventAggregator; _EventAggregator = eventAggregator;
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _handlers = paymentMethodHandlerDictionary;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_paymentHostedService = paymentHostedService; _paymentHostedService = paymentHostedService;
WebhookNotificationManager = webhookNotificationManager; WebhookNotificationManager = webhookNotificationManager;
@ -107,6 +113,8 @@ namespace BTCPayServer.Controllers
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_transactionLinkProviders = transactionLinkProviders; _transactionLinkProviders = transactionLinkProviders;
_paymentModelExtensions = paymentModelExtensions;
_viewProvider = viewProvider;
_fileService = fileService; _fileService = fileService;
_appService = appService; _appService = appService;
} }
@ -126,7 +134,7 @@ namespace BTCPayServer.Controllers
{ {
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id), OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id),
PaymentRequestId = 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 string.IsNullOrEmpty(prBlob.Email) ? null : prBlob.Email
}.ToJObject(), new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore }); }.ToJObject(), new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore });
@ -169,7 +177,10 @@ namespace BTCPayServer.Controllers
} }
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy; entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage; 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.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
entity.CheckoutType = invoice.Checkout.CheckoutType; entity.CheckoutType = invoice.Checkout.CheckoutType;
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail; entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
@ -217,77 +228,29 @@ namespace BTCPayServer.Controllers
} }
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); 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.Status = InvoiceStatusLegacy.New;
entity.UpdateTotals(); 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, await creationContext.BeforeFetchingRates();
invoicePaymentMethodFilter); await FetchRates(creationContext, cancellationToken);
}
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));
}
}
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider); await creationContext.CreatePaymentPrompts();
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var contexts = creationContext.PaymentMethodContexts
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); .Where(s => s.Value.Status is PaymentMethodContext.ContextStatus.WaitingForActivation or PaymentMethodContext.ContextStatus.Created)
.Select(s => s.Value)
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>(); .ToList();
var paymentMethods = new PaymentMethodDictionary(); if (contexts.Count == 0)
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)
{ {
StringBuilder errors = new StringBuilder(); StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any()) if (!store.GetPaymentMethodConfigs(_handlers).Any())
errors.AppendLine( 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/)"); "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 else
@ -299,9 +262,13 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, errors.ToString()); throw new BitpayHttpException(400, errors.ToString());
} }
entity.SetPaymentPrompts(new PaymentPromptDictionary(contexts.Select(c => c.Prompt)));
} }
entity.SetSupportedPaymentMethods(supported); else
entity.SetPaymentMethods(paymentMethods); {
entity.SetPaymentPrompts(new PaymentPromptDictionary());
}
foreach (var app in await getAppsTaggingStore) foreach (var app in await getAppsTaggingStore)
{ {
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id)); entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
@ -313,151 +280,18 @@ namespace BTCPayServer.Controllers
} }
using (logs.Measure("Saving invoice")) using (logs.Measure("Saving invoice"))
{ {
await _InvoiceRepository.CreateInvoiceAsync(entity, additionalSearchTerms); await _InvoiceRepository.CreateInvoiceAsync(creationContext);
var links = new List<WalletObjectLinkData>(); await creationContext.ActivatingPaymentPrompt();
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);
} }
_ = Task.Run(async () => _ = _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
{
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);
});
_EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created)); _EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created));
return entity; 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 rateRules = context.StoreBlob.GetRateRules(_NetworkProvider);
{ await context.FetchingRates(_RateProvider, rateRules, cancellationToken);
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;
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -72,7 +72,7 @@ public partial class UIReportsController : Controller
var vm = new StoreReportsViewModel var vm = new StoreReportsViewModel
{ {
InvoiceTemplateUrl = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }), 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" }, Request = new StoreReportRequest { ViewName = viewName ?? "Payments" },
AvailableViews = ReportService.ReportProviders AvailableViews = ReportService.ReportProviders
.Values .Values

View file

@ -348,7 +348,7 @@ namespace BTCPayServer.Controllers
return View(settings); return View(settings);
} }
settings.BlockExplorerLinks = settings.BlockExplorerLinks 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) .Where(tuple => tuple.Link is not null)
.ToList(); .ToList();

View file

@ -93,7 +93,7 @@ namespace BTCPayServer.Controllers
CustomCSSLink = "", CustomCSSLink = "",
EmbeddedCSS = "", EmbeddedCSS = "",
PaymentMethodItems = 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); var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
model.PaymentMethodItems = 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.Name ??= string.Empty;
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty; model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
model.PaymentMethods ??= new List<string>(); 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 // 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 // them here to reflect user's selection so that they can correct their mistake
model.PaymentMethodItems = 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"); ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method");
} }
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null) if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
@ -386,7 +386,7 @@ namespace BTCPayServer.Controllers
case "pay": case "pay":
{ {
if (handler is { }) if (handler is { })
return await handler?.InitiatePayment(paymentMethodId, payoutIds); return await handler.InitiatePayment(paymentMethodId, payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Message = "Paying via this payment method is not supported", Message = "Paying via this payment method is not supported",

View file

@ -14,9 +14,12 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -110,6 +113,9 @@ namespace BTCPayServer.Controllers
if (store == null) if (store == null)
return NotFound(); return NotFound();
var network = _ExplorerProvider.GetNetwork(vm.CryptoCode);
var oldConf = _handlers.GetLightningConfig(store, network);
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
if (vm.CryptoCode == null) if (vm.CryptoCode == null)
@ -118,21 +124,13 @@ namespace BTCPayServer.Controllers
return View(vm); 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 (vm.LightningNodeType == LightningNodeType.Internal)
{ {
if (!CanUseInternalLightning(network.CryptoCode)) paymentMethod = new LightningPaymentMethodConfig();
{
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use the internal lightning node");
return View(vm);
}
paymentMethod = new LightningSupportedPaymentMethod
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetInternalNode(); paymentMethod.SetInternalNode();
} }
else else
@ -142,47 +140,26 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
return View(vm); return View(vm);
} }
paymentMethod = new LightningPaymentMethodConfig();
ILightningClient? lightningClient = null; paymentMethod.ConnectionString = vm.ConnectionString;
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);
}
} }
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) switch (command)
{ {
case "save": case "save":
var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay); var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod);
store.SetSupportedPaymentMethod(lnurl, new LNURLPaySupportedPaymentMethod() store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig()
{ {
CryptoCode = vm.CryptoCode,
UseBech32Scheme = true, UseBech32Scheme = true,
LUD12Enabled = false LUD12Enabled = false
}); });
@ -192,10 +169,9 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode }); return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
case "test": case "test":
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try 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(); var hasPublicAddress = info.Any();
if (!vm.SkipPortTest && hasPublicAddress) if (!vm.SkipPortTest && hasPublicAddress)
{ {
@ -228,7 +204,8 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
if (lightning == null) if (lightning == null)
{ {
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings."; TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
@ -240,7 +217,7 @@ namespace BTCPayServer.Controllers
{ {
CryptoCode = cryptoCode, CryptoCode = cryptoCode,
StoreId = storeId, StoreId = storeId,
Enabled = !excludeFilters.Match(lightning.PaymentId), Enabled = !excludeFilters.Match(lnId),
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
@ -248,10 +225,11 @@ namespace BTCPayServer.Controllers
}; };
SetExistingValues(store, vm); 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) if (lnurl != null)
{ {
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurl.PaymentId); vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId);
vm.LNURLBech32Mode = lnurl.UseBech32Scheme; vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
vm.LUD12Enabled = lnurl.LUD12Enabled; vm.LUD12Enabled = lnurl.LUD12Enabled;
} }
@ -280,10 +258,10 @@ namespace BTCPayServer.Controllers
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi; blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints; blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback; blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
var lnurlId = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay); var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
blob.SetExcluded(lnurlId, !vm.LNURLEnabled); 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 || ( if (lnurl is null || (
lnurl.UseBech32Scheme != vm.LNURLBech32Mode || lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
lnurl.LUD12Enabled != vm.LUD12Enabled)) lnurl.LUD12Enabled != vm.LUD12Enabled))
@ -291,9 +269,8 @@ namespace BTCPayServer.Controllers
needUpdate = true; needUpdate = true;
} }
store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig
{ {
CryptoCode = vm.CryptoCode,
UseBech32Scheme = vm.LNURLBech32Mode, UseBech32Scheme = vm.LNURLBech32Mode,
LUD12Enabled = vm.LUD12Enabled LUD12Enabled = vm.LUD12Enabled
}); });
@ -325,16 +302,16 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
var network = _ExplorerProvider.GetNetwork(cryptoCode); var network = _ExplorerProvider.GetNetwork(cryptoCode);
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), store);
if (lightning == null) if (lightning == null)
return NotFound(); return NotFound();
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !enabled); storeBlob.SetExcluded(paymentMethodId, !enabled);
if (!enabled) if (!enabled)
{ {
storeBlob.SetExcluded(new PaymentMethodId(network.CryptoCode, PaymentTypes.LNURLPay), true); storeBlob.SetExcluded(PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode), true);
} }
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
@ -351,7 +328,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm) private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{ {
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode); vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store);
if (lightning != null) 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); return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
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;
} }
} }
} }

View file

@ -12,7 +12,10 @@ using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitcoin; using NBitcoin;
@ -20,6 +23,7 @@ using NBitcoin.DataEncoders;
using NBXplorer; using NBXplorer;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -83,7 +87,8 @@ namespace BTCPayServer.Controllers
vm.Network = network; vm.Network = network;
DerivationSchemeSettings strategy = null; DerivationSchemeSettings strategy = null;
PaymentMethodId paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
BitcoinLikePaymentHandler handler = (BitcoinLikePaymentHandler)_handlers[paymentMethodId];
var wallet = _WalletProvider.GetWallet(network); var wallet = _WalletProvider.GetWallet(network);
if (wallet == null) if (wallet == null)
{ {
@ -145,7 +150,11 @@ namespace BTCPayServer.Controllers
} }
else if (!string.IsNullOrEmpty(vm.Config)) 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"); ModelState.AddModelError(nameof(vm.Config), "Config file was not in the correct format");
return View(vm.ViewName, vm); return View(vm.ViewName, vm);
@ -158,17 +167,16 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm); return View(vm.ViewName, vm);
} }
vm.Config = ProtectString(strategy.ToJson()); vm.Config = ProtectString(JToken.FromObject(strategy, handler.Serializer).ToString());
ModelState.Remove(nameof(vm.Config)); ModelState.Remove(nameof(vm.Config));
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
if (vm.Confirmation) if (vm.Confirmation)
{ {
try try
{ {
await wallet.TrackAsync(strategy.AccountDerivation); await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy); store.SetPaymentMethodConfig(_handlers[paymentMethodId], strategy);
storeBlob.SetExcluded(paymentMethodId, false); storeBlob.SetExcluded(paymentMethodId, false);
storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false); storeBlob.PayJoinEnabled = strategy.IsHotWallet && !(vm.SetupRequest?.PayJoinEnabled is false);
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
@ -186,7 +194,7 @@ namespace BTCPayServer.Controllers
// This is success case when derivation scheme is added to the store // This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(WalletSettings), new { storeId = vm.StoreId, cryptoCode = vm.CryptoCode }); 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) private string ProtectString(string str)
@ -256,7 +264,7 @@ namespace BTCPayServer.Controllers
{ {
return NotFound(); return NotFound();
} }
var handler = _handlers.GetBitcoinHandler(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode); var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
var isImport = method == WalletSetupMethod.Seed; var isImport = method == WalletSetupMethod.Seed;
var vm = new WalletSetupViewModel var vm = new WalletSetupViewModel
@ -322,7 +330,7 @@ namespace BTCPayServer.Controllers
vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(); vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString();
vm.AccountKey = response.AccountHDKey.Neuter().ToWif(); vm.AccountKey = response.AccountHDKey.Neuter().ToWif();
vm.KeyPath = response.AccountKeyPath.KeyPath.ToString(); 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); var result = await UpdateWallet(vm);
@ -398,12 +406,13 @@ namespace BTCPayServer.Controllers
(bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet(); (bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet();
var client = _ExplorerProvider.GetExplorerClient(network); var client = _ExplorerProvider.GetExplorerClient(network);
var handler = _handlers.GetBitcoinHandler(cryptoCode);
var vm = new WalletSettingsViewModel var vm = new WalletSettingsViewModel
{ {
StoreId = storeId, StoreId = storeId,
CryptoCode = cryptoCode, CryptoCode = cryptoCode,
WalletId = new WalletId(storeId, cryptoCode), WalletId = new WalletId(storeId, cryptoCode),
Enabled = !excludeFilters.Match(derivation.PaymentId), Enabled = !excludeFilters.Match(handler.PaymentMethodId),
Network = network, Network = network,
IsHotWallet = derivation.IsHotWallet, IsHotWallet = derivation.IsHotWallet,
Source = derivation.Source, Source = derivation.Source,
@ -411,7 +420,7 @@ namespace BTCPayServer.Controllers
DerivationScheme = derivation.AccountDerivation.ToString(), DerivationScheme = derivation.AccountDerivation.ToString(),
DerivationSchemeInput = derivation.AccountOriginal, DerivationSchemeInput = derivation.AccountOriginal,
KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(), KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(),
UriScheme = derivation.Network.NBitcoinNetwork.UriScheme, UriScheme = network.NBitcoinNetwork.UriScheme,
Label = derivation.Label, Label = derivation.Label,
SelectedSigningKey = derivation.SigningKey.ToString(), SelectedSigningKey = derivation.SigningKey.ToString(),
NBXSeedAvailable = derivation.IsHotWallet && NBXSeedAvailable = derivation.IsHotWallet &&
@ -425,7 +434,7 @@ namespace BTCPayServer.Controllers
MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null, MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null,
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}" AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
}).ToList(), }).ToList(),
Config = ProtectString(derivation.ToJson()), Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
PayJoinEnabled = storeBlob.PayJoinEnabled, PayJoinEnabled = storeBlob.PayJoinEnabled,
MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes, MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes,
SpeedPolicy = store.SpeedPolicy, SpeedPolicy = store.SpeedPolicy,
@ -433,10 +442,7 @@ namespace BTCPayServer.Controllers
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
CanUseHotWallet = canUseHotWallet, CanUseHotWallet = canUseHotWallet,
CanUseRPCImport = rpcImport, CanUseRPCImport = rpcImport,
CanUsePayJoin = canUseHotWallet && store CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet),
StoreName = store.StoreName, StoreName = store.StoreName,
}; };
@ -451,7 +457,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> UpdateWalletSettings(WalletSettingsViewModel vm) 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) if (checkResult != null)
{ {
return checkResult; return checkResult;
@ -462,17 +468,17 @@ namespace BTCPayServer.Controllers
{ {
return NotFound(); return NotFound();
} }
var handler = _handlers.GetBitcoinHandler(vm.CryptoCode);
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var currentlyEnabled = !excludeFilters.Match(derivation.PaymentId); var currentlyEnabled = !excludeFilters.Match(handler.PaymentMethodId);
bool enabledChanged = currentlyEnabled != vm.Enabled; bool enabledChanged = currentlyEnabled != vm.Enabled;
bool needUpdate = enabledChanged; bool needUpdate = enabledChanged;
string errorMessage = null; string errorMessage = null;
if (enabledChanged) if (enabledChanged)
{ {
storeBlob.SetExcluded(derivation.PaymentId, !vm.Enabled); storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled);
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
} }
@ -484,7 +490,7 @@ namespace BTCPayServer.Controllers
var signingKey = string.IsNullOrEmpty(vm.SelectedSigningKey) var signingKey = string.IsNullOrEmpty(vm.SelectedSigningKey)
? null ? null
: new BitcoinExtPubKey(vm.SelectedSigningKey, derivation.Network.NBitcoinNetwork); : new BitcoinExtPubKey(vm.SelectedSigningKey, network.NBitcoinNetwork);
if (derivation.SigningKey != signingKey && signingKey != null) if (derivation.SigningKey != signingKey && signingKey != null)
{ {
needUpdate = true; needUpdate = true;
@ -531,9 +537,15 @@ namespace BTCPayServer.Controllers
} }
} }
if (store.SpeedPolicy != vm.SpeedPolicy)
{
store.SpeedPolicy = vm.SpeedPolicy;
needUpdate = true;
}
if (needUpdate) if (needUpdate)
{ {
store.SetSupportedPaymentMethod(derivation); store.SetPaymentMethodConfig(handler, derivation);
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
@ -561,7 +573,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> UpdatePaymentSettings(WalletSettingsViewModel vm) 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) if (checkResult != null)
{ {
return checkResult; return checkResult;
@ -574,12 +586,6 @@ namespace BTCPayServer.Controllers
} }
bool needUpdate = false; bool needUpdate = false;
if (store.SpeedPolicy != vm.SpeedPolicy)
{
needUpdate = true;
store.SpeedPolicy = vm.SpeedPolicy;
}
var blob = store.GetStoreBlob(); var blob = store.GetStoreBlob();
var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled; var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled;
blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration); blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration);
@ -598,21 +604,17 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; 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.Remove(WellKnownTempData.SuccessMessage);
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Severity = StatusMessageModel.StatusSeverity.Warning, 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(); return NotFound();
} }
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); store.SetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), null);
store.SetSupportedPaymentMethod(paymentMethodId, null);
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) }); _EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(storeId, cryptoCode) });
@ -755,7 +756,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(GeneralSettings), new { storeId }); 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(); vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit); var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
@ -769,7 +770,7 @@ namespace BTCPayServer.Controllers
var keyPath = deposit.GetKeyPath(i); var keyPath = deposit.GetKeyPath(i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath); var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive(i); var derivation = line.Derive(i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation, var address = network.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath(i), line.KeyPathTemplate.GetKeyPath(i),
derivation.ScriptPubKey).ToString(); derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath)); vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
@ -792,11 +793,7 @@ namespace BTCPayServer.Controllers
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store) private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{ {
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); return store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode), _handlers);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
} }
private async Task<string> GetSeed(ExplorerClient client, DerivationSchemeSettings derivation) 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;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
@ -82,7 +83,7 @@ namespace BTCPayServer.Controllers
_LangService = langService; _LangService = langService;
_TokenController = tokenController; _TokenController = tokenController;
_WalletProvider = walletProvider; _WalletProvider = walletProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary; _handlers = paymentMethodHandlerDictionary;
_policiesSettings = policiesSettings; _policiesSettings = policiesSettings;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_appService = appService; _appService = appService;
@ -118,7 +119,7 @@ namespace BTCPayServer.Controllers
readonly SettingsRepository _settingsRepository; readonly SettingsRepository _settingsRepository;
private readonly ExplorerClientProvider _ExplorerProvider; private readonly ExplorerClientProvider _ExplorerProvider;
private readonly LanguageService _LangService; private readonly LanguageService _LangService;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly PoliciesSettings _policiesSettings; private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService; private readonly AppService _appService;
@ -337,16 +338,15 @@ namespace BTCPayServer.Controllers
var storeBlob = CurrentStore.GetStoreBlob(); var storeBlob = CurrentStore.GetStoreBlob();
var vm = new CheckoutAppearanceViewModel(); var vm = new CheckoutAppearanceViewModel();
SetCryptoCurrencies(vm, CurrentStore); SetCryptoCurrencies(vm, CurrentStore);
vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider) vm.PaymentMethodCriteria = CurrentStore.GetPaymentMethodConfigs(_handlers)
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.PaymentId)) .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.Key) && s.Value is not LNURLPaymentMethodConfig)
.Where(s => _NetworkProvider.GetNetwork(s.PaymentId.CryptoCode) != null) .Select(c =>
.Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay)
.Select(method =>
{ {
var pmi = c.Key;
var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
criteria.PaymentMethod == method.PaymentId); criteria.PaymentMethod == pmi);
return existing is null return existing is null
? new PaymentMethodCriteriaViewModel { PaymentMethod = method.PaymentId.ToString(), Value = "" } ? new PaymentMethodCriteriaViewModel { PaymentMethod = pmi.ToString(), Value = "" }
: new PaymentMethodCriteriaViewModel : new PaymentMethodCriteriaViewModel
{ {
PaymentMethod = existing.PaymentMethod.ToString(), PaymentMethod = existing.PaymentMethod.ToString(),
@ -391,13 +391,13 @@ namespace BTCPayServer.Controllers
public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData) public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData)
{ {
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider); var enabled = storeData.GetEnabledPaymentIds();
return enabled return enabled
.Select(o => .Select(o =>
new PaymentMethodOptionViewModel.Format() new PaymentMethodOptionViewModel.Format()
{ {
Name = o.ToPrettyString(), Name = o.ToString(),
Value = o.ToString(), Value = o.ToString(),
PaymentId = o PaymentId = o
}).ToArray(); }).ToArray();
@ -405,13 +405,13 @@ namespace BTCPayServer.Controllers
PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData) PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(StoreData storeData)
{ {
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider); var enabled = storeData.GetEnabledPaymentIds();
var defaultPaymentId = storeData.GetDefaultPaymentId(); var defaultPaymentId = storeData.GetDefaultPaymentId();
var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null; var defaultChoice = defaultPaymentId is not null ? defaultPaymentId.FindNearest(enabled) : null;
if (defaultChoice is null) if (defaultChoice is null)
{ {
defaultChoice = enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ?? defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ??
enabled.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.LightningLike) ?? enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode)) ??
enabled.FirstOrDefault(); enabled.FirstOrDefault();
} }
var choices = GetEnabledPaymentMethodChoices(storeData); var choices = GetEnabledPaymentMethodChoices(storeData);
@ -507,15 +507,15 @@ namespace BTCPayServer.Controllers
foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) foreach (var newCriteria in model.PaymentMethodCriteria.ToList())
{ {
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
if (paymentMethodId.PaymentType == PaymentTypes.LightningLike) if (_handlers.TryGet(paymentMethodId) is LightningLikePaymentHandler h)
model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel() model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel()
{ {
PaymentMethod = new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay).ToString(), PaymentMethod = PaymentTypes.LNURL.GetPaymentMethodId(h.Network.CryptoCode).ToString(),
Type = newCriteria.Type, Type = newCriteria.Type,
Value = newCriteria.Value Value = newCriteria.Value
}); });
// Should not be able to set LNUrlPay criteria directly in UI // 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); model.PaymentMethodCriteria.Remove(newCriteria);
} }
blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>(); blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>();
@ -575,54 +575,49 @@ namespace BTCPayServer.Controllers
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode = var derivationByCryptoCode =
store store
.GetSupportedPaymentMethods(_NetworkProvider) .GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers)
.OfType<DerivationSchemeSettings>() .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (DerivationSchemeSettings)c.Value);
.ToDictionary(c => c.Network.CryptoCode.ToUpperInvariant());
var lightningByCryptoCode = store var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider) .GetPaymentMethodConfigs(_handlers)
.OfType<LightningSupportedPaymentMethod>() .Where(c => c.Value is LightningPaymentMethodConfig)
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance) .ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value);
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
derivationSchemes = new List<StoreDerivationScheme>(); derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>(); 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(network.CryptoCode);
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode); var value = strategy?.ToPrettyString() ?? string.Empty;
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
derivationSchemes.Add(new StoreDerivationScheme derivationSchemes.Add(new StoreDerivationScheme
{ {
Crypto = paymentMethodId.CryptoCode, Crypto = network.CryptoCode,
WalletSupported = network.WalletSupported, PaymentMethodId = handler.PaymentMethodId,
Value = value, WalletSupported = network.WalletSupported,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode), Value = value,
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null, WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,
#if ALTCOINS #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 #endif
}); });
break; }
else if (handler is LightningLikePaymentHandler)
case LNURLPayPaymentType: {
break; var lnNetwork = ((IHasNetwork)handler).Network;
var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode);
case LightningPaymentType _: var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null;
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode); lightningNodes.Add(new StoreLightningNode
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null; {
lightningNodes.Add(new StoreLightningNode CryptoCode = lnNetwork.CryptoCode,
{ PaymentMethodId = handler.PaymentMethodId,
CryptoCode = paymentMethodId.CryptoCode, Address = lightning?.GetDisplayableConnectionString(),
Address = lightning?.GetDisplayableConnectionString(), Enabled = isEnabled
Enabled = isEnabled });
});
break;
} }
} }
} }
@ -843,7 +838,7 @@ namespace BTCPayServer.Controllers
var isOD = Regex.Match(derivationScheme, @"\(.*?\)"); var isOD = Regex.Match(derivationScheme, @"\(.*?\)");
if (isOD.Success) if (isOD.Success)
{ {
var derivationSchemeSettings = new DerivationSchemeSettings { Network = network }; var derivationSchemeSettings = new DerivationSchemeSettings();
var result = parser.ParseOutputDescriptor(derivationScheme); var result = parser.ParseOutputDescriptor(derivationScheme);
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim(); derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1; derivationSchemeSettings.AccountDerivation = result.Item1;
@ -1087,8 +1082,8 @@ namespace BTCPayServer.Controllers
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial) if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{ {
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods(); var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = !store.GetSupportedPaymentMethods(_NetworkProvider) StoreNotConfigured = !store.GetPaymentMethodConfigs(_handlers)
.Where(p => !excludeFilter.Match(p.PaymentId)) .Where(p => !excludeFilter.Match(p.Key))
.Any(); .Any();
TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful"; TempData[WellKnownTempData.SuccessMessage] = "Pairing is successful";
if (pairingResult == PairingResult.Partial) if (pairingResult == PairingResult.Partial)

View file

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

View file

@ -79,7 +79,7 @@ namespace BTCPayServer.Controllers
// we just assume that it is 20 blocks // we just assume that it is 20 blocks
var assumedFeeRate = await fr.GetFeeRateAsync(20); 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) if (derivationScheme is null)
return NotFound(); return NotFound();

View file

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

View file

@ -15,11 +15,6 @@ namespace BTCPayServer.Data
return addressInvoiceData.Address; return addressInvoiceData.Address;
return addressInvoiceData.Address.Substring(0, index); 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) public static PaymentMethodId GetPaymentMethodId(this AddressInvoiceData addressInvoiceData)
{ {
if (addressInvoiceData.Address == null) 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 public static class IHasBlobExtensions
{ {
static readonly JsonSerializerSettings DefaultSerializer; static readonly JsonSerializerSettings DefaultSerializerSettings;
static readonly JsonSerializer DefaultSerializer;
static IHasBlobExtensions() static IHasBlobExtensions()
{ {
DefaultSerializer = new JsonSerializerSettings() DefaultSerializerSettings = new JsonSerializerSettings()
{ {
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.None Formatting = Formatting.None
}; };
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializer); NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
DefaultSerializer = JsonSerializer.CreateDefault(DefaultSerializerSettings);
} }
class HasBlobWrapper<B> : IHasBlob<B> class HasBlobWrapper<B> : IHasBlob<B>
{ {
@ -60,7 +62,7 @@ namespace BTCPayServer.Data
public static B? GetBlob<B>(this IHasBlob<B> data, JsonSerializerSettings? settings = null) public static B? GetBlob<B>(this IHasBlob<B> data, JsonSerializerSettings? settings = null)
{ {
if (data.Blob2 is not 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 #pragma warning disable CS0618 // Type or member is obsolete
if (data.Blob is not null && data.Blob.Length != 0) if (data.Blob is not null && data.Blob.Length != 0)
{ {
@ -69,7 +71,7 @@ namespace BTCPayServer.Data
str = Encoding.UTF8.GetString(data.Blob); str = Encoding.UTF8.GetString(data.Blob);
else else
str = ZipUtils.Unzip(data.Blob); 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 #pragma warning restore CS0618 // Type or member is obsolete
return default; return default;
@ -78,19 +80,28 @@ namespace BTCPayServer.Data
public static object? GetBlob(this IHasBlob data, JsonSerializerSettings? settings = null) public static object? GetBlob(this IHasBlob data, JsonSerializerSettings? settings = null)
{ {
if (data.Blob2 is not 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 #pragma warning disable CS0618 // Type or member is obsolete
if (data.Blob is not null && data.Blob.Length != 0) 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 #pragma warning restore CS0618 // Type or member is obsolete
return default; 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) if (blob is null)
data.Blob2 = null; data.Blob2 = null;
else 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 #pragma warning disable CS0618 // Type or member is obsolete
data.Blob = new byte[0]; data.Blob = new byte[0];
#pragma warning restore CS0618 // Type or member is obsolete #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 System.Reflection.Metadata;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using NBitpayClient;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public static class InvoiceDataExtensions 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) public static void SetBlob(this InvoiceData invoiceData, InvoiceEntity blob)
{ {
if (blob.Metadata is null) if (blob.Metadata is null)
blob.Metadata = new InvoiceMetadata(); 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 #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.Price = price;
entity.Networks = networks;
if (entity.Metadata is null)
{
if (entity.Version < InvoiceEntity.GreenfieldInvoices_Version)
{
entity.MigrateLegacyInvoice();
}
else
{
entity.Metadata = new InvoiceMetadata();
}
}
return entity;
} }
else
entity.StoreId = invoiceData.StoreDataId;
entity.ExceptionStatus = state.ExceptionStatus;
entity.Status = state.Status;
if (invoiceData.AddressInvoices != null)
{ {
var entity = invoiceData.HasTypedBlob<InvoiceEntity>().GetBlob(); entity.AvailableAddressHashes = invoiceData.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId()).ToHashSet();
entity.Networks = networks;
return entity;
} }
#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) 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 System.Runtime.InteropServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public static class PaymentDataExtensions 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(); var prompt = invoiceEntity.GetPaymentPrompt(handler.PaymentMethodId) ?? throw new InvalidOperationException($"Payment prompt for {handler.PaymentMethodId} is not found");
paymentData.Blob2 = entity.Network.ToString(entity); 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 paymentData.Amount = entity.Value;
if (paymentData.Blob is not null && paymentData.Blob.Length != 0) paymentData.Currency = entity.Currency;
{ paymentData.Status = entity.Status;
var unziped = ZipUtils.Unzip(paymentData.Blob); paymentData.SetBlob(entity.PaymentMethodId, (PaymentBlob)entity);
var cryptoCode = "BTC"; return entity;
if (JObject.Parse(unziped).TryGetValue("cryptoCode", out var v) && v.Type == JTokenType.String) }
cryptoCode = v.Value<string>(); public static PaymentData SetBlob(this PaymentData paymentData, PaymentMethodId paymentMethodId, PaymentBlob blob)
var network = networks.GetNetwork<BTCPayNetworkBase>(cryptoCode); {
PaymentEntity paymentEntity = null; paymentData.Type = paymentMethodId.ToString();
if (network == null) paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
{ return paymentData;
return null; }
}
else public static PaymentEntity GetBlob(this PaymentData paymentData)
{ {
paymentEntity = network.ToObject<PaymentEntity>(unziped); var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}");
} entity.Status = paymentData.Status!.Value;
paymentEntity.Network = network; entity.Currency = paymentData.Currency;
paymentEntity.Accounted = paymentData.Accounted; entity.PaymentMethodId = PaymentMethodId.Parse(paymentData.Type);
return paymentEntity; entity.Value = paymentData.Amount!.Value;
} entity.Id = paymentData.Id;
#pragma warning restore CS0618 // Type or member is obsolete entity.ReceivedTime = paymentData.Created!.Value;
if (paymentData.Blob2 is not null) return entity;
{
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;
} }
} }
} }

View file

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

View file

@ -9,8 +9,10 @@ using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -25,16 +27,15 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public const string LightningLikePayoutHandlerClearnetNamedClient = public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient); nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService; private readonly UserService _userService;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
public LightningLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider, public LightningLikePayoutHandler(PaymentMethodHandlerDictionary handlers,
IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService) IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService)
{ {
_btcPayNetworkProvider = btcPayNetworkProvider; _handlers = handlers;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_userService = userService; _userService = userService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
@ -42,8 +43,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public bool CanHandle(PaymentMethodId paymentMethod) 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) 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) public async Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken)
{ {
destination = destination.Trim(); destination = destination.Trim();
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode); var network = ((IHasNetwork)_handlers[paymentMethodId]).Network;
try try
{ {
string lnurlTag = null; string lnurlTag = null;
@ -161,12 +162,12 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public async Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData) public async Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData)
{ {
var result = new List<PaymentMethodId>(); var result = new List<PaymentMethodId>();
var methods = storeData.GetEnabledPaymentMethods(_btcPayNetworkProvider).Where(id => id.PaymentId.PaymentType == LightningPaymentType.Instance).OfType<LightningSupportedPaymentMethod>(); var methods = storeData.GetPaymentMethodConfigs<LightningPaymentMethodConfig>(_handlers, true);
foreach (LightningSupportedPaymentMethod supportedPaymentMethod in methods) foreach (var m in methods)
{ {
if (!supportedPaymentMethod.IsInternalNode) if (!m.Value.IsInternalNode)
{ {
result.Add(supportedPaymentMethod.PaymentId); result.Add(m.Key);
continue; continue;
} }
@ -174,7 +175,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
if (!await _userService.IsAdminUser(storeDataUserStore.ApplicationUserId)) if (!await _userService.IsAdminUser(storeDataUserStore.ApplicationUserId))
continue; continue;
result.Add(supportedPaymentMethod.PaymentId); result.Add(m.Key);
break; break;
} }
@ -185,8 +186,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds) public Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds)
{ {
var cryptoCode = _handlers.GetNetwork(paymentMethodId).CryptoCode;
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout", 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.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -32,7 +34,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly LightningClientFactoryService _lightningClientFactoryService; private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly IOptions<LightningNetworkOptions> _options; private readonly IOptions<LightningNetworkOptions> _options;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
@ -43,7 +45,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers, IEnumerable<IPayoutHandler> payoutHandlers,
BTCPayNetworkProvider btcPayNetworkProvider, PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository, StoreRepository storeRepository,
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
IOptions<LightningNetworkOptions> options, IOptions<LightningNetworkOptions> options,
@ -54,7 +56,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_userManager = userManager; _userManager = userManager;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_payoutHandlers = payoutHandlers; _payoutHandlers = payoutHandlers;
_btcPayNetworkProvider = btcPayNetworkProvider; _handlers = handlers;
_lightningClientFactoryService = lightningClientFactoryService; _lightningClientFactoryService = lightningClientFactoryService;
_options = options; _options = options;
_storeRepository = storeRepository; _storeRepository = storeRepository;
@ -101,7 +103,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
await SetStoreContext(); await SetStoreContext();
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
await using var ctx = _applicationDbContextFactory.CreateContext(); await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = await GetPayouts(ctx, pmi, payoutIds); var payouts = await GetPayouts(ctx, pmi, payoutIds);
@ -125,14 +127,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
await SetStoreContext(); await SetStoreContext();
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.FindPayoutHandler(pmi); var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.FindPayoutHandler(pmi);
await using var ctx = _applicationDbContextFactory.CreateContext(); await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId); var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
var results = new List<ResultVM>(); 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 //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 store = payoutDatas.First().StoreData;
var lightningSupportedPaymentMethod = store.GetSupportedPaymentMethods(_btcPayNetworkProvider) var lightningSupportedPaymentMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers);
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == pmi);
if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode) if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode)
{ {

View file

@ -5,12 +5,15 @@ using System.Data;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using NBitcoin; using NBitcoin;
using NBXplorer; using NBXplorer;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@ -24,21 +27,19 @@ namespace BTCPayServer.Data
return defaultPaymentId; 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(); return storeData.GetPaymentMethodConfigs(true)
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks) .Where(m => handlers.Support(m.Key))
.Where(a => !excludeFilter.Match(a.PaymentId)) .OrderByDescending(a => a.Key.ToString() == "BTC")
.OrderByDescending(a => a.PaymentId.CryptoCode == "BTC") .ThenBy(a => a.Key.ToString())
.ThenBy(a => a.PaymentId.CryptoCode) .ThenBy(a => handlers[a.Key].ParsePaymentMethodConfig(a.Value) is LightningPaymentMethodConfig ? 1 : 0)
.ThenBy(a => a.PaymentId.PaymentType == PaymentTypes.LightningLike ? 1 : 0) .ToDictionary(a => a.Key, a => handlers[a.Key].ParsePaymentMethodConfig(a.Value));
.ToArray();
return paymentMethodIds;
} }
public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId? defaultPaymentId) public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId? defaultPaymentId)
@ -60,12 +61,9 @@ namespace BTCPayServer.Data
return result; return result;
} }
public static bool AnyPaymentMethodAvailable(this StoreData storeData, BTCPayNetworkProvider networkProvider) public static bool AnyPaymentMethodAvailable(this StoreData storeData)
{ {
var storeBlob = GetStoreBlob(storeData); return storeData.GetPaymentMethodConfigs(true).Any();
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return GetSupportedPaymentMethods(storeData, networkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
} }
public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob) public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob)
@ -78,106 +76,94 @@ namespace BTCPayServer.Data
return true; 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); var config = GetPaymentMethodConfig(storeData, paymentMethodId, onlyEnabled);
#pragma warning disable CS0618 if (config is null || !handlers.Support(paymentMethodId))
bool btcReturned = false; return null;
return handlers[paymentMethodId].ParsePaymentMethodConfig(config);
if (!string.IsNullOrEmpty(storeData.DerivationStrategies)) }
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); JObject strategies = JObject.Parse(storeData.DerivationStrategies);
foreach (var strat in strategies.Properties()) return strategies[paymentMethodId.ToString()];
{
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);
}
}
} }
#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));
} }
public static void SetPaymentMethodConfig(this StoreData storeData, PaymentMethodId paymentMethodId, JToken? config)
/// <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)
{ {
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); JObject strategies = string.IsNullOrEmpty(storeData.DerivationStrategies) ? new JObject() : JObject.Parse(storeData.DerivationStrategies);
bool existing = false; if (config is null)
foreach (var strat in strategies.Properties().ToList()) 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; continue;
} if (excludeFilter?.Match(paymentMethodId) is true)
if (stratId == paymentMethodId) continue;
{ paymentMethodConfigurations.Add(paymentMethodId, strat.Value);
if (supportedPaymentMethod == null)
{
strat.Remove();
}
else
{
strat.Value = PaymentMethodExtensions.Serialize(supportedPaymentMethod);
}
existing = true;
break;
}
} }
if (!existing && supportedPaymentMethod != null) return paymentMethodConfigurations;
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
storeData.DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
} }
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); return storeData.GetPaymentMethodConfig(paymentMethodId, true) is not null;
var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods();
return paymentMethods.Any(method =>
method.PaymentId.CryptoCode == cryptoCode &&
method.PaymentId.PaymentType == paymentType &&
!excludeFilters.Match(method.PaymentId));
} }
} }
} }

View file

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

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