Better validation of inputs when setting on-chain payment method by API

This commit is contained in:
nicolas.dorier 2025-01-21 20:26:44 +09:00
parent bbe95d780f
commit 8e927eee73
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
5 changed files with 74 additions and 16 deletions

View file

@ -3314,6 +3314,12 @@ namespace BTCPayServer.Tests
await viewOnlyClient.SendHttpRequest($"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", new JObject() { ["config"] = xpub.ToString() }, HttpMethod.Post);
var method = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = JValue.CreateString(xpub.ToString())});
var method2 = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = new JObject() { ["derivationScheme"] = xpub.ToString(), ["label"] = "test", ["accountKeyPath"] = "aaaaaaaa/84'/0'/0'" } });
Assert.Equal("aaaaaaaa", method2.Config["accountKeySettings"][0]["rootFingerprint"].ToString());
Assert.Equal("84'/0'/0'", method2.Config["accountKeySettings"][0]["accountKeyPath"].ToString());
var method3 = await client.UpdateStorePaymentMethod(store.Id, "BTC-CHAIN", new UpdatePaymentMethodRequest() { Enabled = true, Config = new JObject() { ["derivationScheme"] = xpub.ToString() } });
Assert.Equal(method.ToJson(), method3.ToJson());
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address);
await AssertHttpError(403, async () =>
@ -3378,7 +3384,6 @@ namespace BTCPayServer.Tests
Assert.Equal(24, generateResponse.Mnemonic.Words.Length);
Assert.Equal(Wordlist.Japanese, generateResponse.Mnemonic.WordList);
}
[Fact(Timeout = 60 * 2 * 1000)]

View file

