diff --git a/src/Core/Wallet/CoinAmount.cs b/src/Core/Wallet/CoinAmount.cs
new file mode 100644
index 0000000..35f7006
--- /dev/null
+++ b/src/Core/Wallet/CoinAmount.cs
@@ -0,0 +1,35 @@
+using System.Globalization;
+
+namespace PalladiumWallet.Core.Wallet;
+
+///
+/// Conversione satoshi ↔ unità coin (8 decimali) per visualizzazione e input.
+/// Si lavora sempre in satoshi internamente; la stringa è solo presentazione.
+///
+public static class CoinAmount
+{
+ public const long SatsPerCoin = 100_000_000;
+
+ public static string Format(long sats, string unit = "") =>
+ (sats / (decimal)SatsPerCoin).ToString("0.00000000", CultureInfo.InvariantCulture)
+ + (unit.Length > 0 ? " " + unit : "");
+
+ /// Parsa un importo in coin (punto o virgola decimale) in satoshi.
+ public static bool TryParseCoins(string text, out long sats)
+ {
+ sats = 0;
+ text = text.Trim().Replace(',', '.');
+ if (!decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out var coins)
+ || coins < 0)
+ return false;
+ try
+ {
+ sats = (long)(coins * SatsPerCoin);
+ }
+ catch (OverflowException)
+ {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/Core/Wallet/TransactionFactory.cs b/src/Core/Wallet/TransactionFactory.cs
new file mode 100644
index 0000000..a5ac913
--- /dev/null
+++ b/src/Core/Wallet/TransactionFactory.cs
@@ -0,0 +1,119 @@
+using NBitcoin;
+using NBitcoin.Policy;
+using PalladiumWallet.Core.Chain;
+using PalladiumWallet.Core.Crypto;
+using PalladiumWallet.Core.Storage;
+
+namespace PalladiumWallet.Core.Wallet;
+
+/// Esito della costruzione di una transazione.
+public sealed class BuiltTransaction
+{
+ public required Transaction Transaction { get; init; }
+ public required Money Fee { get; init; }
+ public required FeeRate FeeRate { get; init; }
+ public required bool Signed { get; init; }
+
+ /// PSBT per i flussi watch-only/air-gapped/multisig (§6.5).
+ public required PSBT Psbt { get; init; }
+
+ public string ToHex() => Transaction.ToHex();
+ public string Txid => Transaction.GetHash().ToString();
+}
+
+///
+/// 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).
+///
+public sealed class TransactionFactory(HdAccount account)
+{
+ private Network Network => PalladiumNetworks.For(account.Profile.Kind);
+
+ ///
+ /// Costruisce (e se possibile firma) una transazione.
+ ///
+ /// UTXO selezionati (coin control §6.2) o tutti quelli spendibili.
+ /// Tx di provenienza degli UTXO (txid → tx), dalla sincronizzazione.
+ /// Indirizzo destinatario.
+ /// Importo; ignorato se .
+ /// Fee rate fisso in sat/vByte (§6.4).
+ /// Indice del prossimo indirizzo di change (catena interna).
+ /// Invia tutto: fee sottratta dall'importo (§6.1).
+ public BuiltTransaction Build(
+ IReadOnlyList utxos,
+ IReadOnlyDictionary transactions,
+ BitcoinAddress destination,
+ long amountSats,
+ decimal feeRateSatPerVByte,
+ int changeIndex,
+ bool sendAll = false)
+ {
+ var spendable = utxos.Where(u => !u.Frozen).ToList();
+ if (spendable.Count == 0)
+ throw new WalletSpendException("Nessun UTXO spendibile selezionato.");
+
+ var coins = spendable.Select(u => new Coin(
+ new OutPoint(uint256.Parse(u.Txid), (uint)u.Vout),
+ transactions[u.Txid].Outputs[u.Vout])).ToList();
+
+ 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).
+ builder.OptInRBF = true;
+ builder.AddCoins(coins);
+ builder.SetChange(account.GetChangeAddress(changeIndex));
+ builder.SendEstimatedFees(feeRate);
+
+ if (sendAll)
+ builder.Send(destination, coins.Sum(c => (Money)c.Amount)).SubtractFees();
+ else
+ builder.Send(destination, Money.Satoshis(amountSats));
+
+ if (!account.IsWatchOnly)
+ {
+ builder.AddKeys(spendable
+ .Select(u => account.GetExtPrivateKey(u.IsChange, u.AddressIndex))
+ .ToArray());
+ }
+
+ Transaction tx;
+ try
+ {
+ tx = builder.BuildTransaction(sign: !account.IsWatchOnly);
+ }
+ catch (NotEnoughFundsException ex)
+ {
+ throw new WalletSpendException($"Fondi insufficienti: {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())));
+ }
+
+ return new BuiltTransaction
+ {
+ Transaction = tx,
+ Fee = GetFee(tx, coins),
+ FeeRate = feeRate,
+ Signed = !account.IsWatchOnly,
+ Psbt = builder.BuildPSBT(sign: !account.IsWatchOnly),
+ };
+ }
+
+ private static Money GetFee(Transaction tx, IReadOnlyList coins)
+ {
+ var spentOutpoints = tx.Inputs.Select(i => i.PrevOut).ToHashSet();
+ var inputSum = coins.Where(c => spentOutpoints.Contains(c.Outpoint))
+ .Sum(c => (Money)c.Amount);
+ return inputSum - tx.Outputs.Sum(o => o.Value);
+ }
+}
+
+/// Errore di costruzione/firma della spesa (fondi, policy, parametri).
+public sealed class WalletSpendException(string message) : Exception(message);
diff --git a/src/Core/Wallet/WalletLoader.cs b/src/Core/Wallet/WalletLoader.cs
new file mode 100644
index 0000000..2395b83
--- /dev/null
+++ b/src/Core/Wallet/WalletLoader.cs
@@ -0,0 +1,59 @@
+using NBitcoin;
+using PalladiumWallet.Core.Chain;
+using PalladiumWallet.Core.Crypto;
+using PalladiumWallet.Core.Storage;
+
+namespace PalladiumWallet.Core.Wallet;
+
+///
+/// Ponte documento wallet ↔ dominio (blueprint §4.5): dal file ricostruisce
+/// l'HdAccount giusto (da seed o watch-only da xpub). È l'embrione della
+/// factory dei tipi di wallet; crescerà con multisig e importati.
+///
+public static class WalletLoader
+{
+ public static ChainProfile ProfileOf(WalletDocument doc) =>
+ ChainProfiles.For(Enum.Parse(doc.Network, ignoreCase: true));
+
+ public static HdAccount ToAccount(WalletDocument doc)
+ {
+ var profile = ProfileOf(doc);
+ var kind = Enum.Parse(doc.ScriptKind);
+ var path = KeyPath.Parse(doc.AccountPath);
+
+ if (doc.Mnemonic is { } words)
+ {
+ if (!Bip39.TryParse(words, out var mnemonic))
+ throw new InvalidDataException("Mnemonica del file wallet non valida.");
+ return HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, doc.Passphrase), kind, profile, path);
+ }
+
+ if (!Slip132.TryDecodePublic(doc.AccountXpub, profile, out var xpub, out _))
+ throw new InvalidDataException("Xpub del file wallet non valida per questa rete.");
+ return HdAccount.FromAccountXpub(xpub!, kind, profile, path);
+ }
+
+ /// Crea il documento per un nuovo wallet da seed.
+ 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).");
+
+ var account = customPath is null
+ ? HdAccount.FromMnemonic(mnemonic!, passphrase, kind, profile)
+ : HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, passphrase), kind, profile, customPath);
+
+ var doc = new WalletDocument
+ {
+ Network = profile.NetName,
+ ScriptKind = kind.ToString(),
+ Mnemonic = words.Trim(),
+ Passphrase = passphrase,
+ AccountPath = account.AccountPath.ToString(),
+ AccountXpub = account.ToSlip132(),
+ MasterFingerprint = Convert.ToHexString(account.MasterFingerprint.ToBytes()).ToLowerInvariant(),
+ };
+ return (doc, account);
+ }
+}