diff --git a/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs b/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs index 9d682cec4..b7f2d9ae8 100644 --- a/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs +++ b/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs @@ -1,3 +1,6 @@ +using NBitcoin; +using Newtonsoft.Json; + namespace BTCPayServer.Client.Models { public class OnChainPaymentMethodData @@ -22,6 +25,11 @@ namespace BTCPayServer.Client.Models /// public string DerivationScheme { get; set; } + public string Label { get; set; } + + [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] + public RootedKeyPath AccountKeyPath { get; set; } + public OnChainPaymentMethodData() { } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index c06cd0497..2a26997a4 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1244,9 +1244,14 @@ namespace BTCPayServer.Tests new OnChainPaymentMethodData() {Default = true, Enabled = true, DerivationScheme = xpub}); Assert.Equal(xpub,method.DerivationScheme); - + + method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", + new OnChainPaymentMethodData() { Default = true, 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); diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs index 1bedce81d..3d3d5b7f6 100644 --- a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs @@ -10,6 +10,7 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using NBitcoin; using NBXplorer.DerivationStrategy; using StoreData = BTCPayServer.Data.StoreData; @@ -203,6 +204,18 @@ namespace BTCPayServer.Controllers.GreenField var strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network); if (strategy != null) await wallet.TrackAsync(strategy.AccountDerivation); + strategy.Label = paymentMethodData.Label; + var signing = strategy.GetSigningAccountKeySettings(); + if (paymentMethodData.AccountKeyPath is RootedKeyPath r) + { + signing.AccountKeyPath = r.KeyPath; + signing.RootFingerprint = r.MasterFingerprint; + } + else + { + signing.AccountKeyPath = null; + signing.RootFingerprint = null; + } store.SetSupportedPaymentMethod(id, strategy); storeBlob.SetExcluded(id, !paymentMethodData.Enabled); store.SetStoreBlob(storeBlob); @@ -240,7 +253,11 @@ namespace BTCPayServer.Controllers.GreenField ? null : new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode, paymentMethod.AccountDerivation.ToString(), !excluded, - defaultPaymentMethod == paymentMethod.PaymentId); + defaultPaymentMethod == paymentMethod.PaymentId) + { + Label = paymentMethod.Label, + AccountKeyPath = paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath() + }; } } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 7672e94af..ff6c0394e 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -40,6 +40,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -339,8 +340,16 @@ namespace BTCPayServer.Hosting logBuilder.AddProvider(new Serilog.Extensions.Logging.SerilogLoggerProvider(Log.Logger)); } }); + + services.AddSingleton(); + services.SkipModelValidation(); return services; } + + public static void SkipModelValidation(this IServiceCollection services) + { + services.AddSingleton>(); + } private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB. private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services) { diff --git a/BTCPayServer/Hosting/SkippableObjectValidatorProvider.cs b/BTCPayServer/Hosting/SkippableObjectValidatorProvider.cs new file mode 100644 index 000000000..766879a93 --- /dev/null +++ b/BTCPayServer/Hosting/SkippableObjectValidatorProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.Options; +using NBitcoin; + +namespace BTCPayServer.Hosting +{ + public class SkippableObjectValidatorProvider : ObjectModelValidator + { + public interface ISkipValidation + { + bool SkipValidation(object obj); + } + public class SkipValidationType : ISkipValidation + { + public bool SkipValidation(object obj) + { + return obj is T; + } + } + public SkippableObjectValidatorProvider( + IModelMetadataProvider modelMetadataProvider, + IEnumerable skipValidations, + IOptions mvcOptions) + : base(modelMetadataProvider, mvcOptions.Value.ModelValidatorProviders) + { + _mvcOptions = mvcOptions.Value; + SkipValidations = skipValidations.ToList(); + } + + class OverrideValidationVisitor : ValidationVisitor + { + public OverrideValidationVisitor(IEnumerable skipValidations, ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) + { + SkipValidations = skipValidations; + } + + public IEnumerable SkipValidations { get; } + + protected override bool VisitComplexType(IValidationStrategy defaultStrategy) + { + if (SkipValidations.Any(v => v.SkipValidation(Model))) + return true; + return base.VisitComplexType(defaultStrategy); + } + } + + public MvcOptions _mvcOptions { get; } + IEnumerable SkipValidations { get; } + + public override ValidationVisitor GetValidationVisitor( + ActionContext actionContext, + IModelValidatorProvider validatorProvider, + ValidatorCache validatorCache, + IModelMetadataProvider metadataProvider, + ValidationStateDictionary validationState) + { + var visitor = new OverrideValidationVisitor( + SkipValidations, + actionContext, + validatorProvider, + validatorCache, + metadataProvider, + validationState) + { + MaxValidationDepth = _mvcOptions.MaxValidationDepth, + ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails, + }; + + return visitor; + } + } +} diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 7f301ea2b..f69ce135c 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; +using NBitcoin; namespace BTCPayServer.Hosting { @@ -75,7 +76,6 @@ namespace BTCPayServer.Hosting .ConfigureApiBehaviorOptions(options => { var builtInFactory = options.InvalidModelStateResponseFactory; - options.InvalidModelStateResponseFactory = context => { context.HttpContext.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity; diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.on-chain.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.on-chain.json index 1b5086f86..da4c3a5b7 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.on-chain.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.on-chain.json @@ -403,7 +403,17 @@ }, "derivationScheme": { "type": "string", - "description": "The derivation scheme" + "description": "The derivation scheme", + "example": "xpub..." + }, + "label": { + "type": "string", + "description": "A label that will be shown in the UI" + }, + "accountKeyPath": { + "type": "string", + "description": "The wallet fingerprint followed by the keypath to derive the account key used for signing operation or creating PSBTs", + "example": "abcd82a1/84'/0'/0'" } } },