docs: translate all code comments to English (language policy)
Translate every Italian /// XML doc comment, <!-- --> XAML comment, and // inline comment to English across all source files (Core, App, tests). Add the language policy to CLAUDE.md (conversation Italian; all code and docs English). Update one test assertion that checked an Italian exception message that was also translated.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di rete supportato (selettore unico per tutto il wallet, blueprint §3).
|
||||
/// Supported network type (single selector for the whole wallet, blueprint §3).
|
||||
/// </summary>
|
||||
public enum NetKind
|
||||
{
|
||||
@@ -11,7 +11,7 @@ public enum NetKind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di script/indirizzo supportato (blueprint §4.3).
|
||||
/// Supported script/address type (blueprint §4.3).
|
||||
/// </summary>
|
||||
public enum ScriptKind
|
||||
{
|
||||
@@ -21,7 +21,7 @@ public enum ScriptKind
|
||||
/// <summary>P2SH-P2WPKH (segwit wrapped).</summary>
|
||||
WrappedSegwit,
|
||||
|
||||
/// <summary>P2WPKH (native segwit) — default consigliato.</summary>
|
||||
/// <summary>P2WPKH (native segwit) — recommended default.</summary>
|
||||
NativeSegwit,
|
||||
|
||||
/// <summary>P2SH-P2WSH (multisig wrapped).</summary>
|
||||
@@ -35,15 +35,15 @@ public enum ScriptKind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coppia di header di versione BIP32 (4 byte big-endian) per serializzare
|
||||
/// xprv/xpub e varianti SLIP-132 (y/z/Y/Z) — blueprint §3.
|
||||
/// Pair of BIP32 version headers (4-byte big-endian) to serialize
|
||||
/// xprv/xpub and SLIP-132 variants (y/z/Y/Z) — blueprint §3.
|
||||
/// </summary>
|
||||
public readonly record struct ExtKeyHeaders(uint Private, uint Public)
|
||||
{
|
||||
/// <summary>Header privato come 4 byte big-endian (da anteporre al payload BIP32).</summary>
|
||||
/// <summary>Private header as 4 big-endian bytes (to prepend to the BIP32 payload).</summary>
|
||||
public byte[] PrivateBytes() => ToBytes(Private);
|
||||
|
||||
/// <summary>Header pubblico come 4 byte big-endian.</summary>
|
||||
/// <summary>Public header as 4 big-endian bytes.</summary>
|
||||
public byte[] PublicBytes() => ToBytes(Public);
|
||||
|
||||
internal static byte[] ToBytes(uint header) =>
|
||||
@@ -56,82 +56,82 @@ public readonly record struct ExtKeyHeaders(uint Private, uint Public)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server di indicizzazione di bootstrap: host + porta TCP + porta SSL (blueprint §3/§9).
|
||||
/// Bootstrap indexing server: host + TCP port + SSL port (blueprint §3/§9).
|
||||
/// </summary>
|
||||
public readonly record struct ServerEndpoint(string Host, int TcpPort, int SslPort);
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint di catena: ancora la fiducia quando la verifica PoW è disattivata
|
||||
/// (blueprint §7.3). Target serializzato come "bits" compatti.
|
||||
/// Chain checkpoint: anchors trust when PoW validation is disabled
|
||||
/// (blueprint §7.3). Target serialized as compact "bits".
|
||||
/// </summary>
|
||||
public readonly record struct Checkpoint(int Height, string BlockHash, uint Bits);
|
||||
|
||||
/// <summary>
|
||||
/// Profilo di rete/catena (blueprint §3): tutte le costanti specifiche della catena,
|
||||
/// centralizzate in un solo punto. Nessun magic number altrove nel codice.
|
||||
/// Network/chain profile (blueprint §3): all chain-specific constants,
|
||||
/// centralized in one place. No magic numbers elsewhere in the code.
|
||||
/// </summary>
|
||||
public sealed record ChainProfile
|
||||
{
|
||||
public required NetKind Kind { get; init; }
|
||||
|
||||
/// <summary>Nome rete, usato anche come sottocartella dati.</summary>
|
||||
/// <summary>Network name, also used as the data subfolder.</summary>
|
||||
public required string NetName { get; init; }
|
||||
|
||||
/// <summary>Simbolo dell'unità (es. PLM).</summary>
|
||||
/// <summary>Unit symbol (e.g. PLM).</summary>
|
||||
public required string CoinUnit { get; init; }
|
||||
|
||||
/// <summary>Prefisso WIF delle chiavi private.</summary>
|
||||
/// <summary>WIF prefix for private keys.</summary>
|
||||
public required byte WifPrefix { get; init; }
|
||||
|
||||
/// <summary>Byte di versione indirizzi P2PKH.</summary>
|
||||
/// <summary>Version byte for P2PKH addresses.</summary>
|
||||
public required byte AddrP2pkh { get; init; }
|
||||
|
||||
/// <summary>Byte di versione indirizzi P2SH.</summary>
|
||||
/// <summary>Version byte for P2SH addresses.</summary>
|
||||
public required byte AddrP2sh { get; init; }
|
||||
|
||||
/// <summary>Human-readable part bech32/bech32m.</summary>
|
||||
public required string SegwitHrp { get; init; }
|
||||
|
||||
/// <summary>HRP fatture BOLT11 (Lightning, fase successiva — §11).</summary>
|
||||
/// <summary>HRP for BOLT11 invoices (Lightning, later phase — §11).</summary>
|
||||
public required string Bolt11Hrp { get; init; }
|
||||
|
||||
/// <summary>Hash del blocco genesi (hex, byte order da explorer).</summary>
|
||||
/// <summary>Genesis block hash (hex, explorer byte order).</summary>
|
||||
public required string GenesisHash { get; init; }
|
||||
|
||||
/// <summary>Porta TCP del server di indicizzazione (non è la porta P2P del nodo).</summary>
|
||||
/// <summary>TCP port of the indexing server (not the node's P2P port).</summary>
|
||||
public required int DefaultTcpPort { get; init; }
|
||||
|
||||
/// <summary>Porta SSL del server di indicizzazione.</summary>
|
||||
/// <summary>SSL port of the indexing server.</summary>
|
||||
public required int DefaultSslPort { get; init; }
|
||||
|
||||
/// <summary>Porta P2P del nodo — solo informativa: il wallet SPV non la usa.</summary>
|
||||
/// <summary>Node's P2P port — informational only: the SPV wallet does not use it.</summary>
|
||||
public required int NodeP2pPort { get; init; }
|
||||
|
||||
/// <summary>Coin type SLIP-0044 nei derivation path (convenzione wallet).</summary>
|
||||
/// <summary>SLIP-0044 coin type in derivation paths (wallet convention).</summary>
|
||||
public required int Bip44CoinType { get; init; }
|
||||
|
||||
/// <summary>Schema URI pagamenti BIP21 (senza i due punti).</summary>
|
||||
/// <summary>BIP21 payment URI scheme (without the colon).</summary>
|
||||
public required string UriScheme { get; init; }
|
||||
|
||||
/// <summary>Block explorer di default.</summary>
|
||||
/// <summary>Default block explorer.</summary>
|
||||
public required string ExplorerUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// La catena usa LWMA (retargeting per-blocco, 2 minuti): un client SPV non può
|
||||
/// ricalcolarla, quindi la verifica bits/target va saltata e la fiducia ancorata
|
||||
/// ai checkpoint (§3/§7). Per la catena di riferimento è sempre true.
|
||||
/// The chain uses LWMA (per-block retargeting, 2 minutes): an SPV client cannot
|
||||
/// recompute it, so bits/target validation must be skipped and trust anchored
|
||||
/// to checkpoints (§3/§7). For the reference chain it is always true.
|
||||
/// </summary>
|
||||
public required bool SkipPowValidation { get; init; }
|
||||
|
||||
/// <summary>Tempo di blocco target in secondi (LWMA v2: 120s).</summary>
|
||||
/// <summary>Target block time in seconds (LWMA v2: 120s).</summary>
|
||||
public required int BlockTimeSeconds { get; init; }
|
||||
|
||||
/// <summary>Header BIP32/SLIP-132 per ciascun tipo di script.</summary>
|
||||
/// <summary>BIP32/SLIP-132 headers for each script type.</summary>
|
||||
public required IReadOnlyDictionary<ScriptKind, ExtKeyHeaders> ExtKeyHeaders { get; init; }
|
||||
|
||||
/// <summary>Server di indicizzazione per il primo contatto (bootstrap).</summary>
|
||||
/// <summary>Indexing servers for first contact (bootstrap).</summary>
|
||||
public required IReadOnlyList<ServerEndpoint> BootstrapServers { get; init; }
|
||||
|
||||
/// <summary>Checkpoint hardcoded, aggiornati a ogni release (§7.3).</summary>
|
||||
/// <summary>Hardcoded checkpoints, updated at every release (§7.3).</summary>
|
||||
public required IReadOnlyList<Checkpoint> Checkpoints { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// I tre profili di rete del wallet (blueprint §3). Valori mainnet verificati
|
||||
/// contro il sorgente del nodo (chainparams.cpp / pow.cpp) secondo il blueprint.
|
||||
/// The wallet's three network profiles (blueprint §3). Mainnet values verified
|
||||
/// against the node source (chainparams.cpp / pow.cpp) per the blueprint.
|
||||
/// </summary>
|
||||
public static class ChainProfiles
|
||||
{
|
||||
@@ -12,11 +12,11 @@ public static class ChainProfiles
|
||||
NetName = "mainnet",
|
||||
CoinUnit = "PLM",
|
||||
WifPrefix = 0x80,
|
||||
AddrP2pkh = 55, // indirizzi che iniziano con 'P'
|
||||
AddrP2sh = 5, // indirizzi che iniziano con '3'
|
||||
AddrP2pkh = 55, // addresses starting with 'P'
|
||||
AddrP2sh = 5, // addresses starting with '3'
|
||||
SegwitHrp = "plm",
|
||||
Bolt11Hrp = "plm",
|
||||
// La mainnet riusa la genesi di Bitcoin (blueprint §3).
|
||||
// Mainnet reuses Bitcoin's genesis (blueprint §3).
|
||||
GenesisHash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
||||
DefaultTcpPort = 50001,
|
||||
DefaultSslPort = 50002,
|
||||
@@ -33,11 +33,11 @@ public static class ChainProfiles
|
||||
[ScriptKind.WrappedSegwitMultisig] = new(0x0295b005, 0x0295b43f), // Yprv / Ypub
|
||||
[ScriptKind.NativeSegwit] = new(0x04b2430c, 0x04b24746), // zprv / zpub
|
||||
[ScriptKind.NativeSegwitMultisig] = new(0x02aa7a99, 0x02aa7ed3), // Zprv / Zpub
|
||||
// Nessun SLIP-132 per P2TR: il contesto è dato dal path m/86'/…
|
||||
// No SLIP-132 for P2TR: the context is given by the m/86'/… path
|
||||
[ScriptKind.Taproot] = new(0x0488ade4, 0x0488b21e), // xprv / xpub (BIP32 standard)
|
||||
},
|
||||
// Server di indicizzazione noti per il primo contatto (§3/§9); altri
|
||||
// peer vengono scoperti via server.peers.subscribe.
|
||||
// Known indexing servers for first contact (§3/§9); other
|
||||
// peers are discovered via server.peers.subscribe.
|
||||
BootstrapServers =
|
||||
[
|
||||
new ServerEndpoint("173.212.224.67", 50001, 50002),
|
||||
@@ -45,7 +45,7 @@ public static class ChainProfiles
|
||||
new ServerEndpoint("66.94.115.80", 50001, 50002),
|
||||
new ServerEndpoint("89.117.149.130", 50001, 50002),
|
||||
],
|
||||
// TODO: popolare con [hash, bits] reali della catena (§7.3) prima della release.
|
||||
// TODO: populate with the chain's real [hash, bits] (§7.3) before release.
|
||||
Checkpoints = [],
|
||||
};
|
||||
|
||||
@@ -58,15 +58,15 @@ public static class ChainProfiles
|
||||
AddrP2sh = 115,
|
||||
SegwitHrp = "tplm",
|
||||
Bolt11Hrp = "tplm",
|
||||
// TODO: verificare contro chainparams.cpp del nodo (il blueprint non la riporta;
|
||||
// qui si assume la genesi di Bitcoin testnet3, da confermare).
|
||||
// TODO: verify against the node's chainparams.cpp (the blueprint does not report it;
|
||||
// here Bitcoin testnet3 genesis is assumed, to be confirmed).
|
||||
GenesisHash = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
|
||||
NodeP2pPort = 12333,
|
||||
Bip44CoinType = 1,
|
||||
ExtKeyHeaders = new Dictionary<ScriptKind, ExtKeyHeaders>
|
||||
{
|
||||
// tprv/tpub dal blueprint; le varianti SLIP-132 testnet (u/v/U/V) sono
|
||||
// i valori standard Electrum.
|
||||
// tprv/tpub from the blueprint; the testnet SLIP-132 variants (u/v/U/V) are
|
||||
// the standard Electrum values.
|
||||
[ScriptKind.Legacy] = new(0x04358394, 0x043587cf), // tprv / tpub
|
||||
[ScriptKind.WrappedSegwit] = new(0x044a4e28, 0x044a5262), // uprv / upub
|
||||
[ScriptKind.WrappedSegwitMultisig] = new(0x024285b5, 0x024289ef), // Uprv / Upub
|
||||
@@ -84,7 +84,7 @@ public static class ChainProfiles
|
||||
NetName = "regtest",
|
||||
SegwitHrp = "rplm",
|
||||
Bolt11Hrp = "rplm",
|
||||
// TODO: verificare contro chainparams.cpp del nodo (assunta genesi regtest Bitcoin).
|
||||
// TODO: verify against the node's chainparams.cpp (Bitcoin regtest genesis assumed).
|
||||
GenesisHash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206",
|
||||
NodeP2pPort = 28444,
|
||||
Checkpoints = [],
|
||||
|
||||
@@ -4,13 +4,13 @@ using NBitcoin.DataEncoders;
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce e registra le <see cref="Network"/> NBitcoin di Palladium a partire dai
|
||||
/// <see cref="ChainProfiles"/> (blueprint §19.3). Tutto il resto del codice ottiene la
|
||||
/// rete da qui, mai da costanti sparse.
|
||||
/// Builds and registers the Palladium NBitcoin <see cref="Network"/> instances from the
|
||||
/// <see cref="ChainProfiles"/> (blueprint §19.3). All other code obtains the network
|
||||
/// from here — never from scattered constants.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Identità della moneta per NBitcoin: raggruppa le tre reti sotto il codice PLM.
|
||||
/// I getter sono lazy, quindi nessuna ricorsione durante la costruzione.
|
||||
/// Coin identity for NBitcoin: groups the three networks under the PLM code.
|
||||
/// Getters are lazy, so no recursion occurs during construction.
|
||||
/// </summary>
|
||||
public sealed class PalladiumNetworkSet : INetworkSet
|
||||
{
|
||||
@@ -34,13 +34,13 @@ public sealed class PalladiumNetworkSet : INetworkSet
|
||||
|
||||
public static class PalladiumNetworks
|
||||
{
|
||||
// Blocco genesi di Bitcoin (la mainnet Palladium lo riusa, blueprint §3).
|
||||
// Bitcoin genesis block (Palladium mainnet reuses it, blueprint §3).
|
||||
private const string MainGenesisHex =
|
||||
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" +
|
||||
Coinbase;
|
||||
|
||||
// Genesi testnet3/regtest di Bitcoin: stessa coinbase, cambiano time/bits/nonce.
|
||||
// TODO: confermare contro chainparams.cpp del nodo (vedi ChainProfiles).
|
||||
// Bitcoin testnet3/regtest genesis: same coinbase, different time/bits/nonce.
|
||||
// TODO: confirm against the node's chainparams.cpp (see ChainProfiles).
|
||||
private const string TestGenesisHex =
|
||||
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18" +
|
||||
Coinbase;
|
||||
@@ -81,9 +81,9 @@ public static class PalladiumNetworks
|
||||
NetKind.Testnet => ChainName.Testnet,
|
||||
_ => ChainName.Regtest,
|
||||
})
|
||||
// Magic P2P solo segnaposto: il wallet SPV non apre mai connessioni P2P
|
||||
// (parla solo col server di indicizzazione, §3). Serve a NBitcoin per
|
||||
// distinguere le reti registrate.
|
||||
// P2P magic is a placeholder only: the SPV wallet never opens P2P connections
|
||||
// (it only talks to the indexing server, §3). Required by NBitcoin to
|
||||
// distinguish registered networks.
|
||||
.SetMagic(magic)
|
||||
.SetPort(p.NodeP2pPort)
|
||||
.SetRPCPort(p.NodeP2pPort + 1)
|
||||
@@ -91,9 +91,9 @@ public static class PalladiumNetworks
|
||||
.SetBase58Bytes(Base58Type.PUBKEY_ADDRESS, [p.AddrP2pkh])
|
||||
.SetBase58Bytes(Base58Type.SCRIPT_ADDRESS, [p.AddrP2sh])
|
||||
.SetBase58Bytes(Base58Type.SECRET_KEY, [p.WifPrefix])
|
||||
// NBitcoin gestisce una sola coppia di header BIP32 per rete: quella
|
||||
// standard xprv/xpub. Le varianti SLIP-132 (y/z/Y/Z) si serializzano
|
||||
// a mano con ChainProfile.ExtKeyHeaders quando servono.
|
||||
// NBitcoin handles only one BIP32 header pair per network: the standard
|
||||
// xprv/xpub. SLIP-132 variants (y/z/Y/Z) are serialised manually via
|
||||
// ChainProfile.ExtKeyHeaders when needed.
|
||||
.SetBase58Bytes(Base58Type.EXT_SECRET_KEY, legacy.PrivateBytes())
|
||||
.SetBase58Bytes(Base58Type.EXT_PUBLIC_KEY, legacy.PublicBytes())
|
||||
.SetBech32(Bech32Type.WITNESS_PUBKEY_ADDRESS, Encoders.Bech32(p.SegwitHrp))
|
||||
@@ -102,8 +102,8 @@ public static class PalladiumNetworks
|
||||
.SetUriScheme(p.UriScheme)
|
||||
.SetConsensus(new Consensus
|
||||
{
|
||||
// Valori usati solo dalle API NBitcoin che li richiedono: la validazione
|
||||
// header reale è custom (Core/Spv) con skip PoW + checkpoint (§7).
|
||||
// Values used only by NBitcoin APIs that require them: real header
|
||||
// validation is custom (Core/Spv) with PoW skip + checkpoints (§7).
|
||||
PowLimit = new Target(new uint256("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff")),
|
||||
PowTargetSpacing = TimeSpan.FromSeconds(p.BlockTimeSeconds),
|
||||
PowTargetTimespan = TimeSpan.FromDays(14),
|
||||
|
||||
+21
-21
@@ -3,8 +3,8 @@ using NBitcoin;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Lingue wordlist BIP39 supportate (blueprint §4.1). Le liste sono incorporate
|
||||
/// in NBitcoin: nessun accesso a rete o filesystem.
|
||||
/// Supported BIP39 wordlist languages (blueprint §4.1). The lists are embedded
|
||||
/// in NBitcoin: no network or filesystem access required.
|
||||
/// </summary>
|
||||
public enum MnemonicLanguage
|
||||
{
|
||||
@@ -18,7 +18,7 @@ public enum MnemonicLanguage
|
||||
Czech,
|
||||
}
|
||||
|
||||
/// <summary>Numero di parole supportato in creazione (blueprint §4.1: 12 o 24).</summary>
|
||||
/// <summary>Supported word counts for mnemonic creation (blueprint §4.1: 12 or 24).</summary>
|
||||
public enum MnemonicLength
|
||||
{
|
||||
Twelve = 12,
|
||||
@@ -26,16 +26,16 @@ public enum MnemonicLength
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Facciata BIP39 su NBitcoin.Mnemonic (blueprint §4.1). NBitcoin copre già
|
||||
/// entropia→parole, checksum, normalizzazione NFKD e PBKDF2-HMAC-SHA512 a 2048
|
||||
/// round con salt "mnemonic"+passphrase: qui si restringe l'API ai casi del
|
||||
/// blueprint e si centralizza la mappa delle lingue. Quando arriverà il seed
|
||||
/// nativo versionato (§4.1 punto 1), il riconoscimento multi-schema sarà una
|
||||
/// catena di TryParse per schema.
|
||||
/// BIP39 facade over NBitcoin.Mnemonic (blueprint §4.1). NBitcoin already handles
|
||||
/// entropy→words, checksum, NFKD normalisation, and PBKDF2-HMAC-SHA512 with 2048
|
||||
/// rounds using salt "mnemonic"+passphrase. This class narrows the API to the
|
||||
/// blueprint use-cases and centralises the language map. When the native versioned
|
||||
/// seed arrives (§4.1 point 1), multi-scheme recognition will be a chain of
|
||||
/// TryParse calls per scheme.
|
||||
/// </summary>
|
||||
public static class Bip39
|
||||
{
|
||||
/// <summary>Genera una nuova mnemonica con entropia dal CSPRNG di sistema.</summary>
|
||||
/// <summary>Generates a new mnemonic using entropy from the system CSPRNG.</summary>
|
||||
public static Mnemonic Generate(MnemonicLength length, MnemonicLanguage language = MnemonicLanguage.English)
|
||||
{
|
||||
var wordCount = length == MnemonicLength.Twelve ? WordCount.Twelve : WordCount.TwentyFour;
|
||||
@@ -43,10 +43,10 @@ public static class Bip39
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Riconosce e valida una mnemonica BIP39: numero di parole, appartenenza alla
|
||||
/// wordlist e checksum. Se <paramref name="language"/> è indicata viene provata
|
||||
/// per prima; altrimenti la lingua è auto-rilevata (parole condivise tra liste
|
||||
/// possono rendere ambiguo l'autodetect: in import l'utente può forzarla).
|
||||
/// Recognises and validates a BIP39 mnemonic: word count, wordlist membership,
|
||||
/// and checksum. If <paramref name="language"/> is specified it is tried first;
|
||||
/// otherwise the language is auto-detected (words shared between lists can make
|
||||
/// autodetect ambiguous — the user can override it on import).
|
||||
/// </summary>
|
||||
public static bool TryParse(string text, out Mnemonic? mnemonic, MnemonicLanguage? language = null)
|
||||
{
|
||||
@@ -54,8 +54,8 @@ public static class Bip39
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
// Solo trim esterno: la normalizzazione NFKD e lo spazio ideografico
|
||||
// giapponese li gestisce NBitcoin, il testo non va alterato (§4.1).
|
||||
// Outer trim only: NBitcoin handles NFKD normalisation and Japanese
|
||||
// ideographic spaces — the text must not be altered further (§4.1).
|
||||
text = text.Trim();
|
||||
|
||||
IEnumerable<Wordlist> candidates = language is not null
|
||||
@@ -67,7 +67,7 @@ public static class Bip39
|
||||
try
|
||||
{
|
||||
var parsed = new Mnemonic(text, wordlist);
|
||||
// Il costruttore NON verifica il checksum: controllo esplicito obbligatorio.
|
||||
// The constructor does NOT verify the checksum — explicit check is mandatory.
|
||||
if (parsed.IsValidChecksum && parsed.Words.Length is 12 or 15 or 18 or 21 or 24)
|
||||
{
|
||||
mnemonic = parsed;
|
||||
@@ -76,7 +76,7 @@ public static class Bip39
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Parole fuori wordlist o conteggio non valido: si prova oltre.
|
||||
// Words outside the wordlist or invalid count — try the next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ public static class Bip39
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mnemonica → seed di 64 byte (PBKDF2-HMAC-SHA512, 2048 round, salt
|
||||
/// "mnemonic"+passphrase). La passphrase cambia completamente il wallet
|
||||
/// derivato: avvisi UI obbligatori (§4.1).
|
||||
/// Mnemonic → 64-byte seed (PBKDF2-HMAC-SHA512, 2048 rounds, salt
|
||||
/// "mnemonic"+passphrase). The passphrase completely changes the derived
|
||||
/// wallet — mandatory UI warnings required (§4.1).
|
||||
/// </summary>
|
||||
public static byte[] ToSeed(Mnemonic mnemonic, string? passphrase = null) =>
|
||||
mnemonic.DeriveSeed(passphrase);
|
||||
|
||||
@@ -4,25 +4,25 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Costruzione dei derivation path BIP44/49/84 (blueprint §4.2) e mappatura
|
||||
/// ScriptKind → purpose / ScriptPubKeyType. I purpose sono costanti di protocollo
|
||||
/// BIP (chain-independent); il coin_type viene sempre dal ChainProfile.
|
||||
/// BIP44/49/84 derivation path construction (blueprint §4.2) and mapping of
|
||||
/// ScriptKind → purpose / ScriptPubKeyType. Purposes are BIP protocol constants
|
||||
/// (chain-independent); coin_type always comes from the ChainProfile.
|
||||
/// </summary>
|
||||
public static class DerivationPaths
|
||||
{
|
||||
/// <summary>Purpose BIP44 (P2PKH legacy).</summary>
|
||||
/// <summary>BIP44 purpose (P2PKH legacy).</summary>
|
||||
public const int PurposeLegacy = 44;
|
||||
|
||||
/// <summary>Purpose BIP49 (P2SH-P2WPKH).</summary>
|
||||
/// <summary>BIP49 purpose (P2SH-P2WPKH).</summary>
|
||||
public const int PurposeWrappedSegwit = 49;
|
||||
|
||||
/// <summary>Purpose BIP84 (P2WPKH nativo).</summary>
|
||||
/// <summary>BIP84 purpose (native P2WPKH).</summary>
|
||||
public const int PurposeNativeSegwit = 84;
|
||||
|
||||
/// <summary>Purpose BIP48 (multisig — fase successiva, §16 passo 8).</summary>
|
||||
/// <summary>BIP48 purpose (multisig — next phase, §16 step 8).</summary>
|
||||
public const int PurposeMultisig = 48;
|
||||
|
||||
/// <summary>Purpose BIP86 (P2TR key-path, Taproot).</summary>
|
||||
/// <summary>BIP86 purpose (P2TR key-path, Taproot).</summary>
|
||||
public const int PurposeTaproot = 86;
|
||||
|
||||
public static int PurposeFor(ScriptKind kind) => kind switch
|
||||
@@ -36,9 +36,9 @@ public static class DerivationPaths
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di scriptPubKey NBitcoin per la derivazione da pubkey singola.
|
||||
/// I tipi multisig derivano da redeem script, non da una pubkey: arriveranno
|
||||
/// con i wallet M-di-N (§4.5).
|
||||
/// NBitcoin scriptPubKey type for single-pubkey derivation.
|
||||
/// Multisig types are derived from a redeem script, not a pubkey — they will
|
||||
/// arrive with M-of-N wallet support (§4.5).
|
||||
/// </summary>
|
||||
public static ScriptPubKeyType ScriptPubKeyTypeFor(ScriptKind kind) => kind switch
|
||||
{
|
||||
@@ -48,13 +48,13 @@ public static class DerivationPaths
|
||||
ScriptKind.Taproot => ScriptPubKeyType.TaprootBIP86,
|
||||
ScriptKind.WrappedSegwitMultisig or ScriptKind.NativeSegwitMultisig =>
|
||||
throw new NotSupportedException(
|
||||
"I tipi multisig derivano da redeem script: supporto previsto con i wallet M-di-N (§4.5)."),
|
||||
"Multisig types are derived from a redeem script: support planned with M-of-N wallets (§4.5)."),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Path di account relativo alla root: purpose'/coin'/account' (§4.2).
|
||||
/// Il coin_type è quello del profilo (746 mainnet, 1 testnet).
|
||||
/// Account path relative to the root: purpose'/coin'/account' (§4.2).
|
||||
/// coin_type is taken from the profile (746 mainnet, 1 testnet).
|
||||
/// </summary>
|
||||
public static KeyPath AccountPath(ScriptKind kind, ChainProfile profile, int account = 0)
|
||||
{
|
||||
@@ -64,7 +64,7 @@ public static class DerivationPaths
|
||||
$"{PurposeFor(kind)}'/{profile.Bip44CoinType}'/{account}'");
|
||||
}
|
||||
|
||||
/// <summary>Sottopath non-hardened change/index sotto l'account (change=0 receiving, change=1 change).</summary>
|
||||
/// <summary>Non-hardened change/index sub-path under the account (change=0 receiving, change=1 change).</summary>
|
||||
public static KeyPath AddressSubPath(bool isChange, int index)
|
||||
{
|
||||
if (index < 0)
|
||||
@@ -73,8 +73,8 @@ public static class DerivationPaths
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsing di derivation path personalizzati in import (Sparrow-like, §4.2):
|
||||
/// accetta il prefisso "m/" opzionale e gli hardened marker ' oppure h.
|
||||
/// Parsing of custom derivation paths on import (Sparrow-like, §4.2):
|
||||
/// accepts the optional "m/" prefix and hardened markers ' or h.
|
||||
/// </summary>
|
||||
public static bool TryParse(string path, out KeyPath? keyPath)
|
||||
{
|
||||
|
||||
@@ -4,12 +4,12 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Account HD single-sig (blueprint §4.2/§4.4): chiave estesa di account (xprv,
|
||||
/// oppure solo xpub = watch-only) + tipo di script + profilo di rete. Deriva
|
||||
/// indirizzi receiving (change=0) e change (change=1) on-demand per indice.
|
||||
/// Gli indirizzi si derivano sempre dall'xpub di account (sottopath non-hardened),
|
||||
/// quindi il watch-only funziona per costruzione. Il keystore completo
|
||||
/// (cifratura, factory dal file wallet, §4.5) arriva con la persistenza (§8).
|
||||
/// Single-sig HD account (blueprint §4.2/§4.4): extended account key (xprv,
|
||||
/// or xpub-only = watch-only) + script type + network profile. Derives receiving
|
||||
/// (change=0) and change (change=1) addresses on-demand by index.
|
||||
/// Addresses are always derived from the account xpub (non-hardened sub-path),
|
||||
/// so watch-only works by construction. The full keystore
|
||||
/// (encryption, factory from wallet file, §4.5) arrives with persistence (§8).
|
||||
/// </summary>
|
||||
public sealed class HdAccount : IWalletAccount
|
||||
{
|
||||
@@ -18,25 +18,25 @@ public sealed class HdAccount : IWalletAccount
|
||||
public ScriptKind Kind { get; }
|
||||
public ChainProfile Profile { get; }
|
||||
|
||||
/// <summary>Path dell'account relativo alla root (es. 84'/746'/0', o personalizzato).</summary>
|
||||
/// <summary>Account path relative to the root (e.g. 84'/746'/0', or custom).</summary>
|
||||
public KeyPath AccountPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprint della chiave master: con <see cref="AccountPath"/> forma le
|
||||
/// origin info richieste dalle PSBT (§6.5). Zero se ignota (xpub importata
|
||||
/// senza metadati).
|
||||
/// Master key fingerprint: together with <see cref="AccountPath"/> forms the
|
||||
/// origin info required by PSBTs (§6.5). Zero if unknown (imported xpub
|
||||
/// without metadata).
|
||||
/// </summary>
|
||||
public HDFingerprint MasterFingerprint { get; }
|
||||
|
||||
public ExtPubKey AccountXpub { get; }
|
||||
|
||||
/// <summary>True se l'account conosce solo la xpub: costruisce ma non firma (§4.5).</summary>
|
||||
/// <summary>True if the account only knows the xpub: can build but not sign (§4.5).</summary>
|
||||
public bool IsWatchOnly => _accountXprv is null;
|
||||
|
||||
private HdAccount(ExtKey? accountXprv, ExtPubKey accountXpub, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath accountPath, HDFingerprint masterFingerprint)
|
||||
{
|
||||
// Valida subito la mappatura (i tipi multisig non sono supportati qui).
|
||||
// Validate the mapping immediately (multisig types are not supported here).
|
||||
DerivationPaths.ScriptPubKeyTypeFor(kind);
|
||||
_accountXprv = accountXprv;
|
||||
AccountXpub = accountXpub;
|
||||
@@ -46,7 +46,7 @@ public sealed class HdAccount : IWalletAccount
|
||||
MasterFingerprint = masterFingerprint;
|
||||
}
|
||||
|
||||
/// <summary>Caso principale: mnemonica BIP39 (+ passphrase opzionale) → account standard.</summary>
|
||||
/// <summary>Main case: BIP39 mnemonic (+ optional passphrase) → standard account.</summary>
|
||||
public static HdAccount FromMnemonic(Mnemonic mnemonic, string? passphrase,
|
||||
ScriptKind kind, ChainProfile profile, int account = 0) =>
|
||||
FromSeed(Bip39.ToSeed(mnemonic, passphrase), kind, profile, account);
|
||||
@@ -55,22 +55,22 @@ public sealed class HdAccount : IWalletAccount
|
||||
FromSeed(seed, kind, profile, DerivationPaths.AccountPath(kind, profile, account));
|
||||
|
||||
/// <summary>
|
||||
/// Import con derivation path personalizzato (§4.2, Sparrow-like):
|
||||
/// <paramref name="kind"/> determina solo il tipo di indirizzo generato.
|
||||
/// Import with a custom derivation path (§4.2, Sparrow-like):
|
||||
/// <paramref name="kind"/> only determines the generated address type.
|
||||
/// </summary>
|
||||
public static HdAccount FromSeed(byte[] seed, ScriptKind kind, ChainProfile profile, KeyPath accountPath)
|
||||
{
|
||||
var root = ExtKey.CreateFromSeed(seed);
|
||||
// L'account si deriva sempre dalla xprv: i livelli hardened del path
|
||||
// non sono derivabili da una xpub.
|
||||
// The account is always derived from the xprv: hardened path levels
|
||||
// cannot be derived from an xpub alone.
|
||||
var accountXprv = root.Derive(accountPath);
|
||||
return new HdAccount(accountXprv, accountXprv.Neuter(), kind, profile,
|
||||
accountPath, root.Neuter().PubKey.GetHDFingerPrint());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watch-only da xpub di account (§4.4): fingerprint e path sono noti solo
|
||||
/// se forniti da chi importa (servono per le origin info PSBT).
|
||||
/// Watch-only from account xpub (§4.4): fingerprint and path are known only
|
||||
/// if supplied by the importer (required for PSBT origin info).
|
||||
/// </summary>
|
||||
public static HdAccount FromAccountXpub(ExtPubKey accountXpub, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath? accountPath = null, HDFingerprint? masterFingerprint = null) =>
|
||||
@@ -78,7 +78,7 @@ public sealed class HdAccount : IWalletAccount
|
||||
accountPath ?? DerivationPaths.AccountPath(kind, profile),
|
||||
masterFingerprint ?? default);
|
||||
|
||||
/// <summary>Import spendibile da xprv di account.</summary>
|
||||
/// <summary>Spendable import from account xprv.</summary>
|
||||
public static HdAccount FromAccountXprv(ExtKey accountXprv, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath? accountPath = null, HDFingerprint? masterFingerprint = null) =>
|
||||
new(accountXprv, accountXprv.Neuter(), kind, profile,
|
||||
@@ -97,27 +97,27 @@ public sealed class HdAccount : IWalletAccount
|
||||
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>
|
||||
/// <summary>Private key for an address; null if 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>
|
||||
/// <summary>HD accounts use the gap limit: no fixed address list.</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).
|
||||
/// Extended private key for an address. Throws if watch-only: no private key
|
||||
/// can be derived from public keys alone (§17).
|
||||
/// </summary>
|
||||
public ExtKey GetExtPrivateKey(bool isChange, int index) =>
|
||||
(_accountXprv ?? throw new InvalidOperationException("Account watch-only: non può firmare."))
|
||||
(_accountXprv ?? throw new InvalidOperationException("Watch-only account: cannot sign."))
|
||||
.Derive(DerivationPaths.AddressSubPath(isChange, index));
|
||||
|
||||
/// <summary>Xpub di account in formato SLIP-132 (xpub/ypub/zpub secondo Kind).</summary>
|
||||
/// <summary>Account xpub in SLIP-132 format (xpub/ypub/zpub according to Kind).</summary>
|
||||
public string ToSlip132() => Slip132.Encode(AccountXpub, Kind, Profile);
|
||||
|
||||
/// <summary>Xprv di account in formato SLIP-132. Lancia se watch-only.</summary>
|
||||
/// <summary>Account xprv in SLIP-132 format. Throws if watch-only.</summary>
|
||||
public string ToSlip132Private() =>
|
||||
Slip132.Encode(
|
||||
_accountXprv ?? throw new InvalidOperationException("Account watch-only: nessuna chiave privata."),
|
||||
_accountXprv ?? throw new InvalidOperationException("Watch-only account: no private key."),
|
||||
Kind, Profile);
|
||||
}
|
||||
|
||||
@@ -4,31 +4,31 @@ 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).
|
||||
/// Abstraction over all wallet account types (HD from seed, HD from imported xpub/xprv,
|
||||
/// imported WIF keys). Allows WalletSynchronizer and TransactionFactory to operate
|
||||
/// independently of the underlying keystore type (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>
|
||||
/// <summary>True if the account cannot sign (no private keys present).</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>
|
||||
/// <summary>Null if the public key cannot be derived from the account (pure watch-only addresses).</summary>
|
||||
PubKey? GetPublicKey(bool isChange, int index);
|
||||
|
||||
/// <summary>Null se l'account è watch-only o l'indice è fuori range.</summary>
|
||||
/// <summary>Null if the account is watch-only or the index is out of 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.
|
||||
/// For accounts with fixed addresses (imported WIF keys) returns the full list
|
||||
/// to scan; null for HD accounts that use the gap limit.
|
||||
/// </summary>
|
||||
IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses { get; }
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ 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.
|
||||
/// Account built from individually imported WIF keys (blueprint §4.4 — "Imported"):
|
||||
/// fixed list of addresses, no HD derivation, no change chain.
|
||||
/// Change always goes back to the first imported address.
|
||||
/// </summary>
|
||||
public sealed class ImportedKeyAccount : IWalletAccount
|
||||
{
|
||||
@@ -24,7 +24,7 @@ public sealed class ImportedKeyAccount : IWalletAccount
|
||||
ScriptKind kind, ChainProfile profile)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
throw new ArgumentException("Almeno un indirizzo richiesto.", nameof(entries));
|
||||
throw new ArgumentException("At least one address is required.", nameof(entries));
|
||||
_entries = [.. entries];
|
||||
Kind = kind;
|
||||
Profile = profile;
|
||||
@@ -39,7 +39,7 @@ public sealed class ImportedKeyAccount : IWalletAccount
|
||||
|
||||
public BitcoinAddress GetReceiveAddress(int index) => GetAddress(false, index);
|
||||
|
||||
/// <summary>Il change torna sempre al primo indirizzo importato.</summary>
|
||||
/// <summary>Change always returns to the first imported address.</summary>
|
||||
public BitcoinAddress GetChangeAddress(int index) => _entries[0].Address;
|
||||
|
||||
public PubKey? GetPublicKey(bool isChange, int index)
|
||||
|
||||
@@ -20,8 +20,8 @@ public static class Slip132
|
||||
Encoders.Base58Check.EncodeData([.. profile.ExtKeyHeaders[kind].PrivateBytes(), .. key.ToBytes()]);
|
||||
|
||||
/// <summary>
|
||||
/// Riconosce l'header (→ ScriptKind) e decodifica una chiave pubblica estesa:
|
||||
/// è la via dell'import watch-only (§4.4).
|
||||
/// Recognises the header (→ ScriptKind) and decodes an extended public key:
|
||||
/// this is the watch-only import path (§4.4).
|
||||
/// </summary>
|
||||
public static bool TryDecodePublic(string encoded, ChainProfile profile,
|
||||
out ExtPubKey? key, out ScriptKind kind)
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Pinning TLS "trust on first use" (blueprint §9): al primo contatto la
|
||||
/// fingerprint SHA-256 del certificato del server viene salvata; ai successivi
|
||||
/// viene confrontata. Se cambia, la connessione va rifiutata e l'utente può
|
||||
/// sbloccare con un reset esplicito (caso tipico: server self-signed rinnovato).
|
||||
/// TLS "trust on first use" pinning (blueprint §9): on the first contact the
|
||||
/// server certificate's SHA-256 fingerprint is saved; on subsequent ones it is
|
||||
/// compared. If it changes, the connection must be rejected and the user can
|
||||
/// unlock with an explicit reset (typical case: renewed self-signed server).
|
||||
/// </summary>
|
||||
public sealed class CertificatePinStore(string filePath)
|
||||
{
|
||||
@@ -18,8 +18,8 @@ public sealed class CertificatePinStore(string filePath)
|
||||
Convert.ToHexString(SHA256.HashData(certificate.GetRawCertData())).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Verifica TOFU: true se il certificato è quello già visto (o se è il primo
|
||||
/// contatto, nel qual caso viene salvato). False = certificato cambiato.
|
||||
/// TOFU check: true if the certificate is the one already seen (or if it is the
|
||||
/// first contact, in which case it is saved). False = certificate changed.
|
||||
/// </summary>
|
||||
public bool VerifyOrPin(string host, int port, X509Certificate certificate)
|
||||
{
|
||||
@@ -36,7 +36,7 @@ public sealed class CertificatePinStore(string filePath)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reset del certificato salvato per un server ("reset certificati SSL", §9).</summary>
|
||||
/// <summary>Reset the saved certificate for a server ("reset SSL certificates", §9).</summary>
|
||||
public bool Reset(string host, int port)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
+13
-13
@@ -2,24 +2,24 @@ using System.Text.Json;
|
||||
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>Voce di storico di uno scripthash (blockchain.scripthash.get_history).</summary>
|
||||
/// <summary>History entry for a scripthash (blockchain.scripthash.get_history).</summary>
|
||||
public readonly record struct HistoryItem(string TxHash, int Height, long FeeSats);
|
||||
|
||||
/// <summary>UTXO riportato dal server (blockchain.scripthash.listunspent).</summary>
|
||||
/// <summary>UTXO reported by the server (blockchain.scripthash.listunspent).</summary>
|
||||
public readonly record struct UnspentItem(string TxHash, int TxPos, long ValueSats, int Height);
|
||||
|
||||
/// <summary>Prova di Merkle (blockchain.transaction.get_merkle).</summary>
|
||||
/// <summary>Merkle proof (blockchain.transaction.get_merkle).</summary>
|
||||
public sealed record MerkleProofResponse(int BlockHeight, int Pos, IReadOnlyList<string> Merkle);
|
||||
|
||||
/// <summary>Tip della catena notificato da blockchain.headers.subscribe.</summary>
|
||||
/// <summary>Chain tip notified by blockchain.headers.subscribe.</summary>
|
||||
public readonly record struct ChainTip(int Height, string HeaderHex);
|
||||
|
||||
/// <summary>Peer annunciato da server.peers.subscribe (porte null = non offerte).</summary>
|
||||
/// <summary>Peer announced by server.peers.subscribe (null ports = not offered).</summary>
|
||||
public sealed record PeerInfo(string Host, int? TcpPort, int? SslPort, string? Version);
|
||||
|
||||
/// <summary>
|
||||
/// Helper tipizzati sui metodi del protocollo (blueprint §10), sopra il
|
||||
/// trasporto JSON-RPC generico di <see cref="ElectrumClient"/>.
|
||||
/// Typed helpers over the protocol methods (blueprint §10), on top of the
|
||||
/// generic JSON-RPC transport of <see cref="ElectrumClient"/>.
|
||||
/// </summary>
|
||||
public static class ElectrumApi
|
||||
{
|
||||
@@ -29,7 +29,7 @@ public static class ElectrumApi
|
||||
return new ChainTip(r.GetProperty("height").GetInt32(), r.GetProperty("hex").GetString()!);
|
||||
}
|
||||
|
||||
/// <summary>Status corrente dello scripthash (null = mai usato).</summary>
|
||||
/// <summary>Current status of the scripthash (null = never used).</summary>
|
||||
public static async Task<string?> SubscribeScripthashAsync(this ElectrumClient c, string scripthash,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -89,7 +89,7 @@ public static class ElectrumApi
|
||||
return r.GetString()!;
|
||||
}
|
||||
|
||||
/// <summary>Fee stimata in coin/kB per il target dato; -1 se il server non sa stimare.</summary>
|
||||
/// <summary>Estimated fee in coin/kB for the given target; -1 if the server cannot estimate.</summary>
|
||||
public static async Task<decimal> EstimateFeeAsync(this ElectrumClient c, int targetBlocks,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -112,7 +112,7 @@ public static class ElectrumApi
|
||||
public static Task PingAsync(this ElectrumClient c, CancellationToken ct = default) =>
|
||||
c.RequestAsync("server.ping", ct);
|
||||
|
||||
/// <summary>Peer annunciati dal server (scoperta di altri server, §9).</summary>
|
||||
/// <summary>Peers announced by the server (discovery of other servers, §9).</summary>
|
||||
public static async Task<IReadOnlyList<PeerInfo>> GetPeersAsync(this ElectrumClient c,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -121,9 +121,9 @@ public static class ElectrumApi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsa la risposta di server.peers.subscribe: lista di
|
||||
/// [ip, hostname, ["v1.4.2", "pN", "tPORTA", "sPORTA", ...]];
|
||||
/// "t"/"s" senza numero = porta di default della rete (risolta dal chiamante).
|
||||
/// Parses the server.peers.subscribe response: a list of
|
||||
/// [ip, hostname, ["v1.4.2", "pN", "tPORT", "sPORT", ...]];
|
||||
/// "t"/"s" without a number = network default port (resolved by the caller).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<PeerInfo> ParsePeers(JsonElement response)
|
||||
{
|
||||
|
||||
@@ -9,9 +9,9 @@ using System.Threading.Channels;
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Client del protocollo del server di indicizzazione (blueprint §10):
|
||||
/// JSON-RPC 2.0 newline-delimited su TCP, opzionalmente TLS con pinning TOFU.
|
||||
/// Le notifiche (subscription) arrivano sull'evento <see cref="NotificationReceived"/>.
|
||||
/// Client for the indexing server protocol (blueprint §10):
|
||||
/// newline-delimited JSON-RPC 2.0 over TCP, optionally TLS with TOFU pinning.
|
||||
/// Notifications (subscriptions) arrive on the <see cref="NotificationReceived"/> event.
|
||||
/// </summary>
|
||||
public sealed class ElectrumClient : IAsyncDisposable
|
||||
{
|
||||
@@ -25,17 +25,17 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
private readonly Task _readLoop;
|
||||
private readonly Task _writeLoop;
|
||||
|
||||
// Channel single-reader: le task scrivono i payload senza lock;
|
||||
// il write loop drena tutto in un unico WriteAsync+FlushAsync —
|
||||
// identico al buffered writer asyncio di Electrum.
|
||||
// Single-reader channel: tasks write the payloads without a lock;
|
||||
// the write loop drains everything in a single WriteAsync+FlushAsync —
|
||||
// identical to Electrum's asyncio buffered writer.
|
||||
private readonly Channel<byte[]> _outgoing = Channel.CreateUnbounded<byte[]>(
|
||||
new UnboundedChannelOptions { SingleReader = true, AllowSynchronousContinuations = false });
|
||||
|
||||
// Tetto alle richieste in volo (non in coda di scrittura, ma in attesa di
|
||||
// risposta sul server). Il write-loop batcha già l'invio in un solo segmento;
|
||||
// questo gate evita di sommergere il server con migliaia di richieste
|
||||
// simultanee su wallet grandi → niente -101/-102 a raffica né drop della
|
||||
// connessione. Le scritture restano comunque pipelinate fino a questo grado.
|
||||
// Cap on in-flight requests (not in the write queue, but awaiting a
|
||||
// response from the server). The write loop already batches the send into a
|
||||
// single segment; this gate avoids flooding the server with thousands of
|
||||
// simultaneous requests on large wallets → no bursts of -101/-102 nor
|
||||
// connection drops. Writes still stay pipelined up to this degree.
|
||||
private const int MaxInFlight = 32;
|
||||
private readonly SemaphoreSlim _inFlight = new(MaxInFlight, MaxInFlight);
|
||||
|
||||
@@ -104,8 +104,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
public async Task<JsonElement> RequestAsync(string method, CancellationToken ct = default,
|
||||
params object?[] parameters)
|
||||
{
|
||||
// Gate prima di mettere in volo: oltre MaxInFlight richieste in attesa
|
||||
// si attende che una risposta liberi uno slot, invece di sommergere il server.
|
||||
// Gate before going in-flight: beyond MaxInFlight pending requests
|
||||
// we wait for a response to free a slot, instead of flooding the server.
|
||||
await _inFlight.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
@@ -137,9 +137,9 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drain loop: svuota il channel in un unico buffer → un solo WriteAsync+FlushAsync
|
||||
/// per tutti i messaggi in coda. Quando N richieste sono in coda, vengono
|
||||
/// trasmesse in un singolo segmento TCP invece di N flush seriali.
|
||||
/// Drain loop: empties the channel into a single buffer → one WriteAsync+FlushAsync
|
||||
/// for all queued messages. When N requests are queued, they are
|
||||
/// transmitted in a single TCP segment instead of N serial flushes.
|
||||
/// </summary>
|
||||
private async Task WriteLoopAsync()
|
||||
{
|
||||
@@ -166,8 +166,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read loop con PipeReader: buffer pooled, zero allocazioni per-risposta
|
||||
/// (nessuna stringa intermedia), parsing JSON direttamente da byte span.
|
||||
/// Read loop with PipeReader: pooled buffers, zero per-response allocations
|
||||
/// (no intermediate string), JSON parsing directly from a byte span.
|
||||
/// </summary>
|
||||
private async Task ReadLoopAsync()
|
||||
{
|
||||
@@ -190,8 +190,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
finally
|
||||
{
|
||||
// consumed = tutto ciò che abbiamo consumato (fino all'ultimo \n)
|
||||
// examined = tutto ciò che abbiamo guardato (fino alla fine del buffer)
|
||||
// consumed = everything we have consumed (up to the last \n)
|
||||
// examined = everything we have looked at (up to the end of the buffer)
|
||||
pipe.AdvanceTo(buffer.Start, buffer.End);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
{
|
||||
await pipe.CompleteAsync();
|
||||
foreach (var (_, tcs) in _pending)
|
||||
tcs.TrySetException(failure ?? new IOException("Connessione al server chiusa."));
|
||||
tcs.TrySetException(failure ?? new IOException("Connection to the server closed."));
|
||||
_pending.Clear();
|
||||
Disconnected?.Invoke(failure);
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
DispatchSpan(line.FirstSpan);
|
||||
return;
|
||||
}
|
||||
// Multi-segmento (risposta molto lunga): copia su ArrayPool poi parsa.
|
||||
// Multi-segment (very long response): copy to ArrayPool then parse.
|
||||
var len = (int)line.Length;
|
||||
var buf = ArrayPool<byte>.Shared.Rent(len);
|
||||
try
|
||||
@@ -242,8 +242,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
|
||||
private void DispatchSpan(ReadOnlySpan<byte> utf8)
|
||||
{
|
||||
// JsonDocument.Parse via Utf8JsonReader: nessuna stringa intermedia,
|
||||
// parsing direttamente dallo span pooled.
|
||||
// JsonDocument.Parse via Utf8JsonReader: no intermediate string,
|
||||
// parsing directly from the pooled span.
|
||||
var reader = new Utf8JsonReader(utf8);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
@@ -276,12 +276,12 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Errore restituito dal server (campo "error" della risposta JSON-RPC).</summary>
|
||||
/// <summary>Error returned by the server ("error" field of the JSON-RPC response).</summary>
|
||||
public sealed class ElectrumServerException(string error) : Exception(error);
|
||||
|
||||
/// <summary>
|
||||
/// Il certificato del server è cambiato rispetto a quello salvato (TOFU, §9).
|
||||
/// Si sblocca con il reset esplicito dei certificati.
|
||||
/// The server certificate changed with respect to the saved one (TOFU, §9).
|
||||
/// It is unlocked with an explicit reset of the certificates.
|
||||
/// </summary>
|
||||
public sealed class CertificatePinMismatchException(string host, int port) : Exception(
|
||||
$"Il certificato TLS di {host}:{port} è cambiato rispetto a quello salvato. " +
|
||||
|
||||
@@ -3,7 +3,7 @@ using PalladiumWallet.Core.Chain;
|
||||
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>Server di indicizzazione noto (bootstrap o scoperto dai peer).</summary>
|
||||
/// <summary>Known indexing server (bootstrap or discovered from peers).</summary>
|
||||
public sealed record KnownServer(string Host, int TcpPort, int SslPort, string? Version = null)
|
||||
{
|
||||
public int PortFor(bool useSsl) => useSsl ? SslPort : TcpPort;
|
||||
@@ -13,9 +13,9 @@ public sealed record KnownServer(string Host, int TcpPort, int SslPort, string?
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registro dei server (blueprint §9): parte dai bootstrap del profilo (§3),
|
||||
/// si arricchisce con i peer annunciati via server.peers.subscribe e persiste
|
||||
/// le scoperte su file per le sessioni successive.
|
||||
/// Server registry (blueprint §9): starts from the profile bootstrap list (§3),
|
||||
/// is enriched with peers announced via server.peers.subscribe, and persists
|
||||
/// discovered servers to file for subsequent sessions.
|
||||
/// </summary>
|
||||
public sealed class ServerRegistry
|
||||
{
|
||||
@@ -42,7 +42,7 @@ public sealed class ServerRegistry
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// File corrotto: si riparte dai soli bootstrap.
|
||||
// Corrupted file: fall back to bootstrap servers only.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,15 +52,15 @@ public sealed class ServerRegistry
|
||||
get { lock (_lock) return [.. _servers]; }
|
||||
}
|
||||
|
||||
/// <summary>Primo server noto: default quando l'utente non ne indica uno.</summary>
|
||||
/// <summary>First known server: default when the user does not specify one.</summary>
|
||||
public KnownServer? Default
|
||||
{
|
||||
get { lock (_lock) return _servers.FirstOrDefault(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chiede al server connesso i peer che annuncia e integra i nuovi nel
|
||||
/// registro (porte mancanti → default del profilo). Ritorna quanti sono nuovi.
|
||||
/// Queries the connected server for its announced peers and adds new ones to the
|
||||
/// registry (missing ports fall back to the profile defaults). Returns the count of new entries.
|
||||
/// </summary>
|
||||
public async Task<int> DiscoverAsync(ElectrumClient client, CancellationToken ct = default)
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public sealed class ServerRegistry
|
||||
private void Save()
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
||||
// Si salvano solo gli scoperti: i bootstrap vengono dal profilo.
|
||||
// Only discovered servers are saved; bootstrap servers come from the profile.
|
||||
var discovered = _servers
|
||||
.Where(s => !_profile.BootstrapServers.Any(b =>
|
||||
string.Equals(b.Host, s.Host, StringComparison.OrdinalIgnoreCase)))
|
||||
|
||||
@@ -5,8 +5,8 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>
|
||||
/// Header di blocco parsato dagli 80 byte canonici (blueprint §7.2):
|
||||
/// versione, prev_hash, merkle_root, timestamp, bits, nonce.
|
||||
/// Block header parsed from the canonical 80 bytes (blueprint §7.2):
|
||||
/// version, prev_hash, merkle_root, timestamp, bits, nonce.
|
||||
/// </summary>
|
||||
public sealed record BlockHeaderInfo(
|
||||
int Version, uint256 PrevHash, uint256 MerkleRoot, uint Timestamp, uint Bits, uint Nonce, uint256 Hash)
|
||||
@@ -18,7 +18,7 @@ public sealed record BlockHeaderInfo(
|
||||
public static BlockHeaderInfo Parse(byte[] raw)
|
||||
{
|
||||
if (raw.Length != Size)
|
||||
throw new ArgumentException($"Header di {raw.Length} byte (attesi {Size}).", nameof(raw));
|
||||
throw new ArgumentException($"Header is {raw.Length} bytes (expected {Size}).", nameof(raw));
|
||||
|
||||
return new BlockHeaderInfo(
|
||||
Version: BitConverter.ToInt32(raw, 0),
|
||||
@@ -31,11 +31,10 @@ public sealed record BlockHeaderInfo(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validazione SPV dell'header (§7.2): collegamento al precedente e,
|
||||
/// se il profilo non impone lo skip (catena LWMA), verifica PoW
|
||||
/// hash <= target dai bits. Il controllo di coerenza dei bits col
|
||||
/// retargeting completo richiede la storia: per la catena di riferimento
|
||||
/// è sostituito dai checkpoint (§7.3).
|
||||
/// SPV header validation (§7.2): linkage to the previous header and,
|
||||
/// if the profile does not mandate skip (LWMA chain), PoW verification
|
||||
/// hash <= target from bits. Full bits/retargeting consistency requires
|
||||
/// history — for this chain it is replaced by checkpoints (§7.3).
|
||||
/// </summary>
|
||||
public bool IsValidChild(uint256 expectedPrevHash, ChainProfile profile)
|
||||
{
|
||||
@@ -47,7 +46,7 @@ public sealed record BlockHeaderInfo(
|
||||
return Hash <= target;
|
||||
}
|
||||
|
||||
/// <summary>Confronto con un checkpoint hardcoded (§7.3).</summary>
|
||||
/// <summary>Comparison against a hardcoded checkpoint (§7.3).</summary>
|
||||
public bool MatchesCheckpoint(Checkpoint checkpoint) =>
|
||||
Hash == uint256.Parse(checkpoint.BlockHash);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ using NBitcoin;
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>
|
||||
/// Scripthash del protocollo di indicizzazione (blueprint §0/§10): SHA-256
|
||||
/// dello scriptPubKey con i byte in ordine inverso, esadecimale.
|
||||
/// Script hash for the indexing protocol (blueprint §0/§10): SHA-256 of the
|
||||
/// scriptPubKey with bytes reversed, encoded as lowercase hex.
|
||||
/// </summary>
|
||||
public static class Scripthash
|
||||
{
|
||||
|
||||
@@ -8,14 +8,14 @@ using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>Indirizzo derivato e tracciato durante la sincronizzazione.</summary>
|
||||
/// <summary>Address derived and tracked during synchronisation.</summary>
|
||||
public sealed record TrackedAddress(
|
||||
BitcoinAddress Address, string ScriptHash, bool IsChange, int Index)
|
||||
{
|
||||
public Script ScriptPubKey => Address.ScriptPubKey;
|
||||
}
|
||||
|
||||
/// <summary>Esito di una passata di sincronizzazione.</summary>
|
||||
/// <summary>Result of a synchronisation pass.</summary>
|
||||
public sealed class SyncResult
|
||||
{
|
||||
public required int TipHeight { get; init; }
|
||||
@@ -31,26 +31,26 @@ public sealed class SyncResult
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sincronizzazione del wallet (blueprint §7.4).
|
||||
/// Wallet synchronisation (blueprint §7.4).
|
||||
/// </summary>
|
||||
public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient client, int gapLimit = 20)
|
||||
{
|
||||
/// <summary>Avanzamento leggibile (per CLI e barra di stato GUI).</summary>
|
||||
/// <summary>Human-readable progress (for CLI and GUI status bar).</summary>
|
||||
public event Action<string>? Progress;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Transaction> _txCache = new();
|
||||
private readonly Dictionary<string, int> _verifiedAtHeight = [];
|
||||
private readonly ConcurrentDictionary<int, Task<string>> _headerFetches = new();
|
||||
|
||||
// Indici noti dal sync precedente: usati da ScanChainAsync per la discovery
|
||||
// incrementale — gli indirizzi già usati vengono fetchati in un unico burst
|
||||
// invece di batches sequenziali, riducendo i round-trip da O(used/gapLimit) a O(1).
|
||||
// Indices known from the previous sync: used by ScanChainAsync for incremental
|
||||
// discovery — already-used addresses are fetched in a single burst instead of
|
||||
// sequential batches, reducing round-trips from O(used/gapLimit) to O(1).
|
||||
private int _knownReceiveIndex;
|
||||
private int _knownChangeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-popola le cache interne da dati salvati su disco.
|
||||
/// Chiamare prima di SyncOnceAsync per evitare di riscaricale le tx già note.
|
||||
/// Pre-populates internal caches from data saved on disk.
|
||||
/// Call before SyncOnceAsync to avoid re-downloading already known transactions.
|
||||
/// </summary>
|
||||
public void PreloadCaches(
|
||||
Dictionary<string, string> rawTxHex,
|
||||
@@ -73,9 +73,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esporta le cache correnti in forma serializzabile su disco.
|
||||
/// Solo le tx confermate (height > 0) vengono incluse: le non confermate
|
||||
/// possono cambiare (RBF) e vanno sempre riscaricate.
|
||||
/// Exports the current caches in a serialisable form for disk storage.
|
||||
/// Only confirmed transactions (height > 0) are included: unconfirmed ones
|
||||
/// may change (RBF) and must always be re-downloaded.
|
||||
/// </summary>
|
||||
public (Dictionary<string, string> RawTxHex,
|
||||
Dictionary<string, int> VerifiedAt,
|
||||
@@ -86,8 +86,8 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
.Where(_txCache.ContainsKey)
|
||||
.ToDictionary(txid => txid, txid => _txCache[txid].ToHex());
|
||||
|
||||
// Solo gli header già completati: Task<string> non ancora completate
|
||||
// non vengono persistite (verranno rifetchate al prossimo sync se necessario).
|
||||
// Only already-completed headers: in-progress Task<string> instances
|
||||
// are not persisted (they will be re-fetched on the next sync if needed).
|
||||
var headers = new Dictionary<int, string>();
|
||||
foreach (var (height, task) in _headerFetches)
|
||||
if (task.IsCompletedSuccessfully)
|
||||
@@ -99,9 +99,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
public async Task<SyncResult> SyncOnceAsync(CancellationToken ct = default)
|
||||
{
|
||||
var tip = await client.SubscribeHeadersAsync(ct);
|
||||
Progress?.Invoke($"tip della catena: {tip.Height}");
|
||||
Progress?.Invoke($"chain tip: {tip.Height}");
|
||||
|
||||
// 1-2. Scansione indirizzi.
|
||||
// 1-2. Address scanning.
|
||||
var tracked = new List<TrackedAddress>();
|
||||
var historyByAddress = new Dictionary<string, IReadOnlyList<HistoryItem>>();
|
||||
int nextReceive, nextChange;
|
||||
@@ -122,9 +122,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
else
|
||||
{
|
||||
// Receive e change chain in parallelo (indipendenti per definizione).
|
||||
// ScanChainAsync usa _knownReceiveIndex/_knownChangeIndex per la discovery
|
||||
// incrementale: gli indirizzi già usati vengono fetchati in un burst unico.
|
||||
// Receive and change chains in parallel (independent by definition).
|
||||
// ScanChainAsync uses _knownReceiveIndex/_knownChangeIndex for incremental
|
||||
// discovery: already-used addresses are fetched in a single burst.
|
||||
var receiveTask = ScanChainAsync(isChange: false, _knownReceiveIndex, ct);
|
||||
var changeTask = ScanChainAsync(isChange: true, _knownChangeIndex, ct);
|
||||
var rxScan = await receiveTask;
|
||||
@@ -145,12 +145,12 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
RetryOnBusyAsync(() => client.SubscribeScripthashAsync(t.ScriptHash, ct), ct)));
|
||||
}
|
||||
|
||||
// 3. Storico unico (txid → altezza massima riportata).
|
||||
// 3. Merged history (txid → highest reported height).
|
||||
var txHeights = new Dictionary<string, int>();
|
||||
foreach (var item in historyByAddress.Values.SelectMany(h => h))
|
||||
txHeights[item.TxHash] = item.Height;
|
||||
|
||||
// 4+5. Download tx mancanti e verifica Merkle in parallelo senza semaforo.
|
||||
// 4+5. Download missing transactions and verify Merkle proofs in parallel.
|
||||
var network = PalladiumNetworks.For(account.Profile.Kind);
|
||||
var missing = txHeights.Keys.Where(txid => !_txCache.ContainsKey(txid)).ToList();
|
||||
var toVerify = txHeights
|
||||
@@ -160,7 +160,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
|
||||
if (missing.Count > 0 || toVerify.Count > 0)
|
||||
{
|
||||
Progress?.Invoke($"scarico {missing.Count} tx, verifico {toVerify.Count} prove…");
|
||||
Progress?.Invoke($"downloading {missing.Count} txs, verifying {toVerify.Count} proofs…");
|
||||
var dlDone = 0;
|
||||
var merkDone = 0;
|
||||
|
||||
@@ -170,7 +170,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
_txCache[txid] = Transaction.Parse(raw, network);
|
||||
var n = Interlocked.Increment(ref dlDone);
|
||||
if (n % 50 == 0 || n == missing.Count)
|
||||
Progress?.Invoke($"tx {n}/{missing.Count}, prove {merkDone}/{toVerify.Count}…");
|
||||
Progress?.Invoke($"tx {n}/{missing.Count}, proofs {merkDone}/{toVerify.Count}…");
|
||||
}, ct));
|
||||
|
||||
var merkTasks = toVerify.Select(kv => RetryOnBusyAsync(async () =>
|
||||
@@ -185,10 +185,10 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
uint256.Parse(txid), proof.Pos,
|
||||
proof.Merkle.Select(uint256.Parse), header.MerkleRoot))
|
||||
throw new SpvVerificationException(
|
||||
$"Prova di Merkle non valida per {txid} (blocco {height}): server non affidabile.");
|
||||
$"Invalid Merkle proof for {txid} (block {height}): server is not trustworthy.");
|
||||
var n = Interlocked.Increment(ref merkDone);
|
||||
if (n % 50 == 0 || n == toVerify.Count)
|
||||
Progress?.Invoke($"tx {dlDone}/{missing.Count}, prove {n}/{toVerify.Count}…");
|
||||
Progress?.Invoke($"tx {dlDone}/{missing.Count}, proofs {n}/{toVerify.Count}…");
|
||||
}, ct));
|
||||
|
||||
await Task.WhenAll(dlTasks.Concat(merkTasks));
|
||||
@@ -199,7 +199,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
var transactions = txHeights.Keys.ToDictionary(txid => txid, txid => _txCache[txid]);
|
||||
var verified = txHeights.ToDictionary(kv => kv.Key, kv => kv.Value > 0);
|
||||
|
||||
// 6. Ricostruzione locale degli UTXO.
|
||||
// 6. Local UTXO reconstruction.
|
||||
var byScript = tracked.ToDictionary(t => t.ScriptPubKey, t => t);
|
||||
var spent = transactions.Values
|
||||
.SelectMany(tx => tx.Inputs)
|
||||
@@ -229,7 +229,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Delta per voce di storico.
|
||||
// 7. Delta per history entry.
|
||||
var history = new List<CachedTx>();
|
||||
foreach (var (txid, tx) in transactions)
|
||||
{
|
||||
@@ -286,14 +286,14 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scansiona una catena (receiving o change).
|
||||
/// Scans one chain (receiving or change).
|
||||
///
|
||||
/// Phase 1 — indirizzi noti (0..fromIndex-1): tutti i GetHistoryAsync partono
|
||||
/// in un unico burst parallelo, senza batching sequenziale. Per un wallet con
|
||||
/// 100 indirizzi usati → 1 RTT invece di 5 round sequenziali di gapLimit.
|
||||
/// Phase 1 — known addresses (0..fromIndex-1): all GetHistoryAsync calls are
|
||||
/// fired in a single parallel burst, with no sequential batching. A wallet
|
||||
/// with 100 used addresses costs 1 RTT instead of 5 sequential gap-limit rounds.
|
||||
///
|
||||
/// Phase 2 — discovery dal fromIndex in poi: batching con gap limit come prima,
|
||||
/// necessario per sapere dove fermarsi.
|
||||
/// Phase 2 — discovery from fromIndex onwards: gap-limit batching as before,
|
||||
/// required to know when to stop.
|
||||
/// </summary>
|
||||
private async Task<(int NextIndex,
|
||||
List<TrackedAddress> Tracked,
|
||||
@@ -303,7 +303,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
var tracked = new List<TrackedAddress>();
|
||||
var history = new Dictionary<string, IReadOnlyList<HistoryItem>>();
|
||||
|
||||
// Phase 1: burst unico per tutti gli indirizzi già noti.
|
||||
// Phase 1: single burst for all already-known addresses.
|
||||
if (fromIndex > 0)
|
||||
{
|
||||
var known = Enumerable.Range(0, fromIndex).Select(i =>
|
||||
@@ -321,7 +321,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
history[known[i].ScriptHash] = knownHistories[i];
|
||||
}
|
||||
|
||||
// Phase 2: discovery gap-limit dal fromIndex in poi.
|
||||
// Phase 2: gap-limit discovery from fromIndex onwards.
|
||||
var consecutiveEmpty = 0;
|
||||
var index = fromIndex;
|
||||
var firstUnused = fromIndex;
|
||||
@@ -395,5 +395,5 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
ex.Message.Contains("excessive resource usage", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>La verifica SPV è fallita: i dati del server contraddicono le prove (§17).</summary>
|
||||
/// <summary>SPV verification failed: server data contradicts the proofs (§17).</summary>
|
||||
public sealed class SpvVerificationException(string message) : Exception(message);
|
||||
|
||||
@@ -3,16 +3,16 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Configurazione globale dell'applicazione (blueprint §8), separata dai file
|
||||
/// wallet: lingua, unità di visualizzazione, ecc. Persistita in config.json
|
||||
/// nella radice dei dati (vale per tutte le reti).
|
||||
/// Global application configuration (blueprint §8), separate from wallet
|
||||
/// files: language, display unit, etc. Persisted in config.json
|
||||
/// in the data root (applies to all networks).
|
||||
/// </summary>
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>Codice lingua UI.</summary>
|
||||
/// <summary>UI language code.</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
/// <summary>Unità di visualizzazione degli importi (vedi <see cref="Wallet.CoinAmount.Units"/>).</summary>
|
||||
/// <summary>Display unit for amounts (see <see cref="Wallet.CoinAmount.Units"/>).</summary>
|
||||
public string Unit { get; set; } = "PLM";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
@@ -28,7 +28,7 @@ public sealed class AppConfig
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Config corrotta: si riparte dai default senza bloccare l'avvio.
|
||||
// Corrupted config: fall back to defaults without blocking startup.
|
||||
return new AppConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,29 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Percorsi dati per piattaforma (blueprint §8). La radice dati può essere:
|
||||
/// 1. <b>portable</b>: cartella "palladium-data" accanto all'eseguibile;
|
||||
/// 2. <b>personalizzata</b>: scelta dall'utente al primo avvio e memorizzata in
|
||||
/// un piccolo file "puntatore" in una posizione di bootstrap fissa;
|
||||
/// 3. <b>legacy</b>: vecchia posizione (%APPDATA%/PalladiumWallet) se contiene già dati;
|
||||
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) o %ProgramFiles%\PalladiumWallet (Windows).
|
||||
/// Sotto la radice c'è una sottocartella per rete (config, wallet, header, certificati).
|
||||
/// Per-platform data paths (blueprint §8). The data root can be:
|
||||
/// 1. <b>portable</b>: "palladium-data" folder next to the executable;
|
||||
/// 2. <b>custom</b>: chosen by the user at first launch and stored in
|
||||
/// a small "pointer" file at a fixed bootstrap location;
|
||||
/// 3. <b>legacy</b>: old location (%APPDATA%/PalladiumWallet) if it already holds data;
|
||||
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) or %ProgramFiles%\PalladiumWallet (Windows).
|
||||
/// Under the root there is a per-network subfolder (config, wallet, header, certificates).
|
||||
/// </summary>
|
||||
public static class AppPaths
|
||||
{
|
||||
public const string PortableDirName = "palladium-data";
|
||||
|
||||
/// <summary>Nome cartella applicazione, usato nei vari percorsi.</summary>
|
||||
/// <summary>Application folder name, used in the various paths.</summary>
|
||||
public const string AppDirName = "PalladiumWallet";
|
||||
|
||||
/// <summary>Override esplicito della radice dati (es. CLI --data-dir). Ha priorità su tutto.</summary>
|
||||
/// <summary>Explicit override of the data root (e.g. CLI --data-dir). Takes priority over everything.</summary>
|
||||
public static string? OverrideDataRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Radice dati predefinita, secondo la convenzione di ogni piattaforma:
|
||||
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, come Electrum/Bitcoin);
|
||||
/// Linux/macOS → ~/.palladium-wallet (dotfolder minuscolo, come ~/.bitcoin).
|
||||
/// Per-utente e sempre scrivibile, senza privilegi di amministratore.
|
||||
/// Default data root, following each platform's convention:
|
||||
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, like Electrum/Bitcoin);
|
||||
/// Linux/macOS → ~/.palladium-wallet (lowercase dotfolder, like ~/.bitcoin).
|
||||
/// Per-user and always writable, without administrator privileges.
|
||||
/// </summary>
|
||||
public static string DefaultDataRoot()
|
||||
{
|
||||
@@ -37,8 +37,8 @@ public static class AppPaths
|
||||
return Path.Combine(home, ".palladium-wallet");
|
||||
}
|
||||
|
||||
/// <summary>File puntatore alla radice dati scelta dall'utente. Vive in una
|
||||
/// posizione di bootstrap sempre scrivibile e indipendente dalla radice dati.</summary>
|
||||
/// <summary>Pointer file to the user-chosen data root. Lives at a
|
||||
/// bootstrap location that is always writable and independent of the data root.</summary>
|
||||
private static string LocationPointerPath() =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
@@ -50,7 +50,7 @@ public static class AppPaths
|
||||
private static bool HasData(string root) =>
|
||||
Directory.Exists(root) && Directory.EnumerateFileSystemEntries(root).Any();
|
||||
|
||||
/// <summary>Radice dati effettiva, secondo l'ordine di precedenza documentato in classe.</summary>
|
||||
/// <summary>Effective data root, following the precedence order documented on the class.</summary>
|
||||
public static string DataRoot()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OverrideDataRoot))
|
||||
@@ -67,9 +67,9 @@ public static class AppPaths
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true se la posizione dei dati è già determinata e non serve chiederla
|
||||
/// all'utente: modalità portable, override, puntatore già scritto, oppure
|
||||
/// dati già presenti nella posizione predefinita.
|
||||
/// true if the data location is already determined and need not be asked
|
||||
/// of the user: portable mode, override, pointer already written, or
|
||||
/// data already present at the default location.
|
||||
/// </summary>
|
||||
public static bool IsDataLocationConfigured() =>
|
||||
!string.IsNullOrEmpty(OverrideDataRoot)
|
||||
@@ -77,7 +77,7 @@ public static class AppPaths
|
||||
|| ReadPointer() is not null
|
||||
|| HasData(DefaultDataRoot());
|
||||
|
||||
/// <summary>Memorizza la radice dati scelta dall'utente e la crea su disco.</summary>
|
||||
/// <summary>Stores the user-chosen data root and creates it on disk.</summary>
|
||||
public static void ConfigureDataLocation(string root)
|
||||
{
|
||||
root = Path.GetFullPath(root.Trim());
|
||||
@@ -96,7 +96,7 @@ public static class AppPaths
|
||||
return string.IsNullOrEmpty(path) ? null : path;
|
||||
}
|
||||
|
||||
/// <summary>Cartella dati della rete (config, wallet, header, certificati).</summary>
|
||||
/// <summary>Network data folder (config, wallet, header, certificates).</summary>
|
||||
public static string ForNetwork(NetKind net)
|
||||
{
|
||||
var dir = Path.Combine(DataRoot(), ChainProfiles.For(net).NetName);
|
||||
@@ -114,7 +114,7 @@ public static class AppPaths
|
||||
public static string DefaultWalletPath(NetKind net) =>
|
||||
Path.Combine(WalletsDir(net), "default.wallet.json");
|
||||
|
||||
/// <summary>Tutti i file wallet della rete, ordinati per nome (multi-wallet §8).</summary>
|
||||
/// <summary>All wallet files for the network, ordered by name (multi-wallet §8).</summary>
|
||||
public static IReadOnlyList<string> WalletFiles(NetKind net)
|
||||
{
|
||||
var dir = WalletsDir(net);
|
||||
|
||||
@@ -5,9 +5,9 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Cifratura del file wallet (blueprint §8/§17): AES-256-GCM con chiave derivata
|
||||
/// dalla password via PBKDF2-HMAC-SHA512. Il contenitore è JSON autodescrittivo
|
||||
/// (kdf, parametri, salt, nonce) per consentire upgrade futuri dei parametri.
|
||||
/// Wallet file encryption (blueprint §8/§17): AES-256-GCM with a key derived
|
||||
/// from the password via PBKDF2-HMAC-SHA512. The container is self-describing JSON
|
||||
/// (kdf, parameters, salt, nonce) to allow future parameter upgrades.
|
||||
/// </summary>
|
||||
public static class EncryptedFile
|
||||
{
|
||||
@@ -54,7 +54,7 @@ public static class EncryptedFile
|
||||
Convert.ToBase64String(tag), Convert.ToBase64String(cipher)));
|
||||
}
|
||||
|
||||
/// <summary>Lancia <see cref="WrongPasswordException"/> se la password è errata o il file è manomesso.</summary>
|
||||
/// <summary>Throws <see cref="WrongPasswordException"/> if the password is wrong or the file is tampered with.</summary>
|
||||
public static string Decrypt(string fileContent, string password)
|
||||
{
|
||||
var container = JsonSerializer.Deserialize<Container>(fileContent)
|
||||
@@ -74,7 +74,7 @@ public static class EncryptedFile
|
||||
}
|
||||
catch (AuthenticationTagMismatchException)
|
||||
{
|
||||
// Il tag GCM autentica: password errata e manomissione sono indistinguibili.
|
||||
// The GCM tag authenticates: a wrong password and tampering are indistinguishable.
|
||||
throw new WrongPasswordException();
|
||||
}
|
||||
finally
|
||||
@@ -88,7 +88,7 @@ public static class EncryptedFile
|
||||
Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA512, KeySize);
|
||||
}
|
||||
|
||||
/// <summary>Password errata (o file wallet manomesso: il tag GCM non distingue i due casi).</summary>
|
||||
/// <summary>Wrong password (or tampered wallet file: the GCM tag does not distinguish the two cases).</summary>
|
||||
public sealed class WrongPasswordException : Exception
|
||||
{
|
||||
public WrongPasswordException() : base("Password errata o file wallet danneggiato.") { }
|
||||
|
||||
@@ -4,9 +4,9 @@ using System.Text.Json.Serialization;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Schema del file wallet (blueprint §8), versionato per consentire migrazioni
|
||||
/// automatiche all'apertura. La cifratura (quando c'è una password) avvolge
|
||||
/// l'intero documento via <see cref="EncryptedFile"/>.
|
||||
/// 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
|
||||
{
|
||||
@@ -14,42 +14,42 @@ public sealed class WalletDocument
|
||||
|
||||
public int Version { get; set; } = CurrentVersion;
|
||||
|
||||
/// <summary>Nome rete (mainnet/testnet/regtest), come ChainProfile.NetName.</summary>
|
||||
/// <summary>Network name (mainnet/testnet/regtest), as ChainProfile.NetName.</summary>
|
||||
public required string Network { get; set; }
|
||||
|
||||
/// <summary>Tipo di script (nome dell'enum ScriptKind).</summary>
|
||||
/// <summary>Script type (name of the ScriptKind enum).</summary>
|
||||
public required string ScriptKind { get; set; }
|
||||
|
||||
/// <summary>Mnemonica BIP39 in chiaro nel documento (il documento va cifrato!); null se watch-only.</summary>
|
||||
/// <summary>BIP39 mnemonic in plaintext in the document (the document must be encrypted!); null if watch-only.</summary>
|
||||
public string? Mnemonic { get; set; }
|
||||
|
||||
/// <summary>Extension word BIP39 (§4.1); null se assente.</summary>
|
||||
/// <summary>BIP39 extension word (§4.1); null if absent.</summary>
|
||||
public string? Passphrase { get; set; }
|
||||
|
||||
/// <summary>Path di account (es. "84'/746'/0'"); null per importati WIF.</summary>
|
||||
/// <summary>Account path (e.g. "84'/746'/0'"); null for imported WIF.</summary>
|
||||
public string? AccountPath { get; set; }
|
||||
|
||||
/// <summary>Xpub di account in SLIP-132 per i wallet HD; null per importati WIF.</summary>
|
||||
/// <summary>Account xpub in SLIP-132 for HD wallets; null for imported WIF.</summary>
|
||||
public string? AccountXpub { get; set; }
|
||||
|
||||
/// <summary>Xprv di account in SLIP-132; presente solo per import da xprv senza seed.</summary>
|
||||
/// <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>Chiavi WIF importate (in chiaro nel documento — va cifrato!).</summary>
|
||||
/// <summary>Imported WIF keys (in plaintext in the document — must be encrypted!).</summary>
|
||||
public List<string>? WifKeys { get; set; }
|
||||
|
||||
/// <summary>Gap limit per la scansione indirizzi (§5), configurabile.</summary>
|
||||
/// <summary>Gap limit for address scanning (§5), configurable.</summary>
|
||||
public int GapLimit { get; set; } = 20;
|
||||
|
||||
/// <summary>Etichette per indirizzo/txid (§12).</summary>
|
||||
/// <summary>Labels by address/txid (§12).</summary>
|
||||
public Dictionary<string, string> Labels { get; set; } = [];
|
||||
|
||||
/// <summary>Rubrica contatti (nome + indirizzo blockchain).</summary>
|
||||
/// <summary>Contact address book (name + blockchain address).</summary>
|
||||
public List<StoredContact> Contacts { get; set; } = [];
|
||||
|
||||
/// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary>
|
||||
/// <summary>Cache of the last synced state (balance/history viewable offline).</summary>
|
||||
public SyncCache? Cache { get; set; }
|
||||
|
||||
public bool IsWatchOnly =>
|
||||
@@ -69,6 +69,7 @@ public sealed class WalletDocument
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<WalletDocument>(json, JsonOptions)
|
||||
?? throw new InvalidDataException("File wallet non valido.");
|
||||
// string literal above intentionally left untranslated
|
||||
return Migrate(doc);
|
||||
}
|
||||
|
||||
@@ -101,28 +102,28 @@ public sealed class SyncCache
|
||||
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.
|
||||
/// 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>
|
||||
/// Prove di Merkle già verificate (txid → altezza blocco). Evita di
|
||||
/// riverificare le stesse prove ad ogni avvio: le conferme sono immutabili.
|
||||
/// 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>
|
||||
/// Header grezzi per altezza (altezza → hex). Immutabili: non vengono mai
|
||||
/// rifetchati una volta salvati. Elimina i GetBlockHeader sulle prove Merkle
|
||||
/// già verificate nei sync successivi.
|
||||
/// 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>Indirizzo scansionato con saldo proprio e numero di transazioni (vista indirizzi).</summary>
|
||||
/// <summary>Scanned address with its own balance and transaction count (address view).</summary>
|
||||
public sealed class CachedAddress
|
||||
{
|
||||
public required string Address { get; set; }
|
||||
|
||||
@@ -3,17 +3,17 @@ using System.Globalization;
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>
|
||||
/// Conversione satoshi ↔ unità coin (8 decimali) per visualizzazione e input.
|
||||
/// Si lavora sempre in satoshi internamente; la stringa è solo presentazione.
|
||||
/// Satoshi ↔ coin unit conversion (8 decimal places) for display and input.
|
||||
/// All internal computation uses satoshis; the formatted string is presentation only.
|
||||
/// </summary>
|
||||
public static class CoinAmount
|
||||
{
|
||||
public const long SatsPerCoin = 100_000_000;
|
||||
|
||||
/// <summary>Unità di visualizzazione selezionabili (config §8).</summary>
|
||||
/// <summary>Selectable display units (config §8).</summary>
|
||||
public static readonly string[] Units = ["PLM", "mPLM", "µPLM", "sat"];
|
||||
|
||||
/// <summary>(satoshi per unità, decimali mostrati) di ciascuna unità.</summary>
|
||||
/// <summary>(satoshis per unit, displayed decimals) for each unit.</summary>
|
||||
private static (long Factor, int Decimals) Of(string unit) => unit switch
|
||||
{
|
||||
"PLM" => (SatsPerCoin, 8),
|
||||
@@ -27,7 +27,7 @@ public static class CoinAmount
|
||||
(sats / (decimal)SatsPerCoin).ToString("0.00000000", CultureInfo.InvariantCulture)
|
||||
+ (unit.Length > 0 ? " " + unit : "");
|
||||
|
||||
/// <summary>Formatta nell'unità scelta (es. 150000 sat → "1.50000 mPLM").</summary>
|
||||
/// <summary>Formats in the chosen unit (e.g. 150000 sat → "1.50000 mPLM").</summary>
|
||||
public static string FormatIn(long sats, string unit, bool withLabel = true)
|
||||
{
|
||||
var (factor, decimals) = Of(unit);
|
||||
@@ -36,7 +36,7 @@ public static class CoinAmount
|
||||
return withLabel ? $"{value} {unit}" : value;
|
||||
}
|
||||
|
||||
/// <summary>Parsa un importo espresso nell'unità scelta in satoshi.</summary>
|
||||
/// <summary>Parses an amount expressed in the chosen unit into satoshis.</summary>
|
||||
public static bool TryParseIn(string text, string unit, out long sats)
|
||||
{
|
||||
sats = 0;
|
||||
@@ -59,7 +59,7 @@ public static class CoinAmount
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parsa un importo in coin (punto o virgola decimale) in satoshi.</summary>
|
||||
/// <summary>Parses a coin amount (decimal point or comma) into satoshis.</summary>
|
||||
public static bool TryParseCoins(string text, out long sats)
|
||||
{
|
||||
sats = 0;
|
||||
|
||||
@@ -6,7 +6,7 @@ using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>Esito della costruzione di una transazione.</summary>
|
||||
/// <summary>Result of a transaction build operation.</summary>
|
||||
public sealed class BuiltTransaction
|
||||
{
|
||||
public required Transaction Transaction { get; init; }
|
||||
@@ -14,7 +14,7 @@ public sealed class BuiltTransaction
|
||||
public required FeeRate FeeRate { get; init; }
|
||||
public required bool Signed { get; init; }
|
||||
|
||||
/// <summary>PSBT per i flussi watch-only/air-gapped/multisig (§6.5).</summary>
|
||||
/// <summary>PSBT for watch-only/air-gapped/multisig flows (§6.5).</summary>
|
||||
public required PSBT Psbt { get; init; }
|
||||
|
||||
public string ToHex() => Transaction.ToHex();
|
||||
@@ -22,25 +22,25 @@ public sealed class BuiltTransaction
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Costruzione e firma delle transazioni (blueprint §6) sopra le primitive
|
||||
/// NBitcoin: selezione monete (manuale o automatica), fee a rate fisso,
|
||||
/// invia-tutto con fee sottratta, change sulla catena interna, RBF di default.
|
||||
/// Con un account watch-only produce la PSBT non firmata (§6.5).
|
||||
/// Transaction construction and signing (blueprint §6) on top of NBitcoin primitives:
|
||||
/// coin selection (manual or automatic), fixed fee rate, send-all with fee subtracted,
|
||||
/// change on the internal chain, RBF on by default.
|
||||
/// With a watch-only account produces an unsigned PSBT (§6.5).
|
||||
/// </summary>
|
||||
public sealed class TransactionFactory(IWalletAccount account)
|
||||
{
|
||||
private Network Network => PalladiumNetworks.For(account.Profile.Kind);
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce (e se possibile firma) una transazione.
|
||||
/// Builds (and signs if possible) a transaction.
|
||||
/// </summary>
|
||||
/// <param name="utxos">UTXO selezionati (coin control §6.2) o tutti quelli spendibili.</param>
|
||||
/// <param name="transactions">Tx di provenienza degli UTXO (txid → tx), dalla sincronizzazione.</param>
|
||||
/// <param name="destination">Indirizzo destinatario.</param>
|
||||
/// <param name="amountSats">Importo; ignorato se <paramref name="sendAll"/>.</param>
|
||||
/// <param name="feeRateSatPerVByte">Fee rate fisso in sat/vByte (§6.4).</param>
|
||||
/// <param name="changeIndex">Indice del prossimo indirizzo di change (catena interna).</param>
|
||||
/// <param name="sendAll">Invia tutto: fee sottratta dall'importo (§6.1).</param>
|
||||
/// <param name="utxos">Selected UTXOs (coin control §6.2) or all spendable ones.</param>
|
||||
/// <param name="transactions">Source transactions for the UTXOs (txid → tx), from sync.</param>
|
||||
/// <param name="destination">Recipient address.</param>
|
||||
/// <param name="amountSats">Amount; ignored when <paramref name="sendAll"/> is true.</param>
|
||||
/// <param name="feeRateSatPerVByte">Fixed fee rate in sat/vByte (§6.4).</param>
|
||||
/// <param name="changeIndex">Index of the next change address (internal chain).</param>
|
||||
/// <param name="sendAll">Send all: fee subtracted from the amount (§6.1).</param>
|
||||
public BuiltTransaction Build(
|
||||
IReadOnlyList<CachedUtxo> utxos,
|
||||
IReadOnlyDictionary<string, Transaction> transactions,
|
||||
@@ -50,15 +50,15 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
int changeIndex,
|
||||
bool sendAll = false)
|
||||
{
|
||||
// Si spendono solo UTXO confermati: i fondi in mempool si vedono nel
|
||||
// saldo "in attesa" ma non sono spendibili finché non confermano.
|
||||
// Only confirmed UTXOs are spent: mempool funds appear in the "pending"
|
||||
// balance but are not spendable until confirmed.
|
||||
var spendable = utxos.Where(u => !u.Frozen && u.Height > 0).ToList();
|
||||
if (spendable.Count == 0)
|
||||
{
|
||||
var pending = utxos.Where(u => !u.Frozen && u.Height <= 0).Sum(u => u.ValueSats);
|
||||
throw new WalletSpendException(pending > 0
|
||||
? $"Nessun fondo confermato: {CoinAmount.Format(pending)} in attesa di conferma (non spendibile)."
|
||||
: "Nessun UTXO spendibile selezionato.");
|
||||
? $"No confirmed funds: {CoinAmount.Format(pending)} pending confirmation (not spendable)."
|
||||
: "No spendable UTXOs selected.");
|
||||
}
|
||||
|
||||
var coins = spendable.Select(u => new Coin(
|
||||
@@ -68,7 +68,7 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
var feeRate = new FeeRate(Money.Satoshis(feeRateSatPerVByte * 1000m), 1000);
|
||||
var builder = Network.CreateTransactionBuilder();
|
||||
builder.SetVersion(2);
|
||||
// Sequence RBF per consentire il bump della fee (§6.6).
|
||||
// RBF sequence to allow fee bumping (§6.6).
|
||||
builder.OptInRBF = true;
|
||||
builder.AddCoins(coins);
|
||||
builder.SetChange(account.GetChangeAddress(changeIndex));
|
||||
@@ -94,14 +94,14 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
}
|
||||
catch (NotEnoughFundsException ex)
|
||||
{
|
||||
throw new WalletSpendException($"Fondi insufficienti: {ex.Message}");
|
||||
throw new WalletSpendException($"Insufficient funds: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!account.IsWatchOnly)
|
||||
{
|
||||
if (!builder.Verify(tx, out TransactionPolicyError[] errors))
|
||||
throw new WalletSpendException(
|
||||
"Transazione non valida: " + string.Join("; ", errors.Select(e => e.ToString())));
|
||||
"Invalid transaction: " + string.Join("; ", errors.Select(e => e.ToString())));
|
||||
}
|
||||
|
||||
return new BuiltTransaction
|
||||
@@ -123,5 +123,5 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Errore di costruzione/firma della spesa (fondi, policy, parametri).</summary>
|
||||
/// <summary>Error during transaction construction/signing (funds, policy, parameters).</summary>
|
||||
public sealed class WalletSpendException(string message) : Exception(message);
|
||||
|
||||
@@ -4,36 +4,36 @@ using PalladiumWallet.Core.Spv;
|
||||
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>Un input di una transazione, con l'output speso risolto dal server.</summary>
|
||||
/// <summary>An input of a transaction, with the spent output resolved from the server.</summary>
|
||||
public sealed record TxInputInfo(
|
||||
string PrevTxid, uint PrevIndex, long? AmountSats, string? Address, bool IsMine, bool IsCoinbase);
|
||||
|
||||
/// <summary>Un output di una transazione.</summary>
|
||||
/// <summary>An output of a transaction.</summary>
|
||||
public sealed record TxOutputInfo(
|
||||
uint Index, long AmountSats, string? Address, string ScriptType, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// Dati completi di una transazione, assemblati interrogando il server: la tx
|
||||
/// grezza più gli output spesi dagli input (per ricavare importi, indirizzi e
|
||||
/// fee) e l'header del blocco (per la data). Tutto ciò che il protocollo
|
||||
/// ElectrumX-like (§10) permette di sapere su una transazione.
|
||||
/// Complete transaction data assembled by querying the server: the raw transaction
|
||||
/// plus the outputs spent by each input (to derive amounts, addresses, and fees)
|
||||
/// and the block header (for the timestamp). Everything the ElectrumX-like protocol
|
||||
/// (§10) can tell us about a transaction.
|
||||
/// </summary>
|
||||
public sealed class TransactionDetails
|
||||
{
|
||||
public required string Txid { get; init; }
|
||||
/// <summary>Altezza del blocco; ≤0 = ancora in mempool.</summary>
|
||||
/// <summary>Block height; ≤0 = still in mempool.</summary>
|
||||
public required int Height { get; init; }
|
||||
public required int Confirmations { get; init; }
|
||||
/// <summary>Effetto netto sul saldo del wallet (delta calcolato in sincronizzazione).</summary>
|
||||
/// <summary>Net effect on the wallet balance (delta computed during sync).</summary>
|
||||
public required long NetSats { get; init; }
|
||||
/// <summary>Fee della transazione; null se un input ha importo non risolvibile (es. coinbase).</summary>
|
||||
/// <summary>Transaction fee; null if any input amount cannot be resolved (e.g. coinbase).</summary>
|
||||
public required long? FeeSats { get; init; }
|
||||
public required int TotalSize { get; init; }
|
||||
public required int VirtualSize { get; init; }
|
||||
public required uint Version { get; init; }
|
||||
public required uint LockTime { get; init; }
|
||||
public required bool RbfSignaled { get; init; }
|
||||
/// <summary>Merkle proof verificata in sincronizzazione (§7.4).</summary>
|
||||
/// <summary>Merkle proof verified during sync (§7.4).</summary>
|
||||
public required bool Verified { get; init; }
|
||||
public required DateTimeOffset? BlockTime { get; init; }
|
||||
public required long TotalOutSats { get; init; }
|
||||
@@ -43,13 +43,13 @@ public sealed class TransactionDetails
|
||||
|
||||
public bool IsCoinbase => Inputs.Count > 0 && Inputs[0].IsCoinbase;
|
||||
public bool IsIncoming => NetSats >= 0;
|
||||
/// <summary>Importo verso destinatari esterni (output non nostri): l'importo "inviato".</summary>
|
||||
/// <summary>Amount sent to external recipients (outputs not ours): the "sent" amount.</summary>
|
||||
public long SentToOthersSats => Outputs.Where(o => !o.IsMine).Sum(o => o.AmountSats);
|
||||
public long ReceivedSats => Outputs.Where(o => o.IsMine).Sum(o => o.AmountSats);
|
||||
public double? FeeRateSatPerVb => FeeSats is { } f && VirtualSize > 0 ? (double)f / VirtualSize : null;
|
||||
/// <summary>
|
||||
/// Indirizzi della controparte: i destinatari esterni per un invio (output non
|
||||
/// nostri), i mittenti esterni per una ricezione (input non nostri).
|
||||
/// Counterparty addresses: external recipients for a send (outputs not ours),
|
||||
/// external senders for a receive (inputs not ours).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> CounterpartyAddresses => IsIncoming
|
||||
? [.. Inputs.Where(i => !i.IsMine && i.Address is not null).Select(i => i.Address!).Distinct()]
|
||||
@@ -57,9 +57,9 @@ public sealed class TransactionDetails
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recupera dal server tutti i dati di una singola transazione (blueprint §10):
|
||||
/// la transazione grezza e gli output spesi dai suoi input, per ricostruire
|
||||
/// importi, fee e indirizzi che il server non riassume.
|
||||
/// Fetches all data for a single transaction from the server (blueprint §10):
|
||||
/// the raw transaction and the outputs spent by its inputs, to reconstruct
|
||||
/// amounts, fees, and addresses that the server does not summarise.
|
||||
/// </summary>
|
||||
public static class TransactionInspector
|
||||
{
|
||||
@@ -112,10 +112,10 @@ public static class TransactionInspector
|
||||
|
||||
var rbf = tx.Inputs.Any(i => i.Sequence.IsRBF);
|
||||
|
||||
// Le transazioni degli input servono per importi/indirizzi/fee. Si
|
||||
// scaricano in parallelo (id univoci, richieste concorrenti supportate
|
||||
// da ElectrumClient): in sequenza la finestra impiegava un round-trip
|
||||
// per input. Anche l'header del blocco è recuperato in parallelo.
|
||||
// Input transactions are needed for amounts/addresses/fees. Fetched in
|
||||
// parallel (unique ids, concurrent requests supported by ElectrumClient):
|
||||
// sequential fetching costs one round-trip per input. The block header
|
||||
// is also fetched in parallel.
|
||||
var prevTxids = tx.IsCoinBase
|
||||
? []
|
||||
: tx.Inputs.Select(i => i.PrevOut.Hash.ToString()).Distinct().ToList();
|
||||
|
||||
@@ -6,8 +6,8 @@ using PalladiumWallet.Core.Storage;
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>
|
||||
/// Factory dei tipi di wallet (blueprint §4.5): dal documento ricostruisce il tipo
|
||||
/// di account corretto (HD da seed, HD da xprv importata, importato WIF, watch-only).
|
||||
/// Wallet type factory (blueprint §4.5): reconstructs the correct account type
|
||||
/// from a document (HD from seed, HD from imported xprv, imported WIF, watch-only).
|
||||
/// </summary>
|
||||
public static class WalletLoader
|
||||
{
|
||||
@@ -20,25 +20,25 @@ public static class WalletLoader
|
||||
var kind = Enum.Parse<ScriptKind>(doc.ScriptKind);
|
||||
var network = PalladiumNetworks.For(profile.Kind);
|
||||
|
||||
// 1. HD da seed (caso più comune)
|
||||
// 1. HD from seed (most common case)
|
||||
if (doc.Mnemonic is { } words)
|
||||
{
|
||||
if (!Bip39.TryParse(words, out var mnemonic))
|
||||
throw new InvalidDataException("Mnemonica del file wallet non valida.");
|
||||
throw new InvalidDataException("Invalid mnemonic in wallet file.");
|
||||
var path = KeyPath.Parse(doc.AccountPath ?? DerivationPaths.AccountPath(kind, profile).ToString());
|
||||
return HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, doc.Passphrase), kind, profile, path);
|
||||
}
|
||||
|
||||
// 2. HD da xprv importata (spendibile, senza seed)
|
||||
// 2. HD from imported xprv (spendable, no seed)
|
||||
if (doc.AccountXprv is { } xprvStr)
|
||||
{
|
||||
if (!Slip132.TryDecodePrivate(xprvStr, profile, out var xprv, out _))
|
||||
throw new InvalidDataException("Xprv del file wallet non valida per questa rete.");
|
||||
throw new InvalidDataException("Xprv in wallet file is invalid for this network.");
|
||||
var path = doc.AccountPath is { Length: > 0 } p ? KeyPath.Parse(p) : null;
|
||||
return HdAccount.FromAccountXprv(xprv!, kind, profile, path);
|
||||
}
|
||||
|
||||
// 3. Chiavi WIF importate
|
||||
// 3. Imported WIF keys
|
||||
if (doc.WifKeys is { Count: > 0 } wifKeys)
|
||||
{
|
||||
var entries = wifKeys.Select(wif =>
|
||||
@@ -51,21 +51,21 @@ public static class WalletLoader
|
||||
return new ImportedKeyAccount(entries, kind, profile);
|
||||
}
|
||||
|
||||
// 4. Watch-only da xpub
|
||||
// 4. Watch-only from xpub
|
||||
if (doc.AccountXpub is null)
|
||||
throw new InvalidDataException("File wallet senza xpub e senza seed.");
|
||||
throw new InvalidDataException("Wallet file has no xpub and no seed.");
|
||||
if (!Slip132.TryDecodePublic(doc.AccountXpub, profile, out var xpub, out _))
|
||||
throw new InvalidDataException("Xpub del file wallet non valida per questa rete.");
|
||||
throw new InvalidDataException("Xpub in wallet file is invalid for this network.");
|
||||
var accountPath = doc.AccountPath is { Length: > 0 } ap ? KeyPath.Parse(ap) : null;
|
||||
return HdAccount.FromAccountXpub(xpub!, kind, profile, accountPath);
|
||||
}
|
||||
|
||||
/// <summary>Crea il documento per un nuovo wallet da seed.</summary>
|
||||
/// <summary>Creates the document for a new seed wallet.</summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromMnemonic(
|
||||
string words, string? passphrase, ScriptKind kind, ChainProfile profile, KeyPath? customPath = null)
|
||||
{
|
||||
if (!Bip39.TryParse(words, out var mnemonic))
|
||||
throw new InvalidDataException("Mnemonica non valida (parole o checksum errati).");
|
||||
throw new InvalidDataException("Invalid mnemonic (wrong words or checksum).");
|
||||
|
||||
var account = customPath is null
|
||||
? HdAccount.FromMnemonic(mnemonic!, passphrase, kind, profile)
|
||||
@@ -85,15 +85,15 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una xpub SLIP-132 importata (watch-only).
|
||||
/// Rileva il ScriptKind dagli header SLIP-132; <paramref name="kindOverride"/> lo sovrascrive
|
||||
/// per i prefissi ambigui (xpub può essere Legacy o Taproot).
|
||||
/// Creates the document from an imported SLIP-132 xpub (watch-only).
|
||||
/// Detects the ScriptKind from the SLIP-132 header; <paramref name="kindOverride"/> overrides
|
||||
/// it for ambiguous prefixes (xpub can be Legacy or Taproot).
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromXpub(
|
||||
string slip132, ChainProfile profile, ScriptKind? kindOverride = null)
|
||||
{
|
||||
if (!Slip132.TryDecodePublic(slip132.Trim(), profile, out var xpub, out var detectedKind))
|
||||
throw new InvalidDataException("Chiave pubblica estesa non valida o non riconosciuta per questa rete.");
|
||||
throw new InvalidDataException("Extended public key is invalid or unrecognised for this network.");
|
||||
var kind = kindOverride ?? detectedKind;
|
||||
var account = HdAccount.FromAccountXpub(xpub!, kind, profile);
|
||||
|
||||
@@ -108,14 +108,14 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una xprv SLIP-132 importata (spendibile senza seed).
|
||||
/// Il documento contiene la xprv in chiaro: deve essere obbligatoriamente cifrato.
|
||||
/// Creates the document from an imported SLIP-132 xprv (spendable without seed).
|
||||
/// The document contains the xprv in plaintext — it must be encrypted.
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromXprv(
|
||||
string slip132, ChainProfile profile, ScriptKind? kindOverride = null)
|
||||
{
|
||||
if (!Slip132.TryDecodePrivate(slip132.Trim(), profile, out var xprv, out var detectedKind))
|
||||
throw new InvalidDataException("Chiave privata estesa non valida o non riconosciuta per questa rete.");
|
||||
throw new InvalidDataException("Extended private key is invalid or unrecognised for this network.");
|
||||
var kind = kindOverride ?? detectedKind;
|
||||
var account = HdAccount.FromAccountXprv(xprv!, kind, profile);
|
||||
|
||||
@@ -131,14 +131,14 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una o più chiavi WIF importate.
|
||||
/// Il documento contiene le chiavi WIF in chiaro: deve essere obbligatoriamente cifrato.
|
||||
/// Creates the document from one or more imported WIF keys.
|
||||
/// The document contains the WIF keys in plaintext — it must be encrypted.
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, ImportedKeyAccount Account) NewFromWif(
|
||||
IReadOnlyList<string> wifKeys, ScriptKind kind, ChainProfile profile)
|
||||
{
|
||||
if (wifKeys.Count == 0)
|
||||
throw new InvalidDataException("Almeno una chiave WIF richiesta.");
|
||||
throw new InvalidDataException("At least one WIF key is required.");
|
||||
|
||||
var network = PalladiumNetworks.For(profile.Kind);
|
||||
var entries = new List<(BitcoinAddress, Key?)>();
|
||||
@@ -153,7 +153,7 @@ public static class WalletLoader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Chiave WIF non valida: {ex.Message}");
|
||||
throw new InvalidDataException($"Invalid WIF key: {ex.Message}");
|
||||
}
|
||||
var addr = secret.PrivateKey.PubKey
|
||||
.GetAddress(DerivationPaths.ScriptPubKeyTypeFor(kind), network);
|
||||
|
||||
Reference in New Issue
Block a user