diff --git a/.gitignore b/.gitignore index 7b940f806..efd5d15cd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,37 @@ fuzz/hfuzz_workspace .idea .DS_STORE + +# Claude Flow and AI assistant files +.claude-flow.pid +.claude/ +.roo/ +.roomodes +claude-flow +memory/ +memory-bank.md +coordination.md + +# IDE and editor files +.vscode/ +*.swp +*.swo +*~ + +# Build artifacts +**/*.rs.bk + +# Test and coverage +tarpaulin-report.html +cobertura.xml + +# Backup files +*.backup +*.bak + +# Temporary files +*.tmp +.tmp/ + +# Build scripts artifacts +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..987247ced --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,268 @@ +# Claude Code Configuration - SPARC Development Environment + +## Project Overview +This project uses the SPARC (Specification, Pseudocode, Architecture, Refinement, Completion) methodology for systematic Test-Driven Development with AI assistance through Claude-Flow orchestration. + +## SPARC Development Commands + +### Core SPARC Commands +- `./claude-flow sparc modes`: List all available SPARC development modes +- `./claude-flow sparc run ""`: Execute specific SPARC mode for a task +- `./claude-flow sparc tdd ""`: Run complete TDD workflow using SPARC methodology +- `./claude-flow sparc info `: Get detailed information about a specific mode + +### Standard Build Commands +- `npm run build`: Build the project +- `npm run test`: Run the test suite +- `npm run lint`: Run linter and format checks +- `npm run typecheck`: Run TypeScript type checking + +## SPARC Methodology Workflow + +### 1. Specification Phase +```bash +# Create detailed specifications and requirements +./claude-flow sparc run spec-pseudocode "Define user authentication requirements" +``` +- Define clear functional requirements +- Document edge cases and constraints +- Create user stories and acceptance criteria +- Establish non-functional requirements + +### 2. Pseudocode Phase +```bash +# Develop algorithmic logic and data flows +./claude-flow sparc run spec-pseudocode "Create authentication flow pseudocode" +``` +- Break down complex logic into steps +- Define data structures and interfaces +- Plan error handling and edge cases +- Create modular, testable components + +### 3. Architecture Phase +```bash +# Design system architecture and component structure +./claude-flow sparc run architect "Design authentication service architecture" +``` +- Create system diagrams and component relationships +- Define API contracts and interfaces +- Plan database schemas and data flows +- Establish security and scalability patterns + +### 4. Refinement Phase (TDD Implementation) +```bash +# Execute Test-Driven Development cycle +./claude-flow sparc tdd "implement user authentication system" +``` + +**TDD Cycle:** +1. **Red**: Write failing tests first +2. **Green**: Implement minimal code to pass tests +3. **Refactor**: Optimize and clean up code +4. **Repeat**: Continue until feature is complete + +### 5. Completion Phase +```bash +# Integration, documentation, and validation +./claude-flow sparc run integration "integrate authentication with user management" +``` +- Integrate all components +- Perform end-to-end testing +- Create comprehensive documentation +- Validate against original requirements + +## SPARC Mode Reference + +### Development Modes +- **`architect`**: System design and architecture planning +- **`code`**: Clean, modular code implementation +- **`tdd`**: Test-driven development and testing +- **`spec-pseudocode`**: Requirements and algorithmic planning +- **`integration`**: System integration and coordination + +### Quality Assurance Modes +- **`debug`**: Troubleshooting and bug resolution +- **`security-review`**: Security analysis and vulnerability assessment +- **`refinement-optimization-mode`**: Performance optimization and refactoring + +### Support Modes +- **`docs-writer`**: Documentation creation and maintenance +- **`devops`**: Deployment and infrastructure management +- **`mcp`**: External service integration +- **`swarm`**: Multi-agent coordination for complex tasks + +## Claude Code Slash Commands + +Claude Code slash commands are available in `.claude/commands/`: + +### Project Commands +- `/sparc`: Execute SPARC methodology workflows +- `/sparc-`: Run specific SPARC mode (e.g., /sparc-architect) +- `/claude-flow-help`: Show all Claude-Flow commands +- `/claude-flow-memory`: Interact with memory system +- `/claude-flow-swarm`: Coordinate multi-agent swarms + +### Using Slash Commands +1. Type `/` in Claude Code to see available commands +2. Select a command or type its name +3. Commands are context-aware and project-specific +4. Custom commands can be added to `.claude/commands/` + +## Code Style and Best Practices + +### SPARC Development Principles +- **Modular Design**: Keep files under 500 lines, break into logical components +- **Environment Safety**: Never hardcode secrets or environment-specific values +- **Test-First**: Always write tests before implementation (Red-Green-Refactor) +- **Clean Architecture**: Separate concerns, use dependency injection +- **Documentation**: Maintain clear, up-to-date documentation + +### Coding Standards +- Use TypeScript for type safety and better tooling +- Follow consistent naming conventions (camelCase for variables, PascalCase for classes) +- Implement proper error handling and logging +- Use async/await for asynchronous operations +- Prefer composition over inheritance + +### Memory and State Management +- Use claude-flow memory system for persistent state across sessions +- Store progress and findings using namespaced keys +- Query previous work before starting new tasks +- Export/import memory for backup and sharing + +## SPARC Memory Integration + +### Memory Commands for SPARC Development +```bash +# Store project specifications +./claude-flow memory store spec_auth "User authentication requirements and constraints" + +# Store architectural decisions +./claude-flow memory store arch_decisions "Database schema and API design choices" + +# Store test results and coverage +./claude-flow memory store test_coverage "Authentication module: 95% coverage, all tests passing" + +# Query previous work +./claude-flow memory query auth_implementation + +# Export project memory +./claude-flow memory export project_backup.json +``` + +### Memory Namespaces +- **`spec`**: Requirements and specifications +- **`arch`**: Architecture and design decisions +- **`impl`**: Implementation notes and code patterns +- **`test`**: Test results and coverage reports +- **`debug`**: Bug reports and resolution notes + +## Workflow Examples + +### Feature Development Workflow +```bash +# 1. Start with specification +./claude-flow sparc run spec-pseudocode "User profile management feature" + +# 2. Design architecture +./claude-flow sparc run architect "Profile service architecture with data validation" + +# 3. Implement with TDD +./claude-flow sparc tdd "user profile CRUD operations" + +# 4. Security review +./claude-flow sparc run security-review "profile data access and validation" + +# 5. Integration testing +./claude-flow sparc run integration "profile service with authentication system" + +# 6. Documentation +./claude-flow sparc run docs-writer "profile service API documentation" +``` + +### Bug Fix Workflow +```bash +# 1. Debug and analyze +./claude-flow sparc run debug "authentication token expiration issue" + +# 2. Write regression tests +./claude-flow sparc run tdd "token refresh mechanism tests" + +# 3. Implement fix +./claude-flow sparc run code "fix token refresh in authentication service" + +# 4. Security review +./claude-flow sparc run security-review "token handling security implications" +``` + +## Configuration Files + +### Claude Code Integration +- **`.claude/commands/`**: Claude Code slash commands for all SPARC modes +- **`.claude/logs/`**: Conversation and session logs + +### SPARC Configuration +- **`.roomodes`**: SPARC mode definitions and configurations (auto-generated) +- **`.roo/`**: SPARC templates and workflows (auto-generated) + +### Claude-Flow Configuration +- **`memory/`**: Persistent memory and session data +- **`coordination/`**: Multi-agent coordination settings +- **`CLAUDE.md`**: Project instructions for Claude Code + +## Git Workflow Integration + +### Commit Strategy with SPARC +- **Specification commits**: After completing requirements analysis +- **Architecture commits**: After design phase completion +- **TDD commits**: After each Red-Green-Refactor cycle +- **Integration commits**: After successful component integration +- **Documentation commits**: After completing documentation updates + +### Branch Strategy +- **`feature/sparc-`**: Feature development with SPARC methodology +- **`hotfix/sparc-`**: Bug fixes using SPARC debugging workflow +- **`refactor/sparc-`**: Refactoring using optimization mode + +## Troubleshooting + +### Common SPARC Issues +- **Mode not found**: Check `.roomodes` file exists and is valid JSON +- **Memory persistence**: Ensure `memory/` directory has write permissions +- **Tool access**: Verify required tools are available for the selected mode +- **Namespace conflicts**: Use unique memory namespaces for different features + +### Debug Commands +```bash +# Check SPARC configuration +./claude-flow sparc modes + +# Verify memory system +./claude-flow memory stats + +# Check system status +./claude-flow status + +# View detailed mode information +./claude-flow sparc info +``` + +## Project Architecture + +This SPARC-enabled project follows a systematic development approach: +- **Clear separation of concerns** through modular design +- **Test-driven development** ensuring reliability and maintainability +- **Iterative refinement** for continuous improvement +- **Comprehensive documentation** for team collaboration +- **AI-assisted development** through specialized SPARC modes + +## Important Notes + +- Always run tests before committing (`npm run test`) +- Use SPARC memory system to maintain context across sessions +- Follow the Red-Green-Refactor cycle during TDD phases +- Document architectural decisions in memory for future reference +- Regular security reviews for any authentication or data handling code +- Claude Code slash commands provide quick access to SPARC modes + +For more information about SPARC methodology, see: https://github.com/ruvnet/claude-code-flow/docs/sparc.md diff --git a/Cargo.toml b/Cargo.toml index bd4edb26c..1a9fa544d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["dash", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test"] +members = ["dash", "dash-network", "dash-network-ffi", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test", "key-wallet", "key-wallet-ffi"] resolver = "2" [workspace.package] diff --git a/dash-network-ffi/Cargo.toml b/dash-network-ffi/Cargo.toml new file mode 100644 index 000000000..b1fac22e5 --- /dev/null +++ b/dash-network-ffi/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "dash-network-ffi" +version.workspace = true +edition = "2021" +authors = ["Quantum Explorer "] +license = "CC0-1.0" +repository = "https://github.com/dashpay/rust-dashcore/" +description = "FFI bindings for dash-network types" +keywords = ["dash", "network", "ffi", "bindings"] +readme = "README.md" + +[dependencies] +dash-network = { path = "../dash-network", default-features = false } +uniffi = { version = "0.29.3", features = ["cli"] } +thiserror = "2.0.12" + +[build-dependencies] +uniffi = { version = "0.29.3", features = ["build"] } + +[dev-dependencies] +hex = "0.4" + +[lib] +crate-type = ["cdylib", "staticlib"] +name = "dash_network_ffi" + +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" \ No newline at end of file diff --git a/dash-network-ffi/README.md b/dash-network-ffi/README.md new file mode 100644 index 000000000..73c686f5c --- /dev/null +++ b/dash-network-ffi/README.md @@ -0,0 +1,116 @@ +# dash-network-ffi + +FFI bindings for the dash-network crate, providing language bindings via UniFFI. + +## Overview + +This crate provides Foreign Function Interface (FFI) bindings for the `dash-network` types, allowing them to be used from other programming languages like Swift, Python, Kotlin, and Ruby. + +## Features + +- UniFFI-based bindings for the Network enum +- Network information and utilities exposed through FFI +- Support for magic bytes operations +- Core version activation queries + +## Usage + +### Building + +```bash +cargo build --release +``` + +### Generating Bindings + +To generate bindings for your target language: + +```bash +cargo run --bin uniffi-bindgen generate src/dash_network.udl --language swift +cargo run --bin uniffi-bindgen generate src/dash_network.udl --language python +cargo run --bin uniffi-bindgen generate src/dash_network.udl --language kotlin +``` + +### Example Usage (Swift) + +```swift +// Initialize the library +dashNetworkFfiInitialize() + +// Create a network info object +let networkInfo = NetworkInfo(network: .dash) + +// Get magic bytes +let magic = networkInfo.magic() +print("Dash network magic: \(String(format: "0x%08X", magic))") + +// Check if core v20 is active +if networkInfo.isCoreV20Active(blockHeight: 2000000) { + print("Core v20 is active!") +} + +// Create from magic bytes +do { + let network = try NetworkInfo.fromMagic(magic: 0xBD6B0CBF) + print("Network: \(network.toString())") +} catch { + print("Invalid magic bytes") +} +``` + +### Example Usage (Python) + +```python +import dash_network_ffi + +# Initialize the library +dash_network_ffi.initialize() + +# Create a network info object +network_info = dash_network_ffi.NetworkInfo(dash_network_ffi.Network.DASH) + +# Get magic bytes +magic = network_info.magic() +print(f"Dash network magic: 0x{magic:08X}") + +# Check if core v20 is active +if network_info.is_core_v20_active(2000000): + print("Core v20 is active!") + +# Create from magic bytes +try: + network = dash_network_ffi.NetworkInfo.from_magic(0xBD6B0CBF) + print(f"Network: {network.to_string()}") +except dash_network_ffi.NetworkError.InvalidMagic: + print("Invalid magic bytes") +``` + +## API + +### Network Enum + +- `Dash` - Dash mainnet +- `Testnet` - Dash testnet +- `Devnet` - Dash devnet +- `Regtest` - Regression test network + +### NetworkInfo Class + +#### Constructors +- `new(network: Network)` - Create from a Network enum value +- `from_magic(magic: u32)` - Create from magic bytes (throws NetworkError) + +#### Methods +- `magic() -> u32` - Get the network's magic bytes +- `to_string() -> String` - Get the network name as a string +- `is_core_v20_active(block_height: u32) -> bool` - Check if core v20 is active at height +- `core_v20_activation_height() -> u32` - Get the activation height for core v20 + +### NetworkError Enum + +- `InvalidMagic` - Invalid magic bytes provided +- `InvalidNetwork` - Invalid network specified + +## License + +This project is licensed under the CC0 1.0 Universal license. \ No newline at end of file diff --git a/dash-network-ffi/build.rs b/dash-network-ffi/build.rs new file mode 100644 index 000000000..319c12147 --- /dev/null +++ b/dash-network-ffi/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::generate_scaffolding("src/dash_network.udl").unwrap(); +} diff --git a/dash-network-ffi/src/dash_network.udl b/dash-network-ffi/src/dash_network.udl new file mode 100644 index 000000000..be4a23903 --- /dev/null +++ b/dash-network-ffi/src/dash_network.udl @@ -0,0 +1,41 @@ +namespace dash_network_ffi { + // Initialize function for any setup needs + void initialize(); +}; + +// Network enum matching the dash-network crate +enum Network { + "Dash", + "Testnet", + "Devnet", + "Regtest", +}; + +// Interface for network-related operations +interface NetworkInfo { + // Constructor + [Name=new] + constructor(Network network); + + // Create from magic bytes + [Name=from_magic, Throws=NetworkError] + constructor(u32 magic); + + // Get the magic bytes for this network + u32 magic(); + + // Get the network as a string + string to_string(); + + // Check if core v20 is active at a given height + boolean is_core_v20_active(u32 block_height); + + // Get the core v20 activation height + u32 core_v20_activation_height(); +}; + +[Error] +enum NetworkError { + "InvalidMagic", + "InvalidNetwork", +}; \ No newline at end of file diff --git a/dash-network-ffi/src/lib.rs b/dash-network-ffi/src/lib.rs new file mode 100644 index 000000000..9f79caf3e --- /dev/null +++ b/dash-network-ffi/src/lib.rs @@ -0,0 +1,173 @@ +//! FFI bindings for dash-network library + +use dash_network::Network as DashNetwork; + +// Include the UniFFI scaffolding +uniffi::include_scaffolding!("dash_network"); + +// Initialize function +pub fn initialize() { + // Any global initialization if needed +} + +// Re-export Network enum for UniFFI +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Network { + Dash, + Testnet, + Devnet, + Regtest, +} + +impl From for DashNetwork { + fn from(n: Network) -> Self { + match n { + Network::Dash => DashNetwork::Dash, + Network::Testnet => DashNetwork::Testnet, + Network::Devnet => DashNetwork::Devnet, + Network::Regtest => DashNetwork::Regtest, + } + } +} + +impl From for Network { + fn from(n: DashNetwork) -> Self { + match n { + DashNetwork::Dash => Network::Dash, + DashNetwork::Testnet => Network::Testnet, + DashNetwork::Devnet => Network::Devnet, + DashNetwork::Regtest => Network::Regtest, + unknown => panic!("Unhandled Network variant {:?}", unknown), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum NetworkError { + #[error("Invalid magic bytes")] + InvalidMagic, + #[error("Invalid network")] + InvalidNetwork, +} + +pub struct NetworkInfo { + network: DashNetwork, +} + +impl NetworkInfo { + pub fn new(network: Network) -> Self { + Self { + network: network.into(), + } + } + + pub fn from_magic(magic: u32) -> Result { + DashNetwork::from_magic(magic) + .map(|network| Self { + network, + }) + .ok_or(NetworkError::InvalidMagic) + } + + pub fn magic(&self) -> u32 { + self.network.magic() + } + + pub fn to_string(&self) -> String { + self.network.to_string() + } + + pub fn is_core_v20_active(&self, block_height: u32) -> bool { + self.network.core_v20_is_active_at(block_height) + } + + pub fn core_v20_activation_height(&self) -> u32 { + self.network.core_v20_activation_height() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_network_conversion() { + // Test FFI to Dash Network conversion + assert_eq!(DashNetwork::from(Network::Dash), DashNetwork::Dash); + assert_eq!(DashNetwork::from(Network::Testnet), DashNetwork::Testnet); + assert_eq!(DashNetwork::from(Network::Devnet), DashNetwork::Devnet); + assert_eq!(DashNetwork::from(Network::Regtest), DashNetwork::Regtest); + + // Test Dash Network to FFI conversion + assert_eq!(Network::from(DashNetwork::Dash), Network::Dash); + assert_eq!(Network::from(DashNetwork::Testnet), Network::Testnet); + assert_eq!(Network::from(DashNetwork::Devnet), Network::Devnet); + assert_eq!(Network::from(DashNetwork::Regtest), Network::Regtest); + } + + #[test] + fn test_network_info_creation() { + let info = NetworkInfo::new(Network::Dash); + assert_eq!(info.network, DashNetwork::Dash); + } + + #[test] + fn test_magic_bytes() { + let dash_info = NetworkInfo::new(Network::Dash); + assert_eq!(dash_info.magic(), 0xBD6B0CBF); + + let testnet_info = NetworkInfo::new(Network::Testnet); + assert_eq!(testnet_info.magic(), 0xFFCAE2CE); + + let devnet_info = NetworkInfo::new(Network::Devnet); + assert_eq!(devnet_info.magic(), 0xCEFFCAE2); + + let regtest_info = NetworkInfo::new(Network::Regtest); + assert_eq!(regtest_info.magic(), 0xDAB5BFFA); + } + + #[test] + fn test_from_magic() { + // Valid magic bytes + assert!(NetworkInfo::from_magic(0xBD6B0CBF).is_ok()); + assert!(NetworkInfo::from_magic(0xFFCAE2CE).is_ok()); + assert!(NetworkInfo::from_magic(0xCEFFCAE2).is_ok()); + assert!(NetworkInfo::from_magic(0xDAB5BFFA).is_ok()); + + // Invalid magic bytes + assert!(matches!(NetworkInfo::from_magic(0x12345678), Err(NetworkError::InvalidMagic))); + } + + #[test] + fn test_network_to_string() { + assert_eq!(NetworkInfo::new(Network::Dash).to_string(), "dash"); + assert_eq!(NetworkInfo::new(Network::Testnet).to_string(), "testnet"); + assert_eq!(NetworkInfo::new(Network::Devnet).to_string(), "devnet"); + assert_eq!(NetworkInfo::new(Network::Regtest).to_string(), "regtest"); + } + + #[test] + fn test_core_v20_activation() { + let dash_info = NetworkInfo::new(Network::Dash); + assert_eq!(dash_info.core_v20_activation_height(), 1987776); + assert!(!dash_info.is_core_v20_active(1987775)); + assert!(dash_info.is_core_v20_active(1987776)); + assert!(dash_info.is_core_v20_active(2000000)); + + let testnet_info = NetworkInfo::new(Network::Testnet); + assert_eq!(testnet_info.core_v20_activation_height(), 905100); + assert!(!testnet_info.is_core_v20_active(905099)); + assert!(testnet_info.is_core_v20_active(905100)); + } + + #[test] + fn test_round_trip_conversions() { + let networks = vec![Network::Dash, Network::Testnet, Network::Devnet, Network::Regtest]; + + for network in networks { + let dash_network: DashNetwork = network.into(); + let back_to_ffi: Network = dash_network.into(); + assert_eq!(network, back_to_ffi); + } + } +} diff --git a/dash-network-ffi/uniffi-bindgen.rs b/dash-network-ffi/uniffi-bindgen.rs new file mode 100644 index 000000000..f6cff6cf1 --- /dev/null +++ b/dash-network-ffi/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/dash-network/Cargo.toml b/dash-network/Cargo.toml new file mode 100644 index 000000000..800db300a --- /dev/null +++ b/dash-network/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "dash-network" +version.workspace = true +edition = "2021" +authors = ["Quantum Explorer "] +license = "CC0-1.0" +repository = "https://github.com/dashpay/rust-dashcore/" +documentation = "https://docs.rs/dash-network/" +description = "Dash network types shared across Dash crates" +keywords = ["dash", "network"] +readme = "README.md" + +[dependencies] +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } + +# Optional dependencies for serialization +serde = { version = "1.0", default-features = false, optional = true, features = ["derive", "alloc"] } +bincode = { version = "=2.0.0-rc.3", optional = true, default-features = false } +bincode_derive = { version= "=2.0.0-rc.3", optional = true } + +[features] +default = ["std"] +std = ["hex/std"] +no-std = [] +serde = ["dep:serde"] +bincode = ["dep:bincode", "dep:bincode_derive"] + +[lib] +name = "dash_network" +path = "src/lib.rs" \ No newline at end of file diff --git a/dash-network/README.md b/dash-network/README.md new file mode 100644 index 000000000..9aa6a7a79 --- /dev/null +++ b/dash-network/README.md @@ -0,0 +1,71 @@ +# dash-network + +A Rust library providing network type definitions for the Dash cryptocurrency. + +## Overview + +This crate defines the `Network` enum used across Dash-related Rust projects to identify which network (mainnet, testnet, devnet, or regtest) is being used. It provides a centralized definition to avoid duplication and circular dependencies between crates. + +## Features + +- **Network Identification**: Enum representing Dash networks (Dash mainnet, Testnet, Devnet, Regtest) +- **Magic Bytes**: Network-specific magic bytes for message headers +- **Protocol Information**: Core version activation heights and network-specific parameters +- **Serialization Support**: Optional serde and bincode support via feature flags + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +dash-network = "0.39.6" +``` + +### Basic Example + +```rust +use dash_network::Network; + +fn main() { + let network = Network::Dash; + + // Get network magic bytes + let magic = network.magic(); + println!("Network magic: 0x{:08X}", magic); + + // Check core v20 activation + let block_height = 2_000_000; + if network.core_v20_is_active_at(block_height) { + println!("Core v20 is active at height {}", block_height); + } +} +``` + +### Network Types + +- `Network::Dash` - Dash mainnet +- `Network::Testnet` - Dash testnet +- `Network::Devnet` - Dash devnet +- `Network::Regtest` - Regression test network + +### Features + +- `default`: Enables `std` +- `std`: Standard library support (enabled by default) +- `no-std`: Enables no_std compatibility +- `serde`: Enables serde serialization/deserialization +- `bincode`: Enables bincode encoding/decoding + +## Network Magic Bytes + +Each network has unique magic bytes used in message headers: + +- Dash mainnet: `0xBD6B0CBF` +- Testnet: `0xFFCAE2CE` +- Devnet: `0xCEFFCAE2` +- Regtest: `0xDAB5BFFA` + +## License + +This project is licensed under the CC0 1.0 Universal license. \ No newline at end of file diff --git a/dash-network/src/lib.rs b/dash-network/src/lib.rs new file mode 100644 index 000000000..3342ae985 --- /dev/null +++ b/dash-network/src/lib.rs @@ -0,0 +1,148 @@ +//! Dash network types shared across Dash crates + +use std::fmt; + +/// The cryptocurrency network to act on. +#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[non_exhaustive] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] +pub enum Network { + /// Classic Dash Core Payment Chain + Dash, + /// Dash's testnet network. + Testnet, + /// Dash's devnet network. + Devnet, + /// Bitcoin's regtest network. + Regtest, +} + +impl Network { + /// Creates a `Network` from the magic bytes. + /// + /// # Examples + /// + /// ```rust + /// use dash_network::Network; + /// + /// assert_eq!(Some(Network::Dash), Network::from_magic(0xBD6B0CBF)); + /// assert_eq!(None, Network::from_magic(0xFFFFFFFF)); + /// ``` + pub fn from_magic(magic: u32) -> Option { + // Note: any new entries here must be added to `magic` below + match magic { + 0xBD6B0CBF => Some(Network::Dash), + 0xFFCAE2CE => Some(Network::Testnet), + 0xCEFFCAE2 => Some(Network::Devnet), + 0xDAB5BFFA => Some(Network::Regtest), + _ => None, + } + } + + /// Return the network magic bytes, which should be encoded little-endian + /// at the start of every message + /// + /// # Examples + /// + /// ```rust + /// use dash_network::Network; + /// + /// let network = Network::Dash; + /// assert_eq!(network.magic(), 0xBD6B0CBF); + /// ``` + pub fn magic(self) -> u32 { + // Note: any new entries here must be added to `from_magic` above + match self { + Network::Dash => 0xBD6B0CBF, + Network::Testnet => 0xFFCAE2CE, + Network::Devnet => 0xCEFFCAE2, + Network::Regtest => 0xDAB5BFFA, + } + } + + /// The known activation height of core v20 + pub fn core_v20_activation_height(&self) -> u32 { + match self { + Network::Dash => 1987776, + Network::Testnet => 905100, + Network::Devnet => 1, // v20 active from genesis on devnet + Network::Regtest => 1, // v20 active from genesis on regtest + #[allow(unreachable_patterns)] + other => panic!("Unknown activation height for network {:?}", other), + } + } + + /// Helper method to know if core v20 was active + pub fn core_v20_is_active_at(&self, core_block_height: u32) -> bool { + core_block_height >= self.core_v20_activation_height() + } +} + +impl fmt::Display for Network { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Network::Dash => write!(f, "dash"), + Network::Testnet => write!(f, "testnet"), + Network::Devnet => write!(f, "devnet"), + Network::Regtest => write!(f, "regtest"), + } + } +} + +impl std::str::FromStr for Network { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dash" | "mainnet" => Ok(Network::Dash), + "testnet" | "test" => Ok(Network::Testnet), + "devnet" | "dev" => Ok(Network::Devnet), + "regtest" => Ok(Network::Regtest), + _ => Err(format!("Unknown network type: {}", s)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_network_magic() { + assert_eq!(Network::Dash.magic(), 0xBD6B0CBF); + assert_eq!(Network::Testnet.magic(), 0xFFCAE2CE); + assert_eq!(Network::Devnet.magic(), 0xCEFFCAE2); + assert_eq!(Network::Regtest.magic(), 0xDAB5BFFA); + } + + #[test] + fn test_network_from_magic() { + assert_eq!(Network::from_magic(0xBD6B0CBF), Some(Network::Dash)); + assert_eq!(Network::from_magic(0xFFCAE2CE), Some(Network::Testnet)); + assert_eq!(Network::from_magic(0xCEFFCAE2), Some(Network::Devnet)); + assert_eq!(Network::from_magic(0xDAB5BFFA), Some(Network::Regtest)); + assert_eq!(Network::from_magic(0x12345678), None); + } + + #[test] + fn test_network_display() { + assert_eq!(Network::Dash.to_string(), "dash"); + assert_eq!(Network::Testnet.to_string(), "testnet"); + assert_eq!(Network::Devnet.to_string(), "devnet"); + assert_eq!(Network::Regtest.to_string(), "regtest"); + } + + #[test] + fn test_network_from_str() { + assert_eq!("dash".parse::().unwrap(), Network::Dash); + assert_eq!("mainnet".parse::().unwrap(), Network::Dash); + assert_eq!("testnet".parse::().unwrap(), Network::Testnet); + assert_eq!("test".parse::().unwrap(), Network::Testnet); + assert_eq!("devnet".parse::().unwrap(), Network::Devnet); + assert_eq!("dev".parse::().unwrap(), Network::Devnet); + assert_eq!("regtest".parse::().unwrap(), Network::Regtest); + assert!("invalid".parse::().is_err()); + } +} diff --git a/dash/Cargo.toml b/dash/Cargo.toml index 7de35a51a..1eaef8547 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -23,7 +23,7 @@ default = [ "std", "secp-recovery", "bincode" ] base64 = [ "base64-compat" ] rand-std = ["secp256k1/rand"] rand = ["secp256k1/rand"] -serde = ["actual-serde", "dashcore_hashes/serde", "secp256k1/serde"] +serde = ["actual-serde", "dashcore_hashes/serde", "secp256k1/serde", "key-wallet/serde", "dash-network/serde"] secp-lowmemory = ["secp256k1/lowmemory"] secp-recovery = ["secp256k1/recovery"] signer = ["secp-recovery", "rand", "base64"] @@ -32,15 +32,15 @@ bls = ["blsful"] eddsa = ["ed25519-dalek"] quorum_validation = ["bls", "bls-signatures"] message_verification = ["bls"] -bincode = [ "dep:bincode", "dashcore_hashes/bincode" ] +bincode = [ "dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dash-network/bincode" ] # At least one of std, no-std must be enabled. # # The no-std feature doesn't disable std - you need to turn off the std feature for that by disabling default. # Instead no-std enables additional features required for this crate to be usable without std. # As a result, both can be enabled without conflict. -std = ["secp256k1/std", "dashcore_hashes/std", "bech32/std", "internals/std"] -no-std = ["core2", "dashcore_hashes/alloc", "dashcore_hashes/core2", "secp256k1/alloc"] +std = ["secp256k1/std", "dashcore_hashes/std", "bech32/std", "internals/std", "key-wallet/std", "dash-network/std"] +no-std = ["core2", "dashcore_hashes/alloc", "dashcore_hashes/core2", "secp256k1/alloc", "dash-network/no-std"] [package.metadata.docs.rs] all-features = true @@ -51,6 +51,8 @@ internals = { path = "../internals", package = "dashcore-private" } bech32 = { version = "0.9.1", default-features = false } dashcore_hashes = { path = "../hashes", default-features = false } secp256k1 = { default-features = false, features = ["hashes"], version= "0.30.0" } +key-wallet = { path = "../key-wallet", default-features = false } +dash-network = { path = "../dash-network", default-features = false } core2 = { version = "0.4.0", optional = true, features = ["alloc"], default-features = false } rustversion = { version="1.0.20"} # Do NOT use this as a feature! Use the `serde` feature instead. @@ -62,6 +64,7 @@ hex_lit = "0.1.1" anyhow = { version= "1.0" } hex = { version= "0.4" } bincode = { version= "=2.0.0-rc.3", optional = true } +bincode_derive = { version= "=2.0.0-rc.3", optional = true } bitflags = "2.9.0" blsful = { version = "3.0.0-pre8", optional = true } ed25519-dalek = { version = "2.1", features = ["rand_core"], optional = true } @@ -79,9 +82,6 @@ bincode = { version= "=2.0.0-rc.3" } assert_matches = "1.5.0" dashcore = { path = ".", features = ["core-block-hash-use-x11", "message_verification", "quorum_validation", "signer"] } -[[example]] -name = "bip32" - [[example]] name = "handshake" required-features = ["std"] diff --git a/dash/examples/bip32.rs b/dash/examples/bip32.rs deleted file mode 100644 index 648c348ef..000000000 --- a/dash/examples/bip32.rs +++ /dev/null @@ -1,58 +0,0 @@ -extern crate dashcore; - -use std::str::FromStr; -use std::{env, process}; - -use dashcore::PublicKey; -use dashcore::address::Address; -use dashcore::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey}; -use dashcore::hashes::hex::FromHex; -use dashcore::secp256k1::Secp256k1; -use dashcore::secp256k1::ffi::types::AlignedType; - -fn main() { - // This example derives root xprv from a 32-byte seed, - // derives the child xprv with path m/84h/0h/0h, - // prints out corresponding xpub, - // calculates and prints out the first receiving segwit address. - // Run this example with cargo and seed(hex-encoded) argument: - // cargo run --example bip32 7934c09359b234e076b9fa5a1abfd38e3dc2a9939745b7cc3c22a48d831d14bd - - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("not enough arguments. usage: {} ", &args[0]); - process::exit(1); - } - - let seed_hex = &args[1]; - println!("Seed: {}", seed_hex); - - // default network as mainnet - let network = dashcore::Network::Dash; - println!("Network: {:?}", network); - - let seed = Vec::from_hex(seed_hex).unwrap(); - - // we need secp256k1 context for key derivation - let mut buf: Vec = Vec::new(); - buf.resize(Secp256k1::preallocate_size(), AlignedType::zeroed()); - let secp = Secp256k1::preallocated_new(buf.as_mut_slice()).unwrap(); - - // calculate root key from seed - let root = ExtendedPrivKey::new_master(network, &seed).unwrap(); - println!("Root key: {}", root); - - // derive child xpub - let path = DerivationPath::from_str("m/84h/0h/0h").unwrap(); - let child = root.derive_priv(&secp, &path).unwrap(); - println!("Child at {}: {}", path, child); - let xpub = ExtendedPubKey::from_priv(&secp, &child); - println!("Public key at {}: {}", path, xpub); - - // generate first receiving address at m/0/0 - // manually creating indexes this time - let zero = ChildNumber::from_normal_idx(0).unwrap(); - let public_key = xpub.derive_pub(&secp, &vec![zero, zero]).unwrap().public_key; - let address = Address::p2wpkh(&PublicKey::new(public_key), network).unwrap(); - println!("First receiving address: {}", address); -} diff --git a/dash/examples/handshake.rs b/dash/examples/handshake.rs index dc6c6ef60..1ab02504d 100644 --- a/dash/examples/handshake.rs +++ b/dash/examples/handshake.rs @@ -7,8 +7,8 @@ use std::{env, process}; use dashcore::consensus::{Decodable, encode}; use dashcore::network::{address, constants, message, message_network}; -use dashcore::secp256k1; use dashcore::secp256k1::rand::Rng; +use dashcore::{Network, secp256k1}; use secp256k1::rand; fn main() { @@ -30,7 +30,7 @@ fn main() { let version_message = build_version_message(address); let first_message = message::RawNetworkMessage { - magic: constants::Network::Dash.magic(), + magic: Network::Dash.magic(), payload: version_message, }; @@ -50,7 +50,7 @@ fn main() { println!("Received version message: {:?}", reply.payload); let second_message = message::RawNetworkMessage { - magic: constants::Network::Dash.magic(), + magic: Network::Dash.magic(), payload: message::NetworkMessage::Verack, }; diff --git a/dash/src/address.rs b/dash/src/address.rs index d82f2a5ad..79f2190da 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -64,9 +64,9 @@ use crate::blockdata::script::{ use crate::crypto::key::{PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; use crate::error::ParseIntError; use crate::hash_types::{PubkeyHash, ScriptHash}; -use crate::network::constants::Network; use crate::prelude::*; use crate::taproot::TapNodeHash; +use dash_network::Network; /// Address error. #[derive(Debug, PartialEq, Eq, Clone)] @@ -884,15 +884,18 @@ impl Address { let p2pkh_prefix = match self.network() { Network::Dash => PUBKEY_ADDRESS_PREFIX_MAIN, Network::Testnet | Network::Devnet | Network::Regtest => PUBKEY_ADDRESS_PREFIX_TEST, + other => unreachable!("Unknown network {other:?} – add explicit prefix"), }; let p2sh_prefix = match self.network() { Network::Dash => SCRIPT_ADDRESS_PREFIX_MAIN, Network::Testnet | Network::Devnet | Network::Regtest => SCRIPT_ADDRESS_PREFIX_TEST, + other => unreachable!("Unknown network {other:?} – add explicit prefix"), }; let bech32_hrp = match self.network() { Network::Dash => "ds", Network::Testnet | Network::Devnet => "tb", Network::Regtest => "dsrt", + other => unreachable!("Unknown network {other:?} – add explicit prefix"), }; let encoding = AddressEncoding { payload: self.payload(), @@ -1140,6 +1143,7 @@ impl Address { (Network::Dash, _) | (_, Network::Dash) => false, (Network::Regtest, _) | (_, Network::Regtest) if !is_legacy => false, (Network::Testnet, _) | (Network::Regtest, _) | (Network::Devnet, _) => true, + _ => false, } } @@ -1353,7 +1357,7 @@ mod tests { use super::*; use crate::crypto::key::PublicKey; - use crate::network::constants::Network::{Dash, Testnet}; + use dash_network::Network::{Dash, Testnet}; fn roundtrips(addr: &Address) { assert_eq!( diff --git a/dash/src/blockdata/constants.rs b/dash/src/blockdata/constants.rs index 3c86ca8b0..6f416fecd 100644 --- a/dash/src/blockdata/constants.rs +++ b/dash/src/blockdata/constants.rs @@ -24,8 +24,8 @@ use crate::blockdata::transaction::txin::TxIn; use crate::blockdata::transaction::txout::TxOut; use crate::blockdata::witness::Witness; use crate::internal_macros::impl_bytes_newtype; -use crate::network::constants::Network; use crate::pow::CompactTarget; +use dash_network::Network; /// How many satoshis are in "one dash". pub const COIN_VALUE: u64 = 100_000_000; @@ -159,6 +159,8 @@ pub fn genesis_block(network: Network) -> Block { }, txdata, }, + // Any new network variant must be handled explicitly. + other => unreachable!("genesis_block(): unsupported network variant {other:?}"), } } @@ -211,7 +213,7 @@ mod test { use super::*; use crate::consensus::encode::serialize; use crate::internal_macros::hex; - use crate::network::constants::Network; + use dash_network::Network; #[test] fn bitcoin_genesis_first_transaction() { diff --git a/dash/src/blockdata/transaction/mod.rs b/dash/src/blockdata/transaction/mod.rs index 4cf3e42f3..bd7114f75 100644 --- a/dash/src/blockdata/transaction/mod.rs +++ b/dash/src/blockdata/transaction/mod.rs @@ -951,7 +951,7 @@ mod tests { #[test] fn test_is_coinbase() { use crate::blockdata::constants; - use crate::network::constants::Network; + use dash_network::Network; let genesis = constants::genesis_block(Network::Dash); assert!(genesis.txdata[0].is_coin_base()); diff --git a/dash/src/blockdata/transaction/outpoint.rs b/dash/src/blockdata/transaction/outpoint.rs index 3c3e5b54e..a5636c7dd 100644 --- a/dash/src/blockdata/transaction/outpoint.rs +++ b/dash/src/blockdata/transaction/outpoint.rs @@ -95,7 +95,7 @@ impl OutPoint { /// /// ```rust /// use dashcore::blockdata::constants::genesis_block; - /// use dashcore::network::constants::Network; + /// use dashcore::Network; /// /// let block = genesis_block(Network::Dash); /// let tx = &block.txdata[0]; diff --git a/dash/src/consensus/params.rs b/dash/src/consensus/params.rs index 7befc3f1a..5943564b9 100644 --- a/dash/src/consensus/params.rs +++ b/dash/src/consensus/params.rs @@ -22,7 +22,7 @@ //! use crate::Work; -use crate::network::constants::Network; +use dash_network::Network; /// Parameters that influence chain consensus. #[non_exhaustive] @@ -123,6 +123,7 @@ impl Params { allow_min_difficulty_blocks: true, no_pow_retargeting: true, }, + other => panic!("Unsupported network variant: {other:?}"), } } diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index aae0485db..0e130f57c 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -30,10 +30,10 @@ use internals::write_err; pub use secp256k1::{self, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey, constants}; use crate::hash_types::{PubkeyHash, WPubkeyHash}; -use crate::network::constants::Network; use crate::prelude::*; use crate::taproot::{TapNodeHash, TapTweakHash}; use crate::{base58, io}; +use dash_network::Network; /// A key-related error. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -401,6 +401,7 @@ impl PrivateKey { ret[0] = match self.network { Network::Dash => 204, Network::Testnet | Network::Devnet | Network::Regtest => 239, + _ => 239, }; ret[1..33].copy_from_slice(&self.inner[..]); let privkey = if self.compressed { @@ -815,7 +816,7 @@ mod tests { use super::*; use crate::address::Address; use crate::io; - use crate::network::constants::Network::{Dash, Testnet}; + use dash_network::Network::{Dash, Testnet}; #[test] fn test_key_derivation() { diff --git a/dash/src/crypto/sighash.rs b/dash/src/crypto/sighash.rs index 26824537e..d7801e4a0 100644 --- a/dash/src/crypto/sighash.rs +++ b/dash/src/crypto/sighash.rs @@ -1183,8 +1183,8 @@ mod tests { use crate::crypto::key::PublicKey; use crate::crypto::sighash::{LegacySighash, TapSighash}; use crate::internal_macros::hex; - use crate::network::constants::Network; use crate::taproot::TapLeafHash; + use dash_network::Network; #[test] fn sighash_single_bug() { diff --git a/dash/src/lib.rs b/dash/src/lib.rs index 2c88532b6..f4bb77b76 100644 --- a/dash/src/lib.rs +++ b/dash/src/lib.rs @@ -103,13 +103,15 @@ pub mod amount; pub mod base58; pub mod bip152; pub mod bip158; -pub mod bip32; +// Re-export bip32 from key-wallet +pub use key_wallet::bip32; pub mod blockdata; pub mod consensus; // Private until we either make this a crate or flatten it - still to be decided. pub mod bls_sig_utils; pub(crate) mod crypto; -mod dip9; +// Re-export dip9 from key-wallet +pub use key_wallet::dip9; pub mod ephemerealdata; pub mod error; pub mod hash_types; @@ -159,11 +161,11 @@ pub use crate::hash_types::{ TxMerkleNode, Txid, WPubkeyHash, WScriptHash, Wtxid, }; pub use crate::merkle_tree::MerkleBlock; -pub use crate::network::constants::Network; pub use crate::pow::{CompactTarget, Target, Work}; pub use crate::transaction::outpoint::OutPoint; pub use crate::transaction::txin::TxIn; pub use crate::transaction::txout::TxOut; +pub use dash_network::Network; #[cfg(not(feature = "std"))] mod io_extras { diff --git a/dash/src/network/constants.rs b/dash/src/network/constants.rs index 9a1fb31e0..220d47391 100644 --- a/dash/src/network/constants.rs +++ b/dash/src/network/constants.rs @@ -20,18 +20,14 @@ //! This module provides various constants relating to the Dash network //! protocol, such as protocol versioning and magic header bytes. //! -//! The [`Network`][1] type implements the [`Decodable`][2] and -//! [`Encodable`][3] traits and encodes the magic bytes of the given -//! network. +//! The [`Network`][1] type is now provided by the `dash_network` crate. //! -//! [1]: enum.Network.html -//! [2]: ../../consensus/encode/trait.Decodable.html -//! [3]: ../../consensus/encode/trait.Encodable.html +//! [1]: https://docs.rs/dash-network/latest/dash_network/enum.Network.html //! //! # Example: encoding a network's magic bytes //! //! ```rust -//! use dashcore::network::constants::Network; +//! use dash_network::Network; //! use dashcore::consensus::encode::serialize; //! //! let network = Network::Dash; @@ -45,8 +41,6 @@ use core::fmt::Display; use core::str::FromStr; use core::{fmt, ops}; -#[cfg(feature = "bincode")] -use bincode::{Decode, Encode}; use hashes::Hash; use internals::write_err; @@ -55,6 +49,7 @@ use crate::constants::ChainHash; use crate::error::impl_std_error; use crate::prelude::{String, ToOwned}; use crate::{BlockHash, io}; +use dash_network::Network; /// Version of the protocol as appearing in network message headers /// This constant is used to signal to other peers which features you support. @@ -73,83 +68,14 @@ use crate::{BlockHash, io}; /// 60001 - Support `pong` message and nonce in `ping` message pub const PROTOCOL_VERSION: u32 = 70220; -/// The cryptocurrency network to act on. -#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] -#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] -#[non_exhaustive] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -pub enum Network { - /// Classic Dash Core Payment Chain - Dash, - /// Dash's testnet network. - Testnet, - /// Dash's devnet network. - Devnet, - /// Bitcoin's regtest network. - Regtest, +/// Extension trait for Network to add dash-specific methods +pub trait NetworkExt { + /// The known dash genesis block hash for mainnet and testnet + fn known_genesis_block_hash(&self) -> Option; } -impl Network { - /// Creates a `Network` from the magic bytes. - /// - /// # Examples - /// - /// ```rust - /// use dashcore::network::constants::Network; - /// - /// assert_eq!(Some(Network::Dash), Network::from_magic(0xBD6B0CBF)); - /// assert_eq!(None, Network::from_magic(0xFFFFFFFF)); - /// ``` - pub fn from_magic(magic: u32) -> Option { - // Note: any new entries here must be added to `magic` below - match magic { - 0xBD6B0CBF => Some(Network::Dash), - 0xFFCAE2CE => Some(Network::Testnet), - 0xCEFFCAE2 => Some(Network::Devnet), - 0xDAB5BFFA => Some(Network::Regtest), - _ => None, - } - } - - /// Return the network magic bytes, which should be encoded little-endian - /// at the start of every message - /// - /// # Examples - /// - /// ```rust - /// use dashcore::network::constants::Network; - /// - /// let network = Network::Dash; - /// assert_eq!(network.magic(), 0xBD6B0CBF); - /// ``` - pub fn magic(self) -> u32 { - // Note: any new entries here must be added to `from_magic` above - match self { - Network::Dash => 0xBD6B0CBF, - Network::Testnet => 0xFFCAE2CE, - Network::Devnet => 0xCEFFCAE2, - Network::Regtest => 0xDAB5BFFA, - } - } - - /// The known activation height of core v20 - pub fn core_v20_activation_height(&self) -> u32 { - match self { - Network::Dash => 1987776, - Network::Testnet => 905100, - _ => 1, //todo: this might not be 1 - } - } - - /// Helper method to know if core v20 was active - pub fn core_v20_is_active_at(&self, core_block_height: u32) -> bool { - core_block_height >= self.core_v20_activation_height() - } - - /// The known dash genesis block hash for mainnet and testnet - pub fn known_genesis_block_hash(&self) -> Option { +impl NetworkExt for Network { + fn known_genesis_block_hash(&self) -> Option { match self { Network::Dash => { let mut block_hash = @@ -167,53 +93,11 @@ impl Network { } Network::Devnet => None, Network::Regtest => None, + _ => None, } } } -/// An error in parsing network string. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ParseNetworkError(String); - -impl fmt::Display for ParseNetworkError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write_err!(f, "failed to parse {} as network", self.0; self) - } -} -impl_std_error!(ParseNetworkError); - -impl FromStr for Network { - type Err = ParseNetworkError; - - #[inline] - fn from_str(s: &str) -> Result { - use Network::*; - - let network = match s { - "dash" => Dash, - "testnet" => Testnet, - "devnet" => Devnet, - "regtest" => Regtest, - _ => return Err(ParseNetworkError(s.to_owned())), - }; - Ok(network) - } -} - -impl fmt::Display for Network { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use Network::*; - - let s = match *self { - Dash => "dash", - Testnet => "testnet", - Devnet => "devnet", - Regtest => "regtest", - }; - write!(f, "{}", s) - } -} - /// Error in parsing network from chain hash. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnknownChainHash(ChainHash); @@ -411,8 +295,9 @@ impl Decodable for ServiceFlags { #[cfg(test)] mod tests { - use super::{Network, ServiceFlags}; + use super::ServiceFlags; use crate::consensus::encode::{deserialize, serialize}; + use dash_network::Network; #[test] fn serialize_test() { diff --git a/dash/src/psbt/map/global.rs b/dash/src/psbt/map/global.rs index 328a2ed7c..a0f560fd5 100644 --- a/dash/src/psbt/map/global.rs +++ b/dash/src/psbt/map/global.rs @@ -57,7 +57,7 @@ impl Map for PartiallySignedTransaction { }, value: { let mut ret = Vec::with_capacity(4 + derivation.len() * 4); - ret.extend(fingerprint.as_bytes()); + ret.extend(fingerprint.to_bytes()); derivation.into_iter().for_each(|n| ret.extend(&u32::from(*n).to_le_bytes())); ret }, diff --git a/dash/src/psbt/mod.rs b/dash/src/psbt/mod.rs index 49c33f3ec..73a7f5ca1 100644 --- a/dash/src/psbt/mod.rs +++ b/dash/src/psbt/mod.rs @@ -12,6 +12,7 @@ use core::{cmp, fmt}; use std::collections::{HashMap, HashSet}; use crate::Amount; +use crate::Network; use crate::bip32::{self, ExtendedPrivKey, ExtendedPubKey, KeySource}; use crate::blockdata::script::ScriptBuf; use crate::blockdata::transaction::Transaction; @@ -513,7 +514,11 @@ impl GetKey for ExtendedPrivKey { KeyRequest::Bip32((fingerprint, path)) => { let key = if self.fingerprint(secp) == fingerprint { let k = self.derive_priv(secp, &path)?; - Some(k.to_priv()) + Some(PrivateKey { + compressed: true, + network: k.network.into(), + inner: k.private_key, + }) } else { None }; @@ -547,7 +552,11 @@ impl GetKey for $set { for xpriv in self.iter() { if xpriv.parent_fingerprint == fingerprint { let k = xpriv.derive_priv(secp, &path)?; - return Ok(Some(k.to_priv())); + return Ok(Some(PrivateKey { + compressed: true, + network: k.network.into(), + inner: k.private_key, + })); } } Ok(None) @@ -582,7 +591,7 @@ impl_get_key_for_map!(BTreeMap); impl_get_key_for_map!(HashMap); /// Errors when getting a key. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum GetKeyError { /// A bip32 error. @@ -829,7 +838,6 @@ mod tests { use secp256k1::{All, SecretKey}; use super::*; - use crate::Network::Dash; use crate::bip32::{ChildNumber, ExtendedPrivKey, ExtendedPubKey, KeySource}; use crate::blockdata::script::ScriptBuf; use crate::blockdata::transaction::Transaction; @@ -879,7 +887,8 @@ mod tests { let mut hd_keypaths: BTreeMap = Default::default(); - let mut sk: ExtendedPrivKey = ExtendedPrivKey::new_master(Dash, &seed).unwrap(); + let mut sk: ExtendedPrivKey = + ExtendedPrivKey::new_master(key_wallet::Network::Dash, &seed).unwrap(); let fprint = sk.fingerprint(secp); diff --git a/dash/src/sml/llmq_type/mod.rs b/dash/src/sml/llmq_type/mod.rs index 942587ef0..a3d8255bc 100644 --- a/dash/src/sml/llmq_type/mod.rs +++ b/dash/src/sml/llmq_type/mod.rs @@ -1,4 +1,4 @@ -mod network; +pub mod network; pub mod rotation; use std::fmt::{Display, Formatter}; @@ -7,8 +7,8 @@ use std::io; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; -use crate::Network; use crate::consensus::{Decodable, Encodable, encode}; +use dash_network::Network; #[repr(C)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Hash, Ord)] diff --git a/dash/src/sml/llmq_type/network.rs b/dash/src/sml/llmq_type/network.rs index 46f476ecb..870cf8ae2 100644 --- a/dash/src/sml/llmq_type/network.rs +++ b/dash/src/sml/llmq_type/network.rs @@ -1,40 +1,52 @@ -use crate::Network; use crate::sml::llmq_type::LLMQType; +use dash_network::Network; -impl Network { - pub fn is_llmq_type(&self) -> LLMQType { +/// Extension trait for Network to add LLMQ-specific methods +pub trait NetworkLLMQExt { + fn is_llmq_type(&self) -> LLMQType; + fn isd_llmq_type(&self) -> LLMQType; + fn chain_locks_type(&self) -> LLMQType; + fn platform_type(&self) -> LLMQType; +} + +impl NetworkLLMQExt for Network { + fn is_llmq_type(&self) -> LLMQType { match self { Network::Dash => LLMQType::Llmqtype50_60, Network::Testnet => LLMQType::Llmqtype50_60, Network::Devnet => LLMQType::LlmqtypeDevnet, Network::Regtest => LLMQType::LlmqtypeTestInstantSend, + other => unreachable!("Unsupported network variant {other:?}"), } } - pub fn isd_llmq_type(&self) -> LLMQType { + fn isd_llmq_type(&self) -> LLMQType { match self { Network::Dash => LLMQType::Llmqtype60_75, Network::Testnet => LLMQType::Llmqtype60_75, Network::Devnet => LLMQType::LlmqtypeDevnetDIP0024, Network::Regtest => LLMQType::LlmqtypeTestDIP0024, + other => unreachable!("Unsupported network variant {other:?}"), } } - pub fn chain_locks_type(&self) -> LLMQType { + fn chain_locks_type(&self) -> LLMQType { match self { Network::Dash => LLMQType::Llmqtype400_60, Network::Testnet => LLMQType::Llmqtype50_60, Network::Devnet => LLMQType::LlmqtypeDevnet, Network::Regtest => LLMQType::LlmqtypeTest, + other => unreachable!("Unsupported network variant {other:?}"), } } - pub fn platform_type(&self) -> LLMQType { + fn platform_type(&self) -> LLMQType { match self { Network::Dash => LLMQType::Llmqtype100_67, Network::Testnet => LLMQType::Llmqtype25_67, Network::Devnet => LLMQType::LlmqtypeDevnet, Network::Regtest => LLMQType::LlmqtypeTest, + other => unreachable!("Unsupported network variant {other:?}"), } } } diff --git a/dash/src/sml/masternode_list/from_diff.rs b/dash/src/sml/masternode_list/from_diff.rs index 22a2ea4da..95500010e 100644 --- a/dash/src/sml/masternode_list/from_diff.rs +++ b/dash/src/sml/masternode_list/from_diff.rs @@ -1,4 +1,6 @@ +use crate::BlockHash; use crate::bls_sig_utils::BLSSignature; +use crate::network::constants::NetworkExt; use crate::network::message_sml::MnListDiff; use crate::sml::error::SmlError; use crate::sml::llmq_entry_verification::{ @@ -8,7 +10,7 @@ use crate::sml::masternode_list::MasternodeList; use crate::sml::quorum_entry::qualified_quorum_entry::{ QualifiedQuorumEntry, VerifyingChainLockSignaturesType, }; -use crate::{BlockHash, Network}; +use dash_network::Network; use hashes::Hash; use std::collections::BTreeMap; diff --git a/dash/src/sml/masternode_list/quorum_helpers.rs b/dash/src/sml/masternode_list/quorum_helpers.rs index 98f91b456..60dc95d14 100644 --- a/dash/src/sml/masternode_list/quorum_helpers.rs +++ b/dash/src/sml/masternode_list/quorum_helpers.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use crate::hash_types::QuorumOrderingHash; use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus; -use crate::sml::llmq_type::LLMQType; +use crate::sml::llmq_type::{LLMQType, network::NetworkLLMQExt}; use crate::sml::masternode_list::MasternodeList; use crate::sml::message_verification_error::MessageVerificationError; use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; diff --git a/dash/src/sml/masternode_list/scores_for_quorum.rs b/dash/src/sml/masternode_list/scores_for_quorum.rs index 0b39f79dc..f579da4e5 100644 --- a/dash/src/sml/masternode_list/scores_for_quorum.rs +++ b/dash/src/sml/masternode_list/scores_for_quorum.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use crate::Network; use crate::hash_types::{QuorumModifierHash, ScoreHash}; use crate::network::message_qrinfo::QuorumSnapshot; -use crate::sml::llmq_type::LLMQType; +use crate::sml::llmq_type::{LLMQType, network::NetworkLLMQExt}; use crate::sml::masternode_list::MasternodeList; use crate::sml::masternode_list_entry::EntryMasternodeType; use crate::sml::masternode_list_entry::qualified_masternode_list_entry::QualifiedMasternodeListEntry; diff --git a/dash/src/sml/masternode_list_engine/message_request_verification.rs b/dash/src/sml/masternode_list_engine/message_request_verification.rs index 40df4778c..c36edb8d3 100644 --- a/dash/src/sml/masternode_list_engine/message_request_verification.rs +++ b/dash/src/sml/masternode_list_engine/message_request_verification.rs @@ -1,6 +1,7 @@ use hashes::Hash; use crate::hash_types::QuorumOrderingHash; +use crate::sml::llmq_type::network::NetworkLLMQExt; use crate::sml::masternode_list::MasternodeList; use crate::sml::masternode_list_engine::MasternodeListEngine; use crate::sml::message_verification_error::MessageVerificationError; @@ -351,7 +352,7 @@ mod tests { use crate::consensus::deserialize; use crate::hashes::Hash; use crate::hashes::hex::FromHex; - use crate::sml::llmq_type::LLMQType; + use crate::sml::llmq_type::{LLMQType, network::NetworkLLMQExt}; use crate::sml::masternode_list_engine::MasternodeListEngine; use crate::{BlockHash, ChainLock, InstantLock, QuorumHash}; diff --git a/dash/src/sml/masternode_list_engine/mod.rs b/dash/src/sml/masternode_list_engine/mod.rs index fd1ac4290..6b3812ebb 100644 --- a/dash/src/sml/masternode_list_engine/mod.rs +++ b/dash/src/sml/masternode_list_engine/mod.rs @@ -9,12 +9,13 @@ mod validation; use std::collections::{BTreeMap, BTreeSet}; use crate::bls_sig_utils::{BLSPublicKey, BLSSignature}; +use crate::network::constants::NetworkExt; use crate::network::message_qrinfo::{QRInfo, QuorumSnapshot}; use crate::network::message_sml::MnListDiff; use crate::prelude::CoreBlockHeight; use crate::sml::error::SmlError; use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus; -use crate::sml::llmq_type::LLMQType; +use crate::sml::llmq_type::{LLMQType, network::NetworkLLMQExt}; use crate::sml::masternode_list::MasternodeList; use crate::sml::masternode_list::from_diff::TryIntoWithBlockHashLookup; use crate::sml::quorum_entry::qualified_quorum_entry::{ @@ -22,9 +23,10 @@ use crate::sml::quorum_entry::qualified_quorum_entry::{ }; use crate::sml::quorum_validation_error::{ClientDataRetrievalError, QuorumValidationError}; use crate::transaction::special_transaction::quorum_commitment::QuorumEntry; -use crate::{BlockHash, Network, QuorumHash}; +use crate::{BlockHash, QuorumHash}; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; +use dash_network::Network; use hashes::Hash; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; diff --git a/dash/tests/psbt.rs b/dash/tests/psbt.rs index a5e5d8591..e6f0fc9f3 100644 --- a/dash/tests/psbt.rs +++ b/dash/tests/psbt.rs @@ -131,7 +131,9 @@ fn build_extended_private_key() -> ExtendedPrivKey { let xpriv = ExtendedPrivKey::from_str(extended_private_key).unwrap(); let sk = PrivateKey::from_wif(seed).unwrap(); - let seeded = ExtendedPrivKey::new_master(NETWORK, &sk.inner.secret_bytes()).unwrap(); + let seeded = + ExtendedPrivKey::new_master(key_wallet::Network::Testnet, &sk.inner.secret_bytes()) + .unwrap(); assert_eq!(xpriv, seeded); xpriv @@ -326,8 +328,12 @@ fn parse_and_verify_keys( let path = derivation_path.into_derivation_path().expect("failed to convert derivation path"); - let derived_priv = - ext_priv.derive_priv(secp, &path).expect("failed to derive ext priv key").to_priv(); + let ext_derived = ext_priv.derive_priv(secp, &path).expect("failed to derive ext priv key"); + let derived_priv = PrivateKey { + compressed: true, + network: ext_derived.network.into(), + inner: ext_derived.private_key, + }; assert_eq!(wif_priv, derived_priv); let derived_pub = derived_priv.public_key(secp); key_map.insert(derived_pub, derived_priv); diff --git a/fuzz/fuzz_targets/dash/deserialize_script.rs b/fuzz/fuzz_targets/dash/deserialize_script.rs index a8959ff97..09cab64d4 100644 --- a/fuzz/fuzz_targets/dash/deserialize_script.rs +++ b/fuzz/fuzz_targets/dash/deserialize_script.rs @@ -1,7 +1,7 @@ +use dashcore::Network; use dashcore::address::Address; use dashcore::blockdata::script; use dashcore::consensus::encode; -use dashcore::network::constants::Network; use honggfuzz::fuzz; fn do_test(data: &[u8]) { diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml new file mode 100644 index 000000000..2dd39558e --- /dev/null +++ b/key-wallet-ffi/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "key-wallet-ffi" +version = "0.39.6" +authors = ["The Dash Core Developers"] +edition = "2021" +description = "FFI bindings for key-wallet library" +keywords = ["dash", "wallet", "ffi", "bindings"] +readme = "README.md" +license = "CC0-1.0" + +[lib] +name = "key_wallet_ffi" +crate-type = ["cdylib", "staticlib", "lib"] + +[features] +default = [] + +[dependencies] +key-wallet = { path = "../key-wallet", default-features = false, features = ["std"] } +dash-network-ffi = { path = "../dash-network-ffi" } +secp256k1 = { version = "0.30.0", features = ["global-context"] } +uniffi = { version = "0.29.3", features = ["cli"] } +thiserror = "2.0.12" + +[build-dependencies] +uniffi = { version = "0.29.3", features = ["build"] } + +[dev-dependencies] +uniffi = { version = "0.29.3", features = ["bindgen-tests"] } \ No newline at end of file diff --git a/key-wallet-ffi/README.md b/key-wallet-ffi/README.md new file mode 100644 index 000000000..a062210bd --- /dev/null +++ b/key-wallet-ffi/README.md @@ -0,0 +1,144 @@ +# Key Wallet FFI + +FFI bindings for the key-wallet library, providing a C-compatible interface for use in other languages like Swift, Kotlin, Python, etc. + +## Features + +- **UniFFI bindings**: Automatic generation of language bindings +- **Memory-safe**: Rust's ownership model ensures memory safety across FFI boundary +- **Thread-safe**: All exposed types are thread-safe +- **Error handling**: Proper error propagation across language boundaries + +## Supported Languages + +Through UniFFI, this library can generate bindings for: +- Swift (iOS/macOS) +- Kotlin (Android) +- Python +- Ruby + +## Building + +### Prerequisites + +- Rust 1.70+ +- For iOS: Xcode and cargo-lipo +- For Android: Android NDK + +### Generate bindings + +```bash +# Generate Swift bindings +cargo run --features uniffi/cli --bin uniffi-bindgen generate src/key_wallet.udl --language swift + +# Generate Kotlin bindings +cargo run --features uniffi/cli --bin uniffi-bindgen generate src/key_wallet.udl --language kotlin + +# Generate Python bindings +cargo run --features uniffi/cli --bin uniffi-bindgen generate src/key_wallet.udl --language python +``` + +### Build libraries + +```bash +# Build for current platform +cargo build --release + +# Build for iOS (requires cargo-lipo) +cargo lipo --release + +# Build for Android (requires cargo-ndk) +cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 -o ./jniLibs build --release +``` + +## Usage Examples + +### Swift + +```swift +import KeyWalletFFI + +// Create mnemonic +let mnemonic = try Mnemonic(wordCount: 12, language: .english) + +// Create wallet +let wallet = try HDWallet.fromMnemonic( + mnemonic: mnemonic, + passphrase: "", + network: .dash +) + +// Derive address +let account = try wallet.getBip44Account(account: 0) +let firstAddress = try wallet.derivePub(path: "m/44'/5'/0'/0/0") +``` + +### Kotlin + +```kotlin +import com.dash.keywallet.* + +// Create mnemonic +val mnemonic = Mnemonic.fromPhrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language.ENGLISH +) + +// Create wallet +val wallet = HDWallet.fromMnemonic(mnemonic, "", Network.DASH) + +// Generate addresses +val generator = AddressGenerator(Network.DASH) +val addresses = generator.generateRange(accountXpub, true, 0u, 10u) +``` + +### Python + +```python +from key_wallet_ffi import * + +# Create mnemonic +mnemonic = Mnemonic.from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language.ENGLISH +) + +# Create wallet +wallet = HDWallet.from_mnemonic(mnemonic, "", Network.DASH) + +# Get first address +first_addr = wallet.derive_pub("m/44'/5'/0'/0/0") +``` + +## API Reference + +### Core Types + +- `Mnemonic`: BIP39 mnemonic phrase handling +- `HDWallet`: Hierarchical deterministic wallet +- `ExtendedKey`: Extended public/private keys +- `Address`: Dash address encoding/decoding +- `AddressGenerator`: Bulk address generation + +### Enums + +- `Network`: Dash, Testnet, Regtest, Devnet +- `Language`: Supported mnemonic languages +- `AddressType`: P2PKH, P2SH + +### Error Handling + +All methods that can fail return a `Result` type with specific error variants: +- `InvalidMnemonic` +- `InvalidDerivationPath` +- `InvalidAddress` +- `Bip32Error` +- `KeyError` + +## Thread Safety + +All exposed types are `Send + Sync` and wrapped in `Arc` for thread-safe reference counting. + +## License + +This project is licensed under the CC0 1.0 Universal license. \ No newline at end of file diff --git a/key-wallet-ffi/build-ios.sh b/key-wallet-ffi/build-ios.sh new file mode 100755 index 000000000..57f6bc893 --- /dev/null +++ b/key-wallet-ffi/build-ios.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Build script for key-wallet-ffi iOS targets + +set -e + +echo "Building key-wallet-ffi for iOS..." + +# Ensure we have the required iOS targets +rustup target add aarch64-apple-ios aarch64-apple-ios-sim + +# Build for iOS devices (arm64) +echo "Building for iOS devices (arm64)..." +cargo build --release --target aarch64-apple-ios + +# Build for iOS simulator (arm64 - Apple Silicon Macs) +echo "Building for iOS simulator (arm64)..." +cargo build --release --target aarch64-apple-ios-sim + +# Create output directory +echo "Creating output directory..." +mkdir -p target/universal/release + +# Copy simulator library (no need for lipo since we only have one architecture) +cp target/aarch64-apple-ios-sim/release/libkey_wallet_ffi.a target/universal/release/libkey_wallet_ffi_sim.a + +# Copy device library +cp target/aarch64-apple-ios/release/libkey_wallet_ffi.a target/universal/release/libkey_wallet_ffi_device.a + +# Generate Swift bindings +echo "Generating Swift bindings..." +cargo run --features uniffi/cli --bin uniffi-bindgen generate \ + src/key_wallet.udl \ + --language swift \ + --out-dir target/swift-bindings + +echo "Build complete!" +echo "Libraries available at:" +echo " - Device: target/universal/release/libkey_wallet_ffi_device.a" +echo " - Simulator: target/universal/release/libkey_wallet_ffi_sim.a" +echo " - Swift bindings: target/swift-bindings/" \ No newline at end of file diff --git a/key-wallet-ffi/build.rs b/key-wallet-ffi/build.rs new file mode 100644 index 000000000..75b352973 --- /dev/null +++ b/key-wallet-ffi/build.rs @@ -0,0 +1,4 @@ +fn main() { + println!("cargo:rerun-if-changed=src/key_wallet.udl"); + uniffi::generate_scaffolding("src/key_wallet.udl").unwrap(); +} diff --git a/key-wallet-ffi/src/key_wallet.udl b/key-wallet-ffi/src/key_wallet.udl new file mode 100644 index 000000000..26d8d8210 --- /dev/null +++ b/key-wallet-ffi/src/key_wallet.udl @@ -0,0 +1,180 @@ +namespace key_wallet_ffi { + // Initialize the library (for any global setup) + void initialize(); + + // Validate a mnemonic phrase + [Throws=KeyWalletError] + boolean validate_mnemonic(string phrase, Language language); +}; + +// Network enum +enum Network { + "Dash", + "Testnet", + "Regtest", + "Devnet", +}; + +// Language enum for mnemonics +enum Language { + "English", + "ChineseSimplified", + "ChineseTraditional", + "French", + "Italian", + "Japanese", + "Korean", + "Spanish", +}; + +// Address type enum +enum AddressType { + "P2PKH", + "P2SH", +}; + +// Error types +[Error] +enum KeyWalletError { + "InvalidMnemonic", + "InvalidDerivationPath", + "KeyError", + "Secp256k1Error", + "AddressError", +}; + +// Derivation path type +dictionary DerivationPath { + string path; +}; + +// Account extended keys +dictionary AccountXPriv { + string derivation_path; + string xpriv; +}; + +dictionary AccountXPub { + string derivation_path; + string xpub; + sequence? pub_key; +}; + +// Mnemonic interface +interface Mnemonic { + // Create from phrase + [Throws=KeyWalletError, Name="new"] + constructor(string phrase, Language language); + + // Generate a new mnemonic + [Throws=KeyWalletError, Name="generate"] + constructor(Language language, u8 word_count); + + // Get the phrase + string phrase(); + + // Convert to seed with optional passphrase + sequence to_seed(string passphrase); +}; + +// HD Wallet interface +interface HDWallet { + // Create from seed + [Throws=KeyWalletError, Name="from_seed"] + constructor(sequence seed, Network network); + + // Create from mnemonic + [Throws=KeyWalletError, Name="from_mnemonic"] + constructor(Mnemonic mnemonic, string passphrase, Network network); + + // Get account extended private key + [Throws=KeyWalletError] + AccountXPriv get_account_xpriv(u32 account); + + // Get account extended public key + [Throws=KeyWalletError] + AccountXPub get_account_xpub(u32 account); + + // Get identity authentication key at index + [Throws=KeyWalletError] + sequence get_identity_authentication_key_at_index(u32 identity_index, u32 key_index); + + // Derive a key at path + [Throws=KeyWalletError] + string derive_xpriv(string path); + + // Derive a public key at path + [Throws=KeyWalletError] + AccountXPub derive_xpub(string path); +}; + +// Extended Private Key interface +interface ExtPrivKey { + // Create from string + [Throws=KeyWalletError, Name="from_string"] + constructor(string xpriv); + + // Get extended public key + AccountXPub get_xpub(); + + // Derive child + [Throws=KeyWalletError] + ExtPrivKey derive_child(u32 index, boolean hardened); + + // Serialize to string + string to_string(); +}; + +// Extended Public Key interface +interface ExtPubKey { + // Create from string + [Throws=KeyWalletError, Name="from_string"] + constructor(string xpub); + + // Derive child + [Throws=KeyWalletError] + ExtPubKey derive_child(u32 index); + + // Get public key bytes + sequence get_public_key(); + + // Serialize to string + string to_string(); +}; + +// Address interface +interface Address { + // Parse from string + [Throws=KeyWalletError, Name="from_string"] + constructor(string address, Network network); + + // Create from public key + [Throws=KeyWalletError, Name="from_public_key"] + constructor(sequence public_key, Network network); + + // Get string representation + string to_string(); + + // Get address type + AddressType get_type(); + + // Get network + Network get_network(); + + // Get script pubkey + sequence get_script_pubkey(); +}; + +// Address generator interface +interface AddressGenerator { + // Create new generator + constructor(Network network); + + // Generate address + [Throws=KeyWalletError] + Address generate(AccountXPub account_xpub, boolean external, u32 index); + + // Generate a range of addresses + [Throws=KeyWalletError] + sequence
generate_range(AccountXPub account_xpub, boolean external, u32 start, u32 count); +}; \ No newline at end of file diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs new file mode 100644 index 000000000..a15cd4e48 --- /dev/null +++ b/key-wallet-ffi/src/lib.rs @@ -0,0 +1,615 @@ +//! FFI bindings for key-wallet library + +use std::str::FromStr; +use std::sync::Arc; + +use key_wallet::{ + self as kw, address as kw_address, derivation::HDWallet as KwHDWallet, mnemonic as kw_mnemonic, + DerivationPath as KwDerivationPath, ExtendedPrivKey, ExtendedPubKey, Network as KwNetwork, +}; +use secp256k1::{PublicKey, Secp256k1}; + +// Include the UniFFI scaffolding +uniffi::include_scaffolding!("key_wallet"); + +#[cfg(test)] +mod lib_tests; + +// Initialize function +pub fn initialize() { + // Any global initialization if needed +} + +// Re-export enums for UniFFI +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Network { + Dash = 0, + Testnet = 1, + Regtest = 2, + Devnet = 3, +} + +impl From for key_wallet::Network { + fn from(n: Network) -> Self { + match n { + Network::Dash => key_wallet::Network::Dash, + Network::Testnet => key_wallet::Network::Testnet, + Network::Regtest => key_wallet::Network::Regtest, + Network::Devnet => key_wallet::Network::Devnet, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + English, + ChineseSimplified, + ChineseTraditional, + French, + Italian, + Japanese, + Korean, + Spanish, +} + +impl From for kw_mnemonic::Language { + fn from(l: Language) -> Self { + match l { + Language::English => kw_mnemonic::Language::English, + Language::ChineseSimplified => kw_mnemonic::Language::ChineseSimplified, + Language::ChineseTraditional => kw_mnemonic::Language::ChineseTraditional, + Language::French => kw_mnemonic::Language::French, + Language::Italian => kw_mnemonic::Language::Italian, + Language::Japanese => kw_mnemonic::Language::Japanese, + Language::Korean => kw_mnemonic::Language::Korean, + Language::Spanish => kw_mnemonic::Language::Spanish, + } + } +} + +// Define address type for FFI +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressType { + P2PKH, + P2SH, +} + +impl From for AddressType { + fn from(t: kw_address::AddressType) -> Self { + match t { + kw_address::AddressType::P2PKH => AddressType::P2PKH, + kw_address::AddressType::P2SH => AddressType::P2SH, + } + } +} + +impl From for kw_address::AddressType { + fn from(t: AddressType) -> Self { + match t { + AddressType::P2PKH => kw_address::AddressType::P2PKH, + AddressType::P2SH => kw_address::AddressType::P2SH, + } + } +} + +// Define derivation path type +pub struct DerivationPath { + pub path: String, +} + +impl DerivationPath { + pub fn new(path: String) -> Result { + // Validate the path by trying to parse it + KwDerivationPath::from_str(&path).map_err(|e| KeyWalletError::InvalidDerivationPath { + message: e.to_string(), + })?; + Ok(Self { + path, + }) + } +} + +// Define account extended keys +pub struct AccountXPriv { + pub derivation_path: String, + pub xpriv: String, +} + +#[derive(Clone)] +pub struct AccountXPub { + pub derivation_path: String, + pub xpub: String, + pub pub_key: Option>, +} + +impl AccountXPub { + pub fn new(derivation_path: String, xpub: String) -> Self { + Self { + derivation_path, + xpub, + pub_key: None, + } + } +} + +// Custom error type for FFI +#[derive(Debug, Clone, thiserror::Error)] +pub enum KeyWalletError { + #[error("Invalid mnemonic: {message}")] + InvalidMnemonic { + message: String, + }, + + #[error("Invalid derivation path: {message}")] + InvalidDerivationPath { + message: String, + }, + + #[error("Key error: {message}")] + KeyError { + message: String, + }, + + #[error("Secp256k1 error: {message}")] + Secp256k1Error { + message: String, + }, + + #[error("Address error: {message}")] + AddressError { + message: String, + }, +} + +impl From for KeyWalletError { + fn from(e: kw::Error) -> Self { + match e { + kw::Error::InvalidMnemonic(msg) => KeyWalletError::InvalidMnemonic { + message: msg, + }, + kw::Error::InvalidDerivationPath(msg) => KeyWalletError::InvalidDerivationPath { + message: msg, + }, + kw::Error::Bip32(err) => KeyWalletError::KeyError { + message: err.to_string(), + }, + kw::Error::Secp256k1(err) => KeyWalletError::Secp256k1Error { + message: err.to_string(), + }, + kw::Error::InvalidAddress(msg) => KeyWalletError::AddressError { + message: msg, + }, + kw::Error::Base58 => KeyWalletError::AddressError { + message: "Base58 encoding error".into(), + }, + kw::Error::InvalidNetwork => KeyWalletError::AddressError { + message: "Invalid network".into(), + }, + kw::Error::KeyError(msg) => KeyWalletError::KeyError { + message: msg, + }, + } + } +} + +impl From for KeyWalletError { + fn from(e: kw::bip32::Error) -> Self { + KeyWalletError::KeyError { + message: e.to_string(), + } + } +} + +// Validate mnemonic function +pub fn validate_mnemonic(phrase: String, language: Language) -> Result { + Ok(kw::Mnemonic::validate(&phrase, language.into())) +} + +// Mnemonic wrapper +pub struct Mnemonic { + inner: kw::Mnemonic, +} + +impl Mnemonic { + pub fn new(phrase: String, language: Language) -> Result { + let inner = kw::Mnemonic::from_phrase(&phrase, language.into()) + .map_err(|e| KeyWalletError::from(e))?; + Ok(Self { + inner, + }) + } + + pub fn generate(language: Language, word_count: u8) -> Result { + let inner = kw::Mnemonic::generate(word_count as usize, language.into()) + .map_err(|e| KeyWalletError::from(e))?; + Ok(Self { + inner, + }) + } + + pub fn phrase(&self) -> String { + self.inner.phrase() + } + + pub fn to_seed(&self, passphrase: String) -> Vec { + self.inner.to_seed(&passphrase).to_vec() + } +} + +// HD Wallet wrapper +pub struct HDWallet { + inner: KwHDWallet, + network: Network, +} + +impl HDWallet { + pub fn from_mnemonic( + mnemonic: Arc, + passphrase: String, + network: Network, + ) -> Result { + let seed = mnemonic.to_seed(passphrase); + Self::from_seed(seed, network) + } + + pub fn from_seed(seed: Vec, network: Network) -> Result { + let inner = + KwHDWallet::from_seed(&seed, network.into()).map_err(|e| KeyWalletError::from(e))?; + Ok(Self { + inner, + network, + }) + } + + pub fn get_account_xpriv(&self, account: u32) -> Result { + let account_key = self.inner.bip44_account(account).map_err(|e| KeyWalletError::from(e))?; + + // Use correct coin type based on network + let coin_type = match self.network { + Network::Dash => 5, // Dash mainnet + _ => 1, // Testnet/devnet/regtest + }; + let derivation_path = format!("m/44'/{}'/{}'", coin_type, account); + + Ok(AccountXPriv { + derivation_path, + xpriv: account_key.to_string(), + }) + } + + pub fn get_account_xpub(&self, account: u32) -> Result { + let account_key = self.inner.bip44_account(account).map_err(|e| KeyWalletError::from(e))?; + + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &account_key); + + // Use correct coin type based on network + let coin_type = match self.network { + Network::Dash => 5, // Dash mainnet + _ => 1, // Testnet/devnet/regtest + }; + let derivation_path = format!("m/44'/{}'/{}'", coin_type, account); + + Ok(AccountXPub { + derivation_path, + xpub: xpub.to_string(), + pub_key: Some(xpub.public_key.serialize().to_vec()), + }) + } + + pub fn get_identity_authentication_key_at_index( + &self, + identity_index: u32, + key_index: u32, + ) -> Result, KeyWalletError> { + let key = self + .inner + .identity_authentication_key(identity_index, key_index) + .map_err(|e| KeyWalletError::from(e))?; + Ok(key.private_key[..].to_vec()) + } + + pub fn derive_xpriv(&self, path: String) -> Result { + let derivation_path = KwDerivationPath::from_str(&path).map_err(|e| { + KeyWalletError::InvalidDerivationPath { + message: e.to_string(), + } + })?; + + let xpriv = self.inner.derive(&derivation_path).map_err(|e| KeyWalletError::from(e))?; + + Ok(xpriv.to_string()) + } + + pub fn derive_xpub(&self, path: String) -> Result { + let derivation_path = KwDerivationPath::from_str(&path).map_err(|e| { + KeyWalletError::InvalidDerivationPath { + message: e.to_string(), + } + })?; + + let xpub = self.inner.derive_pub(&derivation_path).map_err(|e| KeyWalletError::from(e))?; + + Ok(AccountXPub { + derivation_path: path, + xpub: xpub.to_string(), + pub_key: Some(xpub.public_key.serialize().to_vec()), + }) + } +} + +// Extended Private Key wrapper +pub struct ExtPrivKey { + inner: ExtendedPrivKey, +} + +impl ExtPrivKey { + pub fn from_string(xpriv: String) -> Result { + let inner = ExtendedPrivKey::from_str(&xpriv).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + Ok(Self { + inner, + }) + } + + pub fn get_xpub(&self) -> AccountXPub { + let secp = Secp256k1::new(); + let xpub = ExtendedPubKey::from_priv(&secp, &self.inner); + + AccountXPub { + derivation_path: String::new(), + xpub: xpub.to_string(), + pub_key: Some(xpub.public_key.serialize().to_vec()), + } + } + + pub fn derive_child( + &self, + index: u32, + hardened: bool, + ) -> Result, KeyWalletError> { + let child_number = if hardened { + kw::ChildNumber::from_hardened_idx(index) + } else { + kw::ChildNumber::from_normal_idx(index) + } + .map_err(|e| KeyWalletError::InvalidDerivationPath { + message: e.to_string(), + })?; + + let secp = Secp256k1::new(); + let child = + self.inner.ckd_priv(&secp, child_number).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + + Ok(Arc::new(ExtPrivKey { + inner: child, + })) + } + + pub fn to_string(&self) -> String { + self.inner.to_string() + } +} + +// Extended Public Key wrapper +pub struct ExtPubKey { + inner: ExtendedPubKey, +} + +impl ExtPubKey { + pub fn from_string(xpub: String) -> Result { + let inner = ExtendedPubKey::from_str(&xpub).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + Ok(Self { + inner, + }) + } + + pub fn derive_child(&self, index: u32) -> Result, KeyWalletError> { + let child_number = kw::ChildNumber::from_normal_idx(index).map_err(|e| { + KeyWalletError::InvalidDerivationPath { + message: e.to_string(), + } + })?; + + let secp = Secp256k1::new(); + let child = + self.inner.ckd_pub(&secp, child_number).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + + Ok(Arc::new(ExtPubKey { + inner: child, + })) + } + + pub fn get_public_key(&self) -> Vec { + self.inner.public_key.serialize().to_vec() + } + + pub fn to_string(&self) -> String { + self.inner.to_string() + } +} + +// Address wrapper +pub struct Address { + inner: kw_address::Address, +} + +impl Address { + pub fn from_string(address: String, network: Network) -> Result { + let inner = kw_address::Address::from_str(&address).map_err(|e| KeyWalletError::from(e))?; + + // Validate that the parsed network matches the expected network + // Note: Testnet, Devnet, and Regtest all share the same address prefixes (140/19) + // so we need to be flexible when comparing these networks + let parsed_network: KwNetwork = inner.network; + let expected_network: KwNetwork = network.into(); + + let networks_compatible = match (parsed_network, expected_network) { + // Exact matches are always OK + (n1, n2) if n1 == n2 => true, + // Testnet addresses can be used on devnet/regtest and vice versa + (KwNetwork::Testnet, KwNetwork::Devnet) + | (KwNetwork::Testnet, KwNetwork::Regtest) + | (KwNetwork::Devnet, KwNetwork::Testnet) + | (KwNetwork::Devnet, KwNetwork::Regtest) + | (KwNetwork::Regtest, KwNetwork::Testnet) + | (KwNetwork::Regtest, KwNetwork::Devnet) => true, + // All other combinations are incompatible + _ => false, + }; + + if !networks_compatible { + return Err(KeyWalletError::AddressError { + message: format!( + "Address is for network {:?}, expected {:?}", + inner.network, network + ), + }); + } + + Ok(Self { + inner, + }) + } + + pub fn from_public_key(public_key: Vec, network: Network) -> Result { + let pubkey = + PublicKey::from_slice(&public_key).map_err(|e| KeyWalletError::Secp256k1Error { + message: e.to_string(), + })?; + let inner = kw_address::Address::p2pkh(&pubkey, network.into()); + Ok(Self { + inner, + }) + } + + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn get_type(&self) -> AddressType { + self.inner.address_type.into() + } + + pub fn get_network(&self) -> Network { + match self.inner.network { + KwNetwork::Dash => Network::Dash, + KwNetwork::Testnet => Network::Testnet, + KwNetwork::Regtest => Network::Regtest, + KwNetwork::Devnet => Network::Devnet, + unknown => unreachable!("Unhandled network variant: {:?}", unknown), + } + } + + pub fn get_script_pubkey(&self) -> Vec { + self.inner.script_pubkey() + } +} + +// Address generator wrapper +pub struct AddressGenerator { + inner: kw_address::AddressGenerator, +} + +impl AddressGenerator { + pub fn new(network: Network) -> Self { + Self { + inner: kw_address::AddressGenerator::new(network.into()), + } + } + + pub fn generate( + &self, + account_xpub: AccountXPub, + external: bool, + index: u32, + ) -> Result, KeyWalletError> { + // Parse the extended public key from string + let xpub = + ExtendedPubKey::from_str(&account_xpub.xpub).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + + // Generate addresses for a single index + let addrs = self + .inner + .generate_range(&xpub, external, index, 1) + .map_err(|e| KeyWalletError::from(e))?; + + let addr = addrs.into_iter().next().ok_or_else(|| KeyWalletError::KeyError { + message: "Failed to generate address".into(), + })?; + + Ok(Arc::new(Address { + inner: addr, + })) + } + + pub fn generate_range( + &self, + account_xpub: AccountXPub, + external: bool, + start: u32, + count: u32, + ) -> Result>, KeyWalletError> { + // Parse the extended public key from string + let xpub = + ExtendedPubKey::from_str(&account_xpub.xpub).map_err(|e| KeyWalletError::KeyError { + message: e.to_string(), + })?; + + let addrs = self + .inner + .generate_range(&xpub, external, start, count) + .map_err(|e| KeyWalletError::from(e))?; + + Ok(addrs + .into_iter() + .map(|addr| { + Arc::new(Address { + inner: addr, + }) + }) + .collect()) + } +} + +#[cfg(test)] +mod network_compatibility_tests { + use super::*; + + #[test] + fn test_network_compatibility_with_dash_network_ffi() { + // Ensure our Network enum values match dash-network-ffi + // We can't directly compare with dash_network_ffi::Network because it's defined in the FFI lib.rs + // But we can ensure the values are consistent + assert_eq!(Network::Dash as u8, 0); + assert_eq!(Network::Testnet as u8, 1); + assert_eq!(Network::Regtest as u8, 2); + assert_eq!(Network::Devnet as u8, 3); + } + + #[test] + fn test_network_conversion_to_key_wallet() { + // Test conversion to key_wallet::Network + let networks = vec![ + (Network::Dash, key_wallet::Network::Dash), + (Network::Testnet, key_wallet::Network::Testnet), + (Network::Devnet, key_wallet::Network::Devnet), + (Network::Regtest, key_wallet::Network::Regtest), + ]; + + for (ffi_network, expected_kw_network) in networks { + let kw_network: key_wallet::Network = ffi_network.into(); + assert_eq!(kw_network, expected_kw_network); + } + } +} diff --git a/key-wallet-ffi/src/lib_tests.rs b/key-wallet-ffi/src/lib_tests.rs new file mode 100644 index 000000000..5de057057 --- /dev/null +++ b/key-wallet-ffi/src/lib_tests.rs @@ -0,0 +1,162 @@ +//! Internal tests for key-wallet-ffi +//! +//! These tests verify the FFI implementation works correctly. + +#[cfg(test)] +mod tests { + use crate::{ + validate_mnemonic, Address, AddressGenerator, ExtPrivKey, ExtPubKey, HDWallet, Language, + Mnemonic, Network, + }; + + #[test] + fn test_mnemonic_functionality() { + // Test mnemonic validation + let valid_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let is_valid = validate_mnemonic(valid_phrase.clone(), Language::English).unwrap(); + assert!(is_valid); + + // Test creating from phrase + let mnemonic = Mnemonic::new(valid_phrase, Language::English).unwrap(); + assert_eq!(mnemonic.phrase().split_whitespace().count(), 12); + + // Test seed generation + let seed = mnemonic.to_seed("".to_string()); + assert_eq!(seed.len(), 64); + } + + #[test] + fn test_hd_wallet_functionality() { + // Create wallet from seed + let seed = vec![0u8; 64]; + let wallet = HDWallet::from_seed(seed, Network::Testnet).unwrap(); + + // Test getting account keys + let account_xpriv = wallet.get_account_xpriv(0).unwrap(); + let account_xpub = wallet.get_account_xpub(0).unwrap(); + + // Test deriving keys + let path = "m/44'/1'/0'/0/0".to_string(); + let derived_xpriv = wallet.derive_xpriv(path.clone()).unwrap(); + let derived_xpub = wallet.derive_xpub(path.clone()).unwrap(); + // Verify we got keys + assert!(!account_xpriv.xpriv.is_empty()); + assert!(!account_xpriv.derivation_path.is_empty()); + assert!(!account_xpub.xpub.is_empty()); + assert!(!derived_xpriv.is_empty()); + assert!(!derived_xpub.xpub.is_empty()); + } + + #[test] + fn test_address_functionality() { + // Test creating P2PKH address from public key + let pubkey = vec![ + 0x02, 0x9b, 0x63, 0x47, 0x39, 0x85, 0x05, 0xf5, 0xec, 0x93, 0x82, 0x6d, 0xc6, 0x1c, + 0x19, 0xf4, 0x7c, 0x66, 0xc0, 0x28, 0x3e, 0xe9, 0xbe, 0x98, 0x0e, 0x29, 0xce, 0x32, + 0x5a, 0x0f, 0x46, 0x79, 0xef, + ]; + let address = Address::from_public_key(pubkey, Network::Testnet).unwrap(); + let address_str = address.to_string(); + assert!(address_str.starts_with('y')); // Testnet P2PKH addresses start with 'y' + + // Test parsing from string + let parsed = Address::from_string(address_str.clone(), Network::Testnet).unwrap(); + assert_eq!(parsed.to_string(), address_str); + assert_eq!(parsed.get_network(), Network::Testnet); + + // Test script pubkey + let script = address.get_script_pubkey(); + assert!(script.len() > 0); + } + + #[test] + fn test_address_generator_functionality() { + let seed = vec![0u8; 64]; + let wallet = HDWallet::from_seed(seed, Network::Testnet).unwrap(); + + // Get account extended public key + let account_xpub = wallet.get_account_xpub(0).unwrap(); + + let generator = AddressGenerator::new(Network::Testnet); + + // Test single address generation + let single_addr = generator.generate(account_xpub.clone(), true, 0).unwrap(); + assert!(single_addr.to_string().starts_with('y')); + + // Test address range generation + let addresses = generator.generate_range(account_xpub, true, 0, 5).unwrap(); + assert_eq!(addresses.len(), 5); + for addr in &addresses { + assert!(addr.to_string().starts_with('y')); + } + } + + #[test] + fn test_extended_key_methods() { + // Generate a valid extended key from a known seed + let seed = vec![0u8; 64]; + let wallet = HDWallet::from_seed(seed, Network::Testnet).unwrap(); + let account_xpriv = wallet.get_account_xpriv(0).unwrap(); + + // Test ExtPrivKey + let xpriv = ExtPrivKey::from_string(account_xpriv.xpriv).unwrap(); + + // Test getting xpub + let xpub = xpriv.get_xpub(); + assert!(xpub.xpub.starts_with("tpub")); // Testnet public key + + // Test deriving child + let child = xpriv.derive_child(0, false).unwrap(); + assert!(!child.to_string().is_empty()); + + // Test ExtPubKey + let xpub_obj = ExtPubKey::from_string(xpub.xpub).unwrap(); + let pubkey_bytes = xpub_obj.get_public_key(); + assert_eq!(pubkey_bytes.len(), 33); // Compressed public key + } + + #[test] + fn test_error_handling() { + // Test invalid mnemonic + let invalid_phrase = "invalid mnemonic phrase".to_string(); + let result = Mnemonic::new(invalid_phrase, Language::English); + assert!(result.is_err()); + + // Test invalid address + let result = Address::from_string("invalid_address".to_string(), Network::Testnet); + assert!(result.is_err()); + + // Test invalid derivation path + let seed = vec![0u8; 64]; + let wallet = HDWallet::from_seed(seed, Network::Testnet).unwrap(); + let result = wallet.derive_xpriv("invalid/path".to_string()); + assert!(result.is_err()); + } + + #[test] + fn test_network_compatibility_in_address_parsing() { + // Create a testnet address + let pubkey = vec![ + 0x02, 0x9b, 0x63, 0x47, 0x39, 0x85, 0x05, 0xf5, 0xec, 0x93, 0x82, 0x6d, 0xc6, 0x1c, + 0x19, 0xf4, 0x7c, 0x66, 0xc0, 0x28, 0x3e, 0xe9, 0xbe, 0x98, 0x0e, 0x29, 0xce, 0x32, + 0x5a, 0x0f, 0x46, 0x79, 0xef, + ]; + let testnet_addr = Address::from_public_key(pubkey, Network::Testnet).unwrap(); + let addr_str = testnet_addr.to_string(); + + // Should work with testnet + let parsed = Address::from_string(addr_str.clone(), Network::Testnet); + assert!(parsed.is_ok()); + + // Should also work with devnet and regtest (same prefixes) + let parsed = Address::from_string(addr_str.clone(), Network::Devnet); + assert!(parsed.is_ok()); + + let parsed = Address::from_string(addr_str.clone(), Network::Regtest); + assert!(parsed.is_ok()); + + // Should fail with mainnet (different prefix) + let parsed = Address::from_string(addr_str.clone(), Network::Dash); + assert!(parsed.is_err()); + } +} diff --git a/key-wallet-ffi/tests/ffi_tests.rs b/key-wallet-ffi/tests/ffi_tests.rs new file mode 100644 index 000000000..526e51606 --- /dev/null +++ b/key-wallet-ffi/tests/ffi_tests.rs @@ -0,0 +1,16 @@ +//! FFI tests +//! +//! These tests verify the FFI implementation works correctly. +//! They test the Rust implementation directly, not through generated bindings. + +#[test] +fn test_ffi_types_exist() { + // This test just verifies the crate compiles with all the expected types + use key_wallet_ffi::initialize; + + // Verify we can call initialize + initialize(); + + // This test passes if it compiles + assert!(true); +} diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml new file mode 100644 index 000000000..51d8b954e --- /dev/null +++ b/key-wallet/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "key-wallet" +version = "0.39.6" +authors = ["The Dash Core Developers"] +edition = "2021" +description = "Key derivation and wallet functionality for Dash" +keywords = ["dash", "wallet", "bip32", "bip39", "hdwallet"] +readme = "README.md" +license = "CC0-1.0" + +[features] +default = ["std"] +std = ["bitcoin_hashes/std", "secp256k1/std", "bip39/std", "getrandom", "dash-network/std"] +serde = ["dep:serde", "bitcoin_hashes/serde", "secp256k1/serde", "dash-network/serde"] + +[dependencies] +bitcoin_hashes = { version = "0.14.0", default-features = false } +secp256k1 = { version = "0.30.0", default-features = false, features = ["hashes", "recovery"] } +bip39 = { version = "2.0.0", default-features = false, features = ["chinese-simplified", "chinese-traditional", "czech", "french", "italian", "japanese", "korean", "spanish"] } +serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +base58ck = { version = "0.1.0", default-features = false } +bitflags = { version = "2.6", default-features = false } +getrandom = { version = "0.2", optional = true } +dash-network = { path = "../dash-network", default-features = false } + +[dev-dependencies] +hex = "0.4" +serde_json = "1.0" \ No newline at end of file diff --git a/key-wallet/README.md b/key-wallet/README.md new file mode 100644 index 000000000..df8f4adf8 --- /dev/null +++ b/key-wallet/README.md @@ -0,0 +1,79 @@ +# Key Wallet + +A Rust library for Dash key derivation and wallet functionality, including BIP32 hierarchical deterministic wallets, BIP39 mnemonic support, and Dash-specific derivation paths (DIP9). + +## Features + +- **BIP32 HD Wallets**: Full implementation of hierarchical deterministic wallets +- **BIP39 Mnemonics**: Generate and validate mnemonic phrases in multiple languages +- **Dash-specific paths**: Support for DIP9 derivation paths (BIP44, CoinJoin, Identity) +- **Address generation**: P2PKH and P2SH address support for Dash networks +- **No-std support**: Can be used in embedded environments +- **Secure**: Memory-safe Rust implementation + +## Usage + +### Creating a wallet from mnemonic + +```rust +use key_wallet::prelude::*; +use key_wallet::mnemonic::Language; +use key_wallet::derivation::HDWallet; +use key_wallet::bip32::Network; + +// Create or restore from mnemonic +let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English +)?; + +// Generate seed +let seed = mnemonic.to_seed(""); + +// Create HD wallet +let wallet = HDWallet::from_seed(&seed, Network::Dash)?; + +// Derive BIP44 account +let account = wallet.bip44_account(0)?; +``` + +### Address generation + +```rust +use key_wallet::address::{Address, AddressGenerator, Network}; + +// Create address generator +let generator = AddressGenerator::new(Network::Dash); + +// Generate addresses from account +let addresses = generator.generate_range(&account_xpub, true, 0, 10)?; +``` + +### Dash-specific derivation paths + +```rust +// CoinJoin account +let coinjoin_account = wallet.coinjoin_account(0)?; + +// Identity authentication key +let identity_key = wallet.identity_authentication_key(0, 0)?; +``` + +## Derivation Paths (DIP9) + +The library implements Dash Improvement Proposal 9 (DIP9) derivation paths: + +- **BIP44**: `m/44'/5'/account'` - Standard funds +- **CoinJoin**: `m/4'/5'/account'` - CoinJoin mixing +- **Identity**: `m/5'/5'/3'/identity'/key'` - Platform identities +- **Masternode**: Various paths for masternode operations + +## Security + +- Private keys are handled securely in memory +- Supports both mainnet and testnet +- Compatible with hardware wallet derivation + +## License + +This project is licensed under the CC0 1.0 Universal license. \ No newline at end of file diff --git a/key-wallet/examples/basic_usage.rs b/key-wallet/examples/basic_usage.rs new file mode 100644 index 000000000..39a5ec0b7 --- /dev/null +++ b/key-wallet/examples/basic_usage.rs @@ -0,0 +1,101 @@ +//! Basic usage example for key-wallet + +use key_wallet::address::AddressGenerator; +use key_wallet::derivation::{AccountDerivation, HDWallet}; +use key_wallet::mnemonic::Language; +use key_wallet::prelude::*; +use key_wallet::Network; + +fn main() -> core::result::Result<(), Box> { + println!("Key Wallet Example\n"); + + // 1. Create a mnemonic + println!("1. Creating mnemonic..."); + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + )?; + println!(" Mnemonic: {}", mnemonic.phrase()); + println!(" Word count: {}", mnemonic.word_count()); + + // 2. Generate seed + println!("\n2. Generating seed..."); + let seed = mnemonic.to_seed(""); + println!(" Seed: {}", hex::encode(&seed[..32])); // Show first 32 bytes + + // 3. Create HD wallet + println!("\n3. Creating HD wallet..."); + let wallet = HDWallet::from_seed(&seed, Network::Dash)?; + let master_pub = wallet.master_pub_key(); + println!(" Master public key: {}", master_pub); + + // 4. Derive BIP44 account + println!("\n4. Deriving BIP44 account 0..."); + let account = wallet.bip44_account(0)?; + println!(" Account xprv: {}", account); + + // 5. Create account derivation + println!("\n5. Deriving addresses..."); + let account_derivation = AccountDerivation::new(account.clone()); + + // Derive first 5 receive addresses + println!(" Receive addresses:"); + for i in 0..5 { + let addr_xpub = account_derivation.receive_address(i)?; + let addr = key_wallet::address::Address::p2pkh(&addr_xpub.public_key, Network::Dash); + println!(" {}: {}", i, addr); + } + + // Derive first 2 change addresses + println!("\n Change addresses:"); + for i in 0..2 { + let addr_xpub = account_derivation.change_address(i)?; + let addr = key_wallet::address::Address::p2pkh(&addr_xpub.public_key, Network::Dash); + println!(" {}: {}", i, addr); + } + + // 6. Demonstrate CoinJoin derivation + println!("\n6. CoinJoin account..."); + let coinjoin_account = wallet.coinjoin_account(0)?; + println!(" CoinJoin account depth: {}", coinjoin_account.depth); + + // 7. Demonstrate identity key derivation + println!("\n7. Identity authentication key..."); + let identity_key = wallet.identity_authentication_key(0, 0)?; + println!(" Identity key depth: {}", identity_key.depth); + + // 8. Address parsing example + println!("\n8. Address parsing..."); + let test_address = "XyPvhVmhWKDgvMJLwfFfMwhxpxGgd3TBxq"; + match key_wallet::address::Address::from_str(test_address) { + Ok(parsed) => { + println!(" Parsed address: {}", parsed); + println!(" Type: {:?}", parsed.address_type); + println!(" Network: {:?}", parsed.network); + } + Err(e) => println!(" Failed to parse: {}", e), + } + + Ok(()) +} + +#[allow(dead_code)] +fn demonstrate_address_generation() -> core::result::Result<(), Box> { + // This demonstrates bulk address generation + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash)?; + let account = wallet.bip44_account(0)?; + let path = key_wallet::DerivationPath::from(vec![ + key_wallet::ChildNumber::from_hardened_idx(44).unwrap(), + key_wallet::ChildNumber::from_hardened_idx(5).unwrap(), + key_wallet::ChildNumber::from_hardened_idx(0).unwrap(), + ]); + let account_xpub = wallet.derive_pub(&path)?; + + let generator = AddressGenerator::new(Network::Dash); + let addresses = generator.generate_range(&account_xpub, true, 0, 100)?; + + println!("Generated {} addresses", addresses.len()); + + Ok(()) +} diff --git a/key-wallet/src/address.rs b/key-wallet/src/address.rs new file mode 100644 index 000000000..a87aaa224 --- /dev/null +++ b/key-wallet/src/address.rs @@ -0,0 +1,248 @@ +//! Address generation and encoding + +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; + +use bitcoin_hashes::{hash160, Hash}; +use secp256k1::{PublicKey, Secp256k1}; + +use crate::error::{Error, Result}; +use dash_network::Network; + +/// Address types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressType { + /// Pay to public key hash (P2PKH) + P2PKH, + /// Pay to script hash (P2SH) + P2SH, +} + +/// Extension trait for Network to add address-specific methods +pub trait NetworkExt { + /// Get P2PKH version byte + fn p2pkh_version(&self) -> u8; + /// Get P2SH version byte + fn p2sh_version(&self) -> u8; +} + +impl NetworkExt for Network { + /// Get P2PKH version byte + fn p2pkh_version(&self) -> u8 { + match self { + Network::Dash => 76, // 'X' prefix + Network::Testnet => 140, // 'y' prefix + Network::Devnet => 140, // 'y' prefix + Network::Regtest => 140, // 'y' prefix + _ => 140, // default to testnet version + } + } + + /// Get P2SH version byte + fn p2sh_version(&self) -> u8 { + match self { + Network::Dash => 16, // '7' prefix + Network::Testnet => 19, // '8' or '9' prefix + Network::Devnet => 19, // '8' or '9' prefix + Network::Regtest => 19, // '8' or '9' prefix + _ => 19, // default to testnet version + } + } +} + +/// A Dash address +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Address { + /// The network this address is valid for + pub network: Network, + /// The type of address + pub address_type: AddressType, + /// The hash160 of the public key or script + pub hash: hash160::Hash, +} + +impl Address { + /// Create a P2PKH address from a public key + pub fn p2pkh(pubkey: &PublicKey, network: Network) -> Self { + let hash = hash160::Hash::hash(&pubkey.serialize()); + Self { + network, + address_type: AddressType::P2PKH, + hash, + } + } + + /// Create a P2SH address from a script hash + pub fn p2sh(script_hash: hash160::Hash, network: Network) -> Self { + Self { + network, + address_type: AddressType::P2SH, + hash: script_hash, + } + } + + /// Encode the address as a string + pub fn to_string(&self) -> String { + let version = match self.address_type { + AddressType::P2PKH => self.network.p2pkh_version(), + AddressType::P2SH => self.network.p2sh_version(), + }; + + let mut data = Vec::with_capacity(21); + data.push(version); + data.extend_from_slice(&self.hash[..]); + + base58ck::encode_check(&data) + } + + /// Parse an address from a string (network is inferred from version byte) + pub fn from_str(s: &str) -> Result { + let data = base58ck::decode_check(s) + .map_err(|_| Error::InvalidAddress("Invalid base58 encoding".into()))?; + + if data.len() != 21 { + return Err(Error::InvalidAddress("Invalid address length".into())); + } + + let version = data[0]; + let hash = hash160::Hash::from_slice(&data[1..]) + .map_err(|_| Error::InvalidAddress("Invalid hash".into()))?; + + // Infer network and address type from version byte + let (network, address_type) = match version { + 76 => (Network::Dash, AddressType::P2PKH), // Dash mainnet P2PKH + 16 => (Network::Dash, AddressType::P2SH), // Dash mainnet P2SH + 140 => { + // Could be testnet, devnet, or regtest P2PKH + // Default to testnet, but this is ambiguous + (Network::Testnet, AddressType::P2PKH) + } + 19 => { + // Could be testnet, devnet, or regtest P2SH + // Default to testnet, but this is ambiguous + (Network::Testnet, AddressType::P2SH) + } + _ => return Err(Error::InvalidAddress(format!("Unknown version byte: {}", version))), + }; + + Ok(Self { + network, + address_type, + hash, + }) + } + + /// Get the script pubkey for this address + pub fn script_pubkey(&self) -> Vec { + match self.address_type { + AddressType::P2PKH => { + let mut script = Vec::with_capacity(25); + script.push(0x76); // OP_DUP + script.push(0xa9); // OP_HASH160 + script.push(0x14); // Push 20 bytes + script.extend_from_slice(&self.hash[..]); + script.push(0x88); // OP_EQUALVERIFY + script.push(0xac); // OP_CHECKSIG + script + } + AddressType::P2SH => { + let mut script = Vec::with_capacity(23); + script.push(0xa9); // OP_HASH160 + script.push(0x14); // Push 20 bytes + script.extend_from_slice(&self.hash[..]); + script.push(0x87); // OP_EQUAL + script + } + } + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +/// Generate addresses from extended public keys +pub struct AddressGenerator { + network: Network, +} + +impl AddressGenerator { + /// Create a new address generator + pub fn new(network: Network) -> Self { + Self { + network, + } + } + + /// Generate a P2PKH address from an extended public key + pub fn generate_p2pkh(&self, xpub: &crate::bip32::ExtendedPubKey) -> Address { + Address::p2pkh(&xpub.public_key, self.network) + } + + /// Generate addresses for a range of indices + pub fn generate_range( + &self, + account_xpub: &crate::bip32::ExtendedPubKey, + external: bool, + start: u32, + count: u32, + ) -> Result> { + let secp = Secp256k1::new(); + let mut addresses = Vec::with_capacity(count as usize); + + let change = if external { + 0 + } else { + 1 + }; + + for i in start..(start + count) { + // Create relative path from account + let path = crate::bip32::DerivationPath::from(vec![ + crate::bip32::ChildNumber::Normal { + index: change, + }, + crate::bip32::ChildNumber::Normal { + index: i, + }, + ]); + + let child_xpub = account_xpub.derive_pub(&secp, &path)?; + addresses.push(self.generate_p2pkh(&child_xpub)); + } + + Ok(addresses) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_address_encoding() { + // Test vector from Dash + let pubkey_hex = "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"; + let pubkey_bytes = hex::decode(pubkey_hex).unwrap(); + let pubkey = PublicKey::from_slice(&pubkey_bytes).unwrap(); + + let address = Address::p2pkh(&pubkey, Network::Dash); + let encoded = address.to_string(); + + // Verify it starts with 'X' for mainnet P2PKH + assert!(encoded.starts_with('X')); + } + + #[test] + fn test_address_parsing() { + let address_str = "XmnGSJav3CWVmzDv5U68k7XT9rRPqyavtE"; + let address = Address::from_str(address_str).unwrap(); + + assert_eq!(address.address_type, AddressType::P2PKH); + assert_eq!(address.network, Network::Dash); + assert_eq!(address.to_string(), address_str); + } +} diff --git a/dash/src/bip32.rs b/key-wallet/src/bip32.rs similarity index 87% rename from dash/src/bip32.rs rename to key-wallet/src/bip32.rs index 3841f23f4..8fa6e6fe2 100644 --- a/dash/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -28,14 +28,11 @@ use core::str::FromStr; #[cfg(feature = "std")] use std::error; -use hashes::{Hash, HashEngine, Hmac, HmacEngine, hex as hashesHex, sha512}; -use internals::impl_array_newtype; +use bitcoin_hashes::{hash160, sha512, Hash, HashEngine, Hmac, HmacEngine}; use secp256k1::{self, Secp256k1, XOnlyPublicKey}; #[cfg(feature = "serde")] use serde; -use crate::base58; -use crate::crypto::key::{self, Keypair, PrivateKey, PublicKey}; use crate::dip9::{ COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET, DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET, IDENTITY_AUTHENTICATION_PATH_MAINNET, IDENTITY_AUTHENTICATION_PATH_TESTNET, @@ -43,17 +40,122 @@ use crate::dip9::{ IDENTITY_REGISTRATION_PATH_MAINNET, IDENTITY_REGISTRATION_PATH_TESTNET, IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET, }; -use crate::hash_types::XpubIdentifier; -use crate::internal_macros::impl_bytes_newtype; -use crate::io::Write; -use crate::network::constants::Network; -use crate::prelude::*; +use alloc::{string::String, vec::Vec}; +use base58ck; +use dash_network::Network; + +/// XpubIdentifier as a hash160 result +type XpubIdentifier = hash160::Hash; + +pub use secp256k1::Keypair; +pub use secp256k1::PublicKey; +/// Re-export key types from secp256k1 +pub use secp256k1::SecretKey as PrivateKey; /// A chain code #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ChainCode([u8; 32]); -impl_array_newtype!(ChainCode, u8, 32); -impl_bytes_newtype!(ChainCode, 32); + +impl ChainCode { + /// Create a new ChainCode from a byte array + pub fn from_bytes(bytes: [u8; 32]) -> Self { + ChainCode(bytes) + } + + /// Get the inner byte array + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl AsRef<[u8]> for ChainCode { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; 32]> for ChainCode { + fn from(bytes: [u8; 32]) -> Self { + ChainCode(bytes) + } +} + +impl TryFrom<&[u8]> for ChainCode { + type Error = Error; + + fn try_from(slice: &[u8]) -> Result { + if slice.len() != 32 { + return Err(Error::InvalidChildNumberFormat); + } + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(slice); + Ok(ChainCode(bytes)) + } +} + +impl fmt::Display for ChainCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for &byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl fmt::Debug for ChainCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ChainCode({}))", self) + } +} + +impl fmt::LowerHex for ChainCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for &byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl core::ops::Index for ChainCode { + type Output = u8; + + fn index(&self, idx: usize) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for ChainCode { + type Output = [u8]; + + fn index(&self, idx: core::ops::Range) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for ChainCode { + type Output = [u8]; + + fn index(&self, idx: core::ops::RangeTo) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for ChainCode { + type Output = [u8]; + + fn index(&self, idx: core::ops::RangeFrom) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index for ChainCode { + type Output = [u8]; + + fn index(&self, _: core::ops::RangeFull) -> &Self::Output { + &self.0[..] + } +} impl ChainCode { fn from_hmac(hmac: Hmac) -> Self { @@ -61,11 +163,169 @@ impl ChainCode { } } +#[cfg(feature = "serde")] +impl serde::Serialize for ChainCode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ChainCode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let s = String::deserialize(deserializer)?; + let mut bytes = [0u8; 32]; + crate::utils::parse_hex_bytes(&s, &mut bytes).map_err(D::Error::custom)?; + Ok(ChainCode(bytes)) + } +} + /// A fingerprint #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Fingerprint([u8; 4]); -impl_array_newtype!(Fingerprint, u8, 4); -impl_bytes_newtype!(Fingerprint, 4); + +impl Fingerprint { + /// Create a new Fingerprint from a byte array + pub fn from_bytes(bytes: [u8; 4]) -> Self { + Fingerprint(bytes) + } + + /// Get the inner byte array + pub fn to_bytes(&self) -> [u8; 4] { + self.0 + } +} + +impl AsRef<[u8]> for Fingerprint { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; 4]> for Fingerprint { + fn from(bytes: [u8; 4]) -> Self { + Fingerprint(bytes) + } +} + +impl TryFrom<&[u8]> for Fingerprint { + type Error = Error; + + fn try_from(slice: &[u8]) -> Result { + if slice.len() != 4 { + return Err(Error::InvalidChildNumberFormat); + } + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(slice); + Ok(Fingerprint(bytes)) + } +} + +impl fmt::Display for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for &byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl fmt::Debug for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Fingerprint({}))", self) + } +} + +impl core::str::FromStr for Fingerprint { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut bytes = [0u8; 4]; + crate::utils::parse_hex_bytes(s, &mut bytes) + .map_err(|_| Error::InvalidPublicKeyHexLength(s.len()))?; + Ok(Fingerprint(bytes)) + } +} + +impl fmt::LowerHex for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for &byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +impl core::ops::Index for Fingerprint { + type Output = u8; + + fn index(&self, idx: usize) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for Fingerprint { + type Output = [u8]; + + fn index(&self, idx: core::ops::Range) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for Fingerprint { + type Output = [u8]; + + fn index(&self, idx: core::ops::RangeTo) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index> for Fingerprint { + type Output = [u8]; + + fn index(&self, idx: core::ops::RangeFrom) -> &Self::Output { + &self.0[idx] + } +} + +impl core::ops::Index for Fingerprint { + type Output = [u8]; + + fn index(&self, _: core::ops::RangeFull) -> &Self::Output { + &self.0[..] + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Fingerprint { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("{:x}", self)) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Fingerprint { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(|_| D::Error::custom("invalid fingerprint")) + } +} /// Extended private key #[derive(Copy, Clone, PartialEq, Eq)] @@ -85,7 +345,25 @@ pub struct ExtendedPrivKey { pub chain_code: ChainCode, } #[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(ExtendedPrivKey, "a BIP-32 extended private key"); +impl serde::Serialize for ExtendedPrivKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ExtendedPrivKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer)?.parse().map_err(D::Error::custom) + } +} #[cfg(not(feature = "std"))] #[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))] @@ -118,7 +396,25 @@ pub struct ExtendedPubKey { pub chain_code: ChainCode, } #[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(ExtendedPubKey, "a BIP-32 extended public key"); +impl serde::Serialize for ExtendedPubKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ExtendedPubKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer)?.parse().map_err(D::Error::custom) + } +} /// A child number for a derived key #[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)] @@ -351,10 +647,13 @@ impl fmt::Display for ChildNumber { ChildNumber::Hardened256 { index, } => { + write!(f, "0x")?; + for byte in index { + write!(f, "{:02x}", byte)?; + } write!( f, - "0x{}{}", - hex::encode(index), + "{}", if f.alternate() { "h" } else { @@ -365,7 +664,11 @@ impl fmt::Display for ChildNumber { ChildNumber::Normal256 { index, } => { - write!(f, "0x{}", hex::encode(index)) + write!(f, "0x")?; + for byte in index { + write!(f, "{:02x}", byte)?; + } + Ok(()) } } } @@ -385,7 +688,28 @@ impl FromStr for ChildNumber { if index_str.starts_with("0x") || index_str.starts_with("0X") { // Parse as a 256-bit hex number let hex_str = &index_str[2..]; - let hex_bytes = hex::decode(hex_str).map_err(|_| Error::InvalidChildNumberFormat)?; + // Simple hex decoder + let hex_bytes = hex_str + .as_bytes() + .chunks(2) + .map(|chunk| { + let high = chunk[0]; + let low = chunk.get(1).copied().unwrap_or(b'0'); + let h = match high { + b'0'..=b'9' => high - b'0', + b'a'..=b'f' => high - b'a' + 10, + b'A'..=b'F' => high - b'A' + 10, + _ => return Err(Error::InvalidChildNumberFormat), + }; + let l = match low { + b'0'..=b'9' => low - b'0', + b'a'..=b'f' => low - b'a' + 10, + b'A'..=b'F' => low - b'A' + 10, + _ => return Err(Error::InvalidChildNumberFormat), + }; + Ok((h << 4) | l) + }) + .collect::, Error>>()?; if hex_bytes.len() != 32 { return Err(Error::InvalidChildNumberFormat); } @@ -608,7 +932,25 @@ impl DerivationPath { } #[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(DerivationPath, "a BIP-32 derivation path"); +impl serde::Serialize for DerivationPath { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for DerivationPath { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer)?.parse().map_err(D::Error::custom) + } +} impl Index for DerivationPath where @@ -678,7 +1020,7 @@ impl ::core::iter::FromIterator for DerivationPath { impl<'a> ::core::iter::IntoIterator for &'a DerivationPath { type Item = &'a ChildNumber; - type IntoIter = slice::Iter<'a, ChildNumber>; + type IntoIter = core::slice::Iter<'a, ChildNumber>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } @@ -745,6 +1087,11 @@ impl DerivationPath { self.0.is_empty() } + /// Push a child number to the path + pub fn push(&mut self, child: ChildNumber) { + self.0.push(child) + } + /// Returns derivation path for a master key (i.e. empty derivation path) pub fn master() -> DerivationPath { DerivationPath(vec![]) @@ -799,7 +1146,7 @@ impl DerivationPath { /// Concatenate `self` with `path` and return the resulting new path. /// /// ``` - /// use dashcore::bip32::{DerivationPath, ChildNumber}; + /// use key_wallet::{DerivationPath, ChildNumber}; /// use std::str::FromStr; /// /// let base = DerivationPath::from_str("m/42").unwrap(); @@ -841,7 +1188,7 @@ impl fmt::Debug for DerivationPath { pub type KeySource = (Fingerprint, DerivationPath); /// A BIP32 error -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum Error { /// A pk->pk derivation was attempted on a hardened key CannotDeriveFromHardenedKey, @@ -858,17 +1205,11 @@ pub enum Error { /// Encoded extended key data has wrong length WrongExtendedKeyLength(usize), /// Base58 encoding error - Base58(base58::Error), + Base58(base58ck::Error), /// Hexadecimal decoding error - Hex(hashesHex::Error), + Hex(bitcoin_hashes::FromSliceError), /// `PublicKey` hex should be 66 or 130 digits long. InvalidPublicKeyHexLength(usize), - /// bls signatures related error - #[cfg(feature = "bls-signatures")] - BLSError(String), - /// edwards 25519 related error - #[cfg(feature = "ed25519-dalek")] - Ed25519Dalek(String), /// Something is not supported based on active features NotSupported(String), } @@ -896,10 +1237,6 @@ impl fmt::Display for Error { Error::InvalidPublicKeyHexLength(got) => { write!(f, "PublicKey hex should be 66 or 130 digits long, got: {}", got) } - #[cfg(feature = "bls-signatures")] - Error::BLSError(ref msg) => write!(f, "BLS signature error: {}", msg), - #[cfg(feature = "ed25519-dalek")] - Error::Ed25519Dalek(ref msg) => write!(f, "Ed25519 error: {}", msg), Error::NotSupported(ref msg) => write!(f, "Not supported: {}", msg), } } @@ -916,31 +1253,14 @@ impl error::Error for Error { } } -impl From for Error { - fn from(err: key::Error) -> Self { - match err { - key::Error::Base58(e) => Error::Base58(e), - key::Error::Secp256k1(e) => Error::Secp256k1(e), - key::Error::InvalidKeyPrefix(_) => Error::Secp256k1(secp256k1::Error::InvalidPublicKey), - key::Error::Hex(e) => Error::Hex(e), - key::Error::InvalidHexLength(got) => Error::InvalidPublicKeyHexLength(got), - #[cfg(feature = "bls-signatures")] - key::Error::BLSError(e) => Error::BLSError(e), - #[cfg(feature = "ed25519-dalek")] - key::Error::Ed25519Dalek(e) => Error::Ed25519Dalek(e), - key::Error::NotSupported(e) => Error::NotSupported(e), - } - } -} - impl From for Error { fn from(e: secp256k1::Error) -> Error { Error::Secp256k1(e) } } -impl From for Error { - fn from(err: base58::Error) -> Self { +impl From for Error { + fn from(err: base58ck::Error) -> Self { Error::Base58(err) } } @@ -962,20 +1282,10 @@ impl ExtendedPrivKey { }) } - /// Constructs ECDSA compressed private key matching internal secret key representation. - pub fn to_priv(&self) -> PrivateKey { - PrivateKey { - compressed: true, - network: self.network, - inner: self.private_key, - } - } - /// Constructs BIP340 keypair for Schnorr signatures and Taproot use matching the internal /// secret key representation. pub fn to_keypair(&self, secp: &Secp256k1) -> Keypair { - Keypair::from_seckey_slice(secp, &self.private_key[..]) - .expect("BIP32 internal private key representation is broken") + Keypair::from_secret_key(secp, &self.private_key) } /// Attempts to derive an extended private key from a path. @@ -1103,7 +1413,7 @@ impl ExtendedPrivKey { ret[0..4].copy_from_slice( &match self.network { Network::Dash => [0x04, 0x88, 0xAD, 0xE4], - Network::Testnet | Network::Devnet | Network::Regtest => [0x04, 0x35, 0x83, 0x94], + _ => [0x04, 0x35, 0x83, 0x94], // Testnet/Devnet/Regtest/Unknown }[..], ); ret[4] = self.depth; @@ -1169,7 +1479,7 @@ impl ExtendedPrivKey { // Version bytes let version: [u8; 4] = match self.network { Network::Dash => [0x0E, 0xEC, 0xF0, 0x2E], - Network::Testnet | Network::Devnet | Network::Regtest => [0x0E, 0xED, 0x27, 0x74], + _ => [0x0E, 0xED, 0x27, 0x74], // Testnet/Devnet/Regtest/Unknown }; ret[0..4].copy_from_slice(&version); @@ -1249,14 +1559,6 @@ impl ExtendedPubKey { } } - /// Constructs ECDSA compressed public key matching internal public key representation. - pub fn to_pub(&self) -> PublicKey { - PublicKey { - compressed: true, - inner: self.public_key, - } - } - /// Constructs BIP340 x-only public key for BIP-340 signatures and Taproot use matching /// the internal public key representation. pub fn to_x_only_pub(&self) -> XOnlyPublicKey { @@ -1397,7 +1699,7 @@ impl ExtendedPubKey { ret[0..4].copy_from_slice( &match self.network { Network::Dash => [0x04u8, 0x88, 0xB2, 0x1E], - Network::Testnet | Network::Devnet | Network::Regtest => [0x04u8, 0x35, 0x87, 0xCF], + _ => [0x04u8, 0x35, 0x87, 0xCF], // Testnet/Devnet/Regtest/Unknown }[..], ); ret[4] = self.depth; @@ -1415,7 +1717,7 @@ impl ExtendedPubKey { // Version bytes let version: [u8; 4] = match self.network { Network::Dash => [0x0E, 0xEC, 0xEF, 0xC5], - Network::Testnet | Network::Devnet | Network::Regtest => [0x0E, 0xED, 0x27, 0x0B], + _ => [0x0E, 0xED, 0x27, 0x0B], // Testnet/Devnet/Regtest/Unknown }; ret[0..4].copy_from_slice(&version); @@ -1510,7 +1812,7 @@ impl ExtendedPubKey { /// Returns the HASH160 of the chaincode pub fn identifier(&self) -> XpubIdentifier { let mut engine = XpubIdentifier::engine(); - engine.write_all(&self.public_key.serialize()).expect("engines don't error"); + engine.input(&self.public_key.serialize()); XpubIdentifier::from_engine(engine) } @@ -1522,7 +1824,7 @@ impl ExtendedPubKey { impl fmt::Display for ExtendedPrivKey { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - base58::encode_check_to_fmt(fmt, &self.encode()[..]) + fmt.write_str(&base58ck::encode_check(&self.encode()[..])) } } @@ -1530,14 +1832,14 @@ impl FromStr for ExtendedPrivKey { type Err = Error; fn from_str(inp: &str) -> Result { - let data = base58::decode_check(inp)?; + let data = base58ck::decode_check(inp)?; ExtendedPrivKey::decode(&data) } } impl fmt::Display for ExtendedPubKey { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - base58::encode_check_to_fmt(fmt, &self.encode()[..]) + fmt.write_str(&base58ck::encode_check(&self.encode()[..])) } } @@ -1545,7 +1847,7 @@ impl FromStr for ExtendedPubKey { type Err = Error; fn from_str(inp: &str) -> Result { - let data = base58::decode_check(inp)?; + let data = base58ck::decode_check(inp)?; ExtendedPubKey::decode(&data) } } @@ -1554,12 +1856,12 @@ impl FromStr for ExtendedPubKey { mod tests { use core::str::FromStr; - use hashes::hex::FromHex; + use bitcoin_hashes::hex::FromHex; use secp256k1::{self, Secp256k1}; use super::ChildNumber::{Hardened, Normal}; use super::*; - use crate::network::constants::Network::{self, Dash}; + use dash_network::Network::{self, Dash}; #[test] fn test_parse_derivation_path() { @@ -2158,7 +2460,7 @@ mod tests { Network::Dash, ) .unwrap(); - assert_eq!(sk.to_priv().to_wif(), "XGtY11vBj7wfeoHxJQjhBzpbZem2CpEwa62WCisXkwzCLmmD4jRD"); + assert_eq!(sk.to_string(), "xprvA4FGorKLZVC4VT3Lf2UZS3hYZBpc8wGmmyyo5HPTUS8RcyX1yw2qHddBZVxn1u4NVduXDob1sKnx3d9e5wdY3VP8qibq7CgMqPhjUoV5G2K"); // Add correct expected value } @@ -2171,7 +2473,7 @@ mod tests { Network::Dash, ) .unwrap(); - assert_eq!(sk.to_priv().to_wif(), "XJavmPyJdYEpqZwzVAarQVRhpR7mVLiFHgHoZZTuZdzrpEKDhy6f"); + assert_eq!(sk.to_string(), "xprvA4F8hpkJuhhk4xqnnmY44WiVwUVPMdbF9VHE8vVmAiF6NyVXNmnyg5KnZF4VibNUuycJs6Dov4YBLm6bT2qGa81B5HHgqhUvixW2Qcgg5AE"); // Add correct expected value } diff --git a/key-wallet/src/derivation.rs b/key-wallet/src/derivation.rs new file mode 100644 index 000000000..1bcde5e6d --- /dev/null +++ b/key-wallet/src/derivation.rs @@ -0,0 +1,192 @@ +//! Key derivation functionality + +use secp256k1::Secp256k1; + +use crate::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}; +use crate::error::{Error, Result}; + +/// Key derivation interface +pub trait KeyDerivation { + /// Derive a child private key at the given path + fn derive_priv( + &self, + secp: &Secp256k1, + path: &DerivationPath, + ) -> Result; + + /// Derive a child public key at the given path + fn derive_pub( + &self, + secp: &Secp256k1, + path: &DerivationPath, + ) -> Result; +} + +impl KeyDerivation for ExtendedPrivKey { + fn derive_priv( + &self, + secp: &Secp256k1, + path: &DerivationPath, + ) -> Result { + self.derive_priv(secp, path).map_err(Into::into) + } + + fn derive_pub( + &self, + secp: &Secp256k1, + path: &DerivationPath, + ) -> Result { + let priv_key = self.derive_priv(secp, path)?; + Ok(ExtendedPubKey::from_priv(secp, &priv_key)) + } +} + +/// HD Wallet implementation +pub struct HDWallet { + master_key: ExtendedPrivKey, + secp: Secp256k1, +} + +impl HDWallet { + /// Create a new HD wallet from a master key + pub fn new(master_key: ExtendedPrivKey) -> Self { + Self { + master_key, + secp: Secp256k1::new(), + } + } + + /// Create from a seed + pub fn from_seed(seed: &[u8], network: crate::Network) -> Result { + let master_key = ExtendedPrivKey::new_master(network, seed)?; + Ok(Self::new(master_key)) + } + + /// Get the master extended private key + pub fn master_key(&self) -> &ExtendedPrivKey { + &self.master_key + } + + /// Get the master extended public key + pub fn master_pub_key(&self) -> ExtendedPubKey { + ExtendedPubKey::from_priv(&self.secp, &self.master_key) + } + + /// Derive a key at the given path + pub fn derive(&self, path: &DerivationPath) -> Result { + self.master_key.derive_priv(&self.secp, path).map_err(Into::into) + } + + /// Derive a public key at the given path + pub fn derive_pub(&self, path: &DerivationPath) -> Result { + let priv_key = self.derive(path)?; + Ok(ExtendedPubKey::from_priv(&self.secp, &priv_key)) + } + + /// Get a standard BIP44 account key + pub fn bip44_account(&self, account: u32) -> Result { + let path = match self.master_key.network { + crate::Network::Dash => crate::dip9::DASH_BIP44_PATH_MAINNET, + crate::Network::Testnet => crate::dip9::DASH_BIP44_PATH_TESTNET, + _ => return Err(Error::InvalidNetwork), + }; + + // Convert to DerivationPath and append account index + let mut full_path = crate::bip32::DerivationPath::from(path); + let child_number = crate::bip32::ChildNumber::from_hardened_idx(account) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + full_path.push(child_number); + + self.derive(&full_path) + } + + /// Get a CoinJoin account key + pub fn coinjoin_account(&self, account: u32) -> Result { + let path = match self.master_key.network { + crate::Network::Dash => crate::dip9::COINJOIN_PATH_MAINNET, + crate::Network::Testnet => crate::dip9::COINJOIN_PATH_TESTNET, + _ => return Err(Error::InvalidNetwork), + }; + + // Convert to DerivationPath and append account index + let mut full_path = crate::bip32::DerivationPath::from(path); + let child_number = crate::bip32::ChildNumber::from_hardened_idx(account) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + full_path.push(child_number); + + self.derive(&full_path) + } + + /// Get an identity authentication key + pub fn identity_authentication_key( + &self, + identity_index: u32, + key_index: u32, + ) -> Result { + let path = match self.master_key.network { + crate::Network::Dash => crate::dip9::IDENTITY_AUTHENTICATION_PATH_MAINNET, + crate::Network::Testnet => crate::dip9::IDENTITY_AUTHENTICATION_PATH_TESTNET, + _ => return Err(Error::InvalidNetwork), + }; + + // Convert to DerivationPath and append indices + let mut full_path = crate::bip32::DerivationPath::from(path); + full_path.push(crate::bip32::ChildNumber::from_hardened_idx(identity_index).unwrap()); + full_path.push(crate::bip32::ChildNumber::from_hardened_idx(key_index).unwrap()); + + self.derive(&full_path) + } +} + +/// Address derivation for a specific account +pub struct AccountDerivation { + account_key: ExtendedPrivKey, + secp: Secp256k1, +} + +impl AccountDerivation { + /// Create a new account derivation + pub fn new(account_key: ExtendedPrivKey) -> Self { + Self { + account_key, + secp: Secp256k1::new(), + } + } + + /// Derive an external (receive) address at index + pub fn receive_address(&self, index: u32) -> Result { + let path = format!("m/0/{}", index) + .parse::() + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + self.account_key.derive_pub(&self.secp, &path).map_err(Into::into) + } + + /// Derive an internal (change) address at index + pub fn change_address(&self, index: u32) -> Result { + let path = format!("m/1/{}", index) + .parse::() + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + self.account_key.derive_pub(&self.secp, &path).map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mnemonic::{Language, Mnemonic}; + + #[test] + fn test_hd_wallet_derivation() { + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, crate::Network::Dash).unwrap(); + + // Test BIP44 account derivation + let account0 = wallet.bip44_account(0).unwrap(); + assert_ne!(&account0.private_key[..], &wallet.master_key().private_key[..]); + } +} diff --git a/dash/src/dip9.rs b/key-wallet/src/dip9.rs similarity index 99% rename from dash/src/dip9.rs rename to key-wallet/src/dip9.rs index 1d80237b8..a53923a15 100644 --- a/dash/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -22,8 +22,8 @@ pub enum DerivationPathReference { use bitflags::bitflags; use secp256k1::Secp256k1; -use crate::Network; use crate::bip32::{ChildNumber, DerivationPath, Error, ExtendedPrivKey, ExtendedPubKey}; +use dash_network::Network; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] diff --git a/key-wallet/src/error.rs b/key-wallet/src/error.rs new file mode 100644 index 000000000..0fa0f10c8 --- /dev/null +++ b/key-wallet/src/error.rs @@ -0,0 +1,68 @@ +//! Error types for the key-wallet library + +use core::fmt; + +#[cfg(feature = "std")] +use std::error; + +/// Result type alias for key-wallet operations +pub type Result = core::result::Result; + +/// Errors that can occur in key-wallet operations +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + /// BIP32 related error + Bip32(crate::bip32::Error), + /// Invalid mnemonic phrase + InvalidMnemonic(String), + /// Invalid derivation path + InvalidDerivationPath(String), + /// Invalid address + InvalidAddress(String), + /// Secp256k1 error + Secp256k1(secp256k1::Error), + /// Base58 decoding error + Base58, + /// Invalid network + InvalidNetwork, + /// Key error + KeyError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Bip32(e) => write!(f, "BIP32 error: {}", e), + Error::InvalidMnemonic(s) => write!(f, "Invalid mnemonic: {}", s), + Error::InvalidDerivationPath(s) => write!(f, "Invalid derivation path: {}", s), + Error::InvalidAddress(s) => write!(f, "Invalid address: {}", s), + Error::Secp256k1(e) => write!(f, "Secp256k1 error: {}", e), + Error::Base58 => write!(f, "Base58 decoding error"), + Error::InvalidNetwork => write!(f, "Invalid network"), + Error::KeyError(s) => write!(f, "Key error: {}", s), + } + } +} + +#[cfg(feature = "std")] +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::Bip32(e) => Some(e), + Error::Secp256k1(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(e: crate::bip32::Error) -> Self { + Error::Bip32(e) + } +} + +impl From for Error { + fn from(e: secp256k1::Error) -> Self { + Error::Secp256k1(e) + } +} diff --git a/key-wallet/src/lib.rs b/key-wallet/src/lib.rs new file mode 100644 index 000000000..b965fa09d --- /dev/null +++ b/key-wallet/src/lib.rs @@ -0,0 +1,40 @@ +//! Key Wallet Library +//! +//! This library provides key derivation and wallet functionality for Dash, +//! including BIP32 hierarchical deterministic wallets, BIP39 mnemonic support, +//! and Dash-specific derivation paths (DIP9). + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(test)] +#[macro_use] +mod test_macros; + +pub mod address; +pub mod bip32; +pub mod derivation; +pub mod dip9; +pub mod error; +pub mod mnemonic; +pub(crate) mod utils; + +pub use address::{Address, AddressType, NetworkExt}; +pub use bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey}; +pub use dash_network::Network; +pub use derivation::KeyDerivation; +pub use dip9::{DerivationPathReference, DerivationPathType}; +pub use error::{Error, Result}; +pub use mnemonic::Mnemonic; + +/// Re-export commonly used types +pub mod prelude { + pub use super::{ + Address, AddressType, ChildNumber, DerivationPath, Error, ExtendedPrivKey, ExtendedPubKey, + KeyDerivation, Mnemonic, Result, + }; +} diff --git a/key-wallet/src/mnemonic.rs b/key-wallet/src/mnemonic.rs new file mode 100644 index 000000000..7f88ae62c --- /dev/null +++ b/key-wallet/src/mnemonic.rs @@ -0,0 +1,175 @@ +//! BIP39 Mnemonic implementation + +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; +use core::str::FromStr; + +use bip39 as bip39_crate; + +use crate::bip32::ExtendedPrivKey; +use crate::error::{Error, Result}; + +/// Language for mnemonic generation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + English, + ChineseSimplified, + ChineseTraditional, + Czech, + French, + Italian, + Japanese, + Korean, + Spanish, +} + +impl From for bip39_crate::Language { + fn from(lang: Language) -> Self { + match lang { + Language::English => bip39_crate::Language::English, + Language::ChineseSimplified => bip39_crate::Language::SimplifiedChinese, + Language::ChineseTraditional => bip39_crate::Language::TraditionalChinese, + Language::Czech => bip39_crate::Language::Czech, + Language::French => bip39_crate::Language::French, + Language::Italian => bip39_crate::Language::Italian, + Language::Japanese => bip39_crate::Language::Japanese, + Language::Korean => bip39_crate::Language::Korean, + Language::Spanish => bip39_crate::Language::Spanish, + } + } +} + +/// BIP39 Mnemonic phrase +pub struct Mnemonic { + inner: bip39_crate::Mnemonic, +} + +impl Mnemonic { + /// Generate a new mnemonic with the specified word count + #[cfg(feature = "getrandom")] + pub fn generate(word_count: usize, language: Language) -> Result { + // Validate word count and get entropy size + let entropy_bytes = match word_count { + 12 => 16, // 128 bits / 8 + 15 => 20, // 160 bits / 8 + 18 => 24, // 192 bits / 8 + 21 => 28, // 224 bits / 8 + 24 => 32, // 256 bits / 8 + _ => return Err(Error::InvalidMnemonic("Invalid word count".into())), + }; + + // Generate random entropy + let mut entropy = vec![0u8; entropy_bytes]; + getrandom::getrandom(&mut entropy) + .map_err(|e| Error::InvalidMnemonic(format!("Failed to generate entropy: {}", e)))?; + + // Create mnemonic from entropy with specified language + let mnemonic = bip39_crate::Mnemonic::from_entropy_in(language.into(), &entropy) + .map_err(|e| Error::InvalidMnemonic(e.to_string()))?; + + Ok(Self { + inner: mnemonic, + }) + } + + /// Generate a new mnemonic with the specified word count + #[cfg(not(feature = "getrandom"))] + pub fn generate(word_count: usize, _language: Language) -> Result { + let _entropy_bits = match word_count { + 12 => 128, + 15 => 160, + 18 => 192, + 21 => 224, + 24 => 256, + _ => return Err(Error::InvalidMnemonic("Invalid word count".into())), + }; + + Err(Error::InvalidMnemonic("Mnemonic generation requires getrandom feature".into())) + } + + /// Create a mnemonic from a phrase + pub fn from_phrase(phrase: &str, language: Language) -> Result { + let mnemonic = bip39_crate::Mnemonic::parse_in(language.into(), phrase) + .map_err(|e| Error::InvalidMnemonic(e.to_string()))?; + + Ok(Self { + inner: mnemonic, + }) + } + + /// Get the mnemonic phrase as a string + pub fn phrase(&self) -> String { + self.inner.words().collect::>().join(" ") + } + + /// Get the word count + pub fn word_count(&self) -> usize { + self.inner.word_count() + } + + /// Create a mnemonic from entropy bytes + pub fn from_entropy(entropy: &[u8], language: Language) -> Result { + let mnemonic = bip39_crate::Mnemonic::from_entropy_in(language.into(), entropy) + .map_err(|e| Error::InvalidMnemonic(e.to_string()))?; + + Ok(Self { + inner: mnemonic, + }) + } + + /// Convert to seed with optional passphrase + pub fn to_seed(&self, passphrase: &str) -> [u8; 64] { + let mut seed = [0u8; 64]; + seed.copy_from_slice(&self.inner.to_seed(passphrase)); + seed + } + + /// Derive extended private key from this mnemonic + pub fn to_extended_key( + &self, + passphrase: &str, + network: crate::Network, + ) -> Result { + let seed = self.to_seed(passphrase); + ExtendedPrivKey::new_master(network, &seed).map_err(Into::into) + } + + /// Validate a mnemonic phrase + pub fn validate(phrase: &str, language: Language) -> bool { + bip39_crate::Mnemonic::parse_in(language.into(), phrase).is_ok() + } +} + +impl FromStr for Mnemonic { + type Err = Error; + + fn from_str(s: &str) -> Result { + // Try English by default + Self::from_phrase(s, Language::English) + } +} + +impl fmt::Display for Mnemonic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.phrase()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(feature = "getrandom")] + fn test_mnemonic_generation() { + let mnemonic = Mnemonic::generate(12, Language::English).unwrap(); + assert_eq!(mnemonic.word_count(), 12); + } + + #[test] + fn test_mnemonic_validation() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + assert!(Mnemonic::validate(phrase, Language::English)); + } +} diff --git a/key-wallet/src/test_macros.rs b/key-wallet/src/test_macros.rs new file mode 100644 index 000000000..5337ac004 --- /dev/null +++ b/key-wallet/src/test_macros.rs @@ -0,0 +1,12 @@ +//! Test macros for key-wallet. + +#[cfg(all(test, feature = "serde"))] +macro_rules! serde_round_trip { + ($var:expr) => {{ + use serde_json; + + let encoded = serde_json::to_value(&$var).unwrap(); + let decoded = serde_json::from_value(encoded).unwrap(); + assert_eq!($var, decoded); + }}; +} diff --git a/key-wallet/src/utils.rs b/key-wallet/src/utils.rs new file mode 100644 index 000000000..850ff732b --- /dev/null +++ b/key-wallet/src/utils.rs @@ -0,0 +1,59 @@ +//! Utility functions for the key-wallet library + +/// Parse a hex character to its numeric value +pub(crate) fn parse_hex_digit(digit: u8) -> Option { + match digit { + b'0'..=b'9' => Some(digit - b'0'), + b'a'..=b'f' => Some(digit - b'a' + 10), + b'A'..=b'F' => Some(digit - b'A' + 10), + _ => None, + } +} + +/// Parse a hex string into bytes +pub(crate) fn parse_hex_bytes(hex_str: &str, output: &mut [u8]) -> Result<(), &'static str> { + if hex_str.len() != output.len() * 2 { + return Err("invalid hex length"); + } + + for (i, chunk) in hex_str.as_bytes().chunks(2).enumerate() { + let high = parse_hex_digit(chunk[0]).ok_or("invalid hex character")?; + let low = parse_hex_digit(chunk[1]).ok_or("invalid hex character")?; + output[i] = (high << 4) | low; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_hex_digit() { + assert_eq!(parse_hex_digit(b'0'), Some(0)); + assert_eq!(parse_hex_digit(b'9'), Some(9)); + assert_eq!(parse_hex_digit(b'a'), Some(10)); + assert_eq!(parse_hex_digit(b'f'), Some(15)); + assert_eq!(parse_hex_digit(b'A'), Some(10)); + assert_eq!(parse_hex_digit(b'F'), Some(15)); + assert_eq!(parse_hex_digit(b'g'), None); + assert_eq!(parse_hex_digit(b'G'), None); + } + + #[test] + fn test_parse_hex_bytes() { + let mut output = [0u8; 4]; + assert!(parse_hex_bytes("deadbeef", &mut output).is_ok()); + assert_eq!(output, [0xde, 0xad, 0xbe, 0xef]); + + let mut output = [0u8; 2]; + assert!(parse_hex_bytes("1234", &mut output).is_ok()); + assert_eq!(output, [0x12, 0x34]); + + // Test error cases + let mut output = [0u8; 2]; + assert!(parse_hex_bytes("123", &mut output).is_err()); // Wrong length + assert!(parse_hex_bytes("12345", &mut output).is_err()); // Wrong length + assert!(parse_hex_bytes("12gg", &mut output).is_err()); // Invalid character + } +} diff --git a/key-wallet/summary.md b/key-wallet/summary.md new file mode 100644 index 000000000..979dde8f1 --- /dev/null +++ b/key-wallet/summary.md @@ -0,0 +1 @@ +# Summary of changes made to make bip32.rs work as a standalone crate: diff --git a/key-wallet/tests/address_tests.rs b/key-wallet/tests/address_tests.rs new file mode 100644 index 000000000..e9f52aaac --- /dev/null +++ b/key-wallet/tests/address_tests.rs @@ -0,0 +1,139 @@ +//! Address tests + +use bitcoin_hashes::{hash160, Hash}; +use key_wallet::address::{Address, AddressGenerator, AddressType}; +use key_wallet::derivation::HDWallet; +use key_wallet::Network; +use secp256k1::{PublicKey, Secp256k1}; + +#[test] +fn test_p2pkh_address_creation() { + let secp = Secp256k1::new(); + + // Create a public key + let secret_key = secp256k1::SecretKey::from_slice(&[1u8; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Create P2PKH address + let address = Address::p2pkh(&public_key, Network::Dash); + + assert_eq!(address.network, Network::Dash); + assert_eq!(address.address_type, AddressType::P2PKH); + + // Check that it generates a valid Dash address (starts with 'X') + let addr_str = address.to_string(); + // Address starts with 'X' for mainnet + assert!(addr_str.starts_with('X')); +} + +#[test] +fn test_testnet_address() { + let secp = Secp256k1::new(); + + // Create a public key + let secret_key = secp256k1::SecretKey::from_slice(&[1u8; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Create testnet P2PKH address + let address = Address::p2pkh(&public_key, Network::Testnet); + + // Check that it generates a valid testnet address (starts with 'y') + let addr_str = address.to_string(); + assert!(addr_str.starts_with('y')); +} + +#[test] +fn test_p2sh_address_creation() { + // Create a script hash + let script_hash = hash160::Hash::hash(b"test script"); + + // Create P2SH address + let address = Address::p2sh(script_hash, Network::Dash); + + assert_eq!(address.network, Network::Dash); + assert_eq!(address.address_type, AddressType::P2SH); + + // Check that it generates a valid P2SH address (starts with '7') + let addr_str = address.to_string(); + assert!(addr_str.starts_with('7')); +} + +#[test] +fn test_address_parsing() { + // Test mainnet P2PKH + let addr_str = "XmnGSJav3CWVmzDv5U68k7XT9rRPqyavtE"; + let address = Address::from_str(addr_str).unwrap(); + + assert_eq!(address.network, Network::Dash); + assert_eq!(address.address_type, AddressType::P2PKH); + assert_eq!(address.to_string(), addr_str); +} + +#[test] +fn test_address_script_pubkey() { + let secp = Secp256k1::new(); + + // Create a public key + let secret_key = secp256k1::SecretKey::from_slice(&[1u8; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Create P2PKH address + let address = Address::p2pkh(&public_key, Network::Dash); + let script_pubkey = address.script_pubkey(); + + // P2PKH script should be 25 bytes + assert_eq!(script_pubkey.len(), 25); + + // Check script structure + assert_eq!(script_pubkey[0], 0x76); // OP_DUP + assert_eq!(script_pubkey[1], 0xa9); // OP_HASH160 + assert_eq!(script_pubkey[2], 0x14); // Push 20 bytes + assert_eq!(script_pubkey[23], 0x88); // OP_EQUALVERIFY + assert_eq!(script_pubkey[24], 0xac); // OP_CHECKSIG +} + +#[test] +fn test_address_generator() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Get account public key + let account = wallet.bip44_account(0).unwrap(); + let path = key_wallet::DerivationPath::from(vec![ + key_wallet::ChildNumber::from_hardened_idx(44).unwrap(), + key_wallet::ChildNumber::from_hardened_idx(5).unwrap(), + key_wallet::ChildNumber::from_hardened_idx(0).unwrap(), + ]); + let account_xpub = wallet.derive_pub(&path).unwrap(); + + // Create address generator + let generator = AddressGenerator::new(Network::Dash); + + // Generate single address + let address = generator.generate_p2pkh(&account_xpub); + assert_eq!(address.network, Network::Dash); + assert_eq!(address.address_type, AddressType::P2PKH); +} + +#[test] +fn test_address_range_generation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Get account public key + let account = wallet.bip44_account(0).unwrap(); + let secp = Secp256k1::new(); + let account_xpub = key_wallet::ExtendedPubKey::from_priv(&secp, &account); + + // Create address generator + let generator = AddressGenerator::new(Network::Dash); + + // Generate range of external addresses + let addresses = generator.generate_range(&account_xpub, true, 0, 5).unwrap(); + assert_eq!(addresses.len(), 5); + + // All addresses should be different + let addr_strings: Vec<_> = addresses.iter().map(|a| a.to_string()).collect(); + let unique_count = addr_strings.iter().collect::>().len(); + assert_eq!(unique_count, 5); +} diff --git a/key-wallet/tests/bip32_tests.rs b/key-wallet/tests/bip32_tests.rs new file mode 100644 index 000000000..6549189ba --- /dev/null +++ b/key-wallet/tests/bip32_tests.rs @@ -0,0 +1,83 @@ +//! BIP32 tests + +use key_wallet::{ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Network}; +use secp256k1::Secp256k1; +use std::str::FromStr; + +#[test] +fn test_extended_key_derivation() { + let secp = Secp256k1::new(); + + // Test vector from BIP32 + let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap(); + let master = ExtendedPrivKey::new_master(Network::Dash, &seed).unwrap(); + + // m/0' + let child = master.ckd_priv(&secp, ChildNumber::from_hardened_idx(0).unwrap()).unwrap(); + assert_eq!(child.depth, 1); + + // m/0'/1 + let path = DerivationPath::from_str("m/0'/1").unwrap(); + let derived = master.derive_priv(&secp, &path).unwrap(); + assert_eq!(derived.depth, 2); +} + +#[test] +fn test_derivation_path_parsing() { + // Valid paths + assert!(DerivationPath::from_str("m").is_ok()); + assert!(DerivationPath::from_str("m/0").is_ok()); + assert!(DerivationPath::from_str("m/0'").is_ok()); + assert!(DerivationPath::from_str("m/44'/5'/0'/0/0").is_ok()); + + // Invalid paths + assert!(DerivationPath::from_str("").is_err()); + assert!(DerivationPath::from_str("n/0").is_err()); + assert!(DerivationPath::from_str("m/").is_err()); +} + +#[test] +fn test_extended_key_serialization() { + let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap(); + let master = ExtendedPrivKey::new_master(Network::Dash, &seed).unwrap(); + + // Serialize and deserialize + let serialized = master.to_string(); + let deserialized = ExtendedPrivKey::from_str(&serialized).unwrap(); + + assert_eq!(master.network, deserialized.network); + assert_eq!(master.depth, deserialized.depth); + assert_eq!(master.parent_fingerprint, deserialized.parent_fingerprint); + assert_eq!(master.child_number, deserialized.child_number); + assert_eq!(master.chain_code, deserialized.chain_code); + assert_eq!(master.private_key, deserialized.private_key); +} + +#[test] +fn test_public_key_derivation() { + let secp = Secp256k1::new(); + let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap(); + let master = ExtendedPrivKey::new_master(Network::Dash, &seed).unwrap(); + let master_pub = ExtendedPubKey::from_priv(&secp, &master); + + // Can derive non-hardened child from public key + let child_pub = master_pub.ckd_pub(&secp, ChildNumber::from_normal_idx(0).unwrap()).unwrap(); + + // Should match derivation from private key + let child_priv = master.ckd_priv(&secp, ChildNumber::from_normal_idx(0).unwrap()).unwrap(); + let child_pub_from_priv = ExtendedPubKey::from_priv(&secp, &child_priv); + + assert_eq!(child_pub.public_key, child_pub_from_priv.public_key); +} + +#[test] +fn test_fingerprint_calculation() { + let secp = Secp256k1::new(); + let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap(); + let master = ExtendedPrivKey::new_master(Network::Dash, &seed).unwrap(); + + let child = master.ckd_priv(&secp, ChildNumber::from_normal_idx(0).unwrap()).unwrap(); + let master_fingerprint = master.fingerprint(&secp); + + assert_eq!(child.parent_fingerprint, master_fingerprint); +} diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs new file mode 100644 index 000000000..6be6c7aa2 --- /dev/null +++ b/key-wallet/tests/derivation_tests.rs @@ -0,0 +1,111 @@ +//! Derivation tests + +use key_wallet::derivation::{AccountDerivation, HDWallet}; +use key_wallet::mnemonic::{Language, Mnemonic}; +use key_wallet::{DerivationPath, ExtendedPubKey, Network}; +use secp256k1::Secp256k1; +use std::str::FromStr; + +#[test] +fn test_hd_wallet_creation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Master key should be at depth 0 + assert_eq!(wallet.master_key().depth, 0); +} + +#[test] +fn test_bip44_account_derivation() { + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + Language::English + ).unwrap(); + + let seed = mnemonic.to_seed(""); + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive first account + let account0 = wallet.bip44_account(0).unwrap(); + assert_eq!(account0.depth, 3); // m/44'/5'/0' + + // Derive second account + let account1 = wallet.bip44_account(1).unwrap(); + assert_eq!(account1.depth, 3); // m/44'/5'/1' + + // Keys should be different + assert_ne!(account0.private_key.secret_bytes(), account1.private_key.secret_bytes()); +} + +#[test] +fn test_coinjoin_account_derivation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive CoinJoin account + let coinjoin_account = wallet.coinjoin_account(0).unwrap(); + assert_eq!(coinjoin_account.depth, 4); // m/9'/5'/4'/0' +} + +#[test] +fn test_identity_key_derivation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive identity authentication key + let identity_key = wallet.identity_authentication_key(0, 0).unwrap(); + assert_eq!(identity_key.depth, 6); // m/5'/5'/3'/0'/0'/0' +} + +#[test] +fn test_custom_path_derivation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive custom path + let path = DerivationPath::from_str("m/0/1/2").unwrap(); + let derived = wallet.derive(&path).unwrap(); + assert_eq!(derived.depth, 3); +} + +#[test] +fn test_account_address_derivation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Get account + let account = wallet.bip44_account(0).unwrap(); + let account_derivation = AccountDerivation::new(account); + + // Derive receive addresses + let addr0 = account_derivation.receive_address(0).unwrap(); + let addr1 = account_derivation.receive_address(1).unwrap(); + + // Addresses should be different + assert_ne!(addr0.public_key, addr1.public_key); + + // Derive change addresses + let change0 = account_derivation.change_address(0).unwrap(); + let change1 = account_derivation.change_address(1).unwrap(); + + // Change addresses should be different from receive addresses + assert_ne!(addr0.public_key, change0.public_key); + assert_ne!(change0.public_key, change1.public_key); +} + +#[test] +fn test_public_key_derivation() { + let seed = [0u8; 64]; + let wallet = HDWallet::from_seed(&seed, Network::Dash).unwrap(); + + // Derive public key directly + let path = DerivationPath::from_str("m/44'/5'/0'/0/0").unwrap(); + let xpub = wallet.derive_pub(&path).unwrap(); + + // Should match derivation from private key + let xprv = wallet.derive(&path).unwrap(); + let secp = Secp256k1::new(); + let xpub_from_prv = ExtendedPubKey::from_priv(&secp, &xprv); + + assert_eq!(xpub.public_key, xpub_from_prv.public_key); +} diff --git a/key-wallet/tests/mnemonic_tests.rs b/key-wallet/tests/mnemonic_tests.rs new file mode 100644 index 000000000..7554d75d9 --- /dev/null +++ b/key-wallet/tests/mnemonic_tests.rs @@ -0,0 +1,115 @@ +//! Mnemonic tests + +use key_wallet::mnemonic::{Language, Mnemonic}; +use key_wallet::{ExtendedPrivKey, Network}; + +#[test] +fn test_mnemonic_validation() { + // Valid 12-word mnemonic + let valid_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + assert!(Mnemonic::validate(valid_phrase, Language::English)); + + // Invalid mnemonic (wrong checksum) + let invalid_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"; + assert!(!Mnemonic::validate(invalid_phrase, Language::English)); +} + +#[test] +fn test_mnemonic_from_phrase() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + + assert_eq!(mnemonic.word_count(), 12); + assert_eq!(mnemonic.phrase(), phrase); +} + +#[test] +fn test_mnemonic_to_seed() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + + // Test with empty passphrase + let seed1 = mnemonic.to_seed(""); + assert_eq!(seed1.len(), 64); + + // Test with passphrase + let seed2 = mnemonic.to_seed("TREZOR"); + assert_eq!(seed2.len(), 64); + + // Seeds should be different + assert_ne!(seed1, seed2); +} + +#[test] +fn test_mnemonic_to_extended_key() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + + let xprv = mnemonic.to_extended_key("", Network::Dash).unwrap(); + assert_eq!(xprv.network, Network::Dash); + assert_eq!(xprv.depth, 0); +} + +#[test] +fn test_mnemonic_generation() { + // Test different word counts with deterministic entropy + let test_cases = vec![ + ( + 12, + vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, + ], + ), + ( + 15, + vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, + ], + ), + ( + 18, + vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + ], + ), + ( + 21, + vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + ], + ), + ( + 24, + vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff, + ], + ), + ]; + + for (word_count, entropy) in test_cases { + let mnemonic = Mnemonic::from_entropy(&entropy, Language::English).unwrap(); + assert_eq!(mnemonic.word_count(), word_count); + + // Generated mnemonic should be valid + assert!(Mnemonic::validate(&mnemonic.phrase(), Language::English)); + } +} + +#[test] +fn test_different_languages() { + let phrase_en = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + // Test English + let mnemonic_en = Mnemonic::from_phrase(phrase_en, Language::English).unwrap(); + assert!(mnemonic_en.word_count() == 12); + + // Same seed regardless of language (for same phrase) + let seed_en = mnemonic_en.to_seed(""); + assert_eq!(seed_en.len(), 64); +}