diff --git a/BTCPayServer.Client/BTCPayServerClient.OnChainPaymentMethods.cs b/BTCPayServer.Client/BTCPayServerClient.OnChainPaymentMethods.cs index 2afb3d502..dfe753ced 100644 --- a/BTCPayServer.Client/BTCPayServerClient.OnChainPaymentMethods.cs +++ b/BTCPayServer.Client/BTCPayServerClient.OnChainPaymentMethods.cs @@ -78,5 +78,17 @@ namespace BTCPayServer.Client method: HttpMethod.Get), token); return await HandleResponse(response); } + + public virtual async Task 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(response); + } + } } diff --git a/BTCPayServer.Client/JsonConverters/MnemonicJsonConverter.cs b/BTCPayServer.Client/JsonConverters/MnemonicJsonConverter.cs new file mode 100644 index 000000000..4f7f1a897 --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/MnemonicJsonConverter.cs @@ -0,0 +1,31 @@ +using System; +using NBitcoin; +using NBitcoin.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.JsonConverters +{ + public class MnemonicJsonConverter : JsonConverter + { + public override Mnemonic ReadJson(JsonReader reader, Type objectType, Mnemonic existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + return reader.TokenType switch + { + JsonToken.String => new Mnemonic((string)reader.Value), + JsonToken.Null => null, + _ => throw new JsonObjectException(reader.Path, "Mnemonic must be a json string") + }; + } + + public override void WriteJson(JsonWriter writer, Mnemonic value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue(value.ToString()); + else + { + writer.WriteNull(); + } + } + } +} diff --git a/BTCPayServer.Client/JsonConverters/WordcountJsonConverter.cs b/BTCPayServer.Client/JsonConverters/WordcountJsonConverter.cs new file mode 100644 index 000000000..3f02beda2 --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/WordcountJsonConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NBitcoin; +using Newtonsoft.Json; + +public class WordcountJsonConverter : JsonConverter +{ + static WordcountJsonConverter() + { + _Wordcount = new Dictionary() + { + {18, WordCount.Eighteen}, + {15, WordCount.Fifteen}, + {12, WordCount.Twelve}, + {24, WordCount.TwentyFour}, + {21, WordCount.TwentyOne} + }; + _WordcountReverse = _Wordcount.ToDictionary(kv => kv.Value, kv => kv.Key); + } + + public override bool CanConvert(Type objectType) + { + return typeof(NBitcoin.WordCount).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) || + typeof(NBitcoin.WordCount?).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return default; + if (reader.TokenType != JsonToken.Integer) + throw new NBitcoin.JsonConverters.JsonObjectException( + $"Unexpected json token type, expected Integer, actual {reader.TokenType}", reader); + if (!_Wordcount.TryGetValue((long)reader.Value, out var result)) + throw new NBitcoin.JsonConverters.JsonObjectException( + $"Invalid WordCount, possible values {string.Join(", ", _Wordcount.Keys.ToArray())} (default: 12)", + reader); + return result; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is WordCount wc) + writer.WriteValue(_WordcountReverse[wc]); + } + + readonly static Dictionary _Wordcount = new Dictionary() + { + {18, WordCount.Eighteen}, + {15, WordCount.Fifteen}, + {12, WordCount.Twelve}, + {24, WordCount.TwentyFour}, + {21, WordCount.TwentyOne} + }; + + readonly static Dictionary _WordcountReverse; +} diff --git a/BTCPayServer.Client/JsonConverters/WordlistJsonConverter.cs b/BTCPayServer.Client/JsonConverters/WordlistJsonConverter.cs new file mode 100644 index 000000000..6dd533a51 --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/WordlistJsonConverter.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NBitcoin; +using Newtonsoft.Json; + +public class WordlistJsonConverter : JsonConverter +{ + static WordlistJsonConverter() + { + + _Wordlists = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"English", Wordlist.English}, + {"Japanese", Wordlist.Japanese}, + {"Spanish", Wordlist.Spanish}, + {"ChineseSimplified", Wordlist.ChineseSimplified}, + {"ChineseTraditional", Wordlist.ChineseTraditional}, + {"French", Wordlist.French}, + {"PortugueseBrazil", Wordlist.PortugueseBrazil}, + {"Czech", Wordlist.Czech} + }; + + _WordlistsReverse = _Wordlists.ToDictionary(kv => kv.Value, kv => kv.Key); + } + + public override bool CanConvert(Type objectType) + { + return typeof(Wordlist).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 NBitcoin.JsonConverters.JsonObjectException( + $"Unexpected json token type, expected String, actual {reader.TokenType}", reader); + if (!_Wordlists.TryGetValue((string)reader.Value, out var result)) + throw new NBitcoin.JsonConverters.JsonObjectException( + $"Invalid wordlist, possible values {string.Join(", ", _Wordlists.Keys.ToArray())} (default: English)", + reader); + return result; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is Wordlist wl) + writer.WriteValue(_WordlistsReverse[wl]); + } + + readonly static Dictionary _Wordlists; + readonly static Dictionary _WordlistsReverse; +} diff --git a/BTCPayServer.Client/Models/GenerateOnChainWalletRequest.cs b/BTCPayServer.Client/Models/GenerateOnChainWalletRequest.cs new file mode 100644 index 000000000..48b213215 --- /dev/null +++ b/BTCPayServer.Client/Models/GenerateOnChainWalletRequest.cs @@ -0,0 +1,25 @@ +using BTCPayServer.Client.JsonConverters; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client +{ + public class GenerateOnChainWalletRequest + { + public int AccountNumber { get; set; } = 0; + [JsonConverter(typeof(MnemonicJsonConverter))] + public Mnemonic ExistingMnemonic { get; set; } + [JsonConverter(typeof(WordlistJsonConverter))] + public NBitcoin.Wordlist WordList { get; set; } + + [JsonConverter(typeof(WordcountJsonConverter))] + public NBitcoin.WordCount? WordCount { get; set; } = NBitcoin.WordCount.Twelve; + + [JsonConverter(typeof(StringEnumConverter))] + public NBitcoin.ScriptPubKeyType ScriptPubKeyType { get; set; } = ScriptPubKeyType.Segwit; + public string Passphrase { get; set; } + public bool ImportKeysToRPC { get; set; } + public bool SavePrivateKeys { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs b/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs index 5c8f60745..bbea7642b 100644 --- a/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs +++ b/BTCPayServer.Client/Models/OnChainPaymentMethodData.cs @@ -1,3 +1,5 @@ +using NBitcoin; + namespace BTCPayServer.Client.Models { public class OnChainPaymentMethodData : OnChainPaymentMethodBaseData @@ -17,9 +19,11 @@ namespace BTCPayServer.Client.Models } - public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled) + public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath) { Enabled = enabled; + Label = label; + AccountKeyPath = accountKeyPath; CryptoCode = cryptoCode; DerivationScheme = derivationScheme; } diff --git a/BTCPayServer.Client/Models/OnChainPaymentMethodDataWithSensitiveData.cs b/BTCPayServer.Client/Models/OnChainPaymentMethodDataWithSensitiveData.cs new file mode 100644 index 000000000..3c670c6ce --- /dev/null +++ b/BTCPayServer.Client/Models/OnChainPaymentMethodDataWithSensitiveData.cs @@ -0,0 +1,23 @@ +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) : base(cryptoCode, derivationScheme, enabled, + label, accountKeyPath) + { + Mnemonic = mnemonic; + } + + [JsonConverter(typeof(MnemonicJsonConverter))] + public Mnemonic Mnemonic { get; set; } + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index fc1ab7b5d..8fecff68e 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1427,8 +1427,12 @@ namespace BTCPayServer.Tests using var tester = ServerTester.Create(); await tester.StartAsync(); var user = tester.NewAccount(); + var user2 = tester.NewAccount(); await user.GrantAccessAsync(true); + await user2.GrantAccessAsync(false); + var client = await user.CreateClient(Policies.CanModifyStoreSettings); + var client2 = await user2.CreateClient(Policies.CanModifyStoreSettings); var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings); var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" }); @@ -1437,9 +1441,10 @@ namespace BTCPayServer.Tests await AssertHttpError(403, async () => { await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { }); - }); + }); + var xpriv = new Mnemonic("all all all all all all all all all all all all").DeriveExtKey() - .Derive(KeyPath.Parse("m/84'/0'/0'")); + .Derive(KeyPath.Parse("m/84'/1'/0'")); var xpub = xpriv.Neuter().ToString(Network.RegTest); var firstAddress = xpriv.Derive(KeyPath.Parse("0/0")).Neuter().GetPublicKey().GetAddress(ScriptPubKeyType.Segwit, Network.RegTest).ToString(); await AssertHttpError(404, async () => @@ -1475,6 +1480,60 @@ namespace BTCPayServer.Tests { await client.GetStoreOnChainPaymentMethod(store.Id, "BTC"); }); + + await AssertHttpError(403, async () => + { + await viewOnlyClient.GenerateOnChainWallet(store.Id, "BTC", new GenerateOnChainWalletRequest() { }); + }); + + await AssertValidationError(new []{"SavePrivateKeys", "ImportKeysToRPC"}, async () => + { + await client2.GenerateOnChainWallet(user2.StoreId, "BTC", new GenerateOnChainWalletRequest() + { + SavePrivateKeys = true, + ImportKeysToRPC = true + }); + }); + + + var allMnemonic = new Mnemonic("all all all all all all all all all all all all"); + + + await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + var generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC", + new GenerateOnChainWalletRequest() {ExistingMnemonic = allMnemonic,}); + + Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString()); + Assert.Equal(generateResponse.DerivationScheme, xpub); + + await AssertAPIError("already-configured", async () => + { + await client.GenerateOnChainWallet(store.Id, "BTC", + new GenerateOnChainWalletRequest() {ExistingMnemonic = allMnemonic,}); + }); + + await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC", + new GenerateOnChainWalletRequest() {}); + Assert.NotEqual(generateResponse.Mnemonic.ToString(), allMnemonic.ToString()); + Assert.Equal(generateResponse.Mnemonic.DeriveExtKey().Derive(KeyPath.Parse("m/84'/1'/0'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme); + + await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC", + new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1}); + + Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString()); + + Assert.Equal(new Mnemonic("all all all all all all all all all all all all").DeriveExtKey() + .Derive(KeyPath.Parse("m/84'/1'/1'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme); + + await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC"); + generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC", + new GenerateOnChainWalletRequest() { WordList = Wordlist.Japanese, WordCount = WordCount.TwentyFour}); + + Assert.Equal(24,generateResponse.Mnemonic.Words.Length); + Assert.Equal(Wordlist.Japanese,generateResponse.Mnemonic.WordList); + } [Fact(Timeout = 60 * 2 * 1000)] @@ -1883,7 +1942,7 @@ namespace BTCPayServer.Tests var randK = new Mnemonic(Wordlist.English, WordCount.Twelve).DeriveExtKey().Neuter().ToString(Network.RegTest); await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC", - new OnChainPaymentMethodData("BTC", randK, true)); + new OnChainPaymentMethodData("BTC", randK, true, "testing", null)); void VerifyOnChain(Dictionary dictionary) { diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 4984a5f70..2b95790c6 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NBitcoin; +using NBXplorer.Models; using YamlDotNet.Core.Tokens; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; using Language = BTCPayServer.Client.Models.Language; @@ -883,5 +884,21 @@ namespace BTCPayServer.Controllers.GreenField { return Task.FromResult(GetFromActionResult(_storePaymentMethodsController.GetStorePaymentMethods(storeId, enabled))); } + + public override async Task GenerateOnChainWallet(string storeId, string cryptoCode, GenerateOnChainWalletRequest request, + CancellationToken token = default) + { + return GetFromActionResult(await _chainPaymentMethodsController.GenerateOnChainWallet(storeId, cryptoCode, new GenerateWalletRequest() + { + Passphrase = request.Passphrase, + AccountNumber = request.AccountNumber, + ExistingMnemonic = request.ExistingMnemonic?.ToString(), + WordCount = request.WordCount, + WordList = request.WordList, + SavePrivateKeys = request.SavePrivateKeys, + ScriptPubKeyType = request.ScriptPubKeyType, + ImportKeysToRPC = request.ImportKeysToRPC + })); + } } } diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.WalletGeneration.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.WalletGeneration.cs new file mode 100644 index 000000000..6425f4058 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.WalletGeneration.cs @@ -0,0 +1,103 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; +using BTCPayServer.Payments; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBXplorer.Models; + +namespace BTCPayServer.Controllers.GreenField +{ + public partial class StoreOnChainPaymentMethodsController + { + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate")] + public async Task GenerateOnChainWallet(string storeId, string cryptoCode, + GenerateWalletRequest request) + { + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + if (network is null) + { + return NotFound(); + } + + if (!_walletProvider.IsAvailable(network)) + { + return this.CreateAPIError("not-available", + $"{cryptoCode} services are not currently available"); + } + + var method = GetExistingBtcLikePaymentMethod(cryptoCode); + if (method != null) + { + return this.CreateAPIError("already-configured", + $"{cryptoCode} wallet is already configured for this store"); + } + + var canUseHotWallet = await CanUseHotWallet(); + if (request.SavePrivateKeys && !canUseHotWallet.HotWallet) + { + ModelState.AddModelError(nameof(request.SavePrivateKeys), + "This instance forbids non-admins from having a hot wallet for your store."); + } + + if (request.ImportKeysToRPC && !canUseHotWallet.RPCImport) + { + ModelState.AddModelError(nameof(request.ImportKeysToRPC), + "This instance forbids non-admins from having importing the wallet addresses/keys to the underlying node."); + } + + if (!ModelState.IsValid) + { + return this.CreateValidationError(ModelState); + } + + var client = _explorerClientProvider.GetExplorerClient(network); + GenerateWalletResponse response; + try + { + response = await client.GenerateWalletAsync(request); + if (response == null) + { + return this.CreateAPIError("not-available", + $"{cryptoCode} services are not currently available"); + } + } + catch (Exception e) + { + return this.CreateAPIError("not-available", + $"{cryptoCode} error: {e.Message}"); + } + + var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network); + + derivationSchemeSettings.Source = + string.IsNullOrEmpty(request.ExistingMnemonic) ? "NBXplorerGenerated" : "ImportedSeed"; + derivationSchemeSettings.IsHotWallet = request.SavePrivateKeys; + + var accountSettings = derivationSchemeSettings.GetSigningAccountKeySettings(); + accountSettings.AccountKeyPath = response.AccountKeyPath.KeyPath; + accountSettings.RootFingerprint = response.AccountKeyPath.MasterFingerprint; + derivationSchemeSettings.AccountOriginal = response.DerivationScheme.ToString(); + + var store = Store; + var storeBlob = store.GetStoreBlob(); + store.SetSupportedPaymentMethod(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike), + derivationSchemeSettings); + store.SetStoreBlob(storeBlob); + await _storeRepository.UpdateStore(store); + var rawResult = GetExistingBtcLikePaymentMethod(cryptoCode, store); + var result = new OnChainPaymentMethodDataWithSensitiveData(rawResult.CryptoCode, rawResult.DerivationScheme, + rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic()); + return Ok(result); + } + + private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() + { + return await _authorizationService.CanUseHotWallet(await _settingsRepository.GetPolicies(), User); + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs index 1604f3709..1e7b1331f 100644 --- a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Data; +using BTCPayServer.HostedServices; using BTCPayServer.Payments; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; @@ -12,27 +14,36 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBXplorer.DerivationStrategy; +using NBXplorer.Models; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers.GreenField { [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - public class StoreOnChainPaymentMethodsController : ControllerBase + public partial class StoreOnChainPaymentMethodsController : ControllerBase { private StoreData Store => HttpContext.GetStoreData(); private readonly StoreRepository _storeRepository; private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly BTCPayWalletProvider _walletProvider; + private readonly IAuthorizationService _authorizationService; + private readonly ISettingsRepository _settingsRepository; + private readonly ExplorerClientProvider _explorerClientProvider; public StoreOnChainPaymentMethodsController( StoreRepository storeRepository, BTCPayNetworkProvider btcPayNetworkProvider, - BTCPayWalletProvider walletProvider) + BTCPayWalletProvider walletProvider, + IAuthorizationService authorizationService, + ExplorerClientProvider explorerClientProvider, ISettingsRepository settingsRepository) { _storeRepository = storeRepository; _btcPayNetworkProvider = btcPayNetworkProvider; _walletProvider = walletProvider; + _authorizationService = authorizationService; + _explorerClientProvider = explorerClientProvider; + _settingsRepository = settingsRepository; } public static IEnumerable GetOnChainPaymentMethods(StoreData store, @@ -46,7 +57,7 @@ namespace BTCPayServer.Controllers.GreenField .OfType() .Select(strategy => new OnChainPaymentMethodData(strategy.PaymentId.CryptoCode, - strategy.AccountDerivation.ToString(), !excludedPaymentMethods.Match(strategy.PaymentId))) + strategy.AccountDerivation.ToString(), !excludedPaymentMethods.Match(strategy.PaymentId), strategy.Label, strategy.GetSigningAccountKeySettings().GetRootedKeyPath())) .Where((result) => enabled is null || enabled == result.Enabled) .ToList(); } @@ -279,11 +290,8 @@ namespace BTCPayServer.Controllers.GreenField return paymentMethod == null ? null : new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode, - paymentMethod.AccountDerivation.ToString(), !excluded) - { - Label = paymentMethod.Label, - AccountKeyPath = paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath() - }; + paymentMethod.AccountDerivation.ToString(), !excluded, paymentMethod.Label, + paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath()); } } } 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 aa30086f4..612435de1 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 @@ -405,6 +405,23 @@ "$ref": "#/components/schemas/OnChainPaymentMethodData" } }, + "OnChainPaymentMethodDataWithSensitiveData": { + "allOf": [ + { + "$ref": "#/components/schemas/OnChainPaymentMethodData" + }, + { + "type": "object", + "properties": { + "mnemonic": { + "type": "string", + "description": "The mnemonic used to generate the wallet", + "nullable": false + } + } + } + ] + }, "OnChainPaymentMethodBaseData": { "type": "object", "additionalProperties": false, @@ -467,6 +484,86 @@ "description": "The address generated at the key path" } } + }, + "GenerateOnChainWalletRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "existingMnemonic": { + "type": "string", + "description": "An existing BIP39 mnemonic seed to generate the wallet with" + }, + "passphrase": { + "type": "string", + "description": "A passphrase for the BIP39 mnemonic seed" + }, + "accountNumber": { + "type": "number", + "default": 0, + "description": "The account to derive from the BIP39 mnemonic seed" + }, + "savePrivateKeys": { + "type": "boolean", + "default": false, + "description": "Whether to store the seed inside BTCPay Server to enable some additional services. IF `false` AND `existingMnemonic` IS NOT SPECIFIED, BE SURE TO SECURELY STORE THE SEED IN THE RESPONSE!" + }, + "importKeysToRPC": { + "type": "boolean", + "default": false, + "description": "Whether to import all addresses generated via BTCPay Server into the underlying node wallet. (Private keys will also be imported if `savePrivateKeys` is set to true." + }, + "wordList": { + "type": "string", + "description": "If `existingMnemonic` is not set, a mnemonic is generated using the specified wordList.", + "default": "English", + "x-enumNames": [ + "English", + "Japanese", + "Spanish", + "ChineseSimplified", + "ChineseTraditional", + "French", + "PortugueseBrazil", + "Czech" + ], + "enum": [ + "English", + "Japanese", + "Spanish", + "ChineseSimplified", + "ChineseTraditional", + "French", + "PortugueseBrazil", + "Czech" + ] + }, + "wordCount": { + "type": "number", + "description": "If `existingMnemonic` is not set, a mnemonic is generated using the specified wordCount.", + "default": 12, + "x-enumNames": [ + 12,15,18,21,24 + ], + "enum": [ + 12,15,18,21,24 + ] + }, + "scriptPubKeyType": { + "type": "string", + "description": "the type of wallet to generate", + "default": "Segwit", + "x-enumNames": [ + "Legacy", + "Segwit", + "SegwitP2SH" + ], + "enum": [ + "Legacy", + "Segwit", + "SegwitP2SH" + ] + } + } } } },