diff --git a/src/App/Localization/Loc.cs b/src/App/Localization/Loc.cs
index 51af199..6bfa0db 100644
--- a/src/App/Localization/Loc.cs
+++ b/src/App/Localization/Loc.cs
@@ -110,6 +110,18 @@ public sealed class Loc
"Achtung: ohne Verschlüsselung bleibt der Seed im Klartext auf der Festplatte."],
["wiz.back"] = ["Indietro", "Back", "Atrás", "Retour", "Voltar", "Zurück"],
["wiz.next"] = ["Avanti", "Next", "Siguiente", "Suivant", "Próximo", "Weiter"],
+ ["wiz.scripttype.title"] = ["Tipo di script e indirizzi", "Script type and addresses", "Tipo de script y direcciones", "Type de script et adresses", "Tipo de script e endereços", "Skripttyp und Adressen"],
+ ["wiz.scripttype.hint"] = [
+ "Determina il formato degli indirizzi. Se non sai cosa scegliere, usa Native SegWit.",
+ "Determines the address format. If unsure, use Native SegWit.",
+ "Determina el formato de los direcciones. Si no sabes, usa Native SegWit.",
+ "Détermine le format des adresses. En cas de doute, utilisez Native SegWit.",
+ "Determina o formato dos endereços. Em caso de dúvida, use Native SegWit.",
+ "Bestimmt das Adressformat. Wenn Sie unsicher sind, verwenden Sie Native SegWit."],
+ ["wiz.scripttype.legacy.desc"] = ["BIP44 · m/44'/… · indirizzi P", "BIP44 · m/44'/… · P addresses", "BIP44 · m/44'/… · direcciones P", "BIP44 · m/44'/… · adresses P", "BIP44 · m/44'/… · endereços P", "BIP44 · m/44'/… · P-Adressen"],
+ ["wiz.scripttype.wrapped.desc"] = ["BIP49 · m/49'/… · indirizzi 3", "BIP49 · m/49'/… · 3 addresses", "BIP49 · m/49'/… · direcciones 3", "BIP49 · m/49'/… · adresses 3", "BIP49 · m/49'/… · endereços 3", "BIP49 · m/49'/… · 3-Adressen"],
+ ["wiz.scripttype.native.desc"] = ["BIP84 · m/84'/… · indirizzi plm1q — consigliato", "BIP84 · m/84'/… · plm1q addresses — recommended", "BIP84 · m/84'/… · direcciones plm1q — recomendado", "BIP84 · m/84'/… · adresses plm1q — recommandé", "BIP84 · m/84'/… · endereços plm1q — recomendado", "BIP84 · m/84'/… · plm1q-Adressen — empfohlen"],
+ ["wiz.scripttype.taproot.desc"] = ["BIP86 · m/86'/… · indirizzi plm1p", "BIP86 · m/86'/… · plm1p addresses", "BIP86 · m/86'/… · direcciones plm1p", "BIP86 · m/86'/… · adresses plm1p", "BIP86 · m/86'/… · endereços plm1p", "BIP86 · m/86'/… · plm1p-Adressen"],
// Pannello wallet
["wallet.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
diff --git a/src/App/ViewModels/MainWindowViewModel.Wizard.cs b/src/App/ViewModels/MainWindowViewModel.Wizard.cs
index 1af35ff..6af00bf 100644
--- a/src/App/ViewModels/MainWindowViewModel.Wizard.cs
+++ b/src/App/ViewModels/MainWindowViewModel.Wizard.cs
@@ -23,6 +23,7 @@ public partial class MainWindowViewModel
public const string StepConfirmSeed = "confirm-seed";
public const string StepWords = "words";
public const string StepPassphrase = "passphrase";
+ public const string StepScriptType = "script-type";
public const string StepPassword = "password";
[ObservableProperty]
@@ -34,18 +35,37 @@ public partial class MainWindowViewModel
[NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))]
[NotifyPropertyChangedFor(nameof(IsStepWords))]
[NotifyPropertyChangedFor(nameof(IsStepPassphrase))]
+ [NotifyPropertyChangedFor(nameof(IsStepScriptType))]
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
private string setupStep = StepStart;
public bool IsStepDataLocation => SetupStep == StepDataLocation;
- public bool IsStepStart => SetupStep == StepStart;
+ public bool IsStepStart => SetupStep == StepStart;
public bool IsStepChooseWallet => SetupStep == StepChooseWallet;
- public bool IsStepOpen => SetupStep == StepOpen;
- public bool IsStepShowSeed => SetupStep == StepShowSeed;
- public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed;
- public bool IsStepWords => SetupStep == StepWords;
- public bool IsStepPassphrase => SetupStep == StepPassphrase;
- public bool IsStepPassword => SetupStep == StepPassword;
+ public bool IsStepOpen => SetupStep == StepOpen;
+ public bool IsStepShowSeed => SetupStep == StepShowSeed;
+ public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed;
+ public bool IsStepWords => SetupStep == StepWords;
+ public bool IsStepPassphrase => SetupStep == StepPassphrase;
+ public bool IsStepScriptType => SetupStep == StepScriptType;
+ public bool IsStepPassword => SetupStep == StepPassword;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsLegacySelected))]
+ [NotifyPropertyChangedFor(nameof(IsWrappedSegwitSelected))]
+ [NotifyPropertyChangedFor(nameof(IsNativeSegwitSelected))]
+ [NotifyPropertyChangedFor(nameof(IsTaprootSelected))]
+ private ScriptKind selectedScriptKind = ScriptKind.NativeSegwit;
+
+ public bool IsLegacySelected => SelectedScriptKind == ScriptKind.Legacy;
+ public bool IsWrappedSegwitSelected => SelectedScriptKind == ScriptKind.WrappedSegwit;
+ public bool IsNativeSegwitSelected => SelectedScriptKind == ScriptKind.NativeSegwit;
+ public bool IsTaprootSelected => SelectedScriptKind == ScriptKind.Taproot;
+
+ [RelayCommand] private void SelectLegacy() => SelectedScriptKind = ScriptKind.Legacy;
+ [RelayCommand] private void SelectWrappedSegwit() => SelectedScriptKind = ScriptKind.WrappedSegwit;
+ [RelayCommand] private void SelectNativeSegwit() => SelectedScriptKind = ScriptKind.NativeSegwit;
+ [RelayCommand] private void SelectTaproot() => SelectedScriptKind = ScriptKind.Taproot;
public string DefaultDataPath => AppPaths.DefaultDataRoot();
@@ -96,6 +116,7 @@ public partial class MainWindowViewModel
private void RefreshSetupState()
{
SetupStep = StepStart;
+ SelectedScriptKind = ScriptKind.NativeSegwit;
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
WalletFileExists = AppPaths.WalletFiles(Net).Count > 0;
StatusMessage = "";
@@ -192,6 +213,13 @@ public partial class MainWindowViewModel
[RelayCommand]
private void WizardNextFromPassphrase()
+ {
+ SetupStep = StepScriptType;
+ StatusMessage = "";
+ }
+
+ [RelayCommand]
+ private void WizardNextFromScriptType()
{
PasswordInput = ConfirmPasswordInput = "";
EncryptWallet = true;
@@ -208,7 +236,8 @@ public partial class MainWindowViewModel
StepChooseWallet or StepShowSeed or StepWords => StepStart,
StepConfirmSeed => StepShowSeed,
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
- StepPassword => StepPassphrase,
+ StepScriptType => StepPassphrase,
+ StepPassword => StepScriptType,
_ => StepStart,
};
if (SetupStep == StepStart)
@@ -243,7 +272,7 @@ public partial class MainWindowViewModel
var (doc, account) = WalletLoader.NewFromMnemonic(
MnemonicInput,
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
- ScriptKind.NativeSegwit,
+ SelectedScriptKind,
Profile);
var path = AppPaths.DefaultWalletPath(Net);
for (var n = 2; WalletStore.Exists(path); n++)
diff --git a/src/App/Views/MainView.axaml b/src/App/Views/MainView.axaml
index bf8334f..ad40194 100644
--- a/src/App/Views/MainView.axaml
+++ b/src/App/Views/MainView.axaml
@@ -171,6 +171,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Core/Chain/ChainProfile.cs b/src/Core/Chain/ChainProfile.cs
index ae4f908..70824a5 100644
--- a/src/Core/Chain/ChainProfile.cs
+++ b/src/Core/Chain/ChainProfile.cs
@@ -29,6 +29,9 @@ public enum ScriptKind
/// P2WSH (multisig native).
NativeSegwitMultisig,
+
+ /// P2TR key-path only (Taproot, BIP86) — witness v1, bech32m.
+ Taproot,
}
///
diff --git a/src/Core/Chain/ChainProfiles.cs b/src/Core/Chain/ChainProfiles.cs
index cda8b99..ce7490f 100644
--- a/src/Core/Chain/ChainProfiles.cs
+++ b/src/Core/Chain/ChainProfiles.cs
@@ -33,6 +33,8 @@ 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'/…
+ [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.
@@ -70,6 +72,7 @@ public static class ChainProfiles
[ScriptKind.WrappedSegwitMultisig] = new(0x024285b5, 0x024289ef), // Uprv / Upub
[ScriptKind.NativeSegwit] = new(0x045f18bc, 0x045f1cf6), // vprv / vpub
[ScriptKind.NativeSegwitMultisig] = new(0x02575048, 0x02575483), // Vprv / Vpub
+ [ScriptKind.Taproot] = new(0x04358394, 0x043587cf), // tprv / tpub (BIP32 standard)
},
BootstrapServers = [],
Checkpoints = [],
diff --git a/src/Core/Crypto/DerivationPaths.cs b/src/Core/Crypto/DerivationPaths.cs
index 40731bb..b8215b9 100644
--- a/src/Core/Crypto/DerivationPaths.cs
+++ b/src/Core/Crypto/DerivationPaths.cs
@@ -22,12 +22,16 @@ public static class DerivationPaths
/// Purpose BIP48 (multisig — fase successiva, §16 passo 8).
public const int PurposeMultisig = 48;
+ /// Purpose BIP86 (P2TR key-path, Taproot).
+ public const int PurposeTaproot = 86;
+
public static int PurposeFor(ScriptKind kind) => kind switch
{
ScriptKind.Legacy => PurposeLegacy,
ScriptKind.WrappedSegwit => PurposeWrappedSegwit,
ScriptKind.NativeSegwit => PurposeNativeSegwit,
ScriptKind.WrappedSegwitMultisig or ScriptKind.NativeSegwitMultisig => PurposeMultisig,
+ ScriptKind.Taproot => PurposeTaproot,
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
@@ -41,6 +45,7 @@ public static class DerivationPaths
ScriptKind.Legacy => ScriptPubKeyType.Legacy,
ScriptKind.WrappedSegwit => ScriptPubKeyType.SegwitP2SH,
ScriptKind.NativeSegwit => ScriptPubKeyType.Segwit,
+ 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)."),
diff --git a/tests/PalladiumWallet.Tests/Crypto/Bip86TaprootTests.cs b/tests/PalladiumWallet.Tests/Crypto/Bip86TaprootTests.cs
new file mode 100644
index 0000000..2ac4df6
--- /dev/null
+++ b/tests/PalladiumWallet.Tests/Crypto/Bip86TaprootTests.cs
@@ -0,0 +1,89 @@
+using NBitcoin;
+using PalladiumWallet.Core.Chain;
+using PalladiumWallet.Core.Crypto;
+
+namespace PalladiumWallet.Tests.Crypto;
+
+///
+/// Vettori di test BIP86 (mnemonica abandon-about, senza passphrase).
+/// La chiave pubblica tweakizzata (output key, 32 byte x-only) è chain-independent:
+/// viene verificata contro i vettori ufficiali Bitcoin, poi si controlla che
+/// l'indirizzo PLM abbia il prefisso plm1p (witness v1, bech32m).
+/// Il path m/86'/0'/0' usa coin_type=0 (non 746) per aderire ai vettori BIP86.
+///
+public class Bip86TaprootTests
+{
+ private static HdAccount Account()
+ {
+ Assert.True(Bip39.TryParse(
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
+ out var mnemonic));
+ return HdAccount.FromSeed(
+ Bip39.ToSeed(mnemonic!),
+ ScriptKind.Taproot,
+ ChainProfiles.Mainnet,
+ new KeyPath("86'/0'/0'"));
+ }
+
+ [Fact]
+ public void Il_derivation_path_di_un_account_taproot_usa_purpose_86()
+ {
+ var path = DerivationPaths.AccountPath(ScriptKind.Taproot, ChainProfiles.Mainnet, 0);
+ Assert.Equal($"86'/{ChainProfiles.Mainnet.Bip44CoinType}'/0'", path.ToString());
+ }
+
+ [Fact]
+ public void Gli_indirizzi_plm_taproot_iniziano_con_plm1p()
+ {
+ var account = Account();
+ var addr0 = account.GetReceiveAddress(0).ToString();
+ var addr1 = account.GetReceiveAddress(1).ToString();
+ var change0 = account.GetChangeAddress(0).ToString();
+
+ Assert.StartsWith("plm1p", addr0);
+ Assert.StartsWith("plm1p", addr1);
+ Assert.StartsWith("plm1p", change0);
+ }
+
+ ///
+ /// L'output key (chiave tweakizzata x-only, 32 byte) è identica al vettore BIP86
+ /// indipendentemente dalla rete: la rete cambia solo HRP e checksum, non il programma.
+ /// Indirizzi Bitcoin da https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
+ ///
+ [Theory]
+ [InlineData(false, 0, "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr")]
+ [InlineData(false, 1, "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh")]
+ [InlineData(true, 0, "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7")]
+ public void L_output_key_coincide_col_vettore_bip86(
+ bool isChange, int index, string bitcoinAddress)
+ {
+ var account = Account();
+ var plmAddr = account.GetAddress(isChange, index);
+
+ // Il witness program (output key tweakizzata) è chain-independent
+ var btcAddr = (TaprootAddress)BitcoinAddress.Create(bitcoinAddress, Network.Main);
+ var plmTaproot = (TaprootAddress)plmAddr;
+ Assert.Equal(btcAddr.PubKey, plmTaproot.PubKey);
+ }
+
+ [Fact]
+ public void Il_wallet_taproot_e_watch_only_se_creato_da_xpub()
+ {
+ var full = Account();
+ var watchOnly = HdAccount.FromAccountXpub(
+ full.AccountXpub, ScriptKind.Taproot, ChainProfiles.Mainnet);
+ Assert.True(watchOnly.IsWatchOnly);
+ Assert.Equal(
+ full.GetReceiveAddress(0).ToString(),
+ watchOnly.GetReceiveAddress(0).ToString());
+ }
+
+ [Fact]
+ public void I_tipi_multisig_lanciano_not_supported()
+ {
+ Assert.Throws(
+ () => DerivationPaths.ScriptPubKeyTypeFor(ScriptKind.WrappedSegwitMultisig));
+ Assert.Throws(
+ () => DerivationPaths.ScriptPubKeyTypeFor(ScriptKind.NativeSegwitMultisig));
+ }
+}