Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Bitcoin Output descriptors for wallets #331

Merged
merged 16 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
decd2a5
feat: Bitcoin Output descriptors for wallets
markettes Oct 25, 2023
6d85aba
fix: add bitcoinNetwork as a parameter to the function
markettes Oct 26, 2023
3603891
Merge branch 'main' into 322-add-exporting-of-output-descriptors-to-n…
markettes Oct 26, 2023
371dcb3
fix(WalletParser.cs): change KeyPath from empty string to "/0" to cor…
markettes Oct 26, 2023
dcc9eb8
fix(WalletParser.cs): remove unnecessary conditional statement for de…
markettes Oct 26, 2023
c270d96
fix: rearrange tests
markettes Oct 26, 2023
74582af
fix(WalletParserTests.cs): update assertions in WalletParserTests to …
markettes Oct 26, 2023
4e70aa4
fix(WalletParserTests.cs): update output descriptors in WalletParserT…
markettes Oct 26, 2023
1b9a049
fix: properly deriving and adding the masterfingerprint to the import…
markettes Oct 26, 2023
6a662e3
refactor(WalletParserTests.cs): Remove unused code and fix variable n…
markettes Oct 26, 2023
4f4330c
refactor(WalletParser.cs): change GetOutputDescriptor method to an ex…
markettes Oct 27, 2023
22fe2be
chore(WalletParser.cs): add documentation for GetOutputDescriptor met…
markettes Oct 27, 2023
4cb8a29
fix: enforce the checksum in the tests
markettes Oct 27, 2023
996750a
Merge branch 'main' into 322-add-exporting-of-output-descriptors-to-n…
markettes Oct 27, 2023
5ff98c0
refactor: show derivation scheme instead of derivation path
markettes Oct 27, 2023
e32da49
Merge branch 'main' into 322-add-exporting-of-output-descriptors-to-n…
markettes Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/Helpers/WalletParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Text;
using Humanizer;
using NBitcoin;
using NBitcoin.Scripting;
using NBXplorer.DerivationStrategy;
using NodeGuard.Data.Models;

namespace NodeGuard.Helpers;

Expand Down Expand Up @@ -105,4 +108,120 @@ DerivationStrategyBase Parse(string str)
return strategy;
}
}

/// <summary>
/// Generates an output descriptor for a given wallet based on its type and the Bitcoin network it's associated with.
/// </summary>
/// <param name="wallet">The wallet for which the output descriptor is to be generated.</param>
/// <param name="bitcoinNetwork">The Bitcoin network associated with the wallet.</param>
/// <returns>A string representation of the output descriptor.</returns>
/// <exception cref="System.NotImplementedException">Thrown when the wallet address type is Taproot, which is not currently supported.</exception>
/// <exception cref="System.Exception">Thrown when the output descriptor could not be generated for some reason.</exception>
/// <remarks>
/// This method first determines the network based on the provided string. It then checks if the wallet is a hot wallet or not.
/// If it is, it generates the output descriptor based on the first key in the wallet and the wallet's address type.
/// If it's not a hot wallet, it generates a multi-signature output descriptor based on all the keys in the wallet and the wallet's address type.
/// </remarks>
public static string GetOutputDescriptor(this Wallet wallet, string bitcoinNetwork)
{
var network = Network.GetNetwork(bitcoinNetwork);
OutputDescriptor outputDescriptor = null;
PubKeyProvider pubKeyProvider;

if (wallet.IsHotWallet)
{
var key = wallet.Keys.FirstOrDefault();
pubKeyProvider = PubKeyProvider.NewHD(
new BitcoinExtPubKey(
ExtPubKey.Parse(key.XPUB, network),
network
),
new KeyPath("/0"),
PubKeyProvider.DeriveType.UNHARDENED
);
var fingerprint = GetMasterFingerprint(key.MasterFingerprint);
var rootedKeyPath = new RootedKeyPath(
new HDFingerprint(fingerprint),
KeyPath.Parse(key.Path)
);
pubKeyProvider = PubKeyProvider.NewOrigin(rootedKeyPath, pubKeyProvider);

switch (wallet.WalletAddressType)
{
case WalletAddressType.NativeSegwit:
outputDescriptor = OutputDescriptor.NewWPKH(pubKeyProvider, network);
break;
case WalletAddressType.NestedSegwit:
outputDescriptor = OutputDescriptor.NewWPKH(pubKeyProvider, network);
outputDescriptor = OutputDescriptor.NewSH(outputDescriptor, network);
break;
case WalletAddressType.Legacy:
outputDescriptor = OutputDescriptor.NewPKH(pubKeyProvider, network);
break;
case WalletAddressType.Taproot:
throw new NotImplementedException();
}
}
else
{
var pubKeyProviders = new List<PubKeyProvider>();
foreach (var k in wallet.Keys)
{
var rootedKeyPath = new RootedKeyPath(
new HDFingerprint(GetMasterFingerprint(k.MasterFingerprint)),
KeyPath.Parse(k.Path)
);
pubKeyProvider = PubKeyProvider.NewOrigin(
rootedKeyPath,
PubKeyProvider.NewHD(
new BitcoinExtPubKey(
ExtPubKey.Parse(k.XPUB, network),
network
),
new KeyPath("/0"),
PubKeyProvider.DeriveType.UNHARDENED
)
);
pubKeyProviders.Add(pubKeyProvider);
}
outputDescriptor = OutputDescriptor.NewMulti(
(uint)wallet.MofN,
pubKeyProviders,
!wallet.IsUnSortedMultiSig,
network);

switch (wallet.WalletAddressType)
{
case WalletAddressType.NativeSegwit:
outputDescriptor = OutputDescriptor.NewWSH(outputDescriptor, network);
break;
case WalletAddressType.NestedSegwit:
outputDescriptor = OutputDescriptor.NewSH(outputDescriptor, network);
break;
case WalletAddressType.Legacy:
break;
case WalletAddressType.Taproot:
throw new NotImplementedException();
}
}

return outputDescriptor is not null ? outputDescriptor.ToString() : throw new Exception("Something went wrong");
}

