mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 17:36:59 +01:00
Add Bitcoin Output descriptor support (#2169)
* Add Output descriptor support * Fix exception message and fix Parse Deriv Scheme Settings when electrum * fix test
This commit is contained in:
parent
b8da6847b9
commit
01be74b219
8 changed files with 267 additions and 43 deletions
|
@ -27,7 +27,7 @@
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NBitcoin" Version="5.0.60" />
|
<PackageReference Include="NBitcoin" Version="5.0.68" />
|
||||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
|
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||||
<PackageReference Include="NBitcoin" Version="5.0.60" />
|
<PackageReference Include="NBitcoin" Version="5.0.68" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
|
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -44,6 +44,7 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
|
using NBitcoin.Scripting.Parser;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
|
@ -1040,6 +1041,99 @@ normal:
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
"tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]",
|
||||||
parsed.ToString());
|
parsed.ToString());
|
||||||
|
|
||||||
|
//let's test output descriptor parsing support
|
||||||
|
|
||||||
|
|
||||||
|
//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))"));
|
||||||
|
|
||||||
|
//let's see what we actually support now
|
||||||
|
|
||||||
|
//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() );
|
||||||
|
|
||||||
|
//masterfingerprint and key path are optional
|
||||||
|
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
|
||||||
|
"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/*)"));
|
||||||
|
|
||||||
|
|
||||||
|
parsedDescriptor = mainnetParser.ParseOutputDescriptor(
|
||||||
|
"pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)");
|
||||||
|
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/*)"));
|
||||||
|
|
||||||
|
//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() );
|
||||||
|
|
||||||
|
//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() );
|
||||||
|
|
||||||
|
//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.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() );
|
||||||
|
|
||||||
|
//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.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() );
|
||||||
|
|
||||||
|
|
||||||
|
//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.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() );
|
||||||
|
|
||||||
|
//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() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -72,7 +73,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")]
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")]
|
||||||
public ActionResult<OnChainPaymentMethodPreviewResultData> GetOnChainPaymentMethodPreview(
|
public IActionResult GetOnChainPaymentMethodPreview(
|
||||||
string cryptoCode,
|
string cryptoCode,
|
||||||
int offset = 0, int amount = 10)
|
int offset = 0, int amount = 10)
|
||||||
{
|
{
|
||||||
|
@ -86,25 +87,34 @@ namespace BTCPayServer.Controllers.GreenField
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
try
|
||||||
var strategy = DerivationSchemeSettings.Parse(paymentMethod.DerivationScheme, network);
|
|
||||||
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
|
||||||
|
|
||||||
var line = strategy.AccountDerivation.GetLineFor(deposit);
|
|
||||||
var result = new OnChainPaymentMethodPreviewResultData();
|
|
||||||
for (var i = offset; i < amount; i++)
|
|
||||||
{
|
{
|
||||||
var address = line.Derive((uint)i);
|
var strategy = DerivationSchemeSettings.Parse(paymentMethod.DerivationScheme, network);
|
||||||
result.Addresses.Add(
|
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||||
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
|
|
||||||
{
|
var line = strategy.AccountDerivation.GetLineFor(deposit);
|
||||||
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
|
var result = new OnChainPaymentMethodPreviewResultData();
|
||||||
Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork)
|
for (var i = offset; i < amount; i++)
|
||||||
.ToString()
|
{
|
||||||
});
|
var address = line.Derive((uint)i);
|
||||||
|
result.Addresses.Add(
|
||||||
|
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
|
||||||
|
{
|
||||||
|
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
|
||||||
|
Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork)
|
||||||
|
.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
|
||||||
|
"Invalid Derivation Scheme");
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,36 +135,37 @@ namespace BTCPayServer.Controllers.GreenField
|
||||||
}
|
}
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
|
DerivationSchemeSettings strategy;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
|
strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
|
||||||
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
|
||||||
var line = strategy.AccountDerivation.GetLineFor(deposit);
|
|
||||||
var result = new OnChainPaymentMethodPreviewResultData();
|
|
||||||
for (var i = offset; i < amount; i++)
|
|
||||||
{
|
|
||||||
var derivation = line.Derive((uint)i);
|
|
||||||
result.Addresses.Add(
|
|
||||||
new
|
|
||||||
OnChainPaymentMethodPreviewResultData.
|
|
||||||
OnChainPaymentMethodPreviewResultAddressItem()
|
|
||||||
{
|
|
||||||
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
|
|
||||||
Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
|
|
||||||
line.KeyPathTemplate.GetKeyPath((uint)i),
|
|
||||||
derivation.ScriptPubKey).ToString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
|
ModelState.AddModelError(nameof(OnChainPaymentMethodData.DerivationScheme),
|
||||||
"Invalid Derivation Scheme");
|
"Invalid Derivation Scheme");
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||||
|
var line = strategy.AccountDerivation.GetLineFor(deposit);
|
||||||
|
var result = new OnChainPaymentMethodPreviewResultData();
|
||||||
|
for (var i = offset; i < amount; i++)
|
||||||
|
{
|
||||||
|
var derivation = line.Derive((uint)i);
|
||||||
|
result.Addresses.Add(
|
||||||
|
new
|
||||||
|
OnChainPaymentMethodPreviewResultData.
|
||||||
|
OnChainPaymentMethodPreviewResultAddressItem()
|
||||||
|
{
|
||||||
|
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
|
||||||
|
Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
|
||||||
|
line.KeyPathTemplate.GetKeyPath((uint)i),
|
||||||
|
derivation.ScriptPubKey).ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
|
|
|
@ -7,7 +7,6 @@ using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
using BTCPayServer.Models;
|
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
|
|
@ -36,6 +36,7 @@ using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
using StoreData = BTCPayServer.Data.StoreData;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
|
@ -699,6 +700,26 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
var parser = new DerivationSchemeParser(network);
|
var parser = new DerivationSchemeParser(network);
|
||||||
parser.HintScriptPubKey = hint;
|
parser.HintScriptPubKey = hint;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBitcoin.Scripting;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
|
|
||||||
namespace BTCPayServer
|
namespace BTCPayServer
|
||||||
|
@ -21,6 +22,77 @@ namespace BTCPayServer
|
||||||
BtcPayNetwork = expectedNetwork;
|
BtcPayNetwork = expectedNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (DerivationStrategyBase, RootedKeyPath[]) ParseOutputDescriptor(string str)
|
||||||
|
{
|
||||||
|
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,
|
||||||
|
string suffix = "")
|
||||||
|
{
|
||||||
|
switch (pubKeyProvider)
|
||||||
|
{
|
||||||
|
case PubKeyProvider.Const _:
|
||||||
|
throw new FormatException("Only HD output descriptors are supported.");
|
||||||
|
case PubKeyProvider.HD hd:
|
||||||
|
if (hd.Path != null && hd.Path.ToString() != "0")
|
||||||
|
{
|
||||||
|
throw new FormatException("Custom change paths are not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Parse($"{hd.Extkey}{suffix}"), null);
|
||||||
|
case PubKeyProvider.Origin origin:
|
||||||
|
var innerResult = ExtractFromPkProvider(origin.Inner, suffix);
|
||||||
|
return (innerResult.Item1, new[] {origin.KeyOriginInfo});
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str == null)
|
||||||
|
throw new ArgumentNullException(nameof(str));
|
||||||
|
str = str.Trim();
|
||||||
|
var outputDescriptor = OutputDescriptor.Parse(str);
|
||||||
|
switch(outputDescriptor)
|
||||||
|
{
|
||||||
|
case OutputDescriptor.PK _:
|
||||||
|
case OutputDescriptor.Raw _:
|
||||||
|
case OutputDescriptor.Addr _:
|
||||||
|
throw new FormatException("Only HD output descriptors are supported.");
|
||||||
|
case OutputDescriptor.Combo _:
|
||||||
|
throw new FormatException("Only output descriptors of one format are supported.");
|
||||||
|
case OutputDescriptor.Multi multi:
|
||||||
|
var xpubs = multi.PkProviders.Select(provider => ExtractFromPkProvider(provider));
|
||||||
|
return (
|
||||||
|
Parse(
|
||||||
|
$"{multi.Threshold}-of-{(string.Join('-', xpubs.Select(tuple => tuple.Item1.ToString())))}{(multi.IsSorted?"":"-[keeporder]")}"),
|
||||||
|
xpubs.SelectMany(tuple => tuple.Item2).ToArray());
|
||||||
|
case OutputDescriptor.PKH pkh:
|
||||||
|
return ExtractFromPkProvider(pkh.PkProvider, "-[legacy]");
|
||||||
|
case OutputDescriptor.SH sh:
|
||||||
|
var suffix = "-[p2sh]";
|
||||||
|
if (sh.Inner is OutputDescriptor.Multi)
|
||||||
|
{
|
||||||
|
//non segwit
|
||||||
|
suffix = "-[legacy]";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sh.Inner is OutputDescriptor.Multi || sh.Inner is OutputDescriptor.WPKH ||
|
||||||
|
sh.Inner is OutputDescriptor.WSH)
|
||||||
|
{
|
||||||
|
var ds = ParseOutputDescriptor(sh.Inner.ToString());
|
||||||
|
return (Parse(ds.Item1 + suffix), ds.Item2);
|
||||||
|
};
|
||||||
|
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
|
||||||
|
case OutputDescriptor.WPKH wpkh:
|
||||||
|
return ExtractFromPkProvider(wpkh.PkProvider, "");
|
||||||
|
case OutputDescriptor.WSH wsh:
|
||||||
|
if (wsh.Inner is OutputDescriptor.Multi)
|
||||||
|
{
|
||||||
|
return ParseOutputDescriptor(wsh.Inner.ToString());
|
||||||
|
}
|
||||||
|
throw new FormatException("wsh descriptors are only supported with multisig");
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DerivationStrategyBase ParseElectrum(string str)
|
public DerivationStrategyBase ParseElectrum(string str)
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,8 +18,15 @@ namespace BTCPayServer
|
||||||
throw new ArgumentNullException(nameof(network));
|
throw new ArgumentNullException(nameof(network));
|
||||||
if (derivationStrategy == null)
|
if (derivationStrategy == null)
|
||||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||||
var result = network.NBXplorerNetwork.DerivationStrategyFactory.Parse(derivationStrategy);
|
var result = new DerivationSchemeSettings();
|
||||||
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
result.Network = network;
|
||||||
|
var parser = new DerivationSchemeParser(network);
|
||||||
|
if (TryParseXpub(derivationStrategy, parser, ref result, false) || TryParseXpub(derivationStrategy, parser, ref result, true))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FormatException("Invalid Derivation Scheme");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||||
|
@ -40,6 +47,26 @@ namespace BTCPayServer
|
||||||
|
|
||||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
||||||
{
|
{
|
||||||
|
if (!electrum)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
derivationSchemeSettings.AccountOriginal = xpub.Trim();
|
derivationSchemeSettings.AccountOriginal = xpub.Trim();
|
||||||
|
|
Loading…
Add table
Reference in a new issue