mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
GreenField: Generate Store OnChain Wallet (#2708)
* GreenField: Generate Store OnChain Wallet * Greenfield: Do not generate wallet if already configured
This commit is contained in:
@@ -78,5 +78,17 @@ namespace BTCPayServer.Client
|
|||||||
method: HttpMethod.Get), token);
|
method: HttpMethod.Get), token);
|
||||||
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
BTCPayServer.Client/JsonConverters/MnemonicJsonConverter.cs
Normal file
31
BTCPayServer.Client/JsonConverters/MnemonicJsonConverter.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.JsonConverters
|
||||||
|
{
|
||||||
|
public class MnemonicJsonConverter : JsonConverter<Mnemonic>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
BTCPayServer.Client/JsonConverters/WordcountJsonConverter.cs
Normal file
59
BTCPayServer.Client/JsonConverters/WordcountJsonConverter.cs
Normal file
@@ -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<long, WordCount>()
|
||||||
|
{
|
||||||
|
{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<long, WordCount> _Wordcount = new Dictionary<long, WordCount>()
|
||||||
|
{
|
||||||
|
{18, WordCount.Eighteen},
|
||||||
|
{15, WordCount.Fifteen},
|
||||||
|
{12, WordCount.Twelve},
|
||||||
|
{24, WordCount.TwentyFour},
|
||||||
|
{21, WordCount.TwentyOne}
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly static Dictionary<WordCount, long> _WordcountReverse;
|
||||||
|
}
|
||||||
55
BTCPayServer.Client/JsonConverters/WordlistJsonConverter.cs
Normal file
55
BTCPayServer.Client/JsonConverters/WordlistJsonConverter.cs
Normal file
@@ -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<string, Wordlist>(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<string, Wordlist> _Wordlists;
|
||||||
|
readonly static Dictionary<Wordlist, string> _WordlistsReverse;
|
||||||
|
}
|
||||||
25
BTCPayServer.Client/Models/GenerateOnChainWalletRequest.cs
Normal file
25
BTCPayServer.Client/Models/GenerateOnChainWalletRequest.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
public class OnChainPaymentMethodData : OnChainPaymentMethodBaseData
|
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;
|
Enabled = enabled;
|
||||||
|
Label = label;
|
||||||
|
AccountKeyPath = accountKeyPath;
|
||||||
CryptoCode = cryptoCode;
|
CryptoCode = cryptoCode;
|
||||||
DerivationScheme = derivationScheme;
|
DerivationScheme = derivationScheme;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1427,8 +1427,12 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = ServerTester.Create();
|
using var tester = ServerTester.Create();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
|
var user2 = tester.NewAccount();
|
||||||
await user.GrantAccessAsync(true);
|
await user.GrantAccessAsync(true);
|
||||||
|
await user2.GrantAccessAsync(false);
|
||||||
|
|
||||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings);
|
var client = await user.CreateClient(Policies.CanModifyStoreSettings);
|
||||||
|
var client2 = await user2.CreateClient(Policies.CanModifyStoreSettings);
|
||||||
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
|
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
|
||||||
|
|
||||||
var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" });
|
var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" });
|
||||||
@@ -1438,8 +1442,9 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { });
|
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()
|
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 xpub = xpriv.Neuter().ToString(Network.RegTest);
|
||||||
var firstAddress = xpriv.Derive(KeyPath.Parse("0/0")).Neuter().GetPublicKey().GetAddress(ScriptPubKeyType.Segwit, Network.RegTest).ToString();
|
var firstAddress = xpriv.Derive(KeyPath.Parse("0/0")).Neuter().GetPublicKey().GetAddress(ScriptPubKeyType.Segwit, Network.RegTest).ToString();
|
||||||
await AssertHttpError(404, async () =>
|
await AssertHttpError(404, async () =>
|
||||||
@@ -1475,6 +1480,60 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
|
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)]
|
[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);
|
var randK = new Mnemonic(Wordlist.English, WordCount.Twelve).DeriveExtKey().Neuter().ToString(Network.RegTest);
|
||||||
await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC",
|
await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC",
|
||||||
new OnChainPaymentMethodData("BTC", randK, true));
|
new OnChainPaymentMethodData("BTC", randK, true, "testing", null));
|
||||||
|
|
||||||
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
|
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBXplorer.Models;
|
||||||
using YamlDotNet.Core.Tokens;
|
using YamlDotNet.Core.Tokens;
|
||||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||||
using Language = BTCPayServer.Client.Models.Language;
|
using Language = BTCPayServer.Client.Models.Language;
|
||||||
@@ -883,5 +884,21 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
{
|
{
|
||||||
return Task.FromResult(GetFromActionResult(_storePaymentMethodsController.GetStorePaymentMethods(storeId, enabled)));
|
return Task.FromResult(GetFromActionResult(_storePaymentMethodsController.GetStorePaymentMethods(storeId, enabled)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId, string cryptoCode, GenerateOnChainWalletRequest request,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return GetFromActionResult<OnChainPaymentMethodDataWithSensitiveData>(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
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<IActionResult> GenerateOnChainWallet(string storeId, string cryptoCode,
|
||||||
|
GenerateWalletRequest request)
|
||||||
|
{
|
||||||
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
@@ -12,27 +14,36 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using NBXplorer.Models;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers.GreenField
|
namespace BTCPayServer.Controllers.GreenField
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public class StoreOnChainPaymentMethodsController : ControllerBase
|
public partial class StoreOnChainPaymentMethodsController : ControllerBase
|
||||||
{
|
{
|
||||||
private StoreData Store => HttpContext.GetStoreData();
|
private StoreData Store => HttpContext.GetStoreData();
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||||
private readonly BTCPayWalletProvider _walletProvider;
|
private readonly BTCPayWalletProvider _walletProvider;
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
private readonly ISettingsRepository _settingsRepository;
|
||||||
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||||
|
|
||||||
public StoreOnChainPaymentMethodsController(
|
public StoreOnChainPaymentMethodsController(
|
||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
BTCPayWalletProvider walletProvider)
|
BTCPayWalletProvider walletProvider,
|
||||||
|
IAuthorizationService authorizationService,
|
||||||
|
ExplorerClientProvider explorerClientProvider, ISettingsRepository settingsRepository)
|
||||||
{
|
{
|
||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
_walletProvider = walletProvider;
|
_walletProvider = walletProvider;
|
||||||
|
_authorizationService = authorizationService;
|
||||||
|
_explorerClientProvider = explorerClientProvider;
|
||||||
|
_settingsRepository = settingsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<OnChainPaymentMethodData> GetOnChainPaymentMethods(StoreData store,
|
public static IEnumerable<OnChainPaymentMethodData> GetOnChainPaymentMethods(StoreData store,
|
||||||
@@ -46,7 +57,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
.OfType<DerivationSchemeSettings>()
|
.OfType<DerivationSchemeSettings>()
|
||||||
.Select(strategy =>
|
.Select(strategy =>
|
||||||
new OnChainPaymentMethodData(strategy.PaymentId.CryptoCode,
|
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)
|
.Where((result) => enabled is null || enabled == result.Enabled)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -279,11 +290,8 @@ namespace BTCPayServer.Controllers.GreenField
|
|||||||
return paymentMethod == null
|
return paymentMethod == null
|
||||||
? null
|
? null
|
||||||
: new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
|
: new OnChainPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
|
||||||
paymentMethod.AccountDerivation.ToString(), !excluded)
|
paymentMethod.AccountDerivation.ToString(), !excluded, paymentMethod.Label,
|
||||||
{
|
paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath());
|
||||||
Label = paymentMethod.Label,
|
|
||||||
AccountKeyPath = paymentMethod.GetSigningAccountKeySettings().GetRootedKeyPath()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,6 +405,23 @@
|
|||||||
"$ref": "#/components/schemas/OnChainPaymentMethodData"
|
"$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": {
|
"OnChainPaymentMethodBaseData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -467,6 +484,86 @@
|
|||||||
"description": "The address generated at the key path"
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user