/// <summary>
/// Converts a hexadecimal string representation of a master fingerprint into a byte array.
/// </summary>
/// <param name="masterFingerprint">The hexadecimal string representation of the master fingerprint.</param>
/// <returns>A byte array that represents the master fingerprint.</returns>
/// <remarks>
/// This method works by iterating over the input string two characters at a time (since each byte in a hexadecimal string is represented by two characters), converting those two characters into a byte, and then adding that byte to the output array.
/// </remarks>
public static byte[] GetMasterFingerprint(string masterFingerprint)
{
var internalBytes = Enumerable.Range(0, masterFingerprint.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(masterFingerprint.Substring(x, 2), 16))
.ToArray();
return internalBytes;
}
markettes marked this conversation as resolved.
Show resolved Hide resolved
}
53 changes: 51 additions & 2 deletions src/Pages/Wallets.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@inject ILocalStorageService LocalStorageService
@inject ISchedulerFactory SchedulerFactory
@inject IPriceConversionService PriceConversionService
@inject IInternalWalletRepository InternalWalletRepository
@inject INBXplorerService NBXplorerService

@attribute [Authorize(Roles = "NodeManager, FinanceManager, Superadmin")]
Expand Down Expand Up @@ -80,6 +81,7 @@
<DropdownItem Disabled="!context.Item.IsFinalised" Clicked="@(() => LoadAndOpenModalTextModalUnusedAddress(context.Item))">Get address</DropdownItem>
<DropdownItem Disabled="!context.Item.IsFinalised || context.Item.IsWatchOnly" Clicked="@(() => ShowTransferFundsModal(context.Item))">Transfer funds</DropdownItem>
<DropdownItem Disabled="!context.Item.IsFinalised" Clicked="@(() => RescanWallet(context.Item))">Rescan wallet</DropdownItem>
<DropdownItem Disabled="!context.Item.IsFinalised" Clicked="@(() => LoadAndOpenExportOutputDescriptor(context.Item))">Export output descriptor</DropdownItem>

</DropdownMenu>
</Dropdown>
Expand Down Expand Up @@ -700,6 +702,25 @@
</ModalFooter>
</ModalContent>
</Modal>
<Modal @ref="_exportOutputDescriptorModal">
<ModalContent Size="ModalSize.Large">
<ModalHeader>
@($"Export wallet")
</ModalHeader>
<ModalBody>
<Paragraph>
@("Output descriptor: " + StringHelper.TruncateHeadAndTail(_outputDescriptorContentModal, 25))
<Button Color="Color.Primary" Clicked="@(()=> CopyStrToClipboard(_outputDescriptorContentModal))">Copy</Button>
</Paragraph>
@("Wallet derivation strategy (NBITCOIN): " + StringHelper.TruncateHeadAndTail(_derivationScheme, 25))
<Button Color="Color.Primary" Clicked="@(()=> CopyStrToClipboard(_derivationScheme))">Copy</Button>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary" Clicked="@CloseExportOutputDescriptorModal">Close</Button>
</ModalFooter>
</ModalContent>
</Modal>

<ConfirmationModal
@ref="_multisigTransferModal"
Title="Transferring from a multisig wallet"
Expand Down Expand Up @@ -735,6 +756,10 @@
private List<Key> _selectedWalletKeysPlusInternalWalletKey = new();
private List<Key> _selectedFinanceManagerAvailableKeys = new();
private Key? _selectedWalletKey;

private Modal _exportOutputDescriptorModal;
private string _outputDescriptorContentModal;
private string _derivationScheme;

private Modal _textModalRef;
private string _textModalTitle = string.Empty;
Expand Down Expand Up @@ -917,8 +942,7 @@
}
return _sourceBalanceAfterTransaction;
}



private async Task OnRowUpdated(SavedRowItem<Wallet, Dictionary<string, object>> arg)
{
if (arg.Item == null) return;
Expand Down Expand Up @@ -1619,6 +1643,31 @@
_transferAllFunds = value;
_amountToTransfer = _sourceBalance;
}

private async Task LoadAndOpenExportOutputDescriptor(Wallet contextItem)
{
await CleanTextModal();
if (contextItem != null)
{
try
{
_outputDescriptorContentModal = contextItem.GetOutputDescriptor(Constants.BITCOIN_NETWORK);
_derivationScheme = contextItem.GetDerivationStrategy().ToString();
await _exportOutputDescriptorModal.Show();
}
catch (Exception e)
{
ToastService.ShowError("Error while getting the wallet descriptor");
}
}
}

private async Task CloseExportOutputDescriptorModal()
{
await _exportOutputDescriptorModal.Close(CloseReason.UserClosing);

_outputDescriptorContentModal = string.Empty;
_derivationScheme = string.Empty;
}

}
Loading
Loading