6c24a8bb46
Extract confirmation-threshold logic into UtxoSpendability (shared by TransactionFactory and WalletSynchronizer) and use it to compute ImmatureSats, so coinbase/under-confirmed amounts are shown separately from spendable balance in the GUI and CLI instead of being lumped into "confirmed".
159 lines
5.9 KiB
C#
159 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace PalladiumWallet.Core.Storage;
|
|
|
|
/// <summary>
|
|
/// Wallet file schema (blueprint §8), versioned to allow automatic
|
|
/// migrations on opening. Encryption (when there is a password) wraps
|
|
/// the entire document via <see cref="EncryptedFile"/>.
|
|
/// </summary>
|
|
public sealed class WalletDocument
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
public int Version { get; set; } = CurrentVersion;
|
|
|
|
/// <summary>Network name (mainnet/testnet/regtest), as ChainProfile.NetName.</summary>
|
|
public required string Network { get; set; }
|
|
|
|
/// <summary>Script type (name of the ScriptKind enum).</summary>
|
|
public required string ScriptKind { get; set; }
|
|
|
|
/// <summary>BIP39 mnemonic in plaintext in the document (the document must be encrypted!); null if watch-only.</summary>
|
|
public string? Mnemonic { get; set; }
|
|
|
|
/// <summary>BIP39 extension word (§4.1); null if absent.</summary>
|
|
public string? Passphrase { get; set; }
|
|
|
|
/// <summary>Account path (e.g. "84'/746'/0'"); null for imported WIF.</summary>
|
|
public string? AccountPath { get; set; }
|
|
|
|
/// <summary>Account xpub in SLIP-132 for HD wallets; null for imported WIF.</summary>
|
|
public string? AccountXpub { get; set; }
|
|
|
|
/// <summary>Account xprv in SLIP-132; present only for import from xprv without seed.</summary>
|
|
public string? AccountXprv { get; set; }
|
|
|
|
public string? MasterFingerprint { get; set; }
|
|
|
|
/// <summary>Imported WIF keys (in plaintext in the document — must be encrypted!).</summary>
|
|
public List<string>? WifKeys { get; set; }
|
|
|
|
/// <summary>Gap limit for address scanning (§5), configurable.</summary>
|
|
public int GapLimit { get; set; } = 20;
|
|
|
|
/// <summary>Labels by address/txid (§12).</summary>
|
|
public Dictionary<string, string> Labels { get; set; } = [];
|
|
|
|
/// <summary>Contact address book (name + blockchain address).</summary>
|
|
public List<StoredContact> Contacts { get; set; } = [];
|
|
|
|
/// <summary>Cache of the last synced state (balance/history viewable offline).</summary>
|
|
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<WalletDocument>(json, JsonOptions)
|
|
?? throw new InvalidDataException("File wallet non valido.");
|
|
// string literal above intentionally left untranslated
|
|
return Migrate(doc);
|
|
}
|
|
|
|
/// <summary>Migrazioni di schema all'apertura (§8). Per ora esiste solo la v1.</summary>
|
|
private static WalletDocument Migrate(WalletDocument doc) => doc.Version switch
|
|
{
|
|
CurrentVersion => doc,
|
|
_ => throw new InvalidDataException(
|
|
$"Versione file wallet {doc.Version} non supportata (max {CurrentVersion})."),
|
|
};
|
|
}
|
|
|
|
/// <summary>Contatto in rubrica: nome leggibile + indirizzo blockchain.</summary>
|
|
public sealed class StoredContact
|
|
{
|
|
public required string Name { get; set; }
|
|
public required string Address { get; set; }
|
|
}
|
|
|
|
/// <summary>Stato sincronizzato persistito: permette di mostrare saldo/storico offline.</summary>
|
|
public sealed class SyncCache
|
|
{
|
|
public int TipHeight { get; set; }
|
|
public long ConfirmedSats { get; set; }
|
|
public long UnconfirmedSats { get; set; }
|
|
|
|
/// <summary>Confirmed but not yet spendable (coinbase immature or under min confirmations). Subset of ConfirmedSats.</summary>
|
|
public long ImmatureSats { get; set; }
|
|
public int NextReceiveIndex { get; set; }
|
|
public int NextChangeIndex { get; set; }
|
|
public List<CachedTx> History { get; set; } = [];
|
|
public List<CachedUtxo> Utxos { get; set; } = [];
|
|
public List<CachedAddress> Addresses { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public Dictionary<string, string>? RawTxHex { get; set; }
|
|
|
|
/// <summary>
|
|
/// Already-verified Merkle proofs (txid → block height). Avoids re-verifying
|
|
/// the same proofs on every launch: confirmations are immutable.
|
|
/// </summary>
|
|
public Dictionary<string, int>? VerifiedAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Raw headers by height (height → hex). Immutable: never re-fetched once saved.
|
|
/// Eliminates GetBlockHeader calls for Merkle proofs already verified in
|
|
/// subsequent syncs.
|
|
/// </summary>
|
|
public Dictionary<int, string>? BlockHeaders { get; set; }
|
|
}
|
|
|
|
/// <summary>Scanned address with its own balance and transaction count (address view).</summary>
|
|
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; }
|
|
}
|