feat(wallet): IWalletAccount abstraction and WIF/xpub/xprv keystore import types

Introduce IWalletAccount interface to abstract HD, imported-xprv, WIF,
and watch-only account types. HdAccount implements the interface; the
new ImportedKeyAccount handles lists of WIF keys with fixed addresses
(no gap scanning, change returns to first address).

WalletLoader gains three factory methods:
- NewFromXpub: watch-only from SLIP-132 xpub/ypub/zpub
- NewFromXprv: spendable from SLIP-132 xprv/yprv/zprv
- NewFromWif: spendable from one or more WIF private keys

WalletDocument gains optional AccountXprv and WifKeys fields; AccountPath
and AccountXpub become nullable (absent for WIF wallets). IsWatchOnly
updated to cover all non-signing wallet kinds.

TransactionFactory and CLI OpenWallet() updated to IWalletAccount.
12 new unit tests in ImportedKeyAccountTests cover factory round-trips,
address derivation, and watch-only detection.
This commit is contained in:
2026-06-15 14:20:10 +02:00
parent 002c854497
commit 47b6064964
9 changed files with 459 additions and 18 deletions
+10 -3
View File
@@ -11,7 +11,7 @@ namespace PalladiumWallet.Core.Crypto;
/// quindi il watch-only funziona per costruzione. Il keystore completo
/// (cifratura, factory dal file wallet, §4.5) arriva con la persistenza (§8).
/// </summary>
public sealed class HdAccount
public sealed class HdAccount : IWalletAccount
{
private readonly ExtKey? _accountXprv;
@@ -86,7 +86,7 @@ public sealed class HdAccount
masterFingerprint ?? default);
public BitcoinAddress GetAddress(bool isChange, int index) =>
GetPublicKey(isChange, index).GetAddress(
GetPublicKey(isChange, index)!.GetAddress(
DerivationPaths.ScriptPubKeyTypeFor(Kind),
PalladiumNetworks.For(Profile.Kind));
@@ -94,9 +94,16 @@ public sealed class HdAccount
public BitcoinAddress GetChangeAddress(int index) => GetAddress(isChange: true, index);
public PubKey GetPublicKey(bool isChange, int index) =>
public PubKey? GetPublicKey(bool isChange, int index) =>
AccountXpub.Derive(DerivationPaths.AddressSubPath(isChange, index)).PubKey;
/// <summary>Chiave privata di un indirizzo; null se watch-only (§17).</summary>
public Key? GetPrivateKey(bool isChange, int index) =>
IsWatchOnly ? null : GetExtPrivateKey(isChange, index).PrivateKey;
/// <summary>Gli account HD usano il gap limit: nessuna lista fissa di indirizzi.</summary>
public IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses => null;
/// <summary>
/// Chiave privata estesa di un indirizzo. Lancia se watch-only: nessuna
/// chiave privata è derivabile dalle sole pubbliche (§17).