@ -408,6 +408,7 @@ public partial class UIStoresController
var client = _explorerProvider.GetExplorerClient(network);
var handler = _handlers.GetBitcoinHandler(cryptoCode);
var vm = new WalletSettingsViewModel
{
StoreId = storeId,
@ -417,18 +418,18 @@ public partial class UIStoresController
Network = network,
IsHotWallet = derivation.IsHotWallet,
Source = derivation.Source,
RootFingerprint = derivation.GetSigningAccountKeySettings().RootFingerprint.ToString(),
DerivationScheme = derivation.AccountDerivation.ToString(),
RootFingerprint = derivation.GetSigningAccountKeySettingsOrDefault()?.RootFingerprint.ToString(),
DerivationScheme = derivation.AccountDerivation?.ToString(),
DerivationSchemeInput = derivation.AccountOriginal,
KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(),
KeyPath = derivation.GetSigningAccountKeySettingsOrDefault()?.AccountKeyPath?.ToString(),
UriScheme = network.NBitcoinNetwork.UriScheme,
Label = derivation.Label,
SelectedSigningKey = derivation.SigningKey.ToString(),
SelectedSigningKey = derivation.SigningKey?.ToString(),
NBXSeedAvailable = derivation.IsHotWallet &&
canUseHotWallet &&
!string.IsNullOrEmpty(await client.GetMetadataAsync<string>(derivation.AccountDerivation,
WellknownMetadataKeys.MasterHDKey)),
AccountKeys = derivation.AccountKeySettings
AccountKeys = (derivation.AccountKeySettings ?? [])
.Select(e => new WalletSettingsAccountKeyViewModel
{
AccountKey = e.AccountKey.ToString(),
@ -441,7 +442,7 @@ public partial class UIStoresController
CanUseHotWallet = canUseHotWallet,
CanUseRPCImport = rpcImport,
StoreName = store.StoreName,
CanSetupMultiSig = derivation.AccountKeySettings.Length > 1,
CanSetupMultiSig = (derivation.AccountKeySettings ?? []).Length > 1,
IsMultiSigOnServer = derivation.IsMultiSigOnServer,
DefaultIncludeNonWitnessUtxo = derivation.DefaultIncludeNonWitnessUtxo
};

View file

@ -80,7 +80,12 @@ namespace BTCPayServer
public AccountKeySettings GetSigningAccountKeySettings()
{
return AccountKeySettings.Single(a => a.AccountKey == SigningKey);
return (AccountKeySettings ?? []).Single(a => a.AccountKey == SigningKey);
}
public AccountKeySettings GetSigningAccountKeySettingsOrDefault()
{
return (AccountKeySettings ?? []).SingleOrDefault(a => a.AccountKey == SigningKey);
}
public AccountKeySettings[] AccountKeySettings { get; set; }

View file

@ -23,6 +23,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static Org.BouncyCastle.Math.EC.ECCurve;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Payments.Bitcoin
@ -39,7 +40,7 @@ namespace BTCPayServer.Payments.Bitcoin
private readonly NBXplorerDashboard _dashboard;
private readonly WalletRepository _walletRepository;
private readonly Services.Wallets.BTCPayWalletProvider _WalletProvider;
public JsonSerializer Serializer { get; }
public PaymentMethodId PaymentMethodId { get; private set; }
public BTCPayNetwork Network => _Network;
@ -233,16 +234,50 @@ namespace BTCPayServer.Payments.Bitcoin
return null;
return GetAccountDerivation(value, network);
}
class AlternativeConfig
{
[JsonProperty]
public DerivationStrategyBase DerivationScheme { get; set; }
[JsonProperty]
public string Label { get; set; }
[JsonProperty]
public RootedKeyPath AccountKeyPath { get; set; }
public DerivationSchemeSettings ToDerivationSchemeSettings(BTCPayNetwork network)
{
if (DerivationScheme is null)
return null;
var scheme = new DerivationSchemeSettings(DerivationScheme, network);
scheme.AccountOriginal = DerivationScheme.ToString();
scheme.Label = Label;
if (AccountKeyPath is not null && scheme.AccountKeySettings is [{ } ak, ..])
{
ak.RootFingerprint = AccountKeyPath.MasterFingerprint;
ak.AccountKeyPath = AccountKeyPath.KeyPath;
}
return scheme;
}
}
public Task ValidatePaymentMethodConfig(PaymentMethodConfigValidationContext validationContext)
{
var parser = Network.GetDerivationSchemeParser();
DerivationSchemeSettings settings = new DerivationSchemeSettings();
if (parser.TryParseXpub(validationContext.Config.ToString(), ref settings))
if (validationContext.Config is JValue { Type: JTokenType.String, Value: string config }
&& parser.TryParseXpub(config, ref settings))
{
validationContext.Config = JToken.FromObject(settings, Serializer);
return Task.CompletedTask;
}
var res = validationContext.Config.ToObject<DerivationSchemeSettings>(Serializer);
DerivationSchemeSettings res = null;
try
{
res = validationContext.Config.ToObject<AlternativeConfig>(Serializer)?.ToDerivationSchemeSettings(Network);
if (res != null)
validationContext.Config = JToken.FromObject(res, Serializer);
}
catch { }
res ??= validationContext.Config.ToObject<DerivationSchemeSettings>(Serializer);
if (res is null)
{
validationContext.ModelState.AddModelError(nameof(validationContext.Config), "Invalid derivation scheme settings");
@ -252,6 +287,18 @@ namespace BTCPayServer.Payments.Bitcoin
{
validationContext.ModelState.AddModelError(nameof(res.AccountDerivation), "Invalid account derivation");
}
if (res.AccountKeySettings is null)
{
validationContext.ModelState.AddModelError(nameof(res.AccountKeySettings), "Invalid AccountKeySettings");
}
if (res.SigningKey is null)
{
validationContext.ModelState.AddModelError(nameof(res.SigningKey), "Invalid SigningKey");
}
if (res.GetSigningAccountKeySettingsOrDefault() is null)
{
validationContext.ModelState.AddModelError(nameof(res.AccountKeySettings), "AccountKeySettings doesn't include the SigningKey");
}
return Task.CompletedTask;
}

View file

@ -3,7 +3,7 @@
"/api/v1/stores/{storeId}/payment-methods": {
"get": {
"tags": [
"Store Payment Methods"
"Store (Payment Methods)"
],
"summary": "Get store payment methods",
"description": "View information about the stores' configured payment methods",
@ -79,7 +79,7 @@
"/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}": {
"get": {
"tags": [
"Store Payment Methods"
"Store (Payment Methods)"
],
"summary": "Get store payment method",
"description": "View information about the stores' configured payment method",
@ -144,7 +144,7 @@
},
"put": {
"tags": [
"Store Payment Methods"
"Store (Payment Methods)"
],
"summary": "Update store's payment method",
"description": "Update information about the stores' configured payment method",
@ -209,7 +209,7 @@
},
"delete": {
"tags": [
"Store Payment Methods"
"Store (Payment Methods)"
],
"summary": "Delete store's payment method",
"description": "Delete information about the stores' configured payment method",
@ -369,7 +369,7 @@
},
"tags": [
{
"name": "Store Payment Methods",
"name": "Store (Payment Methods)",
"description": "Store Payment Methods operations"
}
]