using System.Text.Json; using System.Text.Json.Serialization; namespace PalladiumWallet.Core.Storage; /// /// Wallet file schema (blueprint §8), versioned to allow automatic /// migrations on opening. Encryption (when there is a password) wraps /// the entire document via . /// public sealed class WalletDocument { public const int CurrentVersion = 1; public int Version { get; set; } = CurrentVersion; /// Network name (mainnet/testnet/regtest), as ChainProfile.NetName. public required string Network { get; set; } /// Script type (name of the ScriptKind enum). public required string ScriptKind { get; set; } /// BIP39 mnemonic in plaintext in the document (the document must be encrypted!); null if watch-only. public string? Mnemonic { get; set; } /// BIP39 extension word (§4.1); null if absent. public string? Passphrase { get; set; } /// Account path (e.g. "84'/746'/0'"); null for imported WIF. public string? AccountPath { get; set; } /// Account xpub in SLIP-132 for HD wallets; null for imported WIF. public string? AccountXpub { get; set; } /// Account xprv in SLIP-132; present only for import from xprv without seed. public string? AccountXprv { get; set; } public string? MasterFingerprint { get; set; } /// Imported WIF keys (in plaintext in the document — must be encrypted!). public List? WifKeys { get; set; } /// Gap limit for address scanning (§5), configurable. public int GapLimit { get; set; } = 20; /// Labels by address/txid (§12). public Dictionary Labels { get; set; } = []; /// Contact address book (name + blockchain address). public List Contacts { get; set; } = []; /// Cache of the last synced state (balance/history viewable offline). public SyncCache? Cache { get; set; } public bool IsWatchOnly => Mnemonic is null && AccountXprv is null && (WifKeys is null || WifKeys.Count == 0); private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; public string ToJson() => JsonSerializer.Serialize(this, JsonOptions); public static WalletDocument FromJson(string json) { var doc = JsonSerializer.Deserialize(json, JsonOptions) ?? throw new InvalidDataException("File wallet non valido."); // string literal above intentionally left untranslated return Migrate(doc); } /// Migrazioni di schema all'apertura (§8). Per ora esiste solo la v1. private static WalletDocument Migrate(WalletDocument doc) => doc.Version switch { CurrentVersion => doc, _ => throw new InvalidDataException( $"Versione file wallet {doc.Version} non supportata (max {CurrentVersion})."), }; } /// Contatto in rubrica: nome leggibile + indirizzo blockchain. public sealed class StoredContact { public required string Name { get; set; } public required string Address { get; set; } } /// Stato sincronizzato persistito: permette di mostrare saldo/storico offline. public sealed class SyncCache { public int TipHeight { get; set; } public long ConfirmedSats { get; set; } public long UnconfirmedSats { get; set; } /// Confirmed but not yet spendable (coinbase immature or under min confirmations). Subset of ConfirmedSats. public long ImmatureSats { get; set; } public int NextReceiveIndex { get; set; } public int NextChangeIndex { get; set; } public List History { get; set; } = []; public List Utxos { get; set; } = []; public List Addresses { get; set; } = []; /// /// Raw transaction cache (txid → hex). Avoids re-downloading confirmed /// transactions on every launch: confirmed txs are immutable by definition. /// May be partially populated after an interrupted sync (e.g. -101 error); /// the synchronizer resumes from where it left off. /// public Dictionary? RawTxHex { get; set; } /// /// Already-verified Merkle proofs (txid → block height). Avoids re-verifying /// the same proofs on every launch: confirmations are immutable. /// public Dictionary? VerifiedAt { get; set; } /// /// Raw headers by height (height → hex). Immutable: never re-fetched once saved. /// Eliminates GetBlockHeader calls for Merkle proofs already verified in /// subsequent syncs. /// public Dictionary? BlockHeaders { get; set; } } /// Scanned address with its own balance and transaction count (address view). public sealed class CachedAddress { public required string Address { get; set; } public bool IsChange { get; set; } public int Index { get; set; } public long BalanceSats { get; set; } public int TxCount { get; set; } } public sealed class CachedTx { public required string Txid { get; set; } public int Height { get; set; } public long DeltaSats { get; set; } public bool Verified { get; set; } } public sealed class CachedUtxo { public required string Txid { get; set; } public int Vout { get; set; } public long ValueSats { get; set; } public required string Address { get; set; } public bool IsChange { get; set; } public int AddressIndex { get; set; } public int Height { get; set; } public bool IsCoinbase { get; set; } public bool Frozen { get; set; } }