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
+34
View File
@@ -0,0 +1,34 @@
using NBitcoin;
using PalladiumWallet.Core.Chain;
namespace PalladiumWallet.Core.Crypto;
/// <summary>
/// Astrazione su tutti i tipi di account wallet (HD da seed, HD da xpub/xprv importata,
/// chiavi WIF importate). Consente a WalletSynchronizer e TransactionFactory di operare
/// indipendentemente dal tipo di keystore sottostante (blueprint §4.4–§4.5).
/// </summary>
public interface IWalletAccount
{
ScriptKind Kind { get; }
ChainProfile Profile { get; }
/// <summary>True se l'account non può firmare (assenza di chiavi private).</summary>
bool IsWatchOnly { get; }
BitcoinAddress GetAddress(bool isChange, int index);
BitcoinAddress GetReceiveAddress(int index);
BitcoinAddress GetChangeAddress(int index);
/// <summary>Null se la chiave pubblica non è ricavabile dall'account (indirizzi puri watch-only).</summary>
PubKey? GetPublicKey(bool isChange, int index);
/// <summary>Null se l'account è watch-only o l'indice è fuori range.</summary>
Key? GetPrivateKey(bool isChange, int index);
/// <summary>
/// Per gli account con indirizzi fissi (WIF importati) restituisce la lista
/// completa da scansionare; null per gli account HD che usano il gap limit.
/// </summary>
IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses { get; }
}