Add wallet policy support (#6765)

This commit is contained in:
Nicolas Dorier
2025-07-16 09:06:11 +09:00
committed by GitHub
parent 48fab4c5e6
commit 89c836e5f9
26 changed files with 91 additions and 39 deletions

View File

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

View File

@@ -18,6 +18,7 @@ namespace BTCPayServer.Client.Models
public string KeyPath { get; set; }
//The address generated at the key path
public string Address { get; set; }
public int Index { get; set; }
}
}
}

View File

@@ -2,7 +2,7 @@
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<PackageReference Include="NBXplorer.Client" Version="4.3.9" />
<PackageReference Include="NBXplorer.Client" Version="5.0.5" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
</Project>

View File

@@ -8,7 +8,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="NBitcoin.Altcoins" Version="4.0.8" />
<PackageReference Include="NBitcoin.Altcoins" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

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="8.0.13" />
<PackageReference Include="NBitcoin" Version="9.0.0" />
<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

@@ -918,7 +918,7 @@ namespace BTCPayServer.Tests
for (int i = 0; i < 5; i++)
{
var expectedScripts = script.Derive(AddressIntent.Deposit, i).Miniscript.ToScripts();
var actual = scheme.AccountDerivation.GetDerivation(new KeyPath(0, (uint)i));
var actual = ((StandardDerivationStrategyBase)scheme.AccountDerivation).GetDerivation(new KeyPath(0, (uint)i));
Assert.Equal(expectedScripts.ScriptPubKey, actual.ScriptPubKey);
Assert.Equal(expectedScripts.RedeemScript, actual.Redeem);
if (i == 0)
@@ -1029,7 +1029,7 @@ namespace BTCPayServer.Tests
"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD",
settings.AccountOriginal);
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
settings.AccountDerivation.GetDerivation().ScriptPubKey);
((StandardDerivationStrategyBase)settings.AccountDerivation).GetDerivation(new KeyPath()).ScriptPubKey);
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
@@ -1101,8 +1101,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.True(multsig.LexicographicOrder);
Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
var line = nunchuk.AccountDerivation.GetLineFor(DerivationFeature.Deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey);

View File

@@ -176,7 +176,7 @@ public class MultisigTests : UnitTestBase
var resp1 = new GenerateWalletResponse
{
MasterHDKey = key1,
DerivationScheme = parser.Parse(derivation),
DerivationScheme = (StandardDerivationStrategyBase)parser.Parse(derivation),
AccountKeyPath = RootedKeyPath.Parse(keypath)
};
return resp1;

View File

