mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Add Wallet settings menu, do not rebase keypaths when create the PSBT
This commit is contained in:
@@ -1644,12 +1644,13 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.NotNull(psbt);
|
Assert.NotNull(psbt);
|
||||||
|
|
||||||
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey().AsHDKeyCache();
|
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey().AsHDKeyCache();
|
||||||
|
var account = root.Derive(new KeyPath("m/49'/0'/0'"));
|
||||||
Assert.All(psbt.PSBT.Inputs, input =>
|
Assert.All(psbt.PSBT.Inputs, input =>
|
||||||
{
|
{
|
||||||
var keyPath = input.HDKeyPaths.Single();
|
var keyPath = input.HDKeyPaths.Single();
|
||||||
Assert.StartsWith(onchainBTC.AccountKeyPath.ToString(), keyPath.Value.Item2.ToString());
|
Assert.False(keyPath.Value.Item2.IsHardened);
|
||||||
Assert.Equal(root.Derive(keyPath.Value.Item2).GetPublicKey(), keyPath.Key);
|
Assert.Equal(account.Derive(keyPath.Value.Item2).GetPublicKey(), keyPath.Key);
|
||||||
Assert.Equal(keyPath.Value.Item1, onchainBTC.RootFingerprint.Value);
|
Assert.Equal(keyPath.Value.Item1, onchainBTC.AccountKeySettings[0].AccountKey.GetPublicKey().GetHDFingerPrint());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
|
<PackageReference Include="NBitcoin" Version="4.1.2.21" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
||||||
|
|||||||
@@ -202,10 +202,17 @@ namespace BTCPayServer.Controllers
|
|||||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||||
{
|
{
|
||||||
|
var accountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||||
|
if (accountKey != null)
|
||||||
|
{
|
||||||
|
var accountSettings = newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||||
|
if (accountSettings != null)
|
||||||
|
{
|
||||||
|
accountSettings.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||||
|
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
strategy = newStrategy;
|
strategy = newStrategy;
|
||||||
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
|
||||||
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
|
||||||
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
|
||||||
strategy.Source = vm.Source;
|
strategy.Source = vm.Source;
|
||||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers
|
|||||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||||
}
|
}
|
||||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||||
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
|
|
||||||
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("You need to update your version of NBXplorer");
|
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -452,14 +453,16 @@ namespace BTCPayServer.Controllers
|
|||||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||||
|
|
||||||
|
var accountKey = derivationSettings.AccountKeySettings.Where(a => a.IsFullySetup()).FirstOrDefault();
|
||||||
|
accountKey = accountKey ?? derivationSettings.AccountKeySettings.FirstOrDefault();
|
||||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||||
if (derivationSettings.AccountKeyPath == null)
|
if (accountKey.AccountKeyPath == null)
|
||||||
{
|
{
|
||||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||||
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
||||||
derivationSettings.AccountDerivation,
|
derivationSettings.AccountDerivation,
|
||||||
normalOperationTimeout.Token);
|
normalOperationTimeout.Token);
|
||||||
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||||
await Repository.UpdateStore(storeData);
|
await Repository.UpdateStore(storeData);
|
||||||
}
|
}
|
||||||
@@ -468,10 +471,10 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||||
// but some deployment does not have it, so let's use AccountKeyPath instead
|
// but some deployment does not have it, so let's use AccountKeyPath instead
|
||||||
if (derivationSettings.RootFingerprint == null)
|
if (accountKey.RootFingerprint == null)
|
||||||
{
|
{
|
||||||
|
|
||||||
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
|
var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token);
|
||||||
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
}
|
}
|
||||||
@@ -479,15 +482,15 @@ namespace BTCPayServer.Controllers
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||||
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value)
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||||
if (derivationSettings.RootFingerprint == null)
|
if (accountKey.RootFingerprint == null)
|
||||||
{
|
{
|
||||||
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||||
await Repository.UpdateStore(storeData);
|
await Repository.UpdateStore(storeData);
|
||||||
}
|
}
|
||||||
@@ -502,7 +505,7 @@ namespace BTCPayServer.Controllers
|
|||||||
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
||||||
|
|
||||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.RootFingerprint, accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||||
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,6 +531,57 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("{walletId}/settings")]
|
||||||
|
public async Task<IActionResult> WalletSettings(
|
||||||
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
|
WalletId walletId)
|
||||||
|
{
|
||||||
|
var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId);
|
||||||
|
if (derivationSchemeSettings == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var vm = new WalletSettingsViewModel()
|
||||||
|
{
|
||||||
|
Label = derivationSchemeSettings.Label,
|
||||||
|
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
|
||||||
|
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal
|
||||||
|
};
|
||||||
|
vm.AccountKeys = derivationSchemeSettings.AccountKeySettings
|
||||||
|
.Select(e => new WalletSettingsAccountKeyViewModel()
|
||||||
|
{
|
||||||
|
AccountKey = e.AccountKey.ToString(),
|
||||||
|
MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null,
|
||||||
|
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
|
||||||
|
}).ToList();
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("{walletId}/settings")]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> WalletSettings(
|
||||||
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
|
WalletId walletId, WalletSettingsViewModel vm)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
return View(vm);
|
||||||
|
var derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||||
|
if (derivationScheme == null)
|
||||||
|
return NotFound();
|
||||||
|
derivationScheme.Label = vm.Label;
|
||||||
|
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
|
||||||
|
{
|
||||||
|
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
|
||||||
|
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
|
||||||
|
derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null
|
||||||
|
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
|
||||||
|
}
|
||||||
|
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||||
|
store.SetSupportedPaymentMethod(derivationScheme);
|
||||||
|
await Repository.UpdateStore(store);
|
||||||
|
StatusMessage = "Wallet settings updated";
|
||||||
|
return RedirectToAction(nameof(WalletSettings));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||||
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||||
|
result.AccountKeySettings = new AccountKeySettings[1];
|
||||||
|
result.AccountKeySettings[0] = new AccountKeySettings();
|
||||||
|
result.AccountKeySettings[0].AccountKey = result.AccountDerivation.GetExtPubKeys().Single().GetWif(network.NBitcoinNetwork);
|
||||||
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||||
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,7 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,7 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
@@ -121,20 +124,32 @@ namespace BTCPayServer
|
|||||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||||
AccountDerivation = derivationStrategy;
|
AccountDerivation = derivationStrategy;
|
||||||
Network = network;
|
Network = network;
|
||||||
|
AccountKeySettings = derivationStrategy.GetExtPubKeys().Select(c => new AccountKeySettings()
|
||||||
|
{
|
||||||
|
AccountKey = c.GetWif(network.NBitcoinNetwork)
|
||||||
|
}).ToArray();
|
||||||
}
|
}
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public BTCPayNetwork Network { get; set; }
|
public BTCPayNetwork Network { get; set; }
|
||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use GetAccountKeySettings().AccountKeyPath instead")]
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public KeyPath AccountKeyPath { get; set; }
|
public KeyPath AccountKeyPath { get; set; }
|
||||||
|
|
||||||
public DerivationStrategyBase AccountDerivation { get; set; }
|
public DerivationStrategyBase AccountDerivation { get; set; }
|
||||||
public string AccountOriginal { get; set; }
|
public string AccountOriginal { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use GetAccountKeySettings().RootFingerprint instead")]
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public HDFingerprint? RootFingerprint { get; set; }
|
public HDFingerprint? RootFingerprint { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use GetAccountKeySettings().AccountKey instead")]
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
[Obsolete("Use GetAccountKeySettings().AccountKey instead")]
|
||||||
public BitcoinExtPubKey AccountKey
|
public BitcoinExtPubKey AccountKey
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -143,16 +158,49 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountKeySettings[] _AccountKeySettings;
|
||||||
|
public AccountKeySettings[] AccountKeySettings
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Legacy
|
||||||
|
if (_AccountKeySettings == null)
|
||||||
|
{
|
||||||
|
if (this.Network == null)
|
||||||
|
return null;
|
||||||
|
_AccountKeySettings = AccountDerivation.GetExtPubKeys().Select(e => new AccountKeySettings()
|
||||||
|
{
|
||||||
|
AccountKey = e.GetWif(this.Network.NBitcoinNetwork),
|
||||||
|
}).ToArray();
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
_AccountKeySettings[0].AccountKeyPath = AccountKeyPath;
|
||||||
|
_AccountKeySettings[0].RootFingerprint = RootFingerprint;
|
||||||
|
ExplicitAccountKey = null;
|
||||||
|
AccountKeyPath = null;
|
||||||
|
RootFingerprint = null;
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
}
|
||||||
|
return _AccountKeySettings;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_AccountKeySettings = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
||||||
{
|
{
|
||||||
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
|
foreach(var accountKey in AccountKeySettings)
|
||||||
{
|
{
|
||||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
if (accountKey.AccountKeyPath != null && accountKey.RootFingerprint is HDFingerprint fp)
|
||||||
{
|
{
|
||||||
AccountKey = AccountKey,
|
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||||
AccountKeyPath = AccountKeyPath,
|
{
|
||||||
MasterFingerprint = fp
|
AccountKey = accountKey.AccountKey,
|
||||||
};
|
AccountKeyPath = accountKey.AccountKeyPath,
|
||||||
|
MasterFingerprint = fp
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,4 +233,14 @@ namespace BTCPayServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public class AccountKeySettings
|
||||||
|
{
|
||||||
|
public HDFingerprint? RootFingerprint { get; set; }
|
||||||
|
public KeyPath AccountKeyPath { get; set; }
|
||||||
|
public BitcoinExtPubKey AccountKey { get; set; }
|
||||||
|
public bool IsFullySetup()
|
||||||
|
{
|
||||||
|
return AccountKeyPath != null && RootFingerprint is HDFingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
|
{
|
||||||
|
public class WalletSettingsViewModel
|
||||||
|
{
|
||||||
|
public string Label { get; set; }
|
||||||
|
public string DerivationScheme { get; set; }
|
||||||
|
public string DerivationSchemeInput { get; set; }
|
||||||
|
|
||||||
|
public List<WalletSettingsAccountKeyViewModel> AccountKeys { get; set; } = new List<WalletSettingsAccountKeyViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WalletSettingsAccountKeyViewModel
|
||||||
|
{
|
||||||
|
public string AccountKey { get; set; }
|
||||||
|
[Validation.HDFingerPrintValidator]
|
||||||
|
public string MasterFingerprint { get; set; }
|
||||||
|
[Validation.KeyPathValidator]
|
||||||
|
public string AccountKeyPath { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,8 +62,7 @@ namespace BTCPayServer.Services
|
|||||||
return foundKeyPath;
|
return foundKeyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, HDFingerprint? rootFingerprint, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken);
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -114,25 +114,34 @@ namespace BTCPayServer.Services
|
|||||||
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||||
return extpubkey;
|
return extpubkey;
|
||||||
}
|
}
|
||||||
|
class HDKey
|
||||||
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
|
public PubKey PubKey { get; set; }
|
||||||
|
public KeyPath KeyPath { get; set; }
|
||||||
|
}
|
||||||
|
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, HDFingerprint? rootFingerprint, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HashSet<HDFingerprint> knownFingerprints = new HashSet<HDFingerprint>();
|
||||||
|
knownFingerprints.Add(accountKey.GetPublicKey().GetHDFingerPrint());
|
||||||
|
if (rootFingerprint is HDFingerprint fp)
|
||||||
|
knownFingerprints.Add(fp);
|
||||||
var unsigned = psbt.GetGlobalTransaction();
|
var unsigned = psbt.GetGlobalTransaction();
|
||||||
var changeKeyPath = psbt.Outputs
|
var changeKeyPath = psbt.Outputs
|
||||||
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
||||||
.Where(o => o.HDKeyPaths.Any())
|
.Select(o => (Output: o, HDKey: GetHDKey(knownFingerprints, accountKey, o)))
|
||||||
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
.Where(o => o.HDKey != null)
|
||||||
|
.Select(o => o.HDKey.KeyPath)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
var signatureRequests = psbt
|
var signatureRequests = psbt
|
||||||
.Inputs
|
.Inputs
|
||||||
.Where(o => o.HDKeyPaths.Any())
|
.Select(i => (Input: i, HDKey: GetHDKey(knownFingerprints, accountKey, i)))
|
||||||
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
.Where(i => i.HDKey != null)
|
||||||
.Select(i => new SignatureRequest()
|
.Select(i => new SignatureRequest()
|
||||||
{
|
{
|
||||||
InputCoin = i.GetSignableCoin(),
|
InputCoin = i.Input.GetSignableCoin(),
|
||||||
InputTransaction = i.NonWitnessUtxo,
|
InputTransaction = i.Input.NonWitnessUtxo,
|
||||||
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
KeyPath = i.HDKey.KeyPath,
|
||||||
PubKey = i.HDKeyPaths.First().Key
|
PubKey = i.HDKey.PubKey
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||||
if (signedTransaction == null)
|
if (signedTransaction == null)
|
||||||
@@ -151,6 +160,22 @@ namespace BTCPayServer.Services
|
|||||||
return psbt;
|
return psbt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HDKey GetHDKey(HashSet<HDFingerprint> knownFingerprints, BitcoinExtPubKey accountKey, PSBTCoin coin)
|
||||||
|
{
|
||||||
|
// Check if the accountKey match this coin by checking if the non hardened last part of the path
|
||||||
|
// can derive the same pubkey
|
||||||
|
foreach (var key in coin.HDKeyPaths)
|
||||||
|
{
|
||||||
|
if (!knownFingerprints.Contains(key.Value.Item1))
|
||||||
|
continue;
|
||||||
|
var accountKeyPath = key.Value.Item2.GetAccountKeyPath();
|
||||||
|
// We might have a fingerprint collision, let's check
|
||||||
|
if (accountKey.ExtPubKey.Derive(accountKeyPath).GetPublicKey() == key.Key)
|
||||||
|
return new HDKey() { KeyPath = key.Value.Item2, PubKey = key.Key };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
if (_Transport != null)
|
if (_Transport != null)
|
||||||
|
|||||||
32
BTCPayServer/Validation/HDFingerPrintValidator.cs
Normal file
32
BTCPayServer/Validation/HDFingerPrintValidator.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Validation
|
||||||
|
{
|
||||||
|
public class HDFingerPrintValidator : ValidationAttribute
|
||||||
|
{
|
||||||
|
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var str = value as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
{
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new HDFingerprint(Encoders.Hex.DecodeData(str));
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new ValidationResult("Invalid fingerprint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
BTCPayServer/Validation/KeyPathValidator.cs
Normal file
29
BTCPayServer/Validation/KeyPathValidator.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Validation
|
||||||
|
{
|
||||||
|
public class KeyPathValidator : ValidationAttribute
|
||||||
|
{
|
||||||
|
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var str = value as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(str))
|
||||||
|
{
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
if (KeyPath.TryParse(str, out _))
|
||||||
|
{
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ValidationResult("Invalid keypath");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
BTCPayServer/Views/Wallets/WalletSettings.cshtml
Normal file
70
BTCPayServer/Views/Wallets/WalletSettings.cshtml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
@model WalletSettingsViewModel
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewData["Title"] = "Wallet settings";
|
||||||
|
ViewData.SetActivePageAndTitle(WalletsNavPages.Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10 text-center">
|
||||||
|
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<p>
|
||||||
|
Additional information about your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post" asp-action="WalletSettings">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Label"></label>
|
||||||
|
<input asp-for="Label" class="form-control" />
|
||||||
|
<span asp-validation-for="Label" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DerivationScheme"></label>
|
||||||
|
<input asp-for="DerivationScheme" class="form-control" readonly />
|
||||||
|
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(Model.DerivationSchemeInput) && Model.DerivationSchemeInput != Model.DerivationScheme)
|
||||||
|
{
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DerivationSchemeInput"></label>
|
||||||
|
<input asp-for="DerivationSchemeInput" class="form-control" readonly />
|
||||||
|
<span asp-validation-for="DerivationSchemeInput" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (int i = 0; i < Model.AccountKeys.Count; i++)
|
||||||
|
{
|
||||||
|
<hr />
|
||||||
|
<h5>Account key @i</h5>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.AccountKeys[i].AccountKey"></label>
|
||||||
|
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly />
|
||||||
|
<span asp-validation-for="@Model.AccountKeys[i].AccountKey" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.AccountKeys[i].MasterFingerprint"></label>
|
||||||
|
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" />
|
||||||
|
<span asp-validation-for="@Model.AccountKeys[i].MasterFingerprint" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.AccountKeys[i].AccountKeyPath"></label>
|
||||||
|
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" />
|
||||||
|
<span asp-validation-for="@Model.AccountKeys[i].AccountKeyPath" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button name="command" type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,7 @@ namespace BTCPayServer.Views.Wallets
|
|||||||
Send,
|
Send,
|
||||||
Transactions,
|
Transactions,
|
||||||
Rescan,
|
Rescan,
|
||||||
PSBT
|
PSBT,
|
||||||
|
Settings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan">Rescan</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan">Rescan</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT">PSBT</a>
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT">PSBT</a>
|
||||||
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Settings)" asp-action="WalletSettings">Settings</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user