diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj
index d90b37706..7dc11de64 100644
--- a/BTCPayServer.Client/BTCPayServer.Client.csproj
+++ b/BTCPayServer.Client/BTCPayServer.Client.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
index 2ef9c74d9..5364f937a 100644
--- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj
+++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs
index 1bfd68a46..4328f89cf 100644
--- a/BTCPayServer.Tests/FastTests.cs
+++ b/BTCPayServer.Tests/FastTests.cs
@@ -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(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
- Assert.Throws(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
- Assert.Throws(() => { parser.ParseOutputDescriptor("wpkh([8b60afd1/49h/0h/0h]xpub661MyMwAFXkMnyoBjyHndD3QwRbcGVBsTGeNZN6QGVHcfz4MPzBUxjSevweNFQx7SqmMHLdSA4FteGsRrEriu4pnVZMZWnruFFAYZATtcDw/0/*)#9x4vkw48"); }); // invalid checksum
+ Assert.ThrowsAny(() => { parser.Parse("xpubZ661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw"); });
+ Assert.ThrowsAny(() => { parser.ParseOutputDescriptor("invalid"); }); // invalid in general
+ Assert.ThrowsAny(() => { 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(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))"));
- Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))"));
+ Assert.ThrowsAny(() => 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(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
+ Assert.ThrowsAny(() => 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(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
+ Assert.ThrowsAny(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"));
//p2sh-segwit hd wallet
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj
index 3260ad2cb..2f68e2e46 100644
--- a/BTCPayServer/BTCPayServer.csproj
+++ b/BTCPayServer/BTCPayServer.csproj
@@ -51,6 +51,7 @@
+
diff --git a/BTCPayServer/DerivationSchemeParser.cs b/BTCPayServer/DerivationSchemeParser.cs
index 69d4270d5..8964e7fed 100644
--- a/BTCPayServer/DerivationSchemeParser.cs
+++ b/BTCPayServer/DerivationSchemeParser.cs
@@ -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 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);
diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs
index 41a910e7e..fcd408a74 100644
--- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs
+++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs
@@ -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))
{