Compare commits

...

3 Commits

7 changed files with 135 additions and 66 deletions

View File

@ -993,83 +993,103 @@ normal:
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))"));
//let's see what we actually support now
void AssertOuptutDescriptor(string descriptor, string expectedKeyPath, string expectedFingerprint, string expectedDerivationScheme)
{
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(descriptor);
Assert.Equal(KeyPath.Parse(expectedKeyPath), Assert.Single(parsedDescriptor.AccountKeySettings).AccountKeyPath);
Assert.Equal(HDFingerprint.Parse(expectedFingerprint), Assert.Single(parsedDescriptor.AccountKeySettings).RootFingerprint);
Assert.Equal(expectedDerivationScheme, parsedDescriptor.AccountDerivation.ToString());
Assert.Contains(parsedDescriptor.GetOutputDescriptors(), o =>
{
var outputStr = o.ToString();
// Remove checksum, tests don't have it
outputStr = outputStr.Substring(0, outputStr.Length - 9);
return outputStr == descriptor;
});
}
//standard legacy hd wallet
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal(KeyPath.Parse("44'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)",
expectedKeyPath: "44'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]"
);
//masterfingerprint and key path are optional
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
var parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
//a master fingerprint must always be present if youre providing rooted path
Assert.Throws<ParsingException>(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
//but a different deriv path from standard (0/*) is not supported
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
//p2sh-segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(wpkh([d34db33f/49'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(KeyPath.Parse("49'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[p2sh]",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "sh(wpkh([d34db33f/49'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))",
expectedKeyPath: "49'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[p2sh]"
);
//segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"wpkh([d34db33f/84'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal(KeyPath.Parse("84'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath);
Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint);
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",parsedDescriptor.Item1.ToString() );
AssertOuptutDescriptor(
descriptor: "wpkh([d34db33f/84'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)",
expectedKeyPath: "84'/0'/0'",
expectedFingerprint: "d34db33f",
expectedDerivationScheme: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
);
//multisig tests
//legacy
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(multi(1,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
var strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.Item1).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
var strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner);
Assert.True(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]-[keeporder]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]-[keeporder]",parsedDescriptor.AccountDerivation.ToString() );
//segwit
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(parsedDescriptor.Item1).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner);
Assert.False(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]",parsedDescriptor.AccountDerivation.ToString() );
//segwit-p2sh
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)))");
Assert.Equal(2, parsedDescriptor.Item2.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.Item1).Inner).Inner);
Assert.Equal(2, parsedDescriptor.AccountKeySettings.Length);
strat = Assert.IsType<MultisigDerivationStrategy>(Assert.IsType<P2WSHDerivationStrategy>(Assert.IsType<P2SHDerivationStrategy>(parsedDescriptor.AccountDerivation).Inner).Inner);
Assert.False(strat.IsLegacy);
Assert.Equal(1,strat.RequiredSignatures);
Assert.Equal(2,strat.Keys.Count());
Assert.False(strat.LexicographicOrder);
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]-[p2sh]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]-[p2sh]",parsedDescriptor.AccountDerivation.ToString() );
//sorted
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
"sh(sortedmulti(1,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))");
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() );
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.AccountDerivation.ToString() );
}
}
}

View File

