2026-06-11 10:46:36 +02:00
|
|
|
using NBitcoin;
|
|
|
|
|
using PalladiumWallet.Core.Chain;
|
|
|
|
|
|
|
|
|
|
namespace PalladiumWallet.Core.Crypto;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// BIP44/49/84 derivation path construction (blueprint §4.2) and mapping of
|
|
|
|
|
/// ScriptKind → purpose / ScriptPubKeyType. Purposes are BIP protocol constants
|
|
|
|
|
/// (chain-independent); coin_type always comes from the ChainProfile.
|
2026-06-11 10:46:36 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static class DerivationPaths
|
|
|
|
|
{
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>BIP44 purpose (P2PKH legacy).</summary>
|
2026-06-11 10:46:36 +02:00
|
|
|
public const int PurposeLegacy = 44;
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>BIP49 purpose (P2SH-P2WPKH).</summary>
|
2026-06-11 10:46:36 +02:00
|
|
|
public const int PurposeWrappedSegwit = 49;
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>BIP84 purpose (native P2WPKH).</summary>
|
2026-06-11 10:46:36 +02:00
|
|
|
public const int PurposeNativeSegwit = 84;
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>BIP48 purpose (multisig — next phase, §16 step 8).</summary>
|
2026-06-11 10:46:36 +02:00
|
|
|
public const int PurposeMultisig = 48;
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>BIP86 purpose (P2TR key-path, Taproot).</summary>
|
2026-06-15 12:03:46 +02:00
|
|
|
public const int PurposeTaproot = 86;
|
|
|
|
|
|
2026-06-11 10:46:36 +02:00
|
|
|
public static int PurposeFor(ScriptKind kind) => kind switch
|
|
|
|
|
{
|
|
|
|
|
ScriptKind.Legacy => PurposeLegacy,
|
|
|
|
|
ScriptKind.WrappedSegwit => PurposeWrappedSegwit,
|
|
|
|
|
ScriptKind.NativeSegwit => PurposeNativeSegwit,
|
|
|
|
|
ScriptKind.WrappedSegwitMultisig or ScriptKind.NativeSegwitMultisig => PurposeMultisig,
|
2026-06-15 12:03:46 +02:00
|
|
|
ScriptKind.Taproot => PurposeTaproot,
|
2026-06-11 10:46:36 +02:00
|
|
|
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// NBitcoin scriptPubKey type for single-pubkey derivation.
|
|
|
|
|
/// Multisig types are derived from a redeem script, not a pubkey — they will
|
|
|
|
|
/// arrive with M-of-N wallet support (§4.5).
|
2026-06-11 10:46:36 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static ScriptPubKeyType ScriptPubKeyTypeFor(ScriptKind kind) => kind switch
|
|
|
|
|
{
|
|
|
|
|
ScriptKind.Legacy => ScriptPubKeyType.Legacy,
|
|
|
|
|
ScriptKind.WrappedSegwit => ScriptPubKeyType.SegwitP2SH,
|
|
|
|
|
ScriptKind.NativeSegwit => ScriptPubKeyType.Segwit,
|
2026-06-15 12:03:46 +02:00
|
|
|
ScriptKind.Taproot => ScriptPubKeyType.TaprootBIP86,
|
2026-06-11 10:46:36 +02:00
|
|
|
ScriptKind.WrappedSegwitMultisig or ScriptKind.NativeSegwitMultisig =>
|
|
|
|
|
throw new NotSupportedException(
|
2026-06-16 14:40:06 +02:00
|
|
|
"Multisig types are derived from a redeem script: support planned with M-of-N wallets (§4.5)."),
|
2026-06-11 10:46:36 +02:00
|
|
|
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Account path relative to the root: purpose'/coin'/account' (§4.2).
|
|
|
|
|
/// coin_type is taken from the profile (746 mainnet, 1 testnet).
|
2026-06-11 10:46:36 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static KeyPath AccountPath(ScriptKind kind, ChainProfile profile, int account = 0)
|
|
|
|
|
{
|
|
|
|
|
if (account < 0)
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(account));
|
|
|
|
|
return new KeyPath(
|
|
|
|
|
$"{PurposeFor(kind)}'/{profile.Bip44CoinType}'/{account}'");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
/// <summary>Non-hardened change/index sub-path under the account (change=0 receiving, change=1 change).</summary>
|
2026-06-11 10:46:36 +02:00
|
|
|
public static KeyPath AddressSubPath(bool isChange, int index)
|
|
|
|
|
{
|
|
|
|
|
if (index < 0)
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
|
return new KeyPath($"{(isChange ? 1 : 0)}/{index}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Parsing of custom derivation paths on import (Sparrow-like, §4.2):
|
|
|
|
|
/// accepts the optional "m/" prefix and hardened markers ' or h.
|
2026-06-11 10:46:36 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public static bool TryParse(string path, out KeyPath? keyPath)
|
|
|
|
|
{
|
|
|
|
|
keyPath = null;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
|
|
|
return false;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
keyPath = KeyPath.Parse(path.Trim());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (FormatException)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|