Include XPubs in PSBTs for multi sig (#6696)

This commit is contained in:
Nicolas Dorier
2025-04-24 21:36:09 +09:00
committed by GitHub
parent 818d65a4f9
commit dd6c4c771e
7 changed files with 25 additions and 19 deletions

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -19,7 +18,7 @@ namespace BTCPayServer.Blazor.VaultBridge;
public abstract class HWIController : VaultController public abstract class HWIController : VaultController
{ {
override protected string VaultUri => "http://127.0.0.1:65092/hwi-bridge/v1"; protected override string VaultUri => "http://127.0.0.1:65092/hwi-bridge/v1";
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry) private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
@@ -41,7 +40,7 @@ public abstract class HWIController : VaultController
try try
{ {
var network = networkProviders.GetNetwork<BTCPayNetwork>(CryptoCode); var network = networkProviders.GetNetwork<BTCPayNetwork>(CryptoCode);
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork) var hwi = new HwiClient(network.NBitcoinNetwork)
{ {
Transport = new VaultHWITransport(vaultClient), IgnoreInvalidNetwork = network.NBitcoinNetwork.ChainName != ChainName.Mainnet Transport = new VaultHWITransport(vaultClient), IgnoreInvalidNetwork = network.NBitcoinNetwork.ChainName != ChainName.Mainnet
}; };
@@ -89,7 +88,7 @@ public abstract class HWIController : VaultController
{ {
// It seems that this 'if (IsTrezorT(deviceEntry))' can be removed. // It seems that this 'if (IsTrezorT(deviceEntry))' can be removed.
// I have not managed to trigger this anymore with latest 2.8.9 // I have not managed to trigger this anymore with latest 2.8.9
// the passphrase is getting asked during EnumerateEntriesAsync // the passphrase is getting asked during EnumerateEntriesAsync
if (IsTrezorT(deviceEntry)) if (IsTrezorT(deviceEntry))
{ {
ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]); ui.ShowFeedback(FeedbackType.Loading, ui.StringLocalizer["Please, enter the passphrase on the device."]);
@@ -206,7 +205,6 @@ public class SignHWIController : HWIController
ui.ShowRetry(); ui.ShowRetry();
return; return;
} }
derivationSettings.RebaseKeyPaths(psbt); derivationSettings.RebaseKeyPaths(psbt);
// otherwise, let the device check if it can sign anything // otherwise, let the device check if it can sign anything
var signableInputs = psbt.Inputs var signableInputs = psbt.Inputs
@@ -227,7 +225,7 @@ public class SignHWIController : HWIController
if (derivationSettings.IsMultiSigOnServer) if (derivationSettings.IsMultiSigOnServer)
{ {
var alreadySigned = psbt.Inputs.Any(a => var alreadySigned = psbt.Inputs.Any(a =>
a.PartialSigs.Any(a => a.Key == actualPubKey)); a.PartialSigs.Any(o => o.Key == actualPubKey));
if (alreadySigned) if (alreadySigned)
{ {
ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This device already signed PSBT."]); ui.ShowFeedback(FeedbackType.Failed, ui.StringLocalizer["This device already signed PSBT."]);
@@ -264,7 +262,7 @@ public class GetXPubController : HWIController
var firstDepositPath = new KeyPath(0, 0); var firstDepositPath = new KeyPath(0, 0);
var firstDepositAddr = var firstDepositAddr =
network.NBXplorerNetwork.CreateAddress(strategy, firstDepositPath, strategy.GetDerivation(firstDepositPath).ScriptPubKey); network.NBXplorerNetwork.CreateAddress(strategy, firstDepositPath, strategy.GetDerivation(firstDepositPath).ScriptPubKey);
var verif = new VerifyAddress(ui) var verif = new VerifyAddress(ui)
{ {
Device = device, Device = device,

View File

@@ -540,7 +540,7 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()), SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()),
Outputs = outputs, Outputs = outputs,
AlwaysIncludeNonWitnessUTXO = true, AlwaysIncludeNonWitnessUTXO = derivationScheme.DefaultIncludeNonWitnessUtxo,
InputSelection = request.SelectedInputs?.Any() is true, InputSelection = request.SelectedInputs?.Any() is true,
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte, FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
NoChange = request.NoChange NoChange = request.NoChange

View File

@@ -27,7 +27,12 @@ namespace BTCPayServer.Controllers
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken) public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
{ {
var nbx = ExplorerClientProvider.GetExplorerClient(network); var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new(); var psbtRequest = new CreatePSBTRequest()
{
RBF = network.SupportRBF ? true : null,
AlwaysIncludeNonWitnessUTXO = sendModel.AlwaysIncludeNonWitnessUTXO,
IncludeGlobalXPub = derivationSettings.IsMultiSigOnServer,
};
if (sendModel.InputSelection) if (sendModel.InputSelection)
{ {
psbtRequest.IncludeOnlyOutpoints = sendModel.SelectedInputs?.Select(OutPoint.Parse).ToList() ?? new List<OutPoint>(); psbtRequest.IncludeOnlyOutpoints = sendModel.SelectedInputs?.Select(OutPoint.Parse).ToList() ?? new List<OutPoint>();
@@ -40,8 +45,6 @@ namespace BTCPayServer.Controllers
psbtDestination.Amount = Money.Coins(transactionOutput.Amount ?? 0.0m); psbtDestination.Amount = Money.Coins(transactionOutput.Amount ?? 0.0m);
psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput; psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
} }
psbtRequest.RBF = network.SupportRBF ? true : null;
psbtRequest.AlwaysIncludeNonWitnessUTXO = sendModel.AlwaysIncludeNonWitnessUTXO;
psbtRequest.FeePreference = new FeePreference(); psbtRequest.FeePreference = new FeePreference();
if (sendModel.FeeSatoshiPerByte is decimal v and > decimal.Zero) if (sendModel.FeeSatoshiPerByte is decimal v and > decimal.Zero)
@@ -56,8 +59,7 @@ namespace BTCPayServer.Controllers
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken)); var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
if (psbt == null) if (psbt == null)
throw new NotSupportedException(StringLocalizer["You need to update your version of NBXplorer"]); throw new NotSupportedException(StringLocalizer["You need to update your version of NBXplorer"]);
// Not supported by coldcard, remove when they do support it
psbt.PSBT.GlobalXPubs.Clear();
return psbt; return psbt;
} }
@@ -552,12 +554,12 @@ namespace BTCPayServer.Controllers
{ {
await _pendingTransactionService.Broadcasted(GetPendingTxId(walletId, vm.SigningContext.PendingTransactionId)); await _pendingTransactionService.Broadcasted(GetPendingTxId(walletId, vm.SigningContext.PendingTransactionId));
} }
if (!string.IsNullOrEmpty(vm.ReturnUrl)) if (!string.IsNullOrEmpty(vm.ReturnUrl))
{ {
return LocalRedirect(vm.ReturnUrl); return LocalRedirect(vm.ReturnUrl);
} }
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() }); return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
} }
case "analyze-psbt": case "analyze-psbt":