@@ -78,6 +78,7 @@ using Microsoft.Extensions.Caching.Memory;
using PosViewType = BTCPayServer.Client.Models.PosViewType;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Views.Stores;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Tests
{
@@ -730,7 +731,7 @@ namespace BTCPayServer.Tests
// Sending a coin
var txId = tester.ExplorerNode.SendToAddress(
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
((StandardDerivationStrategyBase)btcDerivationScheme).GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
tester.ExplorerNode.Generate(1);
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);

View File

@@ -99,7 +99,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.26
image: nicolasdorier/nbxplorer:2.5.28
restart: unless-stopped
ports:
- "32838:32838"

View File

@@ -62,7 +62,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.26
image: nicolasdorier/nbxplorer:2.5.28
restart: unless-stopped
ports:
- "32838:32838"

View File

@@ -57,7 +57,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.26
image: nicolasdorier/nbxplorer:2.5.28
restart: unless-stopped
ports:
- "32838:32838"

View File

@@ -95,7 +95,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.5.26
image: nicolasdorier/nbxplorer:2.5.28
restart: unless-stopped
ports:
- "32838:32838"

View File

@@ -51,9 +51,9 @@
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.25" />
<PackageReference Include="NBitcoin" Version="8.0.13" />
<PackageReference Include="NBitcoin" Version="9.0.0" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.4" />
<PackageReference Include="BIP78.Sender" Version="0.2.5" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.6" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.11" />
<PackageReference Include="CsvHelper" Version="32.0.3" />

View File

@@ -15,6 +15,8 @@ using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
@@ -115,18 +117,26 @@ namespace BTCPayServer.Controllers.Greenfield
internal static OnChainPaymentMethodPreviewResultData GetPreviewResultData(int offset, int count, BTCPayNetwork network, DerivationStrategyBase strategy)
{
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.GetLineFor(deposit);
var line = strategy.GetLineFor(DerivationFeature.Deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < count; i++)
{
var keyPath = new KeyPath(0, (uint)i);
if (strategy is PolicyDerivationStrategy)
keyPath = null;
var derivation = line.Derive((uint)i);
result.Addresses.Add(
new()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
KeyPath = keyPath?.ToString(),
Index = i,
Address =
network.NBXplorerNetwork.CreateAddress(strategy, deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
#pragma warning disable CS0612 // Type or member is obsolete
// We should be able to derive the address from the scriptPubKey.
// However, Elements has blinded addresses, so we can't derive the address from the scriptPubKey.
// We should probably just use a special if/else just for elements here instead of relying on obsolete stuff.
network.NBXplorerNetwork.CreateAddress(strategy, keyPath ?? new(), derivation.ScriptPubKey)
#pragma warning restore CS0612 // Type or member is obsolete
.ToString()
});
}

View File

@@ -1125,7 +1125,7 @@ namespace BTCPayServer.Controllers
{
var appsById = apps.ToDictionary(a => a.Id);
var searchTexts = appIds.Select(a => appsById.TryGet(a)).Where(a => a != null)
.Select(a => AppService.GetAppSearchTerm(a.AppType, a.Id))
.Select(a => AppService.GetAppSearchTerm(a!.AppType, a!.Id))
.ToList();
searchTexts.Add(fs.TextSearch);
textSearch = string.Join(' ', searchTexts.Where(t => !string.IsNullOrEmpty(t)).ToList());

View File

@@ -184,7 +184,7 @@ public partial class UIStoresController
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), StringLocalizer["Invalid derivation scheme"]);
ModelState.AddModelError(nameof(vm.DerivationScheme), StringLocalizer["NBXplorer is unable to track this derivation scheme. You may need to update it."]);
return View(vm.ViewName, vm);
}
await _storeRepo.UpdateStore(store);

View File