@ -689,16 +689,7 @@ namespace BTCPayServer.Controllers
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = network;
var result = parser.ParseOutputDescriptor(derivationScheme);
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2?.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(parser.Network)
}).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()];
return derivationSchemeSettings;
return parser.ParseOutputDescriptor(derivationScheme);
}
catch (Exception)
{

View File

@ -1092,6 +1092,7 @@ namespace BTCPayServer.Controllers
UriScheme = derivationSchemeSettings.Network.UriScheme,
Label = derivationSchemeSettings.Label,
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
OutputDescriptors = derivationSchemeSettings.GetOutputDescriptors().Select(d => d.ToString()).ToArray(),
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal,
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString(),
NBXSeedAvailable = derivationSchemeSettings.IsHotWallet &&

View File

@ -20,9 +20,23 @@ namespace BTCPayServer
BtcPayNetwork = expectedNetwork;
}
public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str)
public DerivationSchemeSettings ParseOutputDescriptor(string str)
{
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
DerivationSchemeSettings CreateDerivationSchemeSettings(DerivationStrategyBase derivationStrategy, RootedKeyPath[] origins)
{
var derivationSchemeSettings = new DerivationSchemeSettings();
derivationSchemeSettings.Network = BtcPayNetwork;
derivationSchemeSettings.AccountOriginal = str.Trim();
derivationSchemeSettings.AccountDerivation = derivationStrategy;
derivationSchemeSettings.AccountKeySettings = origins?.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = derivationStrategy.GetExtPubKeys().ElementAt(i).GetWif(Network)
}).ToArray() ?? new AccountKeySettings[derivationStrategy.GetExtPubKeys().Count()];
return derivationSchemeSettings;
}
DerivationSchemeSettings ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
string suffix = "")
{
switch (pubKeyProvider)
@ -35,10 +49,10 @@ namespace BTCPayServer
throw new FormatException("Custom change paths are not supported.");
}
return (Parse($"{hd.Extkey}{suffix}"), null);
return CreateDerivationSchemeSettings(Parse($"{hd.Extkey}{suffix}"), null);
case PubKeyProvider.Origin origin:
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
return (innerResult.Item1, new[] {origin.KeyOriginInfo});
return CreateDerivationSchemeSettings(innerResult.AccountDerivation, new[] {origin.KeyOriginInfo});
default:
throw new ArgumentOutOfRangeException();
}
@ -58,10 +72,10 @@ namespace BTCPayServer
throw new FormatException("Only output descriptors of one format are supported.");
case OutputDescriptor.Multi multi:
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
return (
return CreateDerivationSchemeSettings(
Parse(
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"),
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.AccountDerivation.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"),
xpubs.SelectMany(tuple => tuple.AccountKeySettings.Select(a => a.GetRootedKeyPath())).ToArray());
case OutputDescriptor.PKH pkh:
return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]");
case OutputDescriptor.SH sh:
@ -76,7 +90,7 @@ namespace BTCPayServer
sh.Inner is OutputDescriptor.WSH)
{
var ds = ParseOutputDescriptor(sh.Inner.ToString());
return (Parse(ds.Item1 + suffix), ds.Item2);
return CreateDerivationSchemeSettings(Parse(ds.AccountDerivation.ToString() + suffix), ds.AccountKeySettings.Select(d => d.GetRootedKeyPath()).ToArray());
};
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
case OutputDescriptor.WPKH wpkh:

View File

