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
+28 -5
View File
@@ -26,14 +26,20 @@ public sealed class WalletDocument
/// <summary>Extension word BIP39 (§4.1); null se assente.</summary>
public string? Passphrase { get; set; }
/// <summary>Path di account (es. "84'/746'/0'").</summary>
public required string AccountPath { get; set; }
/// <summary>Path di account (es. "84'/746'/0'"); null per importati WIF.</summary>
public string? AccountPath { get; set; }
/// <summary>Xpub di account in SLIP-132: basta da sola per il watch-only.</summary>
public required string AccountXpub { get; set; }
/// <summary>Xpub di account in SLIP-132 per i wallet HD; null per importati WIF.</summary>
public string? AccountXpub { get; set; }
/// <summary>Xprv di account in SLIP-132; presente solo per import da xprv senza seed.</summary>
public string? AccountXprv { get; set; }
public string? MasterFingerprint { get; set; }
/// <summary>Chiavi WIF importate (in chiaro nel documento — va cifrato!).</summary>
public List<string>? WifKeys { get; set; }
/// <summary>Gap limit per la scansione indirizzi (§5), configurabile.</summary>
public int GapLimit { get; set; } = 20;
@@ -46,7 +52,10 @@ public sealed class WalletDocument
/// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary>
public SyncCache? Cache { get; set; }
public bool IsWatchOnly => Mnemonic is null;
public bool IsWatchOnly =>
Mnemonic is null &&
AccountXprv is null &&
(WifKeys is null || WifKeys.Count == 0);
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -90,6 +99,20 @@ public sealed class SyncCache
public List<CachedTx> History { get; set; } = [];
public List<CachedUtxo> Utxos { get; set; } = [];
public List<CachedAddress> Addresses { get; set; } = [];
/// <summary>
/// Cache raw delle transazioni confermate (txid → hex). Evita di riscaricale
/// ad ogni avvio dell'app: le tx confermate sono immutabili per definizione.
/// Popolata anche parzialmente in caso di sync interrotta (es. -101):
/// il synchronizer riprende dal punto in cui si era fermato.
/// </summary>
public Dictionary<string, string>? RawTxHex { get; set; }
/// <summary>
/// Prove di Merkle già verificate (txid → altezza blocco). Evita di
/// riverificare le stesse prove ad ogni avvio: le conferme sono immutabili.
/// </summary>
public Dictionary<string, int>? VerifiedAt { get; set; }
}
/// <summary>Indirizzo scansionato con saldo proprio e numero di transazioni (vista indirizzi).</summary>