@@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using NBitcoin;
using NBitcoin.Scripting;
using NBitcoin.WalletPolicies;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using static NBitcoin.WalletPolicies.MiniscriptNode;
@@ -69,7 +70,7 @@ namespace BTCPayServer
}).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()];
if (result.Item2?.Length > 1)
derivationSchemeSettings.IsMultiSigOnServer = true;
var isTaproot = derivationSchemeSettings.AccountDerivation.GetDerivation().ScriptPubKey.IsScriptType(ScriptType.Taproot);
var isTaproot = derivationSchemeSettings.AccountDerivation.GetLineFor(DerivationFeature.Deposit).Derive(0).ScriptPubKey.IsScriptType(ScriptType.Taproot);
derivationSchemeSettings.DefaultIncludeNonWitnessUtxo = !isTaproot;
return derivationSchemeSettings;
}
@@ -209,10 +210,41 @@ namespace BTCPayServer
ScriptPubKeyType = ScriptPubKeyType.SegwitP2SH,
KeepOrder = desc.Name == "multi"
}), rpks),
_ => throw new FormatException("Not supporting this script policy (BIP388) yet.")
_ => ParsePolicy(factory, str)
};
}
private (DerivationStrategyBase, RootedKeyPath[]) ParsePolicy(DerivationStrategyFactory factory, string str)
{
var policy = factory.Parse(str) as PolicyDerivationStrategy;
if (policy is null)
throw new FormatException("Invalid miniscript derivation");
var v = new RootKeyPathVisitor();
policy.Policy.FullDescriptor.Visit(v);
return (policy, v.RootedKeyPaths.ToArray());
}
class RootKeyPathVisitor : MiniscriptVisitor
{
public List<RootedKeyPath> RootedKeyPaths { get; set; } = new();
public override void Visit(MiniscriptNode node)
{
// Match all '[12345678]xpub/**'
if (node is MiniscriptNode.MultipathNode
{
Target: HDKeyNode hd
})
{
RootedKeyPaths.Add(hd.RootedKeyPath);
}
else
{
base.Visit(node);
}
}
}
private (DerivationStrategyBase, RootedKeyPath[]) ParseLegacyOutputDescriptor(string str)
{
(DerivationStrategyBase, RootedKeyPath[]) ExtractFromPkProvider(PubKeyProvider pubKeyProvider,

View File

@@ -1,3 +1,4 @@
using System.Linq;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Invoices;
@@ -14,12 +15,18 @@ namespace BTCPayServer.Payments.Bitcoin
}
public BitcoinLikePaymentData(OutPoint outpoint, bool rbf, KeyPath keyPath)
public BitcoinLikePaymentData(OutPoint outpoint, bool rbf, KeyPath keyPath, int keyIndex)
{
if (keyPath != null)
// This shouldn't be needed on new version of NBXplorer, but old version of NBXplorer
// are not returning KeyIndex, and it is thus set to '0'.
keyIndex = (int)keyPath.Indexes.Last();
Outpoint = outpoint;
ConfirmationCount = 0;
RBF = rbf;
KeyPath = keyPath;
KeyIndex = keyIndex;
}
[JsonConverter(typeof(SaneOutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
@@ -28,6 +35,8 @@ namespace BTCPayServer.Payments.Bitcoin
public bool RBF { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int? KeyIndex { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public uint256 AssetId { get; set; }

View File

@@ -190,6 +190,7 @@ namespace BTCPayServer.Payments.Bitcoin
paymentMethod.Destination = reserved.Address.ToString();
paymentContext.TrackedDestinations.Add(Network.GetTrackedDestination(reserved.Address.ScriptPubKey));
onchainMethod.KeyPath = reserved.KeyPath;
onchainMethod.KeyIndex = reserved.Index ?? (int)reserved.KeyPath.Indexes.Last();
onchainMethod.AccountDerivation = accountDerivation;
onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&
accountDerivation.ScriptPubKeyType() != ScriptPubKeyType.Legacy &&

View File

@@ -35,6 +35,7 @@ namespace BTCPayServer.Payments.Bitcoin
public FeeRate RecommendedFeeRate { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int KeyIndex { get; set; }
public DerivationStrategyBase AccountDerivation { get; set; }
}
}

View File

@@ -164,7 +164,7 @@ namespace BTCPayServer.Payments.Bitcoin
if (invoice != null)
{
var handler = _handlers[pmi];
var details = new BitcoinLikePaymentData(output.outPoint, evt.TransactionData.Transaction.RBF, output.matchedOutput.KeyPath)
var details = new BitcoinLikePaymentData(output.outPoint, evt.TransactionData.Transaction.RBF, output.matchedOutput.KeyPath, output.matchedOutput.KeyIndex)
{
AssetId = output.matchedOutput.Value.GetAssetId(network)
};
@@ -414,7 +414,7 @@ namespace BTCPayServer.Payments.Bitcoin
Status = PaymentStatus.Processing,
Amount = coin.Value.GetValue(network),
Currency = network.CryptoCode
}.Set(invoice, handler, new BitcoinLikePaymentData(coin.OutPoint, transaction?.Transaction is null ? true : transaction.Transaction.RBF, coin.KeyPath)
}.Set(invoice, handler, new BitcoinLikePaymentData(coin.OutPoint, transaction?.Transaction is null ? true : transaction.Transaction.RBF, coin.KeyPath, coin.KeyIndex)
{
AssetId = coin.Value.GetAssetId(network)
});

View File

@@ -255,7 +255,7 @@ namespace BTCPayServer.Payments.PayJoin
Dictionary<OutPoint, UTXO> selectedUTXOs = new Dictionary<OutPoint, UTXO>();
PSBTOutput? originalPaymentOutput = null;
BitcoinAddress? paymentAddress = null;
KeyPath? paymentAddressIndex = null;
(KeyPath KeyPath, int KeyIndex)? paymentAddressIndex = null;
InvoiceEntity? invoice = null;
DerivationStrategyBase? accountDerivation = null;
WalletId? walletId = null;
@@ -312,7 +312,7 @@ namespace BTCPayServer.Payments.PayJoin
}
paymentAddress = BitcoinAddress.Create(paymentMethod.Destination, network.NBitcoinNetwork);
paymentAddressIndex = paymentDetails.KeyPath;
paymentAddressIndex = (paymentDetails.KeyPath, paymentDetails.KeyIndex);
if (invoice.GetAllBitcoinPaymentData(handler, false).Any())
{
@@ -325,7 +325,8 @@ namespace BTCPayServer.Payments.PayJoin
{
due = Money.Zero;
paymentAddress = walletReceiveMatch.Item2.Address;
paymentAddressIndex = walletReceiveMatch.Item2.KeyPath;
// Old versions of NBX doesn't have the Index property, we can remove ?? (int)walletReceiveMatch.Item2.KeyPath.Indexes.Last() later.
paymentAddressIndex = (walletReceiveMatch.Item2.KeyPath, walletReceiveMatch.Item2.Index ?? (int)walletReceiveMatch.Item2.KeyPath.Indexes.Last());
}
@@ -500,7 +501,7 @@ namespace BTCPayServer.Payments.PayJoin
// broadcast the payjoin.
var outpoint = new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index);
var details = new BitcoinLikePaymentData(outpoint, ctx.OriginalTransaction.RBF, paymentAddressIndex)
var details = new BitcoinLikePaymentData(outpoint, ctx.OriginalTransaction.RBF, paymentAddressIndex.Value.KeyPath, paymentAddressIndex.Value.KeyIndex)
{
ConfirmationCount = -1,
PayjoinInformation = new PayjoinInformation()

View File

@@ -21,11 +21,6 @@ namespace BTCPayServer.Payments.PayJoin.Sender
return ((IHDScriptPubKey)_derivationSchemeSettings.AccountDerivation).Derive(keyPath);
}
public bool CanDeriveHardenedPath()
{
return _derivationSchemeSettings.AccountDerivation.CanDeriveHardenedPath();
}
public Script ScriptPubKey => ((IHDScriptPubKey)_derivationSchemeSettings.AccountDerivation).ScriptPubKey;
public ScriptPubKeyType ScriptPubKeyType => _derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using AccountKeySettings = BTCPayServer.AccountKeySettings;
using BTCPayNetwork = BTCPayServer.BTCPayNetwork;
@@ -37,8 +38,7 @@ public class BSMSWalletFileParser : IWalletFileParser
derivationSchemeSettings = network.GetDerivationSchemeParser().ParseOD(descriptor);
derivationSchemeSettings.Source = "BSMS";
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = derivationSchemeSettings.AccountDerivation.GetLineFor(deposit).Derive(0);
var line = derivationSchemeSettings.AccountDerivation.GetLineFor(DerivationFeature.Deposit).Derive(0);
return testAddress.ScriptPubKey == line.ScriptPubKey;
}
}

View File

@@ -33,6 +33,7 @@ namespace BTCPayServer.Services.Wallets
public Coin Coin { get; set; }
public long Confirmations { get; set; }
public BitcoinAddress Address { get; set; }
public int KeyIndex { get; set; }
}
public class NetworkCoins
{
@@ -500,6 +501,7 @@ namespace BTCPayServer.Services.Wallets
.Select(c => new ReceivedCoin()
{
KeyPath = c.KeyPath,
KeyIndex = c.KeyIndex,
Value = c.Value,
Timestamp = c.Timestamp,
OutPoint = c.Outpoint,

View File

@@ -81,7 +81,7 @@
{
<tr style="@(payment.Replaced ? "text-decoration: line-through" : "")">
<td>@payment.PaymentMethodId</td>
<td>@(payment.CryptoPaymentData.KeyPath?.ToString()?? StringLocalizer["Unknown"])</td>
<td>@(payment.CryptoPaymentData.KeyPath?.ToString() ?? payment.CryptoPaymentData.KeyIndex?.ToString() ?? StringLocalizer["Unknown"])</td>
<td>
<vc:truncate-center text="@payment.DepositAddress.ToString()" classes="truncate-center-id" />
</td>