@ -5,6 +5,7 @@ using System.Text;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Scripting;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -52,15 +53,7 @@ namespace BTCPayServer
{
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
derivationSchemeSettings = derivationSchemeParser.ParseOutputDescriptor(xpub);
return true;
}
catch (Exception)
@ -382,6 +375,45 @@ namespace BTCPayServer
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath);
}
}
static KeyPath[] TrackedPaths = new[] { new KeyPath("0"), new KeyPath("1") };
public OutputDescriptor[] GetOutputDescriptors()
{
var signing = this.GetSigningAccountKeySettings();
if (!signing.RootFingerprint.HasValue || signing.AccountKeyPath is null)
return Array.Empty<OutputDescriptor>();
if (this.AccountDerivation is DirectDerivationStrategy direct)
{
var hdkey = direct.GetExtPubKeys().First().GetWif(this.Network.NBitcoinNetwork);
if (direct.ScriptPubKeyType() == ScriptPubKeyType.Segwit)
{
return TrackedPaths.Select(path => OutputDescriptor.NewWPKH(
PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork)).ToArray();
}
else if (direct.ScriptPubKeyType() == ScriptPubKeyType.Legacy)
{
return TrackedPaths.Select(path => OutputDescriptor.NewPKH(
PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork)).ToArray();
}
}
else if (this.AccountDerivation is P2SHDerivationStrategy p2sh)
{
if (this.AccountDerivation.ScriptPubKeyType() == ScriptPubKeyType.SegwitP2SH)
{
var hdkey = p2sh.GetExtPubKeys().First().GetWif(this.Network.NBitcoinNetwork);
return TrackedPaths.Select(path => OutputDescriptor.NewSH(
OutputDescriptor.NewWPKH(PubKeyProvider.NewOrigin(
signing.AccountKeyPath.ToRootedKeyPath(signing.RootFingerprint.Value),
PubKeyProvider.NewHD(hdkey, path, PubKeyProvider.DeriveType.UNHARDENED)), Network.NBitcoinNetwork), Network.NBitcoinNetwork)).ToArray();
}
}
// TODO support multisig
return Array.Empty<OutputDescriptor>();
}
}
public class AccountKeySettings
{

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.WalletViewModels
public string Label { get; set; }
[DisplayName("Derivation scheme")]
public string DerivationScheme { get; set; }
public string[] OutputDescriptors { get; set; }
public string DerivationSchemeInput { get; set; }
[Display(Name = "Is signing key")]
public string SelectedSigningKey { get; set; }

View File

@ -1,4 +1,4 @@
@using Newtonsoft.Json
@using Newtonsoft.Json
@using System.Text
@using NBitcoin.DataEncoders
@model WalletSettingsViewModel
@ -12,26 +12,36 @@
<div class="row">
<div class="col-md-8 col-lg-6">
<form method="post" asp-action="WalletSettings">
<input type="hidden" asp-for="StoreName"/>
<input type="hidden" asp-for="UriScheme"/>
<input type="hidden" asp-for="StoreName" />
<input type="hidden" asp-for="UriScheme" />
<div class="form-group">
<label asp-for="Label" class="form-label"></label>
<input asp-for="Label" class="form-control"/>
<input asp-for="Label" class="form-control" />
<span asp-validation-for="Label" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DerivationScheme" class="form-label"></label>
<input asp-for="DerivationScheme" class="form-control" readonly/>
<input asp-for="DerivationScheme" class="form-control" readonly />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div>
@if (!string.IsNullOrEmpty(Model.DerivationSchemeInput) && Model.DerivationSchemeInput != Model.DerivationScheme)
{
<div class="form-group">
<label asp-for="DerivationSchemeInput" class="form-label"></label>
<input asp-for="DerivationSchemeInput" class="form-control" readonly/>
<input asp-for="DerivationSchemeInput" class="form-control" readonly />
<span asp-validation-for="DerivationSchemeInput" class="text-danger"></span>
</div>
}
@if (Model.OutputDescriptors.Length != 0)
{
for (int i = 0; i < Model.OutputDescriptors.Length; i++)
{
<div class="form-group">
<label asp-for="@Model.OutputDescriptors[i]" class="form-label"></label>
<input asp-for="@Model.OutputDescriptors[i]" class="form-control" readonly />
</div>
}
}
@for (var i = 0; i < Model.AccountKeys.Count; i++)
{
<div class="d-flex mt-5 mb-3">
@ -40,25 +50,25 @@
</div>
<div class="form-group">
<label asp-for="@Model.AccountKeys[i].AccountKey" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly/>
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly />
<span asp-validation-for="@Model.AccountKeys[i].AccountKey" class="text-danger"></span>
</div>
<div class="row">
<div class="form-group col-auto">
<label asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" style="max-width:16ch;"/>
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" style="max-width:16ch;" />
<span asp-validation-for="@Model.AccountKeys[i].MasterFingerprint" class="text-danger"></span>
</div>
<div class="form-group col-auto">
<label asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-label"></label>
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" style="max-width:16ch;"/>
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" style="max-width:16ch;" />
<span asp-validation-for="@Model.AccountKeys[i].AccountKeyPath" class="text-danger"></span>
</div>
</div>
@if (Model.IsMultiSig)
{
<div class="form-check">
<input asp-for="SelectedSigningKey" class="form-check-input" type="radio" value="@Model.AccountKeys[i].AccountKey"/>
<input asp-for="SelectedSigningKey" class="form-check-input" type="radio" value="@Model.AccountKeys[i].AccountKey" />
<label asp-for="SelectedSigningKey" class="form-check-label"></label>
</div>
}