View File

@@ -316,6 +316,7 @@ namespace BTCPayServer.Controllers
{ {
RBF = true, RBF = true,
AlwaysIncludeNonWitnessUTXO = paymentMethod.DefaultIncludeNonWitnessUtxo, AlwaysIncludeNonWitnessUTXO = paymentMethod.DefaultIncludeNonWitnessUtxo,
IncludeGlobalXPub = paymentMethod.IsMultiSigOnServer,
IncludeOnlyOutpoints = bumpableUTXOs, IncludeOnlyOutpoints = bumpableUTXOs,
SpendAllMatchingOutpoints = true, SpendAllMatchingOutpoints = true,
FeePreference = new FeePreference() FeePreference = new FeePreference()
@@ -369,6 +370,7 @@ namespace BTCPayServer.Controllers
{ {
RBF = true, RBF = true,
AlwaysIncludeNonWitnessUTXO = paymentMethod.DefaultIncludeNonWitnessUtxo, AlwaysIncludeNonWitnessUTXO = paymentMethod.DefaultIncludeNonWitnessUtxo,
IncludeGlobalXPub = paymentMethod.IsMultiSigOnServer,
IncludeOnlyOutpoints = tx.Transaction.Inputs.Select(i => i.PrevOut).ToList(), IncludeOnlyOutpoints = tx.Transaction.Inputs.Select(i => i.PrevOut).ToList(),
SpendAllMatchingOutpoints = true, SpendAllMatchingOutpoints = true,
DisableFingerprintRandomization = true, DisableFingerprintRandomization = true,
@@ -1329,6 +1331,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, public async Task<IActionResult> WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
WalletSendVaultModel model) WalletSendVaultModel model)
{ {
TempData.SetStatusSuccess(StringLocalizer["Transaction successfully signed"].Value);
return await RedirectToWalletPSBTReady(walletId, new WalletPSBTReadyViewModel return await RedirectToWalletPSBTReady(walletId, new WalletPSBTReadyViewModel
{ {
SigningContext = model.SigningContext, SigningContext = model.SigningContext,

View File

@@ -85,8 +85,8 @@ namespace BTCPayServer
{ {
return Encoding.UTF8.GetString(protector.Unprotect(Convert.FromBase64String(str))); return Encoding.UTF8.GetString(protector.Unprotect(Convert.FromBase64String(str)));
} }
/// <summary> /// <summary>
/// Outputs a serializer which will serialize default and null members. /// Outputs a serializer which will serialize default and null members.
/// This is useful for discovering the API. /// This is useful for discovering the API.
@@ -554,7 +554,8 @@ namespace BTCPayServer
{ {
PSBT = psbt, PSBT = psbt,
DerivationScheme = derivationSchemeSettings.AccountDerivation, DerivationScheme = derivationSchemeSettings.AccountDerivation,
AlwaysIncludeNonWitnessUTXO = true AlwaysIncludeNonWitnessUTXO = true,
IncludeGlobalXPub = derivationSchemeSettings.IsMultiSigOnServer
}); });
if (result == null) if (result == null)
return null; return null;
@@ -638,7 +639,7 @@ namespace BTCPayServer
var h = (BitcoinLikePaymentHandler)handlers[pmi]; var h = (BitcoinLikePaymentHandler)handlers[pmi];
return h; return h;
} }
public static BTCPayNetwork? TryGetNetwork<TId, THandler>(this HandlersDictionary<TId, THandler> handlers, TId id) public static BTCPayNetwork? TryGetNetwork<TId, THandler>(this HandlersDictionary<TId, THandler> handlers, TId id)
where THandler : IHandler<TId> where THandler : IHandler<TId>
where TId : notnull where TId : notnull
{ {

View File

@@ -1610,6 +1610,7 @@ namespace BTCPayServer.Services
"Transaction fee rate:": "", "Transaction fee rate:": "",
"Transaction Id": "", "Transaction Id": "",
"Transaction signed successfully, proceeding to review...": "", "Transaction signed successfully, proceeding to review...": "",
"Transaction successfully signed": "",
"transactions": "", "transactions": "",
"Translations": "", "Translations": "",
"Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "", "Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "",

View File

@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HWI/@EntryIndexedValue">HWI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LNURL/@EntryIndexedValue">LNURL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LNURL/@EntryIndexedValue">LNURL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NB/@EntryIndexedValue">NBX</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NB/@EntryIndexedValue">NBX</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NBXplorer/@EntryIndexedValue">NBXplorer</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NBXplorer/@EntryIndexedValue">NBXplorer</s:String>