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:
@@ -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).
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using NBitcoin;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Account da chiavi WIF singole importate (blueprint §4.4 — "Imported"):
|
||||
/// lista fissa di indirizzi, nessuna derivazione HD, nessuna catena di change.
|
||||
/// Il change va sempre al primo indirizzo importato.
|
||||
/// </summary>
|
||||
public sealed class ImportedKeyAccount : IWalletAccount
|
||||
{
|
||||
private readonly (BitcoinAddress Address, Key? PrivateKey)[] _entries;
|
||||
|
||||
public ScriptKind Kind { get; }
|
||||
public ChainProfile Profile { get; }
|
||||
public bool IsWatchOnly => _entries.All(e => e.PrivateKey is null);
|
||||
|
||||
public IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses =>
|
||||
_entries.Select((e, i) => (e.Address, false, i)).ToList();
|
||||
|
||||
public ImportedKeyAccount(
|
||||
IReadOnlyList<(BitcoinAddress Address, Key? PrivateKey)> entries,
|
||||
ScriptKind kind, ChainProfile profile)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
throw new ArgumentException("Almeno un indirizzo richiesto.", nameof(entries));
|
||||
_entries = [.. entries];
|
||||
Kind = kind;
|
||||
Profile = profile;
|
||||
}
|
||||
|
||||
public BitcoinAddress GetAddress(bool isChange, int index)
|
||||
{
|
||||
if (isChange || index < 0 || index >= _entries.Length)
|
||||
return _entries[0].Address;
|
||||
return _entries[index].Address;
|
||||
}
|
||||
|
||||
public BitcoinAddress GetReceiveAddress(int index) => GetAddress(false, index);
|
||||
|
||||
/// <summary>Il change torna sempre al primo indirizzo importato.</summary>
|
||||
public BitcoinAddress GetChangeAddress(int index) => _entries[0].Address;
|
||||
|
||||
public PubKey? GetPublicKey(bool isChange, int index)
|
||||
{
|
||||
if (isChange || index < 0 || index >= _entries.Length)
|
||||
return null;
|
||||
return _entries[index].PrivateKey?.PubKey;
|
||||
}
|
||||
|
||||
public Key? GetPrivateKey(bool isChange, int index)
|
||||
{
|
||||
if (isChange || index < 0 || index >= _entries.Length)
|
||||
return null;
|
||||
return _entries[index].PrivateKey;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user