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; }
}