diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index 412d8f49..4f6e575c 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -5,20 +5,14 @@ import { type BIP32Arg, BIP32 } from "../bip32.js"; import { type ECPairArg, ECPair } from "../ecpair.js"; import type { UtxolibName } from "../utxolibCompat.js"; import type { CoinName } from "../coinName.js"; +import type { InputScriptType } from "./scriptType.js"; + +export type { InputScriptType }; export type NetworkName = UtxolibName | CoinName; export type ScriptId = { chain: number; index: number }; -export type InputScriptType = - | "p2shP2pk" - | "p2sh" - | "p2shP2wsh" - | "p2wsh" - | "p2trLegacy" - | "p2trMusig2ScriptPath" - | "p2trMusig2KeyPath"; - export type OutPoint = { txid: string; vout: number; diff --git a/packages/wasm-utxo/js/fixedScriptWallet/index.ts b/packages/wasm-utxo/js/fixedScriptWallet/index.ts index 62c02aef..6628d975 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/index.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/index.ts @@ -1,14 +1,17 @@ +import { FixedScriptWalletNamespace } from "../wasm/wasm_utxo.js"; +import type { CoinName } from "../coinName.js"; + export { RootWalletKeys, type WalletKeysArg, type IWalletKeys } from "./RootWalletKeys.js"; export { ReplayProtection, type ReplayProtectionArg } from "./ReplayProtection.js"; export { outputScript, address } from "./address.js"; export { Dimensions } from "./Dimensions.js"; +export { type OutputScriptType, type InputScriptType, type ScriptType } from "./scriptType.js"; // Bitcoin-like PSBT (for all non-Zcash networks) export { BitGoPsbt, type NetworkName, type ScriptId, - type InputScriptType, type ParsedInput, type ParsedOutput, type ParsedTransaction, @@ -26,3 +29,34 @@ export { type ZcashNetworkName, type CreateEmptyZcashOptions, } from "./ZcashBitGoPsbt.js"; + +import type { ScriptType } from "./scriptType.js"; + +/** + * Check if a network supports a given fixed-script wallet script type + * + * @param coin - Coin name (e.g., "btc", "ltc", "doge") + * @param scriptType - Output script type or input script type to check + * @returns `true` if the network supports the script type, `false` otherwise + * + * @example + * ```typescript + * // Bitcoin supports all script types + * supportsScriptType("btc", "p2tr"); // true + * + * // Litecoin supports segwit but not taproot + * supportsScriptType("ltc", "p2wsh"); // true + * supportsScriptType("ltc", "p2tr"); // false + * + * // Dogecoin only supports legacy scripts + * supportsScriptType("doge", "p2sh"); // true + * supportsScriptType("doge", "p2wsh"); // false + * + * // Also works with input script types + * supportsScriptType("btc", "p2trMusig2KeyPath"); // true + * supportsScriptType("doge", "p2trLegacy"); // false + * ``` + */ +export function supportsScriptType(coin: CoinName, scriptType: ScriptType): boolean { + return FixedScriptWalletNamespace.supports_script_type(coin, scriptType); +} diff --git a/packages/wasm-utxo/js/fixedScriptWallet/scriptType.ts b/packages/wasm-utxo/js/fixedScriptWallet/scriptType.ts new file mode 100644 index 00000000..52a14811 --- /dev/null +++ b/packages/wasm-utxo/js/fixedScriptWallet/scriptType.ts @@ -0,0 +1,32 @@ +/** + * Fixed-script wallet output script types (2-of-3 multisig) + * + * This type represents the abstract script type, independent of chain (external/internal). + * Use this for checking network support or when you need the script type without derivation info. + */ +export type OutputScriptType = + | "p2sh" + | "p2shP2wsh" + | "p2wsh" + | "p2tr" // alias for p2trLegacy + | "p2trLegacy" + | "p2trMusig2"; + +/** + * Input script types for fixed-script wallets + * + * These are more specific than output types and include single-sig and taproot variants. + */ +export type InputScriptType = + | "p2shP2pk" + | "p2sh" + | "p2shP2wsh" + | "p2wsh" + | "p2trLegacy" + | "p2trMusig2ScriptPath" + | "p2trMusig2KeyPath"; + +/** + * Union of all script types that can be checked for network support + */ +export type ScriptType = OutputScriptType | InputScriptType; diff --git a/packages/wasm-utxo/src/address/networks.rs b/packages/wasm-utxo/src/address/networks.rs index 5d6429de..cef4f1c4 100644 --- a/packages/wasm-utxo/src/address/networks.rs +++ b/packages/wasm-utxo/src/address/networks.rs @@ -13,6 +13,7 @@ use super::{ ZCASH_TEST, }; use crate::bitcoin::Script; +use crate::fixed_script_wallet::wallet_scripts::OutputScriptType; use crate::networks::Network; use miniscript::bitcoin::WitnessVersion; @@ -120,6 +121,15 @@ impl OutputScriptSupport { } Ok(()) } + + /// Check if the network supports a given fixed-script wallet script type + pub fn supports_script_type(&self, script_type: OutputScriptType) -> bool { + match script_type { + OutputScriptType::P2sh => true, // all networks support legacy scripts + OutputScriptType::P2shP2wsh | OutputScriptType::P2wsh => self.segwit, + OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 => self.taproot, + } + } } impl Network { diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index 5e775fd3..f2b2d457 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -184,16 +184,13 @@ fn get_default_sighash_type( network: Network, chain: crate::fixed_script_wallet::wallet_scripts::Chain, ) -> miniscript::bitcoin::psbt::PsbtSighashType { - use crate::fixed_script_wallet::wallet_scripts::Chain; + use crate::fixed_script_wallet::wallet_scripts::OutputScriptType; use miniscript::bitcoin::sighash::{EcdsaSighashType, TapSighashType}; // For taproot, always use Default if matches!( - chain, - Chain::P2trInternal - | Chain::P2trExternal - | Chain::P2trMusig2Internal - | Chain::P2trMusig2External + chain.script_type, + OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 ) { return TapSighashType::Default.into(); } @@ -758,7 +755,7 @@ impl BitGoPsbt { options: WalletInputOptions, ) -> Result { use crate::fixed_script_wallet::to_pub_triple; - use crate::fixed_script_wallet::wallet_scripts::{Chain, WalletScripts}; + use crate::fixed_script_wallet::wallet_scripts::{Chain, OutputScriptType, WalletScripts}; use miniscript::bitcoin::psbt::Input; use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash}; use miniscript::bitcoin::{transaction::Sequence, Amount, OutPoint, TxIn, TxOut}; @@ -799,18 +796,8 @@ impl BitGoPsbt { // Create the PSBT input let mut psbt_input = Input::default(); - // Determine if segwit based on chain type - let is_segwit = matches!( - chain_enum, - Chain::P2shP2wshExternal - | Chain::P2shP2wshInternal - | Chain::P2wshExternal - | Chain::P2wshInternal - | Chain::P2trInternal - | Chain::P2trExternal - | Chain::P2trMusig2Internal - | Chain::P2trMusig2External - ); + // Determine if segwit based on chain type (all types except P2sh are segwit) + let is_segwit = chain_enum.script_type != OutputScriptType::P2sh; if let (false, Some(tx_bytes)) = (is_segwit, options.prev_tx) { // Non-segwit with prev_tx: use non_witness_utxo diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs index b4b03a1a..801027d9 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs @@ -4,7 +4,9 @@ use miniscript::bitcoin::secp256k1::{self, PublicKey}; use miniscript::bitcoin::{OutPoint, ScriptBuf, TapLeafHash, XOnlyPublicKey}; use crate::bitcoin::bip32::KeySource; -use crate::fixed_script_wallet::{Chain, ReplayProtection, RootWalletKeys, WalletScripts}; +use crate::fixed_script_wallet::{ + Chain, OutputScriptType, ReplayProtection, RootWalletKeys, WalletScripts, +}; use crate::Network; pub type Bip32DerivationMap = std::collections::BTreeMap; @@ -655,12 +657,12 @@ pub enum InputScriptType { impl InputScriptType { pub fn from_script_id(script_id: ScriptId, psbt_input: &Input) -> Result { let chain = Chain::try_from(script_id.chain).map_err(|e| e.to_string())?; - match chain { - Chain::P2shExternal | Chain::P2shInternal => Ok(InputScriptType::P2sh), - Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => Ok(InputScriptType::P2shP2wsh), - Chain::P2wshExternal | Chain::P2wshInternal => Ok(InputScriptType::P2wsh), - Chain::P2trInternal | Chain::P2trExternal => Ok(InputScriptType::P2trLegacy), - Chain::P2trMusig2Internal | Chain::P2trMusig2External => { + match chain.script_type { + OutputScriptType::P2sh => Ok(InputScriptType::P2sh), + OutputScriptType::P2shP2wsh => Ok(InputScriptType::P2shP2wsh), + OutputScriptType::P2wsh => Ok(InputScriptType::P2wsh), + OutputScriptType::P2trLegacy => Ok(InputScriptType::P2trLegacy), + OutputScriptType::P2trMusig2 => { // check if tap_script_sigs or tap_scripts are set if !psbt_input.tap_script_sigs.is_empty() || !psbt_input.tap_scripts.is_empty() { Ok(InputScriptType::P2trMusig2ScriptPath) diff --git a/packages/wasm-utxo/src/fixed_script_wallet/test_utils/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/test_utils/mod.rs index 7fc102ee..9323d3a2 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/test_utils/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/test_utils/mod.rs @@ -4,7 +4,7 @@ pub mod fixtures; pub mod psbt_compare; use super::wallet_keys::XpubTriple; -use super::wallet_scripts::{Chain, WalletScripts}; +use super::wallet_scripts::{Chain, OutputScriptType, Scope, WalletScripts}; use crate::bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub}; use crate::bitcoin::psbt::{Input as PsbtInput, Output as PsbtOutput, Psbt}; use crate::bitcoin::{Transaction, TxIn, TxOut}; @@ -38,7 +38,7 @@ pub fn create_external_output(seed: &str) -> PsbtOutput { let xpubs = get_test_wallet_keys(seed); let _scripts = WalletScripts::from_wallet_keys( &RootWalletKeys::new(xpubs), - Chain::P2wshExternal, + Chain::new(OutputScriptType::P2wsh, Scope::External), 0, &Network::Bitcoin.output_script_support(), ) diff --git a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/checkmultisig.rs b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/checkmultisig.rs index 5b35528a..0adaa696 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/checkmultisig.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/checkmultisig.rs @@ -100,15 +100,12 @@ mod tests { use crate::bitcoin::blockdata::script::Builder; use crate::fixed_script_wallet::wallet_keys::tests::get_test_wallet_keys; use crate::fixed_script_wallet::wallet_keys::to_pub_triple; - use crate::fixed_script_wallet::wallet_scripts::Chain; #[test] fn test_parse_multisig_script_2_of_3_valid() { // Get test keys let wallet_keys = get_test_wallet_keys("test_parse"); - let derived_keys = wallet_keys - .derive_for_chain_and_index(Chain::P2shExternal as u32, 0) - .unwrap(); + let derived_keys = wallet_keys.derive_for_chain_and_index(0, 0).unwrap(); let pub_triple = to_pub_triple(&derived_keys); // Build a valid 2-of-3 multisig script @@ -126,9 +123,7 @@ mod tests { // Test multiple different key sets for seed in ["seed1", "seed2", "seed3"] { let wallet_keys = get_test_wallet_keys(seed); - let derived_keys = wallet_keys - .derive_for_chain_and_index(Chain::P2shExternal as u32, 42) - .unwrap(); + let derived_keys = wallet_keys.derive_for_chain_and_index(0, 42).unwrap(); let original_keys = to_pub_triple(&derived_keys); // Build script from keys @@ -168,9 +163,7 @@ mod tests { fn test_parse_multisig_script_2_of_3_wrong_quorum() { // Create a valid key for testing let wallet_keys = get_test_wallet_keys("test_wrong_quorum"); - let derived_keys = wallet_keys - .derive_for_chain_and_index(Chain::P2shExternal as u32, 0) - .unwrap(); + let derived_keys = wallet_keys.derive_for_chain_and_index(0, 0).unwrap(); let pub_triple = to_pub_triple(&derived_keys); // Build script with wrong quorum (OP_1 instead of OP_2) @@ -194,9 +187,7 @@ mod tests { fn test_parse_multisig_script_2_of_3_wrong_total() { // Create a valid key for testing let wallet_keys = get_test_wallet_keys("test_wrong_total"); - let derived_keys = wallet_keys - .derive_for_chain_and_index(Chain::P2shExternal as u32, 0) - .unwrap(); + let derived_keys = wallet_keys.derive_for_chain_and_index(0, 0).unwrap(); let pub_triple = to_pub_triple(&derived_keys); // Build script with wrong total (OP_4 instead of OP_3) @@ -220,9 +211,7 @@ mod tests { fn test_parse_multisig_script_2_of_3_missing_checkmultisig() { // Create a valid key for testing let wallet_keys = get_test_wallet_keys("test_missing_checkmultisig"); - let derived_keys = wallet_keys - .derive_for_chain_and_index(Chain::P2shExternal as u32, 0) - .unwrap(); + let derived_keys = wallet_keys.derive_for_chain_and_index(0, 0).unwrap(); let pub_triple = to_pub_triple(&derived_keys); // Build script without OP_CHECKMULTISIG diff --git a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs index 8d598ce8..89bce8b4 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs @@ -62,15 +62,15 @@ impl WalletScripts { chain: Chain, script_support: &OutputScriptSupport, ) -> Result { - match chain { - Chain::P2shExternal | Chain::P2shInternal => { + match chain.script_type { + OutputScriptType::P2sh => { script_support.assert_legacy()?; let script = build_multisig_script_2_of_3(keys); Ok(WalletScripts::P2sh(ScriptP2sh { redeem_script: script, })) } - Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => { + OutputScriptType::P2shP2wsh => { script_support.assert_segwit()?; let script = build_multisig_script_2_of_3(keys); Ok(WalletScripts::P2shP2wsh(ScriptP2shP2wsh { @@ -78,18 +78,18 @@ impl WalletScripts { witness_script: script, })) } - Chain::P2wshExternal | Chain::P2wshInternal => { + OutputScriptType::P2wsh => { script_support.assert_segwit()?; let script = build_multisig_script_2_of_3(keys); Ok(WalletScripts::P2wsh(ScriptP2wsh { witness_script: script, })) } - Chain::P2trInternal | Chain::P2trExternal => { + OutputScriptType::P2trLegacy => { script_support.assert_taproot()?; Ok(WalletScripts::P2trLegacy(ScriptP2tr::new(keys, false))) } - Chain::P2trMusig2Internal | Chain::P2trMusig2External => { + OutputScriptType::P2trMusig2 => { script_support.assert_taproot()?; Ok(WalletScripts::P2trMusig2(ScriptP2tr::new(keys, true))) } @@ -103,7 +103,7 @@ impl WalletScripts { script_support: &OutputScriptSupport, ) -> Result { let derived_keys = wallet_keys - .derive_for_chain_and_index(chain as u32, index) + .derive_for_chain_and_index(chain.value(), index) .unwrap(); WalletScripts::new(&to_pub_triple(&derived_keys), chain, script_support) } @@ -119,39 +119,45 @@ impl WalletScripts { } } -/// BitGo-Defined mappings between derivation path component and script type +/// Whether a chain is for receiving (external) or change (internal) addresses. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Chain { - P2shExternal = 0, - P2shInternal = 1, - P2shP2wshExternal = 10, - P2shP2wshInternal = 11, - P2wshExternal = 20, - P2wshInternal = 21, - P2trInternal = 30, - P2trExternal = 31, - P2trMusig2Internal = 40, - P2trMusig2External = 41, +pub enum Scope { + /// External chains are for receiving addresses (even chain values: 0, 10, 20, 30, 40). + External, + /// Internal chains are for change addresses (odd chain values: 1, 11, 21, 31, 41). + Internal, } -/// Useful for iterating over enum values -const ALL_CHAINS: [Chain; 10] = [ - Chain::P2shExternal, - Chain::P2shInternal, - Chain::P2shP2wshExternal, - Chain::P2shP2wshInternal, - Chain::P2wshExternal, - Chain::P2wshInternal, - Chain::P2trInternal, - Chain::P2trExternal, - Chain::P2trMusig2Internal, - Chain::P2trMusig2External, -]; +/// BitGo-Defined mappings between derivation path component and script type. +/// +/// A Chain combines an `OutputScriptType` with a `Scope` (external/internal). +/// The chain value is used in derivation paths: `m/0/0/{chain}/{index}`. +/// +/// Chain values are normalized: external = base, internal = base + 1. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Chain { + pub script_type: OutputScriptType, + pub scope: Scope, +} impl Chain { - #[allow(dead_code)] - pub fn all() -> &'static [Chain; 10] { - &ALL_CHAINS + /// Create a new Chain from script type and scope. + pub const fn new(script_type: OutputScriptType, scope: Scope) -> Self { + Self { script_type, scope } + } + + /// Get the u32 chain value for derivation paths. + pub const fn value(&self) -> u32 { + (match self.script_type { + OutputScriptType::P2sh => 0, + OutputScriptType::P2shP2wsh => 10, + OutputScriptType::P2wsh => 20, + OutputScriptType::P2trLegacy => 30, + OutputScriptType::P2trMusig2 => 40, + }) + match self.scope { + Scope::External => 0, + Scope::Internal => 1, + } } } @@ -159,12 +165,20 @@ impl TryFrom for Chain { type Error = String; fn try_from(value: u32) -> Result { - for chain in &ALL_CHAINS { - if *chain as u32 == value { - return Ok(*chain); - } - } - Err(format!("no chain for {}", value)) + let (script_type, scope) = match value { + 0 => (OutputScriptType::P2sh, Scope::External), + 1 => (OutputScriptType::P2sh, Scope::Internal), + 10 => (OutputScriptType::P2shP2wsh, Scope::External), + 11 => (OutputScriptType::P2shP2wsh, Scope::Internal), + 20 => (OutputScriptType::P2wsh, Scope::External), + 21 => (OutputScriptType::P2wsh, Scope::Internal), + 30 => (OutputScriptType::P2trLegacy, Scope::External), + 31 => (OutputScriptType::P2trLegacy, Scope::Internal), + 40 => (OutputScriptType::P2trMusig2, Scope::External), + 41 => (OutputScriptType::P2trMusig2, Scope::Internal), + _ => return Err(format!("no chain for {}", value)), + }; + Ok(Chain::new(script_type, scope)) } } @@ -177,6 +191,85 @@ impl FromStr for Chain { } } +/// Fixed-script wallet script types (2-of-3 multisig) +/// +/// This enum represents the abstract script type, independent of chain (external/internal). +/// Use this for checking network support or when you need the script type without derivation info. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum OutputScriptType { + /// Legacy Pay-To-Script-Hash (chains 0, 1) + P2sh, + /// Wrapped-Segwit Pay-To-Script-Hash (chains 10, 11) + P2shP2wsh, + /// Native Segwit Pay-To-Witness-Script-Hash (chains 20, 21) + P2wsh, + /// Legacy Taproot, script-path only (chains 30, 31) + P2trLegacy, + /// Taproot with MuSig2 key-path support (chains 40, 41) + P2trMusig2, +} + +/// All OutputScriptType variants for iteration. +const ALL_SCRIPT_TYPES: [OutputScriptType; 5] = [ + OutputScriptType::P2sh, + OutputScriptType::P2shP2wsh, + OutputScriptType::P2wsh, + OutputScriptType::P2trLegacy, + OutputScriptType::P2trMusig2, +]; + +impl FromStr for OutputScriptType { + type Err = String; + + /// Parse a script type string into an OutputScriptType. + /// + /// Accepts both output script types and input script types: + /// - Output types: "p2sh", "p2shP2wsh", "p2wsh", "p2tr"/"p2trLegacy", "p2trMusig2" + /// - Input types: "p2shP2pk" (→ P2sh), "p2trMusig2ScriptPath"/"p2trMusig2KeyPath" (→ P2trMusig2) + fn from_str(s: &str) -> Result { + match s { + // Output script types + "p2sh" => Ok(OutputScriptType::P2sh), + "p2shP2wsh" => Ok(OutputScriptType::P2shP2wsh), + "p2wsh" => Ok(OutputScriptType::P2wsh), + // "p2tr" is kept as alias for backwards compatibility + "p2tr" | "p2trLegacy" => Ok(OutputScriptType::P2trLegacy), + "p2trMusig2" => Ok(OutputScriptType::P2trMusig2), + // Input script types (normalized to output types) + "p2shP2pk" => Ok(OutputScriptType::P2sh), + "p2trMusig2ScriptPath" | "p2trMusig2KeyPath" => Ok(OutputScriptType::P2trMusig2), + _ => Err(format!( + "Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2trLegacy, p2trMusig2", + s + )), + } + } +} + +impl OutputScriptType { + /// Returns all possible OutputScriptType values. + pub fn all() -> &'static [OutputScriptType; 5] { + &ALL_SCRIPT_TYPES + } + + /// Get the string representation of the script type + pub fn as_str(&self) -> &'static str { + match self { + OutputScriptType::P2sh => "p2sh", + OutputScriptType::P2shP2wsh => "p2shP2wsh", + OutputScriptType::P2wsh => "p2wsh", + OutputScriptType::P2trLegacy => "p2trLegacy", + OutputScriptType::P2trMusig2 => "p2trMusig2", + } + } +} + +impl std::fmt::Display for OutputScriptType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + /// Return derived WalletKeys. All keys are derived with the same path. #[allow(dead_code)] pub fn derive_xpubs_with_path( @@ -200,7 +293,7 @@ pub fn derive_xpubs( let p = DerivationPath::from_str("m/0/0") .unwrap() .child(ChildNumber::Normal { - index: chain as u32, + index: chain.value(), }) .child(ChildNumber::Normal { index }); derive_xpubs_with_path(xpubs, ctx, p) @@ -212,6 +305,19 @@ mod tests { use crate::fixed_script_wallet::wallet_keys::tests::get_test_wallet_keys; use crate::Network; + const ALL_CHAINS: [Chain; 10] = [ + Chain::new(OutputScriptType::P2sh, Scope::External), + Chain::new(OutputScriptType::P2sh, Scope::Internal), + Chain::new(OutputScriptType::P2shP2wsh, Scope::External), + Chain::new(OutputScriptType::P2shP2wsh, Scope::Internal), + Chain::new(OutputScriptType::P2wsh, Scope::External), + Chain::new(OutputScriptType::P2wsh, Scope::Internal), + Chain::new(OutputScriptType::P2trLegacy, Scope::External), + Chain::new(OutputScriptType::P2trLegacy, Scope::Internal), + Chain::new(OutputScriptType::P2trMusig2, Scope::External), + Chain::new(OutputScriptType::P2trMusig2, Scope::Internal), + ]; + fn assert_output_script(keys: &RootWalletKeys, chain: Chain, expected_script: &str) { let scripts = WalletScripts::from_wallet_keys( keys, @@ -225,84 +331,40 @@ mod tests { } fn test_build_multisig_chain_with(keys: &RootWalletKeys, chain: Chain) { - match chain { - Chain::P2shExternal => { - assert_output_script( - keys, - chain, - "a914999a8eb861e3fabae1efe4fb16ff4752e1f5976687", - ); - } - Chain::P2shInternal => { - assert_output_script( - keys, - chain, - "a914487ca5843f23b9f3b85a00136bec647846d179ab87", - ); - } - Chain::P2shP2wshExternal => { - assert_output_script( - keys, - chain, - "a9141219b6d9430fffb8de14f14969a5c07172c4613b87", - ); - } - Chain::P2shP2wshInternal => { - assert_output_script( - keys, - chain, - "a914cbfab1a5a25afab05ff420bd9dd0958c6f1a7a2f87", - ); - } - Chain::P2wshExternal => { - assert_output_script( - keys, - chain, - "0020ce670e65fd69ef2eb1aa6087643a18ae5bff198ca20ef26da546e85962386c76", - ); + use OutputScriptType::*; + use Scope::*; + + let expected = match (chain.script_type, chain.scope) { + (P2sh, External) => "a914999a8eb861e3fabae1efe4fb16ff4752e1f5976687", + (P2sh, Internal) => "a914487ca5843f23b9f3b85a00136bec647846d179ab87", + (P2shP2wsh, External) => "a9141219b6d9430fffb8de14f14969a5c07172c4613b87", + (P2shP2wsh, Internal) => "a914cbfab1a5a25afab05ff420bd9dd0958c6f1a7a2f87", + (P2wsh, External) => { + "0020ce670e65fd69ef2eb1aa6087643a18ae5bff198ca20ef26da546e85962386c76" } - Chain::P2wshInternal => { - assert_output_script( - keys, - chain, - "00209cca08a252f9846a1417afbe46ed96bf09d5ec6d25f0effb7d841188d5992b7c", - ); + (P2wsh, Internal) => { + "00209cca08a252f9846a1417afbe46ed96bf09d5ec6d25f0effb7d841188d5992b7c" } - Chain::P2trInternal => { - assert_output_script( - keys, - chain, - "51203a81504b836967a69399fcf3822adfdb7d61061e42418f6aad0d473cbcc69b86", - ); + (P2trLegacy, External) => { + "51203a81504b836967a69399fcf3822adfdb7d61061e42418f6aad0d473cbcc69b86" } - Chain::P2trExternal => { - assert_output_script( - keys, - chain, - "512093e5e3c8885a6f87b4449e1bffa3ba8a45a9ee634dc27408394c7d9b68f01adc", - ); + (P2trLegacy, Internal) => { + "512093e5e3c8885a6f87b4449e1bffa3ba8a45a9ee634dc27408394c7d9b68f01adc" } - Chain::P2trMusig2Internal => { - assert_output_script( - keys, - chain, - "5120c7c4dd55b2bf3cd7ea5b27d3da521699ce761aa345523d8486f0336364957ef2", - ); + (P2trMusig2, External) => { + "5120c7c4dd55b2bf3cd7ea5b27d3da521699ce761aa345523d8486f0336364957ef2" } - Chain::P2trMusig2External => { - assert_output_script( - keys, - chain, - "51202629eea5dbef6841160a0b752dedd4b8e206f046835ee944848679d6dea2ac2c", - ); + (P2trMusig2, Internal) => { + "51202629eea5dbef6841160a0b752dedd4b8e206f046835ee944848679d6dea2ac2c" } - } + }; + assert_output_script(keys, chain, expected); } #[test] fn test_build_multisig_chain() { let keys = get_test_wallet_keys("lol"); - for chain in Chain::all() { + for chain in &ALL_CHAINS { test_build_multisig_chain_with(&keys, *chain); } } @@ -317,16 +379,27 @@ mod tests { taproot: false, }; - let result = - WalletScripts::from_wallet_keys(&keys, Chain::P2wshExternal, 0, &no_segwit_support); + use OutputScriptType::*; + use Scope::*; + + let result = WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2wsh, External), + 0, + &no_segwit_support, + ); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("Network does not support segwit")); - let result = - WalletScripts::from_wallet_keys(&keys, Chain::P2shP2wshExternal, 0, &no_segwit_support); + let result = WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2shP2wsh, External), + 0, + &no_segwit_support, + ); assert!(result.is_err()); assert!(result .unwrap_err() @@ -339,8 +412,12 @@ mod tests { taproot: false, }; - let result = - WalletScripts::from_wallet_keys(&keys, Chain::P2trExternal, 0, &no_taproot_support); + let result = WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2trLegacy, External), + 0, + &no_taproot_support, + ); assert!(result.is_err()); assert!(result .unwrap_err() @@ -349,7 +426,7 @@ mod tests { let result = WalletScripts::from_wallet_keys( &keys, - Chain::P2trMusig2External, + Chain::new(P2trMusig2, External), 0, &no_taproot_support, ); @@ -360,14 +437,19 @@ mod tests { .contains("Network does not support taproot")); // Test that legacy scripts work regardless of support flags - let result = - WalletScripts::from_wallet_keys(&keys, Chain::P2shExternal, 0, &no_segwit_support); + let result = WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2sh, External), + 0, + &no_segwit_support, + ); assert!(result.is_ok()); // Test real-world network scenarios // Dogecoin doesn't support segwit or taproot let doge_support = Network::Dogecoin.output_script_support(); - let result = WalletScripts::from_wallet_keys(&keys, Chain::P2wshExternal, 0, &doge_support); + let result = + WalletScripts::from_wallet_keys(&keys, Chain::new(P2wsh, External), 0, &doge_support); assert!(result.is_err()); assert!(result .unwrap_err() @@ -376,7 +458,12 @@ mod tests { // Litecoin supports segwit but not taproot let ltc_support = Network::Litecoin.output_script_support(); - let result = WalletScripts::from_wallet_keys(&keys, Chain::P2trExternal, 0, <c_support); + let result = WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2trLegacy, External), + 0, + <c_support, + ); assert!(result.is_err()); assert!(result .unwrap_err() @@ -384,23 +471,73 @@ mod tests { .contains("Network does not support taproot")); // Litecoin should support segwit scripts - let result = WalletScripts::from_wallet_keys(&keys, Chain::P2wshExternal, 0, <c_support); + let result = + WalletScripts::from_wallet_keys(&keys, Chain::new(P2wsh, External), 0, <c_support); assert!(result.is_ok()); // Bitcoin should support all script types let btc_support = Network::Bitcoin.output_script_support(); - assert!( - WalletScripts::from_wallet_keys(&keys, Chain::P2shExternal, 0, &btc_support).is_ok() + assert!(WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2sh, External), + 0, + &btc_support + ) + .is_ok()); + assert!(WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2wsh, External), + 0, + &btc_support + ) + .is_ok()); + assert!(WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2trLegacy, External), + 0, + &btc_support + ) + .is_ok()); + assert!(WalletScripts::from_wallet_keys( + &keys, + Chain::new(P2trMusig2, External), + 0, + &btc_support + ) + .is_ok()); + } + + #[test] + fn test_output_script_type_from_str() { + use OutputScriptType::*; + + // Output script types + assert_eq!(OutputScriptType::from_str("p2sh").unwrap(), P2sh); + assert_eq!(OutputScriptType::from_str("p2shP2wsh").unwrap(), P2shP2wsh); + assert_eq!(OutputScriptType::from_str("p2wsh").unwrap(), P2wsh); + assert_eq!(OutputScriptType::from_str("p2tr").unwrap(), P2trLegacy); + assert_eq!( + OutputScriptType::from_str("p2trLegacy").unwrap(), + P2trLegacy ); - assert!( - WalletScripts::from_wallet_keys(&keys, Chain::P2wshExternal, 0, &btc_support).is_ok() + assert_eq!( + OutputScriptType::from_str("p2trMusig2").unwrap(), + P2trMusig2 ); - assert!( - WalletScripts::from_wallet_keys(&keys, Chain::P2trExternal, 0, &btc_support).is_ok() + + // Input script types (normalized to output types) + assert_eq!(OutputScriptType::from_str("p2shP2pk").unwrap(), P2sh); + assert_eq!( + OutputScriptType::from_str("p2trMusig2ScriptPath").unwrap(), + P2trMusig2 ); - assert!( - WalletScripts::from_wallet_keys(&keys, Chain::P2trMusig2External, 0, &btc_support) - .is_ok() + assert_eq!( + OutputScriptType::from_str("p2trMusig2KeyPath").unwrap(), + P2trMusig2 ); + + // Invalid script types + assert!(OutputScriptType::from_str("invalid").is_err()); + assert!(OutputScriptType::from_str("p2pkh").is_err()); } } diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs index 3705fd82..e06c62a7 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs @@ -7,7 +7,7 @@ use crate::error::WasmUtxoError; use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::{ parse_shared_chain_and_index, InputScriptType, }; -use crate::fixed_script_wallet::wallet_scripts::Chain; +use crate::fixed_script_wallet::wallet_scripts::{Chain, OutputScriptType}; use miniscript::bitcoin::VarInt; use wasm_bindgen::prelude::*; @@ -230,17 +230,11 @@ fn get_input_weights_for_chain( ) -> Result { let chain_enum = Chain::try_from(chain).map_err(|e| e.to_string())?; - match chain_enum { - Chain::P2shExternal | Chain::P2shInternal => { - Ok(get_input_weights_for_type(InputScriptType::P2sh)) - } - Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => { - Ok(get_input_weights_for_type(InputScriptType::P2shP2wsh)) - } - Chain::P2wshExternal | Chain::P2wshInternal => { - Ok(get_input_weights_for_type(InputScriptType::P2wsh)) - } - Chain::P2trExternal | Chain::P2trInternal => { + match chain_enum.script_type { + OutputScriptType::P2sh => Ok(get_input_weights_for_type(InputScriptType::P2sh)), + OutputScriptType::P2shP2wsh => Ok(get_input_weights_for_type(InputScriptType::P2shP2wsh)), + OutputScriptType::P2wsh => Ok(get_input_weights_for_type(InputScriptType::P2wsh)), + OutputScriptType::P2trLegacy => { // Legacy p2tr - always script path // user+bitgo = level 1, user+backup = level 2 let is_recovery = cosigner == Some("backup"); @@ -253,7 +247,7 @@ fn get_input_weights_for_chain( is_segwit: true, }) } - Chain::P2trMusig2External | Chain::P2trMusig2Internal => { + OutputScriptType::P2trMusig2 => { // p2trMusig2 - keypath for user+bitgo, scriptpath for user+backup let is_recovery = cosigner == Some("backup"); if is_recovery { @@ -356,14 +350,12 @@ impl WasmDimensions { })?; // For p2trMusig2, check if it's keypath or scriptpath - let script_type = match chain_enum { - Chain::P2shExternal | Chain::P2shInternal => InputScriptType::P2sh, - Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => { - InputScriptType::P2shP2wsh - } - Chain::P2wshExternal | Chain::P2wshInternal => InputScriptType::P2wsh, - Chain::P2trExternal | Chain::P2trInternal => InputScriptType::P2trLegacy, - Chain::P2trMusig2External | Chain::P2trMusig2Internal => { + let script_type = match chain_enum.script_type { + OutputScriptType::P2sh => InputScriptType::P2sh, + OutputScriptType::P2shP2wsh => InputScriptType::P2shP2wsh, + OutputScriptType::P2wsh => InputScriptType::P2wsh, + OutputScriptType::P2trLegacy => InputScriptType::P2trLegacy, + OutputScriptType::P2trMusig2 => { // Check if tap_scripts are populated to distinguish keypath/scriptpath if !psbt_input.tap_script_sigs.is_empty() || !psbt_input.tap_scripts.is_empty() diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs index bf56c7d9..b26f1451 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -3,11 +3,13 @@ mod dimensions; pub use dimensions::WasmDimensions; use std::collections::HashMap; +use std::str::FromStr; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; use crate::address::networks::AddressFormat; use crate::error::WasmUtxoError; +use crate::fixed_script_wallet::wallet_scripts::OutputScriptType; use crate::fixed_script_wallet::{Chain, WalletScripts}; use crate::utxolib_compat::UtxolibNetwork; use crate::wasm::bip32::WasmBIP32; @@ -84,6 +86,27 @@ impl FixedScriptWalletNamespace { .map_err(|e| WasmUtxoError::new(&format!("Failed to generate address: {}", e)))?; Ok(address) } + + /// Check if a network supports a given fixed-script wallet script type + /// + /// # Arguments + /// * `coin` - Coin name (e.g., "btc", "ltc", "doge") + /// * `script_type` - Script type name: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2" + /// + /// # Returns + /// `true` if the network supports the script type, `false` otherwise + /// + /// # Examples + /// - Bitcoin supports all script types (p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2) + /// - Litecoin supports segwit but not taproot (p2sh, p2shP2wsh, p2wsh) + /// - Dogecoin only supports legacy scripts (p2sh) + #[wasm_bindgen] + pub fn supports_script_type(coin: &str, script_type: &str) -> Result { + let network = crate::networks::Network::from_coin_name(coin) + .ok_or_else(|| WasmUtxoError::new(&format!("Unknown coin: {}", coin)))?; + let st = OutputScriptType::from_str(script_type).map_err(|e| WasmUtxoError::new(&e))?; + Ok(network.output_script_support().supports_script_type(st)) + } } #[wasm_bindgen] pub struct BitGoPsbt {