Compare commits
3 Commits
bugfix/vau
...
qiebrq
Author | SHA1 | Date | |
---|---|---|---|
28ef453206 | |||
47f03c814b | |||
db70458cd3 |
@ -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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 &&
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
}
|
||||
|
Reference in New Issue
Block a user