Merge pull request #6684 from btcpayserver/wallet-policy

Add support for wallet policy descriptors (BIP388)
This commit is contained in:
Nicolas Dorier
2025-04-21 16:09:05 +09:00
committed by GitHub
6 changed files with 213 additions and 19 deletions

View File

@@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.2" />
<PackageReference Include="NBitcoin" Version="7.0.48" />
<PackageReference Include="NBitcoin" Version="8.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="NBitcoin" Version="7.0.48" />
<PackageReference Include="NBitcoin" Version="8.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.2.0" />

View File

@@ -36,6 +36,7 @@ using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.RPC;
using NBitcoin.Scripting.Parser;
using NBitcoin.WalletPolicies;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
@@ -893,6 +894,39 @@ namespace BTCPayServer.Tests
Assert.Equal(4, tor.Services.Length);
}
[Theory]
[InlineData("bcrt1q9zf7xapkmujeee9mfua9a7n6lkehrsv2h0z3nm", "wpkh([aaaaaaaa/84h/1h/0h]tpubDC6aWgsG4Tgiqu9vkK8Hoehc4Zu7oSmQ5G38ZhRJPSr1TabHocNeZ3DqqxnjLD6Zs16kwmEYCsBvGN3xE25BTmZ8ByxEdF2L9b2swdSxm1L/<0;1>/*)")]
[InlineData("mxfapt6u6tZ41UbFxNaEhq8QkKqYm6tvy3", "pkh([aaaaaaaa/44h/1h/0h]tpubDDV486pBqkML6Ywhznz8DS3VS95h3q4A2pUMCc6yy739QpKMg3gA8EXGrjraDBDxrhLsezepjCEfBtak5wngDH4vMh6aXKV8hPN7JsMtdEf/<0;1>/*)")]
[InlineData("2MsMb9CdVZwESkctbQfD5s8v3CszzbSxgUy", "sh(wpkh([aaaaaaaa/49h/1h/0h]tpubDDJa9q5audQLxtPyhrgapyByEHHSWQxKrADKA8dX8xNqAV5zrnnCVyHP9aNxojxi27Beus36V8D4Lqd6dZEDonxVMofgDK92zNLfeKhC54J/<0;1>/*))")]
[InlineData("bcrt1p7cwzrrg5te7r6tpham2qu7vws2x6vlswn9404raasc6aptuv3n2ss3zlrs","tr([aaaaaaaa/86h/1h/0h]tpubDCw9VEKFjxDk4MCLbPeYpsVP5aEP4sCgj1wbexMv38Y67YzczDHgjxBz2rqJDGwiNx8FH8uXyEUwYBruqJBBHW2Lrn4LPAZ3kj1qDkXuV2m/<0;1>/*)")]
[InlineData("2NAd39MvEYKqxfdZzEeW1PX3mYoL66kshWy","sh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")]
[InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")]
[InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*))")]
[InlineData("2MygJNZseGwenG6ASz2nbBAXrMR8gfUbu7h","sh(wsh(multi(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")]
[InlineData("2NEw4G7BMVJhcUXs79vpJcahYzpN8kwgE2N","sh(wsh(multi(2,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*))")]
[InlineData("bcrt1qy9tspfhktwm22cp54qv2pxqqwtney2w494xc4mapw3akhjtqx85sfkp6dh","wsh(sortedmulti(2,[aaaaaaaa/45h]tpubD9DcTBTiabXsjPBnVBKvNadiuhmw7rR56FqePc1kRgwQTiETnHBvgFbWtce3yTgsbRQ2ST1hA5eDoy3V4FwRvHFyEWiQxGaYFxRDDH4eBKb/<0;1>/*,[aaaaaaaa/45h]tpubD8HUcsDEkpLdQEHDDmA8fkLeb6yWXEyTCyzdpLbbEjLQhiiBHSfeYFDDmoEe5Nf9f6YY4LRdUwhkmwSBsSpn1PN7191pcDzP2APhrGXMVg6/<0;1>/*))")]
public void CanParseDerivationSchemesBIP388(string expectedAddress, string policy)
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var parser = new DerivationSchemeParser(networkProvider.BTC);
var scheme = parser.ParseOutputDescriptor(policy);
var script = Miniscript.Parse(policy, new MiniscriptParsingSettings(networkProvider.BTC.NBitcoinNetwork)
{
Dialect = MiniscriptDialect.BIP388,
AllowedParameters = ParameterTypeFlags.None
});
for (int i = 0; i < 5; i++)
{
var expectedScripts = script.Derive(AddressIntent.Deposit, i).Miniscript.ToScripts();
var actual = scheme.Item1.GetDerivation(new KeyPath(0, (uint)i));
Assert.Equal(expectedScripts.ScriptPubKey, actual.ScriptPubKey);
Assert.Equal(expectedScripts.RedeemScript, actual.Redeem);
if (i == 0)
Assert.Equal(expectedAddress, expectedScripts.ScriptPubKey.GetDestinationAddress(networkProvider.BTC.NBitcoinNetwork)!.ToString());
}
}
[Fact]
public void CanParseDerivationSchemes()
{
@@ -938,9 +972,9 @@ namespace BTCPayServer.Tests
Assert.True(((DirectDerivationStrategy)strategyBase).Segwit);
// Failure cases
Assert.Throws<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.Throws<ParsingException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
Assert.ThrowsAny<FormatException>(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
Assert.ThrowsAny<FormatException>(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
Assert.ThrowsAny<FormatException>(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
}
public static WalletFileParsers GetParsers()
{
@@ -2126,15 +2160,15 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
//we don't support every descriptor, only the ones which represent an HD wallet with stndard derivation paths
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))"));
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))"));
//let's see what we actually support now
@@ -2150,7 +2184,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
"pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.Item1.ToString());
//a master fingerprint must always be present if youre providing rooted path
Assert.Throws<ParsingException>(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
@@ -2158,7 +2192,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.Item1.ToString());
//but a different deriv path from standard (0/*) is not supported
Assert.Throws<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
Assert.ThrowsAny<FormatException>(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
//p2sh-segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(

View File

@@ -51,6 +51,7 @@
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.25" />
<PackageReference Include="NBitcoin" Version="8.0.8" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.4" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />

View File

@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using NBitcoin;
using NBitcoin.Scripting;
using NBitcoin.WalletPolicies;
using NBXplorer.DerivationStrategy;
using static NBitcoin.WalletPolicies.MiniscriptNode;
namespace BTCPayServer
{
@@ -22,6 +25,159 @@ namespace BTCPayServer
}
public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
try
{
return ParseLegacyOutputDescriptor(str);
}
catch
{
return ParseMiniscript(str);
}
}
private (DerivationStrategyBase, RootedKeyPath[]) ParseMiniscript(string str)
{
bool ExtractMultisigs(ReadOnlySpan<MiniscriptNode> nodes, [MaybeNullWhen(false)] out BitcoinExtPubKey[] keys, [MaybeNullWhen(false)] out RootedKeyPath[] keyPaths)
{
keys = new BitcoinExtPubKey[nodes.Length];
keyPaths = new RootedKeyPath[nodes.Length];
int i = 0;
foreach (var n in nodes)
{
if (n is MultipathNode { Target: HDKeyNode { Key: BitcoinExtPubKey pk, RootedKeyPath: {} kpath }, DepositIndex: 0, ChangeIndex: 1 })
{
keys[i] = pk;
keyPaths[i] = kpath;
i++;
}
else
{
return false;
}
}
return true;
}
var factory = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory;
var script = Miniscript.Parse(str, new MiniscriptParsingSettings(Network)
{
Dialect = MiniscriptDialect.BIP388,
AllowedParameters = ParameterTypeFlags.None
});
return script.RootNode switch
{
// ---Single sigs---
// Taproot
TaprootNode
{
InternalKeyNode: MultipathNode multi,
ScriptTreeRootNode: null
} when ExtractMultisigs([multi], out var pks, out var rpks)
=> (factory.CreateDirectDerivationStrategy(pks[0], new()
{
ScriptPubKeyType = ScriptPubKeyType.TaprootBIP86
}), rpks),
// P2PKH
FragmentSingleParameter
{
Descriptor: { Name: "pkh" },
X: MultipathNode multi
} when ExtractMultisigs([multi], out var pks, out var rpks)
=> (factory.CreateDirectDerivationStrategy(pks[0], new()
{
ScriptPubKeyType = ScriptPubKeyType.Legacy
}), rpks),
// P2WPKH
FragmentSingleParameter
{
Descriptor: { Name: "wpkh" },
X: MultipathNode multi
} when ExtractMultisigs([multi], out var pks, out var rpks)
=> (factory.CreateDirectDerivationStrategy(pks[0], new()
{
ScriptPubKeyType = ScriptPubKeyType.Segwit
}), rpks),
// Wrapped P2WPKH
FragmentSingleParameter
{
Descriptor: { Name: "sh" },
X: FragmentSingleParameter {
Descriptor: { Name: "wpkh" },
X: MultipathNode multi
}
} when ExtractMultisigs([multi], out var pks, out var rpks)
=> (factory.CreateDirectDerivationStrategy(pks[0], new()
{
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH
}), rpks),
// ---Multi sigs---
// Multsig SH
FragmentSingleParameter
{
Descriptor: { Name: "sh" },
X: FragmentUnboundedParameters {
Descriptor: { Name: "multi" or "sortedmulti" } desc
} multiNode
} when
multiNode.Parameters.ToArray() is
[ Value.CountValue { Count: var cnt }, .. { } multis]
&& ExtractMultisigs(multis, out var pks, out var rpks)
=> (factory.CreateMultiSigDerivationStrategy(pks, cnt, new()
{
ScriptPubKeyType = ScriptPubKeyType.Legacy,
KeepOrder = desc.Name == "multi"
}), rpks),
// P2WSH
FragmentSingleParameter
{
Descriptor: { Name: "wsh" },
X: FragmentUnboundedParameters {
Descriptor: { Name: "multi" or "sortedmulti" } desc
} multiNode
} when
multiNode.Parameters.ToArray() is
[ Value.CountValue { Count: var cnt }, .. { } multis]
&& ExtractMultisigs(multis, out var pks, out var rpks)
=> (factory.CreateMultiSigDerivationStrategy(pks, cnt, new()
{
ScriptPubKeyType = ScriptPubKeyType.Segwit,
KeepOrder = desc.Name == "multi"
}), rpks),
// Wrapped P2WSH
FragmentSingleParameter
{
Descriptor: { Name: "sh" },
X: FragmentSingleParameter
{
Descriptor: { Name: "wsh" },
X: FragmentUnboundedParameters {
Descriptor: { Name: "multi" or "sortedmulti" } desc
} multiNode
}
} when
multiNode.Parameters.ToArray() is
[ Value.CountValue { Count: var cnt }, .. { } multis]
&& ExtractMultisigs(multis, out var pks, out var rpks)
=> (factory.CreateMultiSigDerivationStrategy(pks, cnt, new()
{
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH,
KeepOrder = desc.Name == "multi"
}), rpks),
_ => throw new FormatException("Not supporting this script policy (BIP388) yet.")
};
}
private (DerivationStrategyBase, RootedKeyPath[]) ParseLegacyOutputDescriptor(string str)
{
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
string suffix = "")
@@ -52,11 +208,11 @@ namespace BTCPayServer
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted ? "" : "-[keeporder]")}"),
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
}
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
//nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported
var outputDescriptor = OutputDescriptor.Parse(str, Network);
switch (outputDescriptor)
{
@@ -97,6 +253,7 @@ namespace BTCPayServer
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
}
}
public DerivationStrategyBase Parse(string str)
{
ArgumentNullException.ThrowIfNull(str);

View File

@@ -197,7 +197,8 @@ namespace BTCPayServer.Payments.PayJoin
psbtFormat = false;
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
return BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt"));
ctx.OriginalTransaction = tx;
ctx.OriginalTransaction = tx.Clone();
tx.RemoveSignatures();
psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT;
for (int i = 0; i < tx.Inputs.Count; i++)
@@ -474,6 +475,7 @@ namespace BTCPayServer.Payments.PayJoin
}
var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
newTx.RemoveSignatures();
var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork);
foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value))
{