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:
2026-06-16 14:40:06 +02:00
parent 4b82a0852c
commit 3d5a226a5a
59 changed files with 721 additions and 716 deletions
+7 -7
View File
@@ -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;
+22 -22
View File
@@ -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);
+20 -20
View File
@@ -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();
+23 -23
View File
@@ -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);