From c1924232fb1d4ed2d92dcf62af108104edb3052d Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Thu, 14 May 2026 15:56:54 -0700 Subject: [PATCH] remove key-wallet-ffi crate usage, platform-wallet-ffi wraps all the logic Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 18 - Cargo.toml | 1 - .../src/core_addresses.rs | 29 + .../src/key_wallet_ffi.rs | 95 +++ packages/rs-platform-wallet-ffi/src/lib.rs | 4 + .../rs-platform-wallet-ffi/src/persistence.rs | 1 - packages/rs-sdk-ffi/cbindgen.toml | 3 +- packages/rs-unified-sdk-ffi/Cargo.toml | 3 +- packages/rs-unified-sdk-ffi/src/lib.rs | 1 - .../SwiftDashSDK/KeyWallet/Account.swift | 65 -- .../KeyWallet/AccountCollection.swift | 54 -- .../SwiftDashSDK/KeyWallet/Address.swift | 66 -- .../SwiftDashSDK/KeyWallet/AddressPool.swift | 119 --- .../SwiftDashSDK/KeyWallet/BLSAccount.swift | 20 - .../SwiftDashSDK/KeyWallet/EdDSAAccount.swift | 20 - .../KeyWallet/KeyDerivation.swift | 345 +-------- .../SwiftDashSDK/KeyWallet/KeyWallet.swift | 52 -- .../KeyWallet/KeyWalletTypes.swift | 484 ------------- .../KeyWallet/ManagedAccount.swift | 174 ----- .../KeyWallet/ManagedAccountCollection.swift | 299 -------- .../KeyWallet/ManagedPlatformAccount.swift | 101 --- .../SwiftDashSDK/KeyWallet/Mnemonic.swift | 161 +--- .../Sources/SwiftDashSDK/KeyWallet/README.md | 335 --------- .../SwiftDashSDK/KeyWallet/Transaction.swift | 117 --- .../KeyWallet/TransactionContext.swift | 45 -- .../KeyWallet/TransactionRecord.swift | 28 - .../SwiftDashSDK/KeyWallet/TxOutput.swift | 21 - .../SwiftDashSDK/KeyWallet/Wallet.swift | 527 -------------- .../KeyWallet/WalletManager.swift | 685 ------------------ .../PlatformWalletPersistenceHandler.swift | 68 +- .../SwiftDashSDK/Wallet/WalletModels.swift | 119 --- .../Core/Models/DashAddress.swift | 13 +- .../PlatformWalletTests.swift | 10 - packages/swift-sdk/build_ios.sh | 1 - 34 files changed, 215 insertions(+), 3869 deletions(-) create mode 100644 packages/rs-platform-wallet-ffi/src/core_addresses.rs create mode 100644 packages/rs-platform-wallet-ffi/src/key_wallet_ffi.rs delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Address.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWallet.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedPlatformAccount.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/README.md delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionContext.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionRecord.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TxOutput.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift delete mode 100644 packages/swift-sdk/Sources/SwiftDashSDK/Wallet/WalletModels.swift diff --git a/Cargo.lock b/Cargo.lock index 8b498337182..829e5742743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3798,29 +3798,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "key-wallet-ffi" -version = "0.42.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=53130869e5b9343ae59016323e5e5269e717a8fd#53130869e5b9343ae59016323e5e5269e717a8fd" -dependencies = [ - "cbindgen 0.29.2", - "dash-network", - "dashcore", - "hex", - "key-wallet", - "key-wallet-manager", - "libc", - "secp256k1", - "tokio", -] - [[package]] name = "key-wallet-manager" version = "0.42.0" source = "git+https://github.com/dashpay/rust-dashcore?rev=53130869e5b9343ae59016323e5e5269e717a8fd#53130869e5b9343ae59016323e5e5269e717a8fd" dependencies = [ "async-trait", - "bincode", "dashcore", "key-wallet", "rayon", @@ -5912,7 +5895,6 @@ name = "rs-unified-sdk-ffi" version = "3.1.0-dev.1" dependencies = [ "dash-network", - "key-wallet-ffi", "platform-wallet-ffi", "rs-sdk-ffi", ] diff --git a/Cargo.toml b/Cargo.toml index 13f883ece09..91b64117e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,6 @@ dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5 dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "53130869e5b9343ae59016323e5e5269e717a8fd" } diff --git a/packages/rs-platform-wallet-ffi/src/core_addresses.rs b/packages/rs-platform-wallet-ffi/src/core_addresses.rs new file mode 100644 index 00000000000..471413208b8 --- /dev/null +++ b/packages/rs-platform-wallet-ffi/src/core_addresses.rs @@ -0,0 +1,29 @@ +//! Dash Core (on-chain) address validation. + +use dash_network::ffi::FFINetwork; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::str::FromStr; + +/// Validate that `address` is a well-formed Dash address on `network`. +/// Any null, non-UTF-8, malformed, or wrong-network input returns `false`. +/// +/// # Safety +/// `address` must be a valid NUL-terminated C string. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_address_validate( + address: *const c_char, + network: FFINetwork, +) -> bool { + if address.is_null() { + return false; + } + let Ok(address_str) = CStr::from_ptr(address).to_str() else { + return false; + }; + let Ok(parsed) = dashcore::Address::from_str(address_str) else { + return false; + }; + let net: dashcore::Network = network.into(); + parsed.require_network(net).is_ok() +} diff --git a/packages/rs-platform-wallet-ffi/src/key_wallet_ffi.rs b/packages/rs-platform-wallet-ffi/src/key_wallet_ffi.rs new file mode 100644 index 00000000000..80918b1798c --- /dev/null +++ b/packages/rs-platform-wallet-ffi/src/key_wallet_ffi.rs @@ -0,0 +1,95 @@ +//! BIP-39 mnemonic and BIP-32 derivation primitives. +//! +//! Thin C-ABI wrappers over the `key-wallet` crate — the surface that +//! `key-wallet-ffi` used to provide before it was dropped from this repo. + +use crate::error::*; +use crate::{check_ptr, unwrap_result_or_return}; +use dash_network::ffi::FFINetwork; +use key_wallet::mnemonic::{Language, Mnemonic}; +use std::os::raw::c_char; + +/// BIP-39 wordlist language. Mirrors `key_wallet::mnemonic::Language`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum FFILanguage { + English = 0, + ChineseSimplified = 1, + ChineseTraditional = 2, + Czech = 3, + French = 4, + Italian = 5, + Japanese = 6, + Korean = 7, + Portuguese = 8, + Spanish = 9, +} + +impl From for Language { + fn from(language: FFILanguage) -> Self { + match language { + FFILanguage::English => Language::English, + FFILanguage::ChineseSimplified => Language::ChineseSimplified, + FFILanguage::ChineseTraditional => Language::ChineseTraditional, + FFILanguage::Czech => Language::Czech, + FFILanguage::French => Language::French, + FFILanguage::Italian => Language::Italian, + FFILanguage::Japanese => Language::Japanese, + FFILanguage::Korean => Language::Korean, + FFILanguage::Portuguese => Language::Portuguese, + FFILanguage::Spanish => Language::Spanish, + } + } +} + +/// Generate a fresh BIP-39 mnemonic of `word_count` words (12, 15, 18, +/// 21, or 24) in `language`. Writes a heap C string into `out_mnemonic` +/// (caller frees via `platform_wallet_string_free`). +/// +/// # Safety +/// `out_mnemonic` must be a valid writable `*mut c_char` location. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_generate_mnemonic( + word_count: u32, + language: FFILanguage, + out_mnemonic: *mut *mut c_char, +) -> PlatformWalletFFIResult { + check_ptr!(out_mnemonic); + let phrase = unwrap_result_or_return!(Mnemonic::generate(word_count as usize, language.into())); + let c_str = unwrap_result_or_return!(std::ffi::CString::new(phrase.to_string())); + *out_mnemonic = c_str.into_raw(); + PlatformWalletFFIResult::ok() +} + +/// Build the DIP-9 identity-authentication path +/// `m/9'/'/5'/0'/identity_index'/key_index'`. Writes a heap C +/// string into `out_path` (caller frees via `platform_wallet_string_free`). +/// Returns 0 on success, -1 on failure. +/// +/// # Safety +/// `out_path` must be a valid writable `*mut c_char` location. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_derive_identity_authentication_path( + network: FFINetwork, + identity_index: u32, + key_index: u32, + out_path: *mut *mut c_char, +) -> i32 { + if out_path.is_null() { + return -1; + } + use key_wallet::bip32::{DerivationPath, KeyDerivationType}; + let net: key_wallet::Network = network.into(); + let derivation = DerivationPath::identity_authentication_path( + net, + KeyDerivationType::ECDSA, + identity_index, + key_index, + ); + let path_str = format!("{}", derivation); + let Ok(c_str) = std::ffi::CString::new(path_str) else { + return -1; + }; + *out_path = c_str.into_raw(); + 0 +} diff --git a/packages/rs-platform-wallet-ffi/src/lib.rs b/packages/rs-platform-wallet-ffi/src/lib.rs index 0085d8c8547..e072d3447b1 100644 --- a/packages/rs-platform-wallet-ffi/src/lib.rs +++ b/packages/rs-platform-wallet-ffi/src/lib.rs @@ -14,6 +14,7 @@ pub mod contact; pub mod contact_persistence; pub mod contact_request; pub mod core_address_types; +pub mod core_addresses; pub mod core_wallet; pub mod core_wallet_types; pub mod dashpay; @@ -41,6 +42,7 @@ pub mod identity_top_up; pub mod identity_transfer; pub mod identity_update; pub mod identity_withdrawal; +pub mod key_wallet_ffi; pub mod managed_identity; pub mod manager; pub mod manager_diagnostics; @@ -71,6 +73,7 @@ pub use contact::*; pub use contact_persistence::*; pub use contact_request::*; pub use core_address_types::*; +pub use core_addresses::*; pub use core_wallet::*; pub use core_wallet_types::*; pub use dashpay::*; @@ -97,6 +100,7 @@ pub use identity_top_up::*; pub use identity_transfer::*; pub use identity_update::*; pub use identity_withdrawal::*; +pub use key_wallet_ffi::*; pub use managed_identity::*; pub use manager::*; pub use manager_diagnostics::*; diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index e873156a6e2..549f53617dc 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -2313,4 +2313,3 @@ unsafe fn slice_from_raw<'a>(ptr: *const u8, len: usize) -> &'a [u8] { slice::from_raw_parts(ptr, len) } } - diff --git a/packages/rs-sdk-ffi/cbindgen.toml b/packages/rs-sdk-ffi/cbindgen.toml index 8813a02e04b..e313734fcc3 100644 --- a/packages/rs-sdk-ffi/cbindgen.toml +++ b/packages/rs-sdk-ffi/cbindgen.toml @@ -16,8 +16,7 @@ documentation_style = "c99" [export] include = ["dash_sdk_*", "dash_core_*", "dash_unified_sdk_*"] -# Exclude types that come from key-wallet-ffi to avoid duplication -exclude = ["FFIAccountType", "FFIAccountTypePreference", "FFIAccountTypeUsed", "FFIAccountCreationOptionType"] +exclude = [] prefix = "" item_types = ["enums", "structs", "unions", "typedefs", "opaque", "functions"] diff --git a/packages/rs-unified-sdk-ffi/Cargo.toml b/packages/rs-unified-sdk-ffi/Cargo.toml index f853c1319df..9ca6aa76860 100644 --- a/packages/rs-unified-sdk-ffi/Cargo.toml +++ b/packages/rs-unified-sdk-ffi/Cargo.toml @@ -8,7 +8,6 @@ rust-version.workspace = true crate-type = ["staticlib", "cdylib"] [dependencies] -key-wallet-ffi = { workspace = true } platform-wallet-ffi = { path = "../rs-platform-wallet-ffi" } rs-sdk-ffi = { path = "../rs-sdk-ffi" } dash-network = { workspace = true, features = ["ffi"] } @@ -18,4 +17,4 @@ default = [] # Forwards to `platform-wallet-ffi/shielded`. Pulls Orchard / ZK # shielded sync support into the unified iOS framework. Off by # default so the framework's transparent surface stays slim. -shielded = ["platform-wallet-ffi/shielded"] \ No newline at end of file +shielded = ["platform-wallet-ffi/shielded"] diff --git a/packages/rs-unified-sdk-ffi/src/lib.rs b/packages/rs-unified-sdk-ffi/src/lib.rs index 582fbbd2e19..0751c3e99f2 100644 --- a/packages/rs-unified-sdk-ffi/src/lib.rs +++ b/packages/rs-unified-sdk-ffi/src/lib.rs @@ -1,4 +1,3 @@ pub use dash_network; -pub use key_wallet_ffi; pub use platform_wallet_ffi; pub use rs_sdk_ffi; diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift deleted file mode 100644 index 1062584d3ce..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Account.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a wallet account -public class Account { - private let handle: UnsafeMutablePointer - private weak var wallet: Wallet? - - internal init(handle: UnsafeMutablePointer, wallet: Wallet) { - self.handle = handle - self.wallet = wallet - } - - deinit { - account_free(handle) - } - - // The account-specific functionality would be implemented here - // For now, this is a placeholder that manages the FFI handle lifecycle - - // MARK: - Derivation (account-based) - - /// Derive a private key (WIF) using this account and a master xpriv derived from the given path. - /// - Parameters: - /// - wallet: The parent wallet used to derive the master extended private key - /// - masterPath: The account root derivation path (e.g., "m/9'/5'/3'/1'") - /// - index: The child index to derive (e.g., 0 for the first key) - /// - Returns: The private key encoded as WIF - public func derivePrivateKeyWIF(wallet: Wallet, masterPath: String, index: UInt32) throws -> String { - var error = FFIError() - // Derive master extended private key for this account root - let masterPtr = masterPath.withCString { pathCStr in - wallet_derive_extended_private_key(wallet.ffiHandle, pathCStr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - if let m = masterPtr { - extended_private_key_free(m) - } - } - - guard let master = masterPtr else { - throw KeyWalletError(ffiError: error) - } - - // Derive child private key as WIF at the given index - let wifPtr = account_derive_private_key_as_wif_at(self.handle, master, index, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = wifPtr else { - throw KeyWalletError(ffiError: error) - } - let wif = String(cString: ptr) - string_free(ptr) - return wif - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift deleted file mode 100644 index e71fc2c527d..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AccountCollection.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a collection of accounts -public class AccountCollection { - private let handle: UnsafeMutablePointer - private weak var wallet: Wallet? - - internal init(handle: UnsafeMutablePointer, wallet: Wallet) { - self.handle = handle - self.wallet = wallet - } - - deinit { - account_collection_free(handle) - } - - // MARK: - Provider Accounts (BLS) - - /// Get the provider operator keys account (BLS) - public func getProviderOperatorKeys() -> BLSAccount? { - guard let rawPointer = account_collection_get_provider_operator_keys(handle) else { - return nil - } - let accountHandle = rawPointer.assumingMemoryBound(to: FFIBLSAccount.self) - return BLSAccount(handle: accountHandle, wallet: wallet) - } - - // MARK: - Provider Accounts (EdDSA) - - /// Get the provider platform keys account (EdDSA) - public func getProviderPlatformKeys() -> EdDSAAccount? { - guard let rawPointer = account_collection_get_provider_platform_keys(handle) else { - return nil - } - let accountHandle = rawPointer.assumingMemoryBound(to: FFIEdDSAAccount.self) - return EdDSAAccount(handle: accountHandle, wallet: wallet) - } - - // MARK: - Summary - - /// Get a summary of all accounts in this collection - public func getSummary() -> AccountCollectionSummary? { - guard let summaryPtr = account_collection_summary_data(handle) else { - return nil - } - - defer { - account_collection_summary_free(summaryPtr) - } - - return AccountCollectionSummary(ffiSummary: summaryPtr.pointee) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Address.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Address.swift deleted file mode 100644 index 0984b37b708..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Address.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Address utilities -public class Address { - - /// Address type enumeration - public enum AddressType: UInt8 { - case p2pkh = 0 - case p2sh = 1 - case other = 2 - case unknown = 255 - } - - /// Validate an address - /// - Parameters: - /// - address: The address to validate - /// - network: The network type - /// - Returns: True if the address is valid - public static func validate(_ address: String, network: Network = .mainnet) -> Bool { - var error = FFIError() - - let isValid = address.withCString { addressCStr in - address_validate(addressCStr, network.ffiValue, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - return isValid - } - - /// Get the type of an address - /// - Parameters: - /// - address: The address to check - /// - network: The network type - /// - Returns: The address type - public static func getType(of address: String, network: Network = .mainnet) -> AddressType { - var error = FFIError() - - let typeRaw = address.withCString { addressCStr in - address_get_type(addressCStr, network.ffiValue, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - // Map the raw value to our enum - switch typeRaw { - case 0: - return .p2pkh - case 1: - return .p2sh - case 2: - return .other - default: - return .unknown - } - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift deleted file mode 100644 index b2ca4e88223..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for an address pool from a managed account -public class AddressPool { - private let handle: UnsafeMutablePointer - - internal init(handle: UnsafeMutablePointer) { - self.handle = handle - } - - deinit { - address_pool_free(handle) - } - - // MARK: - Address Access - - /// Get an address at a specific index - /// - Parameter index: The index of the address to retrieve - /// - Returns: The address information if it exists - public func getAddress(at index: UInt32) throws -> AddressInfo { - var error = FFIError() - - guard let infoPtr = address_pool_get_address_at_index(handle, index, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - defer { - address_info_free(infoPtr) - } - - return AddressInfo(ffiInfo: infoPtr.pointee) - } - - /// Get addresses in a range - /// - Parameters: - /// - startIndex: The starting index (inclusive) - /// - endIndex: The ending index (exclusive) - /// - Returns: Array of address information - public func getAddresses(from startIndex: UInt32, to endIndex: UInt32) throws -> [AddressInfo] { - var error = FFIError() - var count: Int = 0 - - guard let infosPtr = address_pool_get_addresses_in_range( - handle, startIndex, endIndex, &count, &error - ) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - defer { - address_info_array_free(infosPtr, count) - } - - var addresses: [AddressInfo] = [] - for i in 0.. 0 { - self.scriptPubKey = Data(bytes: scriptPtr, count: ffiInfo.script_pubkey_len) - } else { - self.scriptPubKey = Data() - } - - // Copy public key if available - if let pubKeyPtr = ffiInfo.public_key, ffiInfo.public_key_len > 0 { - self.publicKey = Data(bytes: pubKeyPtr, count: ffiInfo.public_key_len) - } else { - self.publicKey = nil - } - - self.index = ffiInfo.index - - // Copy derivation path - if let pathPtr = ffiInfo.path { - self.path = String(cString: pathPtr) - } else { - self.path = "" - } - - self.used = ffiInfo.used - self.generatedAt = Date(timeIntervalSince1970: TimeInterval(ffiInfo.generated_at)) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift deleted file mode 100644 index f5d4713f7eb..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/BLSAccount.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a BLS account (used for provider keys) -public class BLSAccount { - internal let handle: UnsafeMutablePointer - private weak var wallet: Wallet? - - internal init(handle: UnsafeMutablePointer, wallet: Wallet?) { - self.handle = handle - self.wallet = wallet - } - - deinit { - bls_account_free(handle) - } - - // BLS account specific functionality can be added here - // This class manages the lifecycle of BLS provider key accounts -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift deleted file mode 100644 index e612a7cfc4c..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/EdDSAAccount.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for an EdDSA account (used for platform P2P keys) -public class EdDSAAccount { - internal let handle: UnsafeMutablePointer - private weak var wallet: Wallet? - - internal init(handle: UnsafeMutablePointer, wallet: Wallet?) { - self.handle = handle - self.wallet = wallet - } - - deinit { - eddsa_account_free(handle) - } - - // EdDSA account specific functionality can be added here - // This class manages the lifecycle of EdDSA platform P2P key accounts -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyDerivation.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyDerivation.swift index 0060d20b5cf..ead6b192307 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyDerivation.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyDerivation.swift @@ -4,190 +4,6 @@ import DashSDKFFI /// Key derivation utilities public class KeyDerivation { - /// Create a new master extended private key from seed - /// - Parameters: - /// - seed: The seed bytes - /// - network: The network type - /// - Returns: Extended private key handle - public static func createMasterKey(seed: Data, network: Network = .mainnet) throws -> ExtendedPrivateKey { - var error = FFIError() - - let xprivPtr = seed.withUnsafeBytes { seedBytes in - let seedPtr = seedBytes.bindMemory(to: UInt8.self).baseAddress - return derivation_new_master_key(seedPtr, seed.count, network.ffiValue, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let handle = xprivPtr else { - throw KeyWalletError(ffiError: error) - } - - return ExtendedPrivateKey(handle: handle) - } - - /// Get BIP44 account path - /// - Parameters: - /// - network: The network type - /// - accountIndex: The account index - /// - Returns: The derivation path string - public static func getBIP44AccountPath(network: Network = .mainnet, - accountIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_bip44_account_path( - network.ffiValue, accountIndex, pathBuffer, maxPathLen, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return String(cString: pathBuffer) - } - - /// Get BIP44 payment path - /// - Parameters: - /// - network: The network type - /// - accountIndex: The account index - /// - isChange: Whether this is a change address - /// - addressIndex: The address index - /// - Returns: The derivation path string - public static func getBIP44PaymentPath(network: Network = .mainnet, - accountIndex: UInt32, - isChange: Bool, - addressIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_bip44_payment_path( - network.ffiValue, accountIndex, isChange, addressIndex, - pathBuffer, maxPathLen, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return String(cString: pathBuffer) - } - - /// Get CoinJoin path - /// - Parameters: - /// - network: The network type - /// - accountIndex: The account index - /// - Returns: The derivation path string - public static func getCoinJoinPath(network: Network = .mainnet, - accountIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_coinjoin_path( - network.ffiValue, accountIndex, pathBuffer, maxPathLen, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return String(cString: pathBuffer) - } - - /// Get identity registration path - /// - Parameters: - /// - network: The network type - /// - identityIndex: The identity index - /// - Returns: The derivation path string - public static func getIdentityRegistrationPath(network: Network = .mainnet, - identityIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_identity_registration_path( - network.ffiValue, identityIndex, pathBuffer, maxPathLen, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return String(cString: pathBuffer) - } - - /// Get identity top-up path - /// - Parameters: - /// - network: The network type - /// - identityIndex: The identity index - /// - topupIndex: The top-up index - /// - Returns: The derivation path string - public static func getIdentityTopUpPath(network: Network = .mainnet, - identityIndex: UInt32, - topupIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_identity_topup_path( - network.ffiValue, identityIndex, topupIndex, - pathBuffer, maxPathLen, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return String(cString: pathBuffer) - } - /// Get identity authentication path /// - Parameters: /// - network: The network type @@ -197,163 +13,16 @@ public class KeyDerivation { public static func getIdentityAuthenticationPath(network: Network = .mainnet, identityIndex: UInt32, keyIndex: UInt32) throws -> String { - var error = FFIError() - let maxPathLen = 256 - let pathBuffer = UnsafeMutablePointer.allocate(capacity: maxPathLen) - defer { - pathBuffer.deallocate() - } - - let success = derivation_identity_authentication_path( - network.ffiValue, identityIndex, keyIndex, - pathBuffer, maxPathLen, &error) + var pathBuffer: UnsafeMutablePointer? = nil + let rc = platform_wallet_derive_identity_authentication_path( + network.ffiValue, identityIndex, keyIndex, &pathBuffer) - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) + guard rc == 0, let pathBuffer else { + throw PlatformWalletError.walletOperation( + "platform_wallet_derive_identity_authentication_path returned \(rc)") } + defer { platform_wallet_string_free(pathBuffer) } return String(cString: pathBuffer) } - - /// Parse a derivation path string to indices - /// - Parameter path: The derivation path string - /// - Returns: Tuple of (indices, hardened flags) - public static func parsePath(_ path: String) throws -> (indices: [UInt32], hardened: [Bool]) { - var error = FFIError() - var indicesPtr: UnsafeMutablePointer? - var hardenedPtr: UnsafeMutablePointer? - var count: size_t = 0 - - let success = path.withCString { pathCStr in - derivation_path_parse(pathCStr, &indicesPtr, &hardenedPtr, &count, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - if let indices = indicesPtr, let hardened = hardenedPtr { - derivation_path_free(indices, hardened, count) - } - } - - guard success, let indices = indicesPtr, let hardened = hardenedPtr else { - throw KeyWalletError(ffiError: error) - } - - // Copy the data before freeing - var indicesArray: [UInt32] = [] - var hardenedArray: [Bool] = [] - - for i in 0.. - - internal init(handle: UnsafeMutablePointer) { - self.handle = handle - } - - deinit { - derivation_xpriv_free(handle) - } - - /// Convert to extended public key - public func toPublicKey() throws -> ExtendedPublicKey { - var error = FFIError() - guard let xpubHandle = derivation_xpriv_to_xpub(handle, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - return ExtendedPublicKey(handle: xpubHandle) - } - - /// Get string representation - public func toString() throws -> String { - var error = FFIError() - guard let strPtr = derivation_xpriv_to_string(handle, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - let str = String(cString: strPtr) - derivation_string_free(strPtr) - return str - } -} - -/// Extended public key handle -public class ExtendedPublicKey { - private let handle: UnsafeMutablePointer - - internal init(handle: UnsafeMutablePointer) { - self.handle = handle - } - - deinit { - derivation_xpub_free(handle) - } - - /// Get string representation - public func toString() throws -> String { - var error = FFIError() - guard let strPtr = derivation_xpub_to_string(handle, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - let str = String(cString: strPtr) - derivation_string_free(strPtr) - return str - } - - /// Get fingerprint (4 bytes) - public func getFingerprint() throws -> Data { - var error = FFIError() - var fingerprint = Data(count: 4) - - let success = fingerprint.withUnsafeMutableBytes { bytes in - let ptr = bytes.bindMemory(to: UInt8.self).baseAddress - return derivation_xpub_fingerprint(handle, ptr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return fingerprint - } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWallet.swift deleted file mode 100644 index 47541039840..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWallet.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -/// Main module for Dash Key Wallet functionality -/// -/// The KeyWallet module provides comprehensive wallet management capabilities for Dash, -/// including HD key derivation, address generation, transaction management, and provider keys. -/// -/// ## Key Features: -/// - Hierarchical Deterministic (HD) wallet support (BIP32/BIP44) -/// - Multiple account types (standard, CoinJoin, identity, provider) -/// - Address pool management with gap limits -/// - Transaction building and signing -/// - Provider key generation for masternodes -/// - Multi-wallet management -/// -/// ## Usage Example: -/// ```swift -/// // Initialize the library -/// KeyWallet.initialize() -/// -/// // Generate a new wallet -/// let mnemonic = try Mnemonic.generate() -/// let wallet = try Wallet(mnemonic: mnemonic, network: .testnet) -/// -/// // Check wallet balance -/// let balance = try wallet.getBalance() -/// print("Confirmed: \(balance.confirmed), Unconfirmed: \(balance.unconfirmed)") -/// ``` -public class KeyWallet { - - /// Initialize the key wallet library - /// Call this once at application startup - public static func initialize() { - _ = Wallet.initialize() - } - - /// Get the library version - public static var version: String { - return Wallet.version - } - - private init() {} -} - -// Re-export all public types for convenience -public typealias KeyWalletWallet = Wallet -public typealias KeyWalletAccount = Account -public typealias KeyWalletManager = WalletManager -public typealias KeyWalletMnemonic = Mnemonic -public typealias KeyWalletTransaction = Transaction -public typealias KeyWalletAddress = Address -public typealias KeyWalletDerivation = KeyDerivation diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift deleted file mode 100644 index d4f4c63254d..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyWalletTypes.swift +++ /dev/null @@ -1,484 +0,0 @@ -import Foundation -import DashSDKFFI - -// MARK: - Account Types - -/// Account type for wallet accounts -public enum AccountType: UInt32 { - case standardBIP44 = 0 - case standardBIP32 = 1 - case coinJoin = 2 - case identityRegistration = 3 - case identityTopUp = 4 - case identityTopUpNotBound = 5 - case identityInvitation = 6 - case providerVotingKeys = 7 - case providerOwnerKeys = 8 - case providerOperatorKeys = 9 - case providerPlatformKeys = 10 - - /// Convert to the upstream FFI discriminant enum. - /// - /// Upstream renamed the FFI account discriminant from `FFIAccountType` - /// (the old enum) to `FFIAccountKind` (the new enum) and reused the - /// `FFIAccountType` name for a richer struct that bundles the - /// discriminant with index / Dashpay-pointer / key-class fields. - /// The Swift `AccountType` enum here only models the discriminant - /// case — Dashpay and PlatformPayment variants need richer - /// construction paths and aren't surfaced through this type today. - var ffiValue: FFIAccountKind { - FFIAccountKind(rawValue: self.rawValue) - } - - init(ffiType: FFIAccountKind) { - self = AccountType(rawValue: ffiType.rawValue) ?? .standardBIP44 - } -} - -// MARK: - Address Pool Types - -/// Address pool type -public enum AddressPoolType: UInt32 { - case external = 0 // Receive addresses - case `internal` = 1 // Change addresses - case single = 2 // Single pool for non-standard accounts - - var ffiValue: FFIAddressPoolType { - FFIAddressPoolType(rawValue: self.rawValue) - } - - init(ffiType: FFIAddressPoolType) { - self = AddressPoolType(rawValue: ffiType.rawValue) ?? .external - } -} - -// MARK: - Mnemonic Language - -/// Language for mnemonic generation -public enum MnemonicLanguage: UInt32 { - case english = 0 - case chineseSimplified = 1 - case chineseTraditional = 2 - case czech = 3 - case french = 4 - case italian = 5 - case japanese = 6 - case korean = 7 - case portuguese = 8 - case spanish = 9 - - var ffiValue: FFILanguage { - FFILanguage(rawValue: self.rawValue) - } - - init(ffiLanguage: FFILanguage) { - self = MnemonicLanguage(rawValue: ffiLanguage.rawValue) ?? .english - } -} - -// MARK: - Account Creation Options - -/// Options for account creation when creating a wallet -public enum AccountCreationOption { - /// Create default accounts (BIP44 account 0, CoinJoin account 0, and special accounts) - case `default` - /// Create all specified accounts plus all special purpose accounts - case allAccounts - /// Create only BIP44 accounts (no CoinJoin or special accounts) - case bip44AccountsOnly - /// Create specific accounts with full control - case specificAccounts(bip44: [UInt32], bip32: [UInt32], coinJoin: [UInt32], - topUp: [UInt32], specialTypes: [AccountType]) - /// Create no accounts at all - case noAccounts - - func toFFIOptions() -> FFIWalletAccountCreationOptions { - var options = FFIWalletAccountCreationOptions() - - switch self { - case .default: - options.option_type = FFIAccountCreationOptionType(rawValue: 0) // DEFAULT - case .allAccounts: - options.option_type = FFIAccountCreationOptionType(rawValue: 1) // ALL_ACCOUNTS - case .bip44AccountsOnly: - options.option_type = FFIAccountCreationOptionType(rawValue: 2) // BIP44_ACCOUNTS_ONLY - case .specificAccounts(let bip44, let bip32, let coinJoin, let topUp, let specialTypes): - options.option_type = FFIAccountCreationOptionType(rawValue: 3) // SPECIFIC_ACCOUNTS - - // Note: These would need to be stored and passed properly - // This is simplified - actual implementation would need to manage memory - options.bip44_count = bip44.count - options.bip32_count = bip32.count - options.coinjoin_count = coinJoin.count - options.topup_count = topUp.count - options.special_account_types_count = specialTypes.count - case .noAccounts: - options.option_type = FFIAccountCreationOptionType(rawValue: 4) // NO_ACCOUNTS - } - - return options - } -} - -// Note: DerivationPathType removed (FFIDerivationPathType not present in current headers). - -// MARK: - Result Types - -/// Balance information for a wallet or account -public struct Balance: Equatable, Codable, Sendable { - public let confirmed: UInt64 - public let unconfirmed: UInt64 - public let immature: UInt64 - public let locked: UInt64 - public let total: UInt64 - - init(ffiBalance: FFIBalance) { - self.confirmed = ffiBalance.confirmed - self.unconfirmed = ffiBalance.unconfirmed - self.immature = ffiBalance.immature - self.locked = ffiBalance.locked - self.total = ffiBalance.total - } - - /// Public initializer for Balance - public init(confirmed: UInt64 = 0, unconfirmed: UInt64 = 0, immature: UInt64 = 0, locked: UInt64 = 0) { - self.confirmed = confirmed - self.unconfirmed = unconfirmed - self.immature = immature - self.locked = locked - self.total = confirmed + unconfirmed + immature - } - - /// Spendable balance (only confirmed, excluding locked) - public var spendable: UInt64 { - confirmed > locked ? confirmed - locked : 0 - } - - // MARK: - Formatting Helpers - - /// Format confirmed balance as DASH string - public var formattedConfirmed: String { - formatDash(confirmed) - } - - /// Format unconfirmed balance as DASH string - public var formattedUnconfirmed: String { - formatDash(unconfirmed) - } - - /// Format total balance as DASH string - public var formattedTotal: String { - formatDash(total) - } - - /// Format an amount in duffs as DASH string - /// - Parameter amount: Amount in duffs (1 DASH = 100,000,000 duffs) - /// - Returns: Formatted string like "1.23456789 DASH" - private func formatDash(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } -} - -/// Address pool information -public struct AddressPoolInfo { - public let poolType: AddressPoolType - public let generatedCount: UInt32 - public let usedCount: UInt32 - public let currentGap: UInt32 - public let gapLimit: UInt32 - public let highestUsedIndex: Int32 - - init(ffiInfo: FFIAddressPoolInfo) { - self.poolType = AddressPoolType(ffiType: ffiInfo.pool_type) - self.generatedCount = ffiInfo.generated_count - self.usedCount = ffiInfo.used_count - self.currentGap = ffiInfo.current_gap - self.gapLimit = ffiInfo.gap_limit - self.highestUsedIndex = ffiInfo.highest_used_index - } -} - -/// Transaction check result -public struct TransactionCheckResult { - public let isRelevant: Bool - public let totalReceived: UInt64 - public let totalSent: UInt64 - public let affectedAccountsCount: UInt32 - - init(ffiResult: FFITransactionCheckResult) { - self.isRelevant = ffiResult.is_relevant - self.totalReceived = ffiResult.total_received - self.totalSent = ffiResult.total_sent - self.affectedAccountsCount = ffiResult.affected_accounts_count - } -} - -/// Transaction context details (wraps FFITransactionContext + FFIBlockInfo) -public struct TransactionContextDetails { - public let context: TransactionContextType - public let height: UInt32 - public let blockHash: Data? - public let timestamp: UInt32 - - init(ffiContext: FFITransactionContext) { - self.context = TransactionContextType(ffiContext: ffiContext.context_type) - self.height = ffiContext.block_info.height - self.timestamp = ffiContext.block_info.timestamp - let hashBytes = withUnsafeBytes(of: ffiContext.block_info.block_hash) { Data($0) } - self.blockHash = hashBytes.allSatisfy({ $0 == 0 }) ? nil : hashBytes - } -} - -/// UTXO information -public struct UTXO: Identifiable, Equatable, Sendable { - public let txid: Data - public let vout: UInt32 - public let amount: UInt64 - public let address: String - public let scriptPubKey: Data - public let height: UInt32 - public let confirmations: UInt32 - - /// Unique identifier combining transaction ID and output index - public var id: String { - "\(txid.map { String(format: "%02x", $0) }.joined()):\(vout)" - } - - /// Whether this UTXO has at least 6 confirmations - public var isConfirmed: Bool { - confirmations >= 6 - } - - /// Whether this UTXO can be spent (requires 6 confirmations) - public var isSpendable: Bool { - isConfirmed - } - - init(ffiUTXO: FFIUTXO) { - // Copy txid (32 bytes) - self.txid = withUnsafeBytes(of: ffiUTXO.txid) { Data($0) } - self.vout = ffiUTXO.vout - self.amount = ffiUTXO.amount - - // Copy address string - if let addressPtr = ffiUTXO.address { - self.address = String(cString: addressPtr) - } else { - self.address = "" - } - - // Copy script pubkey - if let scriptPtr = ffiUTXO.script_pubkey, ffiUTXO.script_len > 0 { - self.scriptPubKey = Data(bytes: scriptPtr, count: ffiUTXO.script_len) - } else { - self.scriptPubKey = Data() - } - - self.height = ffiUTXO.height - self.confirmations = ffiUTXO.confirmations - } - - /// Public initializer for UTXO (for creating from app data) - public init( - txid: Data, - vout: UInt32, - amount: UInt64, - address: String, - scriptPubKey: Data, - height: UInt32 = 0, - confirmations: UInt32 = 0 - ) { - self.txid = txid - self.vout = vout - self.amount = amount - self.address = address - self.scriptPubKey = scriptPubKey - self.height = height - self.confirmations = confirmations - } -} - -// MARK: - Account Collection Types - -/// Summary of accounts in a collection -public struct AccountCollectionSummary { - public let bip44Indices: [UInt32] - public let bip32Indices: [UInt32] - public let coinJoinIndices: [UInt32] - public let identityTopUpIndices: [UInt32] - public let hasIdentityRegistration: Bool - public let hasIdentityInvitation: Bool - public let hasIdentityTopUpNotBound: Bool - public let hasProviderVotingKeys: Bool - public let hasProviderOwnerKeys: Bool - public let hasProviderOperatorKeys: Bool - public let hasProviderPlatformKeys: Bool - - init(ffiSummary: FFIAccountCollectionSummary) { - // Convert BIP44 indices - if ffiSummary.bip44_count > 0, let indices = ffiSummary.bip44_indices { - self.bip44Indices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.bip44_count)) - } else { - self.bip44Indices = [] - } - - // Convert BIP32 indices - if ffiSummary.bip32_count > 0, let indices = ffiSummary.bip32_indices { - self.bip32Indices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.bip32_count)) - } else { - self.bip32Indices = [] - } - - // Convert CoinJoin indices - if ffiSummary.coinjoin_count > 0, let indices = ffiSummary.coinjoin_indices { - self.coinJoinIndices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.coinjoin_count)) - } else { - self.coinJoinIndices = [] - } - - // Convert identity top-up indices - if ffiSummary.identity_topup_count > 0, let indices = ffiSummary.identity_topup_indices { - self.identityTopUpIndices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.identity_topup_count)) - } else { - self.identityTopUpIndices = [] - } - - // Copy boolean flags - self.hasIdentityRegistration = ffiSummary.has_identity_registration - self.hasIdentityInvitation = ffiSummary.has_identity_invitation - self.hasIdentityTopUpNotBound = ffiSummary.has_identity_topup_not_bound - self.hasProviderVotingKeys = ffiSummary.has_provider_voting_keys - self.hasProviderOwnerKeys = ffiSummary.has_provider_owner_keys - self.hasProviderOperatorKeys = ffiSummary.has_provider_operator_keys - self.hasProviderPlatformKeys = ffiSummary.has_provider_platform_keys - } -} - -/// Summary of managed accounts in a collection -public struct ManagedAccountCollectionSummary { - public let bip44Indices: [UInt32] - public let bip32Indices: [UInt32] - public let coinJoinIndices: [UInt32] - public let identityTopUpIndices: [UInt32] - public let hasIdentityRegistration: Bool - public let hasIdentityInvitation: Bool - public let hasIdentityTopUpNotBound: Bool - public let hasProviderVotingKeys: Bool - public let hasProviderOwnerKeys: Bool - public let hasProviderOperatorKeys: Bool - public let hasProviderPlatformKeys: Bool - - init(ffiSummary: FFIManagedCoreAccountCollectionSummary) { - // Convert BIP44 indices - if ffiSummary.bip44_count > 0, let indices = ffiSummary.bip44_indices { - self.bip44Indices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.bip44_count)) - } else { - self.bip44Indices = [] - } - - // Convert BIP32 indices - if ffiSummary.bip32_count > 0, let indices = ffiSummary.bip32_indices { - self.bip32Indices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.bip32_count)) - } else { - self.bip32Indices = [] - } - - // Convert CoinJoin indices - if ffiSummary.coinjoin_count > 0, let indices = ffiSummary.coinjoin_indices { - self.coinJoinIndices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.coinjoin_count)) - } else { - self.coinJoinIndices = [] - } - - // Convert identity top-up indices - if ffiSummary.identity_topup_count > 0, let indices = ffiSummary.identity_topup_indices { - self.identityTopUpIndices = Array(UnsafeBufferPointer(start: indices, count: ffiSummary.identity_topup_count)) - } else { - self.identityTopUpIndices = [] - } - - // Copy boolean flags - self.hasIdentityRegistration = ffiSummary.has_identity_registration - self.hasIdentityInvitation = ffiSummary.has_identity_invitation - self.hasIdentityTopUpNotBound = ffiSummary.has_identity_topup_not_bound - self.hasProviderVotingKeys = ffiSummary.has_provider_voting_keys - self.hasProviderOwnerKeys = ffiSummary.has_provider_owner_keys - self.hasProviderOperatorKeys = ffiSummary.has_provider_operator_keys - self.hasProviderPlatformKeys = ffiSummary.has_provider_platform_keys - } -} - -// MARK: - Error Handling - -/// Key wallet errors -public enum KeyWalletError: Error { - case invalidInput(String) - case allocationFailed(String) - case invalidMnemonic(String) - case invalidDerivationPath(String) - case invalidNetwork(String) - case invalidAddress(String) - case invalidTransaction(String) - case walletError(String) - case serializationError(String) - case notFound(String) - case notSupported(String) - case invalidState(String) - case internalError(String) - case unknown(String) - - init(ffiError: FFIError) { - let message = ffiError.message != nil ? String(cString: ffiError.message!) : "Unknown error" - - switch ffiError.code { - case FFIErrorCode(rawValue: 1): // INVALID_INPUT - self = .invalidInput(message) - case FFIErrorCode(rawValue: 2): // ALLOCATION_FAILED - self = .allocationFailed(message) - case FFIErrorCode(rawValue: 3): // INVALID_MNEMONIC - self = .invalidMnemonic(message) - case FFIErrorCode(rawValue: 4): // INVALID_DERIVATION_PATH - self = .invalidDerivationPath(message) - case FFIErrorCode(rawValue: 5): // INVALID_NETWORK - self = .invalidNetwork(message) - case FFIErrorCode(rawValue: 6): // INVALID_ADDRESS - self = .invalidAddress(message) - case FFIErrorCode(rawValue: 7): // INVALID_TRANSACTION - self = .invalidTransaction(message) - case FFIErrorCode(rawValue: 8): // WALLET_ERROR - self = .walletError(message) - case FFIErrorCode(rawValue: 9): // SERIALIZATION_ERROR - self = .serializationError(message) - case FFIErrorCode(rawValue: 10): // NOT_FOUND - self = .notFound(message) - case FFIErrorCode(rawValue: 11): // INVALID_STATE - self = .invalidState(message) - case FFIErrorCode(rawValue: 12): // INTERNAL_ERROR - self = .internalError(message) - default: - self = .unknown(message) - } - } -} - -extension KeyWalletError: LocalizedError { - public var errorDescription: String? { - switch self { - case .invalidInput(let msg): return "Invalid Input: \(msg)" - case .allocationFailed(let msg): return "Allocation Failed: \(msg)" - case .invalidMnemonic(let msg): return "Invalid Mnemonic: \(msg)" - case .invalidDerivationPath(let msg): return "Invalid Derivation Path: \(msg)" - case .invalidNetwork(let msg): return "Invalid Network: \(msg)" - case .invalidAddress(let msg): return "Invalid Address: \(msg)" - case .invalidTransaction(let msg): return "Invalid Transaction: \(msg)" - case .walletError(let msg): return "Wallet Error: \(msg)" - case .serializationError(let msg): return "Serialization Error: \(msg)" - case .notFound(let msg): return "Not Found: \(msg)" - case .notSupported(let msg): return "Not Supported: \(msg)" - case .invalidState(let msg): return "Invalid State: \(msg)" - case .internalError(let msg): return "Internal Error: \(msg)" - case .unknown(let msg): return "Unknown Error: \(msg)" - } - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift deleted file mode 100644 index af2a7dd54db..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccount.swift +++ /dev/null @@ -1,174 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a managed account with address pool management -public class ManagedAccount { - internal let handle: UnsafeMutablePointer - private let manager: WalletManager - - internal init(handle: UnsafeMutablePointer, manager: WalletManager) { - self.handle = handle - self.manager = manager - } - - deinit { - managed_core_account_free(handle) - } - - // MARK: - Properties - - /// Get the network this account is on - public var network: Network { - let ffiNetwork = managed_core_account_get_network(handle) - return Network(ffiNetwork: ffiNetwork) - } - - /// Get the account type - public var accountType: AccountType? { - var index: UInt32 = 0 - let ffiType = managed_core_account_get_account_type(handle, &index) - return AccountType(ffiType: ffiType) - } - - // `isWatchOnly` was removed in lockstep with upstream dropping the - // per-core-account flag (it's now a wallet-level property on - // `WalletType::WatchOnly`). The corresponding C getter - // `managed_core_account_get_is_watch_only` is gone too. Query - // watch-only from the parent wallet handle if needed. - - /// Get the account index - public var index: UInt32 { - return managed_core_account_get_index(handle) - } - - /// Get the UTXO count - public var utxoCount: UInt32 { - return managed_core_account_get_utxo_count(handle) - } - - // `transactionCount` and `getTransactions()` accessors were removed - // in lockstep with upstream gating - // `managed_core_account_get_transaction_count` / - // `managed_core_account_get_transactions` behind the - // `keep-finalized-transactions` Cargo feature (off by default). - // The production model delivers tx history through the - // platform-wallet event channel, not from the in-memory - // per-account map. Consumers that need history should subscribe - // to wallet events on the Rust side. - - // MARK: - Balance - - /// Get the balance for this account - public func getBalance() -> Balance { - var ffiBalance = FFIBalance() - let success = managed_core_account_get_balance(handle, &ffiBalance) - - guard success else { - // managed_core_account_get_balance can only fail if handle or ffiBalance are null - preconditionFailure("managed_core_account_get_balance failed, invalid state") - } - - return Balance(ffiBalance: ffiBalance) - } - - // MARK: - Address Pools - - /// Get the external address pool - public func getExternalAddressPool() -> AddressPool? { - guard let poolHandle = managed_core_account_get_external_address_pool(handle) else { - return nil - } - return AddressPool(handle: poolHandle) - } - - /// Get the internal address pool - public func getInternalAddressPool() -> AddressPool? { - guard let poolHandle = managed_core_account_get_internal_address_pool(handle) else { - return nil - } - return AddressPool(handle: poolHandle) - } - - /// Get an address pool by type - /// - Parameter poolType: The type of address pool to get - /// - Returns: The address pool if it exists - public func getAddressPool(type poolType: AddressPoolType) -> AddressPool? { - guard let poolHandle = managed_core_account_get_address_pool(handle, poolType.ffiValue) else { - return nil - } - return AddressPool(handle: poolHandle) - } -} - -// MARK: - Wallet Transaction - -/// Information about a transaction from a managed account -public struct WalletTransaction: Identifiable { - /// Transaction ID (hex string) - public let txid: String - /// Net amount for the account (positive = received, negative = sent) - public let netAmount: Int64 - /// Block height, 0 if not confirmed - public let height: UInt32 - /// Block hash if confirmed (hex string) - public let blockHash: String? - /// Unix timestamp - public let timestamp: UInt64 - /// Fee if known - public let fee: UInt64? - /// Whether this is our transaction - public let isOurs: Bool - - public init( - txid: String, - netAmount: Int64, - height: UInt32, - blockHash: String?, - timestamp: UInt64, - fee: UInt64?, - isOurs: Bool - ) { - self.txid = txid - self.netAmount = netAmount - self.height = height - self.blockHash = blockHash - self.timestamp = timestamp - self.fee = fee - self.isOurs = isOurs - } - - /// Transaction date - public var date: Date { - return Date(timeIntervalSince1970: TimeInterval(timestamp)) - } - - /// Is the transaction confirmed - public var isConfirmed: Bool { - return blockHash != nil && height > 0 - } - - /// Formatted amount string - public var formattedAmount: String { - let dash = Double(abs(netAmount)) / 100_000_000.0 - let sign = netAmount < 0 ? "-" : "+" - return "\(sign)\(String(format: "%.8f", dash)) DASH" - } - - /// Formatted fee string - public var formattedFee: String? { - guard let fee = fee else { return nil } - let dash = Double(fee) / 100_000_000.0 - return String(format: "%.8f DASH", dash) - } - - /// Truncated txid for display (first 8 + last 6 chars) - public var truncatedTxid: String { - guard txid.count > 14 else { return txid } - let start = txid.prefix(8) - let end = txid.suffix(6) - return "\(start)...\(end)" - } - - // Identifiable conformance - public var id: String { txid } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift deleted file mode 100644 index cfe33bc67d6..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedAccountCollection.swift +++ /dev/null @@ -1,299 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a collection of managed accounts -public class ManagedAccountCollection { - private let handle: UnsafeMutablePointer - private let manager: WalletManager - - internal init(handle: UnsafeMutablePointer, manager: WalletManager) { - self.handle = handle - self.manager = manager - } - - deinit { - managed_account_collection_free(handle) - } - - // MARK: - BIP44 Accounts - - /// Get a BIP44 account by index - /// - Parameter index: The account index - /// - Returns: The managed account if it exists - public func getBIP44Account(at index: UInt32) -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_bip44_account(handle, index) else { - return nil - } - - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Get all BIP44 account indices - public func getBIP44Indices() -> [UInt32] { - var indices: UnsafeMutablePointer? - var count: Int = 0 - - let success = managed_account_collection_get_bip44_indices(handle, &indices, &count) - - guard success, let indicesPtr = indices, count > 0 else { - return [] - } - - defer { - indicesPtr.deallocate() - } - - return Array(UnsafeBufferPointer(start: indicesPtr, count: count)) - } - - // MARK: - BIP32 Accounts - - /// Get a BIP32 account by index - /// - Parameter index: The account index - /// - Returns: The managed account if it exists - public func getBIP32Account(at index: UInt32) -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_bip32_account(handle, index) else { - return nil - } - - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Get all BIP32 account indices - public func getBIP32Indices() -> [UInt32] { - var indices: UnsafeMutablePointer? - var count: Int = 0 - - let success = managed_account_collection_get_bip32_indices(handle, &indices, &count) - - guard success, let indicesPtr = indices, count > 0 else { - return [] - } - - defer { - indicesPtr.deallocate() - } - - return Array(UnsafeBufferPointer(start: indicesPtr, count: count)) - } - - // MARK: - CoinJoin Accounts - - /// Get a CoinJoin account by index - /// - Parameter index: The account index - /// - Returns: The managed account if it exists - public func getCoinJoinAccount(at index: UInt32) -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_coinjoin_account(handle, index) else { - return nil - } - - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Get all CoinJoin account indices - public func getCoinJoinIndices() -> [UInt32] { - var indices: UnsafeMutablePointer? - var count: Int = 0 - - let success = managed_account_collection_get_coinjoin_indices(handle, &indices, &count) - - guard success, let indicesPtr = indices, count > 0 else { - return [] - } - - defer { - indicesPtr.deallocate() - } - - return Array(UnsafeBufferPointer(start: indicesPtr, count: count)) - } - - // MARK: - Identity Accounts - - /// Get the identity registration account - public func getIdentityRegistrationAccount() -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_identity_registration(handle) else { - return nil - } - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if identity registration account exists - public var hasIdentityRegistration: Bool { - return managed_account_collection_has_identity_registration(handle) - } - - /// Get an identity top-up account by registration index - /// - Parameter registrationIndex: The registration index - /// - Returns: The managed account if it exists - public func getIdentityTopUpAccount(registrationIndex: UInt32) -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_identity_topup(handle, registrationIndex) else { - return nil - } - - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Get all identity top-up account indices - public func getIdentityTopUpIndices() -> [UInt32] { - var indices: UnsafeMutablePointer? - var count: Int = 0 - - let success = managed_account_collection_get_identity_topup_indices(handle, &indices, &count) - - guard success, let indicesPtr = indices, count > 0 else { - return [] - } - - defer { - indicesPtr.deallocate() - } - - return Array(UnsafeBufferPointer(start: indicesPtr, count: count)) - } - - /// Get the identity top-up not bound account - public func getIdentityTopUpNotBoundAccount() -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_identity_topup_not_bound(handle) else { - return nil - } - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if identity top-up not bound account exists - public var hasIdentityTopUpNotBound: Bool { - return managed_account_collection_has_identity_topup_not_bound(handle) - } - - /// Get the identity invitation account - public func getIdentityInvitationAccount() -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_identity_invitation(handle) else { - return nil - } - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if identity invitation account exists - public var hasIdentityInvitation: Bool { - return managed_account_collection_has_identity_invitation(handle) - } - - // MARK: - Provider Accounts - - /// Get the provider voting keys account - public func getProviderVotingKeysAccount() -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_provider_voting_keys(handle) else { - return nil - } - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if provider voting keys account exists - public var hasProviderVotingKeys: Bool { - return managed_account_collection_has_provider_voting_keys(handle) - } - - /// Get the provider owner keys account - public func getProviderOwnerKeysAccount() -> ManagedAccount? { - guard let accountHandle = managed_account_collection_get_provider_owner_keys(handle) else { - return nil - } - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if provider owner keys account exists - public var hasProviderOwnerKeys: Bool { - return managed_account_collection_has_provider_owner_keys(handle) - } - - /// Get the provider operator keys account - public func getProviderOperatorKeysAccount() -> ManagedAccount? { - guard let rawPointer = managed_account_collection_get_provider_operator_keys(handle) else { - return nil - } - let accountHandle = rawPointer.assumingMemoryBound(to: FFIManagedCoreAccount.self) - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if provider operator keys account exists - public var hasProviderOperatorKeys: Bool { - return managed_account_collection_has_provider_operator_keys(handle) - } - - /// Get the provider platform keys account - public func getProviderPlatformKeysAccount() -> ManagedAccount? { - guard let rawPointer = managed_account_collection_get_provider_platform_keys(handle) else { - return nil - } - let accountHandle = rawPointer.assumingMemoryBound(to: FFIManagedCoreAccount.self) - return ManagedAccount(handle: accountHandle, manager: manager) - } - - /// Check if provider platform keys account exists - public var hasProviderPlatformKeys: Bool { - return managed_account_collection_has_provider_platform_keys(handle) - } - - // MARK: - Platform Payment Accounts - - /// Whether there are any platform payment accounts in this collection. - public var hasPlatformPaymentAccounts: Bool { - managed_account_collection_has_platform_payment_accounts(handle) - } - - /// The number of platform payment accounts in this collection. - public var platformPaymentCount: UInt32 { - managed_account_collection_platform_payment_count(handle) - } - - /// Get a platform payment account by account index and key class. - /// - Parameters: - /// - accountIndex: The account index (hardened). - /// - keyClass: The key class level in the derivation path (typically 0). - /// - Returns: The managed platform account, or nil if it does not exist. - public func getPlatformPaymentAccount(accountIndex: UInt32, keyClass: UInt32) -> ManagedPlatformAccount? { - guard let accountHandle = managed_account_collection_get_platform_payment_account( - handle, accountIndex, keyClass - ) else { - return nil - } - return ManagedPlatformAccount(handle: accountHandle) - } - - /// Get all platform payment account keys from this collection. - /// - Returns: Array of account key identifiers (account index + key class). - public func getPlatformPaymentKeys() -> [PlatformPaymentAccountKey] { - var keysPtr: UnsafeMutablePointer? - var count: Int = 0 - - let success = managed_account_collection_get_platform_payment_keys(handle, &keysPtr, &count) - - guard success, let ptr = keysPtr, count > 0 else { - return [] - } - - defer { - managed_account_collection_free_platform_payment_keys(ptr, count) - } - - return (0.. ManagedAccountCollectionSummary? { - guard let summaryPtr = managed_account_collection_summary_data(handle) else { - return nil - } - - defer { - managed_account_collection_summary_free(summaryPtr) - } - - return ManagedAccountCollectionSummary(ffiSummary: summaryPtr.pointee) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedPlatformAccount.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedPlatformAccount.swift deleted file mode 100644 index 171ca72b6d0..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/ManagedPlatformAccount.swift +++ /dev/null @@ -1,101 +0,0 @@ -// ManagedPlatformAccount.swift -// SwiftDashSDK -// -// Swift wrapper for a managed platform payment account (DIP-17). -// Platform payment accounts are identified by (account_index, key_class) -// and hold credit/duff balances with their own address pool. -// -// FFIManagedPlatformAccount is an opaque C type with a minimal body so Swift -// can use typed pointers (UnsafeMutablePointer). - -import Foundation -import DashSDKFFI - -// MARK: - Platform Payment Account Key - -/// Identifies a platform payment account by its account index and key class. -public struct PlatformPaymentAccountKey: Sendable, Equatable { - /// Account index (hardened). - public let account: UInt32 - /// Key class (hardened). - public let keyClass: UInt32 - - public init(account: UInt32, keyClass: UInt32) { - self.account = account - self.keyClass = keyClass - } -} - -// MARK: - Managed Platform Account - -/// Swift wrapper for an FFI-managed platform payment account. -/// -/// Provides access to credit/duff balances, address counts, and the -/// account's address pool. The underlying FFI handle is freed on deinit. -public class ManagedPlatformAccount { - private let handle: UnsafeMutablePointer - - internal init(handle: UnsafeMutablePointer) { - self.handle = handle - } - - deinit { - managed_platform_account_free(handle) - } - - // MARK: - Properties - - /// The network this account belongs to. - public var network: Network { - let ffiNetwork = managed_platform_account_get_network(handle) - return Network(ffiNetwork: ffiNetwork) - } - - /// The account index (hardened). - public var accountIndex: UInt32 { - managed_platform_account_get_account_index(handle) - } - - /// The key class (hardened). - public var keyClass: UInt32 { - managed_platform_account_get_key_class(handle) - } - - /// Total balance in credits (1000 credits = 1 duff). - public var creditBalance: UInt64 { - managed_platform_account_get_credit_balance(handle) - } - - /// Total balance in duffs (credit_balance / 1000). - public var duffBalance: UInt64 { - managed_platform_account_get_duff_balance(handle) - } - - /// Number of addresses that have been funded (have non-zero balance). - public var fundedAddressCount: UInt32 { - managed_platform_account_get_funded_address_count(handle) - } - - /// Total number of derived addresses in this account. - public var totalAddressCount: UInt32 { - managed_platform_account_get_total_address_count(handle) - } - - /// Whether this is a watch-only account. - public var isWatchOnly: Bool { - managed_platform_account_get_is_watch_only(handle) - } - - // MARK: - Address Pool - - /// Get the address pool for this platform account. - /// - /// Platform accounts have a single address pool (no internal/external split). - /// - Returns: The address pool, or nil if unavailable. - public func getAddressPool() -> AddressPool? { - guard let poolHandle = managed_platform_account_get_address_pool(handle) else { - return nil - } - return AddressPool(handle: poolHandle) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift index 20d0e231da0..9370c0e0822 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift @@ -1,13 +1,6 @@ import Foundation import DashSDKFFI -private func scrubMnemonicBytes(_ bytes: inout [UInt8]) { - bytes.withUnsafeMutableBufferPointer { buffer in - guard let base = buffer.baseAddress else { return } - memset_s(base, buffer.count, 0, buffer.count) - } -} - /// Utility class for mnemonic operations public class Mnemonic { @@ -18,135 +11,37 @@ public class Mnemonic { /// - Returns: The generated mnemonic phrase public static func generate(wordCount: UInt32 = 24, language: MnemonicLanguage = .english) throws -> String { - guard [12, 15, 18, 21, 24].contains(wordCount) else { - throw KeyWalletError.invalidInput("Word count must be 12, 15, 18, 21, or 24") - } - - var error = FFIError() - let mnemonicPtr = mnemonic_generate_with_language(wordCount, language.ffiValue, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = mnemonicPtr else { - throw KeyWalletError(ffiError: error) - } - - let mnemonic = String(cString: ptr) - mnemonic_free(ptr) - - return mnemonic - } - - /// Validate a mnemonic phrase - /// - Parameter mnemonic: The mnemonic phrase to validate - /// - Returns: True if valid - public static func validate(_ mnemonic: String) -> Bool { - var error = FFIError() - - let isValid = mnemonic.withCString { mnemonicCStr in - mnemonic_validate(mnemonicCStr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - return isValid - } - - /// Convert mnemonic to seed - /// - Parameters: - /// - mnemonic: The mnemonic phrase - /// - passphrase: Optional BIP39 passphrase - /// - Returns: The seed data (typically 64 bytes) - public static func toSeed(mnemonic: String, passphrase: String? = nil) throws -> Data { - try toSeed(mnemonicUTF8Bytes: Data(mnemonic.utf8), passphrase: passphrase) + var cstr: UnsafeMutablePointer? = nil + let result = platform_wallet_generate_mnemonic(wordCount, language.ffiValue, &cstr) + try result.check() + guard let ptr = cstr else { + throw PlatformWalletError.invalidParameter( + "platform_wallet_generate_mnemonic returned a null pointer" + ) + } + let phrase = String(cString: ptr) + platform_wallet_string_free(ptr) + return phrase } +} - /// Convert mnemonic UTF-8 bytes to seed without first - /// materializing a Swift `String` at the call site. - public static func toSeed(mnemonicUTF8Bytes: Data, passphrase: String? = nil) throws -> Data { - guard !mnemonicUTF8Bytes.isEmpty else { - throw KeyWalletError.invalidInput("Mnemonic must not be empty") - } - - var error = FFIError() - var seed = Data(count: 64) - var seedLen: size_t = 64 - var mnemonicBytes = [UInt8](mnemonicUTF8Bytes) - guard !mnemonicBytes.contains(0) else { - scrubMnemonicBytes(&mnemonicBytes) - throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL") - } - mnemonicBytes.append(0) - - let success = mnemonicBytes.withUnsafeBufferPointer { mnemonicBuf in - guard let mnemonicBase = mnemonicBuf.baseAddress else { - return false - } - return mnemonicBase.withMemoryRebound(to: CChar.self, capacity: mnemonicBuf.count) { mnemonicCStr in - seed.withUnsafeMutableBytes { seedBytes in - let seedPtr = seedBytes.bindMemory(to: UInt8.self).baseAddress - - if let passphrase = passphrase { - return passphrase.withCString { passphraseCStr in - mnemonic_to_seed(mnemonicCStr, passphraseCStr, - seedPtr, &seedLen, &error) - } - } else { - return mnemonic_to_seed(mnemonicCStr, nil, - seedPtr, &seedLen, &error) - } - } - } - } - - defer { - scrubMnemonicBytes(&mnemonicBytes) - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - // Resize if necessary - if seedLen < 64 { - seed = seed.prefix(seedLen) - } - - return seed +public enum MnemonicLanguage: UInt32 { + case english = 0 + case chineseSimplified = 1 + case chineseTraditional = 2 + case czech = 3 + case french = 4 + case italian = 5 + case japanese = 6 + case korean = 7 + case portuguese = 8 + case spanish = 9 + + var ffiValue: FFILanguage { + FFILanguage(rawValue: self.rawValue) } - /// Get word count from a mnemonic phrase - /// - Parameter mnemonic: The mnemonic phrase - /// - Returns: The number of words - public static func wordCount(of mnemonic: String) throws -> UInt32 { - var error = FFIError() - - let count = mnemonic.withCString { mnemonicCStr in - mnemonic_word_count(mnemonicCStr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - // Check if there was an error - if error.code != FFIErrorCode(rawValue: 0) { - throw KeyWalletError(ffiError: error) - } - - return count + init(ffiLanguage: FFILanguage) { + self = MnemonicLanguage(rawValue: ffiLanguage.rawValue) ?? .english } } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/README.md b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/README.md deleted file mode 100644 index 149218dfe78..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/README.md +++ /dev/null @@ -1,335 +0,0 @@ -# Dash Key Wallet Swift Interface - -This directory contains the Swift wrapper for the Dash key-wallet-ffi library, providing comprehensive wallet management capabilities for iOS and macOS applications. - -## Overview - -The KeyWallet module provides: -- HD wallet support (BIP32/BIP44) -- Multiple account types (standard, CoinJoin, identity, provider) -- Enhanced address pool management with ManagedAccount -- Transaction building and signing -- Provider key generation for masternodes (BLS and EdDSA) -- Multi-wallet management with managed account collections - -## Architecture - -### Core Components - -1. **Wallet** - Main wallet class for key derivation and account management -2. **ManagedWallet** - Extended wallet with address pool management and transaction checking -3. **WalletManager** - Multi-wallet manager for handling multiple wallets -4. **Account** - Individual account within a wallet -5. **ManagedAccount** - Enhanced account with address pool management -6. **ManagedAccountCollection** - Collection of all managed accounts in a wallet -7. **AccountCollection** - Collection of regular accounts with provider key support -8. **AddressPool** - Manages external/internal address pools for an account -9. **BLSAccount** - Specialized account for BLS provider keys -10. **EdDSAAccount** - Specialized account for EdDSA platform P2P keys -11. **Mnemonic** - Mnemonic generation and validation utilities -12. **Transaction** - Transaction building, signing, and checking -13. **ProviderKeys** - Provider key generation for masternode operations -14. **Address** - Address validation and type detection -15. **KeyDerivation** - Low-level key derivation utilities - -### FFI Integration - -The Swift interface uses the C FFI bindings from key-wallet-ffi through the CKeyWalletFFI module. Memory management is handled automatically using Swift's ARC and proper cleanup in deinit methods. - -## Usage Examples - -### Basic Wallet Creation - -```swift -import SwiftDashSDK - -// Initialize the library -KeyWallet.initialize() - -// Generate a new mnemonic -let mnemonic = try Mnemonic.generate(wordCount: 24) - -// Create wallet from mnemonic -let wallet = try Wallet( - mnemonic: mnemonic, - passphrase: nil, - network: .testnet -) - -// Get wallet ID -let walletId = try wallet.id -print("Wallet ID: \(walletId.toHexString())") -``` - -### Address Generation - -```swift -// Create managed wallet for address pool management -let managed = try ManagedWallet(wallet: wallet) - -// Get next receive address -let receiveAddress = try managed.getNextReceiveAddress(wallet: wallet) -print("Receive address: \(receiveAddress)") - -// Get next change address -let changeAddress = try managed.getNextChangeAddress(wallet: wallet) -print("Change address: \(changeAddress)") - -// Get a range of addresses -let addresses = try managed.getExternalAddressRange( - wallet: wallet, - accountIndex: 0, - startIndex: 0, - endIndex: 10 -) -``` - -### Transaction Management - -```swift -// Build and sign a transaction in one step -let outputs = [ - Transaction.Output(address: "XqHiz8EXYbTAtBEYs4pWTHh7ipEDQcNQeT", amount: 100000000) -] - -let result = try Transaction.buildAndSign( - manager: walletManager, - wallet: wallet, - accountIndex: 0, - outputs: outputs, - feeRate: .normal -) -print("Fee paid: \(result.fee) duffs") - -// Check if a transaction belongs to the wallet -let checkResult = try Transaction.check( - wallet: wallet, - transactionData: result.transactionData, - context: .mempool -) - -if checkResult.isRelevant { - print("Transaction affects this wallet") - print("Received: \(checkResult.totalReceived)") - print("Sent: \(checkResult.totalSent)") -} -``` - -### Provider Keys for Masternodes - -```swift -// Generate provider voting key -let votingKey = try ProviderKeys.generateKey( - wallet: wallet, - keyType: .voting, - keyIndex: 0, - includePrivate: true -) - -print("Voting public key: \(votingKey.publicKey.toHexString())") -print("Derivation path: \(votingKey.derivationPath)") - -// Get address for funding -let fundingAddress = try ProviderKeys.getAddress( - wallet: wallet, - keyType: .voting, - keyIndex: 0 -) -``` - -### Multi-Wallet Management - -```swift -// Create wallet manager -let manager = try WalletManager() - -// Add wallets -let walletId1 = try manager.addWallet( - mnemonic: mnemonic1, - network: .mainnet -) - -let walletId2 = try manager.addWallet( - mnemonic: mnemonic2, - network: .mainnet -) - -// Get all wallet IDs -let walletIds = try manager.getWalletIds() - -// Get next receive address for a wallet -let address = try manager.getReceiveAddress( - walletId: walletId1, - network: .mainnet, - accountIndex: 0 -) -``` - -### Managed Accounts (New API) - -```swift -// Get a managed account from wallet manager -let managedAccount = try manager.getManagedAccount( - walletId: walletId, - network: .mainnet, - accountIndex: 0, - accountType: .standardBIP44 -) - -// Get account properties -print("Network: \(managedAccount.network)") -print("Account type: \(managedAccount.accountType)") -print("Is watch-only: \(managedAccount.isWatchOnly)") -print("Transaction count: \(managedAccount.transactionCount)") - -// Get balance -let balance = try managedAccount.getBalance() -print("Confirmed: \(balance.confirmed), Unconfirmed: \(balance.unconfirmed)") - -// Access address pools -if let externalPool = managedAccount.getExternalAddressPool() { - // Get specific address - let addressInfo = try externalPool.getAddress(at: 0) - print("Address: \(addressInfo.address)") - print("Path: \(addressInfo.path)") - print("Used: \(addressInfo.used)") - - // Get range of addresses - let addresses = try externalPool.getAddresses(from: 0, to: 10) - for addr in addresses { - print("\(addr.index): \(addr.address)") - } -} - -// Get managed account collection -let collection = try manager.getManagedAccountCollection( - walletId: walletId, - network: .mainnet -) - -// Access different account types -if let bip44Account = collection.getBIP44Account(at: 0) { - print("BIP44 account found") -} - -if collection.hasIdentityRegistration { - if let identityAccount = collection.getIdentityRegistrationAccount() { - print("Identity registration account available") - } -} - -// Get summary of all accounts -if let summary = collection.getSummary() { - print("BIP44 accounts: \(summary.bip44Indices)") - print("Has provider keys: \(summary.hasProviderVotingKeys)") -} -``` - -### Account Collections - -```swift -// Get account collection from wallet -let accountCollection = try wallet.getAccountCollection() - -// Get provider accounts -if let blsOperatorAccount = accountCollection.getProviderOperatorKeys() { - // BLS operator keys account - print("BLS operator account available") -} - -if let eddsaPlatformAccount = accountCollection.getProviderPlatformKeys() { - // EdDSA platform P2P keys account - print("EdDSA platform account available") -} - -// Get collection summary -if let summary = accountCollection.getSummary() { - print("Account summary:") - print("- BIP44 indices: \(summary.bip44Indices)") - print("- Identity accounts: Registration=\(summary.hasIdentityRegistration)") - print("- Provider accounts: Voting=\(summary.hasProviderVotingKeys)") -} -``` - -## Account Types - -The wallet supports multiple account types: - -- **StandardBIP44**: Regular BIP44 accounts (m/44'/5'/account'/x/x) -- **StandardBIP32**: BIP32 accounts (m/account'/x/x) -- **CoinJoin**: Privacy-enhanced transactions -- **IdentityRegistration**: Funding for identity registration -- **IdentityTopUp**: Funding for identity top-ups (with registration index) -- **IdentityTopUpNotBound**: Identity top-up not bound to specific identity -- **IdentityInvitation**: Funding for identity invitations -- **ProviderVotingKeys**: Masternode voting keys (BLS) -- **ProviderOwnerKeys**: Masternode owner keys (BLS) -- **ProviderOperatorKeys**: Masternode operator keys (BLS) -- **ProviderPlatformKeys**: Platform P2P keys (EdDSA) - -### Account Creation Options - -When creating a wallet, you can specify account creation options: - -- **`.default`**: Create default accounts (BIP44 account 0, CoinJoin account 0, and special accounts) -- **`.allAccounts`**: Create all specified accounts plus all special purpose accounts -- **`.bip44AccountsOnly`**: Create only BIP44 accounts (no CoinJoin or special accounts) -- **`.specificAccounts`**: Create specific accounts with full control -- **`.none`**: Create no accounts at all (uses `NO_ACCOUNTS` enum value) - -## Network Support - -The library supports all Dash networks: -- Mainnet -- Testnet -- Regtest -- Devnet - -## Error Handling - -All operations that can fail throw `KeyWalletError` with detailed error information: - -```swift -do { - let wallet = try Wallet(mnemonic: mnemonic, network: .testnet) -} catch KeyWalletError.invalidMnemonic(let message) { - print("Invalid mnemonic: \(message)") -} catch KeyWalletError.invalidState(let message) { - print("Invalid state: \(message)") -} catch { - print("Unexpected error: \(error)") -} -``` - -## Memory Management - -The Swift interface handles all memory management automatically: -- FFI resources are properly freed in deinit methods -- Temporary C strings are managed with proper lifetime -- Arrays and buffers are correctly allocated and freed -- No manual memory management required - -## Thread Safety - -The underlying Rust library provides thread-safe operations. However, Swift wrapper objects should be used from a single thread or properly synchronized when shared across threads. - -## Requirements - -- iOS 13.0+ / macOS 10.15+ -- Swift 5.0+ -- Linked with key_wallet_ffi static library - -## Building - -1. Build the key-wallet-ffi library for iOS: - ```bash - cd /path/to/rust-dashcore/key-wallet-ffi - ./build_ios.sh - ``` - -2. Link the generated xcframework in your Xcode project - -3. Import the module: - ```swift - import SwiftDashSDK - ``` diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift deleted file mode 100644 index b0b589d97b9..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Transaction.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Result of building and signing a transaction -public struct BuildAndSignResult: Sendable { - /// The signed transaction bytes - public let transactionData: Data - /// The fee paid in duffs - public let fee: UInt64 -} - -/// Transaction utilities for wallet operations -public class Transaction { - - /// Transaction output for building transactions - public struct Output { - public let address: String - public let amount: UInt64 - - public init(address: String, amount: UInt64) { - self.address = address - self.amount = amount - } - - func toFFI() -> FFITxOutput { - return address.withCString { addressCStr in - FFITxOutput(address: addressCStr, amount: amount) - } - } - } - - /// Check if a transaction belongs to a wallet - /// - Parameters: - /// - wallet: The wallet to check against - /// - transactionData: The transaction bytes - /// - context: The transaction context - /// - blockHeight: The block height (0 for mempool) - /// - blockHash: The block hash (nil for mempool) - /// - timestamp: The timestamp - /// - updateState: Whether to update wallet state if transaction is relevant - /// - Returns: Transaction check result - public static func check(wallet: Wallet, - transactionData: Data, - context: TransactionContextType = .mempool, - blockHeight: UInt32 = 0, - blockHash: Data? = nil, - timestamp: UInt64 = 0, - updateState: Bool = true) throws -> TransactionCheckResult { - var error = FFIError() - var result = FFITransactionCheckResult() - - // Build FFIBlockInfo - var blockInfo = FFIBlockInfo() - blockInfo.height = blockHeight - blockInfo.timestamp = UInt32(timestamp) - if let hash = blockHash, hash.count == 32 { - hash.withUnsafeBytes { buf in - withUnsafeMutableBytes(of: &blockInfo.block_hash) { dst in - dst.copyBytes(from: buf.prefix(32)) - } - } - } - - let contextType = FFITransactionContextType(rawValue: context.rawValue) - - let success = transactionData.withUnsafeBytes { txBytes in - let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress - - return wallet_check_transaction( - wallet.ffiHandle, - txPtr, transactionData.count, - contextType, blockInfo, - nil, 0, // islock_data, islock_len - updateState, &result, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - transaction_check_result_free(&result) - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return TransactionCheckResult(ffiResult: result) - } - - /// Classify a transaction for routing - /// - Parameter transactionData: The transaction bytes - /// - Returns: A string describing the transaction type - public static func classify(_ transactionData: Data) throws -> String { - var error = FFIError() - - let classificationPtr = transactionData.withUnsafeBytes { txBytes in - let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress - return transaction_classify(txPtr, transactionData.count, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = classificationPtr else { - throw KeyWalletError(ffiError: error) - } - - let classification = String(cString: ptr) - string_free(ptr) - - return classification - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionContext.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionContext.swift deleted file mode 100644 index b5bfbcb01c2..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionContext.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import DashSDKFFI - -public enum TransactionContextType: UInt32 { - case mempool = 0 - case instantSend = 1 - case inBlock = 2 - case inChainLockedBlock = 3 - - var ffiValue: FFITransactionContextType { - FFITransactionContextType(rawValue: self.rawValue) - } - - init(ffiContext: FFITransactionContextType) { - self = TransactionContextType(rawValue: ffiContext.rawValue) ?? .mempool - } -} - -public class BlockInfo { - let height: UInt32 - let block_hash: Data - let timestamp: UInt32 - - init(ffi: FFIBlockInfo) { - self.height = ffi.height - self.block_hash = withUnsafeBytes(of: ffi.block_hash) { Data($0) } - self.timestamp = ffi.timestamp - } -} - -public class TransactionContext { - let context_type: TransactionContextType - let block_info: BlockInfo - let islock_data: Data - - init(ffi: FFITransactionContext) { - self.context_type = TransactionContextType(ffiContext: ffi.context_type) - self.block_info = BlockInfo(ffi: ffi.block_info) - if let islockPtr = ffi.islock_data, ffi.islock_len > 0 { - self.islock_data = Data(bytes: islockPtr, count: ffi.islock_len) - } else { - self.islock_data = Data() - } - } -} \ No newline at end of file diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionRecord.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionRecord.swift deleted file mode 100644 index 8ca9513c76d..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TransactionRecord.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import DashSDKFFI - -// This struct is not mapping all fields of FFITransactionRecord -// for the lack of wrappers -public struct NotOwnedTransactionRecord { - let txid: Data - let net_amount: Int64 - let context: TransactionContext - let fee: UInt64 - let tx_data: Data - let label: String? - - public init(handle: UnsafePointer) { - let p = handle.pointee - - self.txid = withUnsafeBytes(of: p.txid) { Data($0) } - self.net_amount = p.net_amount - self.fee = p.fee - self.tx_data = p.tx_data != nil - ? Data(bytes: p.tx_data, count: p.tx_len) - : Data() - self.label = p.label != nil - ? String(cString: p.label) - : nil - self.context = TransactionContext(ffi: p.context) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TxOutput.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TxOutput.swift deleted file mode 100644 index 645969527bc..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/TxOutput.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import DashSDKFFI - -public class TxOutput { - let address: UnsafeMutablePointer - let amount: UInt64 - - public init(address: String, amount: UInt64) { - let address = address.trimmingCharacters(in: .whitespacesAndNewlines) - self.address = strdup(address) - self.amount = amount - } - - deinit { - free(address) - } - - func toFFI() -> FFITxOutput { - return FFITxOutput(address: address, amount: amount) - } -} \ No newline at end of file diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift deleted file mode 100644 index ee3b224afd8..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Wallet.swift +++ /dev/null @@ -1,527 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for a Dash wallet with HD key derivation -public class Wallet { - internal let handle: UnsafeMutablePointer - private let ownsHandle: Bool - - // MARK: - Static Methods - - /// Initialize the key wallet library (call once at app startup) - public static func initialize() -> Bool { - return key_wallet_ffi_initialize() - } - - /// Get library version - public static var version: String { - guard let versionPtr = key_wallet_ffi_version() else { - return "Unknown" - } - return String(cString: versionPtr) - } - - // MARK: - Initialization - - /// Create a wallet from a mnemonic phrase - /// - Parameters: - /// - mnemonic: The mnemonic phrase - /// - network: The network type - /// - accountOptions: Account creation options - public init(mnemonic: String, - network: Network = .mainnet, - accountOptions: AccountCreationOption = .default) throws { - - var error = FFIError() - let walletPtr: UnsafeMutablePointer? - - if case .specificAccounts = accountOptions { - // Use the with_options variant for specific accounts - var options = accountOptions.toFFIOptions() - - // Note: For production, we'd need to properly manage the memory for the arrays - // This is a simplified version - walletPtr = mnemonic.withCString { mnemonicCStr in - wallet_create_from_mnemonic_with_options( - mnemonicCStr, - network.ffiValue, - &options, - &error - ) - } - } else { - // Use simpler variant for default options - walletPtr = mnemonic.withCString { mnemonicCStr in - wallet_create_from_mnemonic( - mnemonicCStr, - network.ffiValue, - &error - ) - } - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let handle = walletPtr else { - throw KeyWalletError(ffiError: error) - } - - self.handle = handle - self.ownsHandle = true - } - - /// Create a wallet from seed bytes - /// - Parameters: - /// - seed: The seed bytes (typically 64 bytes) - /// - network: The network type - /// - accountOptions: Account creation options - public init(seed: Data, network: Network = .mainnet, - accountOptions: AccountCreationOption = .default) throws { - self.ownsHandle = true - - var error = FFIError() - let walletPtr: UnsafeMutablePointer? = seed.withUnsafeBytes { seedBytes in - let seedPtr = seedBytes.bindMemory(to: UInt8.self).baseAddress - - if case .specificAccounts = accountOptions { - var options = accountOptions.toFFIOptions() - return wallet_create_from_seed_with_options( - seedPtr, - seed.count, - network.ffiValue, - &options, - &error - ) - } else { - return wallet_create_from_seed( - seedPtr, - seed.count, - network.ffiValue, - &error - ) - } - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let handle = walletPtr else { - throw KeyWalletError(ffiError: error) - } - - self.handle = handle - } - - /// Create a watch-only wallet from extended public key - /// - Parameters: - /// - xpub: The extended public key string - /// - network: The network type - public init(xpub: String, network: Network = .mainnet) throws { - // Create an empty wallet first (no accounts) - var error = FFIError() - var options = AccountCreationOption.noAccounts.toFFIOptions() - - // Create a random wallet with no accounts - let walletPtr = wallet_create_random_with_options(network.ffiValue, &options, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let handle = walletPtr else { - throw KeyWalletError(ffiError: error) - } - - self.handle = handle - self.ownsHandle = true - - // Now add the watch-only account with the provided xpub - do { - _ = try addAccount(type: .standardBIP44, index: 0, xpub: xpub) - } catch { - // Clean up the wallet if adding account failed - wallet_free(handle) - throw error - } - } - - /// Create a new random wallet - /// - Parameters: - /// - network: The network type - /// - accountOptions: Account creation options - public static func createRandom(network: Network = .mainnet, - accountOptions: AccountCreationOption = .default) throws -> Wallet { - var error = FFIError() - let walletPtr: UnsafeMutablePointer? - - if case .specificAccounts = accountOptions { - var options = accountOptions.toFFIOptions() - walletPtr = wallet_create_random_with_options(network.ffiValue, &options, &error) - } else { - walletPtr = wallet_create_random(network.ffiValue, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = walletPtr else { - throw KeyWalletError(ffiError: error) - } - - // Create a wrapper that takes ownership - let wallet = Wallet(handle: ptr, network: network) - return wallet - } - - /// Private initializer for internal use (takes ownership) - private init(handle: UnsafeMutablePointer, network: Network) { - self.handle = handle - self.ownsHandle = true - } - - // MARK: - Wallet Properties - - /// Get the wallet ID (32-byte hash) - public var id: Data { - get throws { - var id = Data(count: 32) - var error = FFIError() - - let success = id.withUnsafeMutableBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_get_id(handle, idPtr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return id - } - } - - /// Check if wallet has a mnemonic - public var hasMnemonic: Bool { - var error = FFIError() - let result = wallet_has_mnemonic(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - return result - } - - /// Check if wallet is watch-only - public var isWatchOnly: Bool { - var error = FFIError() - let result = wallet_is_watch_only(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - return result - } - - // MARK: - Account Management - - /// Get an account by type and index - /// - Parameters: - /// - type: The account type - /// - index: The account index - /// - Returns: An account handle - public func getAccount(type: AccountType, index: UInt32 = 0) throws -> Account { - let result = wallet_get_account(handle, index, type.ffiValue) - - defer { - if result.error_message != nil { - var mutableResult = result - account_result_free_error(&mutableResult) - } - } - - guard let accountHandle = result.account else { - var error = FFIError() - error.code = FFIErrorCode(rawValue: UInt32(result.error_code)) - if let msg = result.error_message { - error.message = msg - } - throw KeyWalletError(ffiError: error) - } - - return Account(handle: accountHandle, wallet: self) - } - - /// Get an identity top-up account with a specific registration index - /// - Parameter registrationIndex: The identity registration index - /// - Returns: An account handle - public func getTopUpAccount(registrationIndex: UInt32) throws -> Account { - let result = wallet_get_top_up_account_with_registration_index( - handle, registrationIndex) - - defer { - if result.error_message != nil { - var mutableResult = result - account_result_free_error(&mutableResult) - } - } - - guard let accountHandle = result.account else { - var error = FFIError() - error.code = FFIErrorCode(rawValue: UInt32(result.error_code)) - if let msg = result.error_message { - error.message = msg - } - throw KeyWalletError(ffiError: error) - } - - return Account(handle: accountHandle, wallet: self) - } - - /// Add an account to the wallet - /// - Parameters: - /// - type: The account type - /// - index: The account index - /// - xpub: Optional extended public key for watch-only accounts - /// - Returns: The newly added account - public func addAccount(type: AccountType, index: UInt32, xpub: String? = nil) throws -> Account { - let result: FFIAccountResult - - if let xpub = xpub { - result = xpub.withCString { xpubCStr in - wallet_add_account_with_string_xpub( - handle, type.ffiValue, index, xpubCStr) - } - } else { - result = wallet_add_account( - handle, type.ffiValue, index) - } - - defer { - if result.error_message != nil { - var mutableResult = result - account_result_free_error(&mutableResult) - } - } - - guard let accountHandle = result.account else { - var error = FFIError() - error.code = FFIErrorCode(rawValue: UInt32(result.error_code)) - if let msg = result.error_message { - error.message = msg - } - throw KeyWalletError(ffiError: error) - } - - return Account(handle: accountHandle, wallet: self) - } - - /// Add a Platform Payment account (DIP-17) to the wallet. - /// - /// Platform Payment accounts use derivation path: - /// `m/9'/coin_type'/17'/account'/key_class'/index` - /// - /// - Parameters: - /// - accountIndex: The account index (hardened). - /// - keyClass: The key class (hardened). 0 = receive. - public func addPlatformPaymentAccount(accountIndex: UInt32 = 0, keyClass: UInt32 = 0) throws { - let result = wallet_add_platform_payment_account(handle, accountIndex, keyClass) - - defer { - if result.error_message != nil { - var mutableResult = result - account_result_free_error(&mutableResult) - } - } - - guard result.account != nil else { - var error = FFIError() - error.code = FFIErrorCode(rawValue: UInt32(result.error_code)) - if let msg = result.error_message { - error.message = msg - } - throw KeyWalletError(ffiError: error) - } - } - - /// Get the number of accounts in the wallet - public var accountCount: UInt32 { - var error = FFIError() - let count = wallet_get_account_count(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - return count - } - - // MARK: - Key Derivation - - /// Get the extended public key for an account - /// - Parameter accountIndex: The account index - /// - Returns: The extended public key string - public func getAccountXpub(accountIndex: UInt32) throws -> String { - var error = FFIError() - let xpubPtr = wallet_get_account_xpub(handle, accountIndex, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = xpubPtr else { - throw KeyWalletError(ffiError: error) - } - - let xpub = String(cString: ptr) - string_free(ptr) - - return xpub - } - - /// Get the extended private key for an account (only for non-watch-only wallets) - /// - Parameter accountIndex: The account index - /// - Returns: The extended private key string - public func getAccountXpriv(accountIndex: UInt32) throws -> String { - guard !isWatchOnly else { - throw KeyWalletError.invalidState("Cannot get private key from watch-only wallet") - } - - var error = FFIError() - let xprivPtr = wallet_get_account_xpriv(handle, accountIndex, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = xprivPtr else { - throw KeyWalletError(ffiError: error) - } - - let xpriv = String(cString: ptr) - string_free(ptr) - - return xpriv - } - - /// Derive a private key at a specific path - /// - Parameter derivationPath: The BIP32 derivation path - /// - Returns: The private key in WIF format - public func derivePrivateKey(path: String) throws -> String { - guard !isWatchOnly else { - throw KeyWalletError.invalidState("Cannot derive private key from watch-only wallet") - } - - var error = FFIError() - let wifPtr = path.withCString { pathCStr in - wallet_derive_private_key_as_wif(handle, pathCStr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = wifPtr else { - throw KeyWalletError(ffiError: error) - } - - let wif = String(cString: ptr) - string_free(ptr) - - return wif - } - - /// Derive a public key at a specific path - /// - Parameter derivationPath: The BIP32 derivation path - /// - Returns: The public key as hex string - public func derivePublicKey(path: String) throws -> String { - var error = FFIError() - let hexPtr = path.withCString { pathCStr in - wallet_derive_public_key_as_hex(handle, pathCStr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = hexPtr else { - throw KeyWalletError(ffiError: error) - } - - let hex = String(cString: ptr) - string_free(ptr) - - return hex - } - - // MARK: - Internal - - /// Get the raw FFI handle (for internal use) - // MARK: - Account Collection - - /// Get a collection of all accounts in this wallet - /// - Parameter network: The network type - /// - Returns: The account collection - public func getAccountCollection() throws -> AccountCollection { - var error = FFIError() - - guard let collectionHandle = wallet_get_account_collection(handle, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - return AccountCollection(handle: collectionHandle, wallet: self) - } - - internal var ffiHandle: UnsafeMutablePointer { handle } - - // Non-owning initializer for wallets obtained from WalletManager - public init(nonOwningHandle handle: UnsafeRawPointer) { - self.handle = UnsafeMutablePointer(mutating: handle.bindMemory(to: FFIWallet.self, capacity: 1)) - self.ownsHandle = false - } - - - deinit { - if ownsHandle { - wallet_free(handle) - } - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift deleted file mode 100644 index 7f188f28343..00000000000 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift +++ /dev/null @@ -1,685 +0,0 @@ -import Foundation -import DashSDKFFI - -/// Swift wrapper for wallet manager that manages multiple wallets -public class WalletManager { - private let handle: UnsafeMutablePointer - internal let network: Network - private let ownsHandle: Bool - - /// Create a new standalone wallet manager - /// Note: Consider using SPVClient.getWalletManager() instead if you have an SPV client - public init(network: Network = .mainnet,) throws { - var error = FFIError() - guard let managerHandle = wallet_manager_create(network.ffiValue, &error) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - self.handle = managerHandle - self.network = network - self.ownsHandle = true - } - - /// Create a wallet manager wrapper from an existing handle (does not own the handle) - /// - Parameter handle: The FFI wallet manager handle - internal init(handle: UnsafeMutablePointer) throws { - var error = FFIError() - let network = wallet_manager_network(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - // Check if there was an error - if error.code != FFIErrorCode(rawValue: 0) { - throw KeyWalletError(ffiError: error) - } - - self.handle = handle - self.network = Network(ffiNetwork: network) - self.ownsHandle = false - } - - deinit { - if ownsHandle { - wallet_manager_free(handle) - } - } - - // MARK: - Wallet Management - - /// Add a wallet from mnemonic - /// - Parameters: - /// - mnemonic: The mnemonic phrase - /// - passphrase: Optional BIP39 passphrase - /// - accountOptions: Account creation options - /// - Returns: The wallet ID - @discardableResult - public func addWallet(mnemonic: String, - accountOptions: AccountCreationOption = .default) throws -> Data { - var error = FFIError() - - let success = mnemonic.withCString { mnemonicCStr in - if case .specificAccounts = accountOptions { - var options = accountOptions.toFFIOptions() - return wallet_manager_add_wallet_from_mnemonic_with_options( - handle, mnemonicCStr, &options, &error) - } else { - return wallet_manager_add_wallet_from_mnemonic( - handle, mnemonicCStr, &error) - } - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - // Get the wallet IDs to return the newly added wallet ID - return try getWalletIds().last ?? Data() - } - - /// Get all wallet IDs - /// - Returns: Array of wallet IDs (32-byte Data objects) - public func getWalletIds() throws -> [Data] { - var error = FFIError() - var walletIdsPtr: UnsafeMutablePointer? - var count: size_t = 0 - - let success = wallet_manager_get_wallet_ids(handle, &walletIdsPtr, &count, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - if let ptr = walletIdsPtr { - wallet_manager_free_wallet_ids(ptr, count) - } - } - - guard success, let ptr = walletIdsPtr else { - throw KeyWalletError(ffiError: error) - } - - var walletIds: [Data] = [] - for i in 0.. Wallet? { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - var error = FFIError() - let walletPtr = walletId.withUnsafeBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_wallet(handle, idPtr, &error) - } - defer { - if error.message != nil { - error_message_free(error.message) - } - } - guard let ptr = walletPtr else { - if error.code == FFIErrorCode(rawValue: 10) { // NOT_FOUND - return nil - } - throw KeyWalletError(ffiError: error) - } - // Wrap as non-owning wallet; the manager retains ownership - let wallet = Wallet(nonOwningHandle: UnsafeRawPointer(ptr)) - return wallet - } - - /// Get the number of wallets - public var walletCount: Int { - get throws { - var error = FFIError() - let count = wallet_manager_wallet_count(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - // Check if there was an error - if error.code != FFIErrorCode(rawValue: 0) { - throw KeyWalletError(ffiError: error) - } - - return count - } - } - - // MARK: - Address Management - - /// Get next receive address for a wallet - /// - Parameters: - /// - walletId: The wallet ID - /// - accountIndex: The account index - /// - Returns: The next receive address - public func getReceiveAddress(walletId: Data, accountIndex: UInt32 = 0) throws -> String { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - - var error = FFIError() - - // First get the managed wallet info - guard let managedInfo = walletId.withUnsafeBytes({ idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_managed_wallet_info(handle, idPtr, &error) - }) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - defer { - managed_wallet_info_free(managedInfo) - } - - // Get the wallet - guard let wallet = walletId.withUnsafeBytes({ idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_wallet(handle, idPtr, &error) - }) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - // Now get the receive address - let addressPtr = managed_wallet_get_next_bip44_receive_address( - managedInfo, wallet, accountIndex, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = addressPtr else { - throw KeyWalletError(ffiError: error) - } - - let address = String(cString: ptr) - address_free(ptr) - - return address - } - - /// Get next change address for a wallet - /// - Parameters: - /// - walletId: The wallet ID - /// - accountIndex: The account index - /// - Returns: The next change address - public func getChangeAddress(walletId: Data, accountIndex: UInt32 = 0) throws -> String { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - - var error = FFIError() - - // First get the managed wallet info - guard let managedInfo = walletId.withUnsafeBytes({ idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_managed_wallet_info(handle, idPtr, &error) - }) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - defer { - managed_wallet_info_free(managedInfo) - } - - // Get the wallet - guard let wallet = walletId.withUnsafeBytes({ idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_wallet(handle, idPtr, &error) - }) else { - defer { - if error.message != nil { - error_message_free(error.message) - } - } - throw KeyWalletError(ffiError: error) - } - - // Now get the change address - let addressPtr = managed_wallet_get_next_bip44_change_address( - managedInfo, wallet, accountIndex, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let ptr = addressPtr else { - throw KeyWalletError(ffiError: error) - } - - let address = String(cString: ptr) - address_free(ptr) - - return address - } - - - // MARK: - Balance - - /// Get wallet balance - /// - Parameter walletId: The wallet ID - /// - Returns: Tuple of (confirmed, unconfirmed) balance - public func getWalletBalance(walletId: Data) throws -> (confirmed: UInt64, unconfirmed: UInt64) { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - - var error = FFIError() - var confirmed: UInt64 = 0 - var unconfirmed: UInt64 = 0 - - let success = walletId.withUnsafeBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_get_wallet_balance( - handle, idPtr, &confirmed, &unconfirmed, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return (confirmed: confirmed, unconfirmed: unconfirmed) - } - - // MARK: - Transaction Processing - - /// Process a transaction through all wallets - /// - Parameters: - /// - transactionData: The transaction bytes - /// - contextDetails: Transaction context details - /// - updateStateIfFound: Whether to update wallet state if transaction is relevant - /// - Returns: True if transaction was relevant to at least one wallet - @discardableResult - public func processTransaction(_ transactionData: Data, - contextDetails: TransactionContextDetails, - updateStateIfFound: Bool = true) throws -> Bool { - var error = FFIError() - // Build FFITransactionContext from TransactionContextDetails - var ffiContext = FFITransactionContext() - ffiContext.context_type = FFITransactionContextType(rawValue: contextDetails.context.rawValue) - ffiContext.block_info.height = contextDetails.height - ffiContext.block_info.timestamp = contextDetails.timestamp - if let hash = contextDetails.blockHash, hash.count == 32 { - hash.withUnsafeBytes { buf in - withUnsafeMutableBytes(of: &ffiContext.block_info.block_hash) { dst in - dst.copyBytes(from: buf.prefix(32)) - } - } - } - - let success = transactionData.withUnsafeBytes { txBytes in - let txPtr = txBytes.bindMemory(to: UInt8.self).baseAddress - return wallet_manager_process_transaction( - handle, txPtr, transactionData.count, - &ffiContext, - updateStateIfFound, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return success - } - - // MARK: - Block Height Management - - /// Get the current block height for a network - /// - Parameter network: The network type - /// - Returns: The current block height - public func currentHeight() throws -> UInt32 { - var error = FFIError() - - let height = wallet_manager_current_height(handle, &error) - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - // Check if there was an error - if error.code != FFIErrorCode(rawValue: 0) { - throw KeyWalletError(ffiError: error) - } - - return height - } - - // MARK: - Managed Accounts - - /// Get a managed account from a wallet - /// - Parameters: - /// - walletId: The wallet ID - /// - accountIndex: The account index - /// - accountType: The type of account to get - /// - Returns: The managed account - public func getManagedAccount(walletId: Data, accountIndex: UInt32, accountType: AccountType) throws -> ManagedAccount { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - - var result = walletId.withUnsafeBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return managed_wallet_get_account(handle, idPtr, accountIndex, accountType.ffiValue) - } - - defer { - if result.error_message != nil { - managed_core_account_result_free_error(&result) - } - } - - guard let accountHandle = result.account else { - let errorMessage = result.error_message != nil ? String(cString: result.error_message!) : "Unknown error" - throw KeyWalletError.walletError(errorMessage) - } - - return ManagedAccount(handle: accountHandle, manager: self) - } - - /// Get a managed top-up account with a specific registration index - /// - Parameters: - /// - walletId: The wallet ID - /// - registrationIndex: The registration index - /// - Returns: The managed account - public func getManagedTopUpAccount(walletId: Data, registrationIndex: UInt32) throws -> ManagedAccount { - guard walletId.count == 32 else { - throw KeyWalletError.invalidInput("Wallet ID must be exactly 32 bytes") - } - - var result = walletId.withUnsafeBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return managed_wallet_get_top_up_account_with_registration_index( - handle, idPtr, registrationIndex) - } - - defer { - if result.error_message != nil { - managed_core_account_result_free_error(&result) - } - } - - guard let accountHandle = result.account else { - let errorMessage = result.error_message != nil ? String(cString: result.error_message!) : "Unknown error" - throw KeyWalletError.walletError(errorMessage) - } - - return ManagedAccount(handle: accountHandle, manager: self) - } - - /// Get a collection of all managed accounts for a wallet - /// - Parameters: - /// - walletId: The wallet ID - /// - Returns: The managed account collection - public func getManagedAccountCollection(walletId: Data) -> ManagedAccountCollection? { - guard walletId.count == 32 else { - return nil - } - - var error = FFIError() - - let collectionHandle = walletId.withUnsafeBytes { idBytes in - let idPtr = idBytes.bindMemory(to: UInt8.self).baseAddress - return managed_wallet_get_account_collection(handle, idPtr, &error) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard let collection = collectionHandle else { - return nil - } - - return ManagedAccountCollection(handle: collection, manager: self) - } - - /// Ensure a wallet has a DIP-17 Platform Payment account created. - /// If it already exists, this is a no-op. - /// - /// - Parameters: - /// - walletId: The wallet ID (32 bytes) - /// - accountIndex: Account index (default 0) - /// - keyClass: Key class (default 0 = receive) - public func ensurePlatformPaymentAccount( - walletId: Data, - accountIndex: UInt32 = 0, - keyClass: UInt32 = 0 - ) throws { - guard let wallet = try getWallet(id: walletId) else { - throw KeyWalletError.notFound("Wallet not found") - } - - // Check if the account already exists - guard let collection = getManagedAccountCollection(walletId: walletId) else { - throw KeyWalletError.notFound("Account collection not found") - } - if collection.getPlatformPaymentAccount(accountIndex: accountIndex, keyClass: keyClass) != nil { - return // Already exists - } - - // Create the platform payment account - try wallet.addPlatformPaymentAccount(accountIndex: accountIndex, keyClass: keyClass) - } - - /// Get platform payment addresses for a wallet, suitable for BLAST sync. - /// - /// Returns (derivation index, address key bytes) tuples from the platform - /// payment account's address pool. - /// - /// - Parameters: - /// - walletId: The wallet ID (32 bytes) - /// - accountIndex: Account index (default 0) - /// - keyClass: Key class (default 0 = receive) - /// - Returns: Array of (index, key) tuples for BLAST sync, or empty if no platform account. - public func getPlatformAddresses( - walletId: Data, - accountIndex: UInt32 = 0, - keyClass: UInt32 = 0 - ) throws -> [(index: UInt32, key: Data)] { - guard let collection = getManagedAccountCollection(walletId: walletId) else { - return [] - } - - guard let platformAccount = collection.getPlatformPaymentAccount( - accountIndex: accountIndex, keyClass: keyClass - ) else { - return [] - } - - guard let pool = platformAccount.getAddressPool() else { - return [] - } - - // Get all generated addresses from the pool - let addresses = try pool.getAddresses(from: 0, to: 0) // 0,0 = all addresses - return addresses.compactMap { info in - // Convert scriptPubKey to GroveDB storage key via Rust FFI - var outKey = Data(count: 21) - var outKeyLen: UInt32 = 21 // capacity of outKey buffer - let success = info.scriptPubKey.withUnsafeBytes { scriptBuffer -> Bool in - guard let scriptBase = scriptBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return false - } - return outKey.withUnsafeMutableBytes { keyBuffer -> Bool in - guard let keyBase = keyBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return false - } - return dash_sdk_script_to_platform_address_key( - scriptBase, - UInt32(info.scriptPubKey.count), - keyBase, - &outKeyLen - ) - } - } - guard success, outKeyLen > 0 else { return nil } - return (index: info.index, key: outKey.prefix(Int(outKeyLen))) - } - } - - internal var ffiHandle: UnsafeMutablePointer { handle } - - // MARK: - Serialization - - /// Add a wallet from mnemonic and return serialized wallet bytes - /// - Parameters: - /// - mnemonic: The mnemonic phrase - /// - passphrase: Optional BIP39 passphrase - /// - birthHeight: Optional birth height for wallet - /// - accountOptions: Account creation options - /// - downgradeToPublicKeyWallet: If true, creates a watch-only or externally signable wallet - /// - allowExternalSigning: If true AND downgradeToPublicKeyWallet is true, creates an externally signable wallet - /// - Returns: Tuple containing (walletId: Data, serializedWallet: Data) - public func addWalletAndSerialize( - mnemonic: String, - passphrase: String? = nil, - birthHeight: UInt32 = 0, - accountOptions: AccountCreationOption = .default, - downgradeToPublicKeyWallet: Bool = false, - allowExternalSigning: Bool = false - ) throws -> (walletId: Data, serializedWallet: Data) { - if let passphrase, !passphrase.isEmpty { - throw KeyWalletError.invalidInput( - "BIP-39 passphrase support was removed upstream " + - "(rust-dashcore #747); pass nil or an empty string" - ) - } - - var error = FFIError() - var walletBytesPtr: UnsafeMutablePointer? - var walletBytesLen: size_t = 0 - var walletId = [UInt8](repeating: 0, count: 32) - - let success = mnemonic.withCString { mnemonicCStr in - var options = accountOptions.toFFIOptions() - return wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( - handle, - mnemonicCStr, - birthHeight, - &options, - downgradeToPublicKeyWallet, - allowExternalSigning, - &walletBytesPtr, - &walletBytesLen, - &walletId, - &error - ) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - // Free the allocated bytes after copying - if let ptr = walletBytesPtr { - wallet_manager_free_wallet_bytes(ptr, walletBytesLen) - } - } - - guard success, let bytesPtr = walletBytesPtr else { - throw KeyWalletError(ffiError: error) - } - - // Copy the data before freeing (which happens in defer) - let serializedData = Data(bytes: bytesPtr, count: Int(walletBytesLen)) - let walletIdData = Data(walletId) - - return (walletId: walletIdData, serializedWallet: serializedData) - } - - /// Import a wallet from serialized bytes - /// - Parameters: - /// - walletBytes: The serialized wallet data - /// - Returns: The wallet ID of the imported wallet - public func importWallet(from walletBytes: Data) throws -> Data { - guard !walletBytes.isEmpty else { - throw KeyWalletError.invalidInput("Wallet bytes cannot be empty") - } - - var error = FFIError() - var walletId = [UInt8](repeating: 0, count: 32) - - let success = walletBytes.withUnsafeBytes { bytes in - wallet_manager_import_wallet_from_bytes( - handle, - bytes.bindMemory(to: UInt8.self).baseAddress, - size_t(walletBytes.count), - &walletId, - &error - ) - } - - defer { - if error.message != nil { - error_message_free(error.message) - } - } - - guard success else { - throw KeyWalletError(ffiError: error) - } - - return Data(walletId) - } -} diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift index 93dd0c88aea..6d2495de47b 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift @@ -1643,18 +1643,10 @@ public class PlatformWalletPersistenceHandler { return nil } - // 3. Mnemonic UTF-8 bytes → 64-byte BIP39 seed. - let seed: Data - do { - seed = try Mnemonic.toSeed(mnemonicUTF8Bytes: mnemonicUTF8Bytes) - } catch { - print("⚠️ deriveAndStoreIdentityKey: mnemonic-to-seed failed: \(error.localizedDescription)") - return nil - } - - // 4. Build the DIP-9 authentication path. The string form + // 3. Build the DIP-9 authentication path. The string form // doubles as the keychain account suffix so the explorer - // can render it. + // can render it, and is the path fed to the derivation + // call below. let derivationPath: String do { derivationPath = try KeyDerivation.getIdentityAuthenticationPath( @@ -1667,30 +1659,50 @@ public class PlatformWalletPersistenceHandler { return nil } - // 5. Derive the 32-byte scalar via the FFI bridge. The - // bridge writes into a caller-provided buffer; we zero - // the scratch `Data` on the way out for hygiene (the - // keychain item is the real home for the bytes). + // 4. Mnemonic + path → 32-byte ECDSA scalar in one FFI call. + // Mnemonic stays raw UTF-8 bytes (never a Swift `String`) and + // is scrubbed after; the chain-code buffer is required by the + // FFI but discarded. + var mnemonicCBytes = mnemonicUTF8Bytes + guard !mnemonicCBytes.contains(0) else { + mnemonicCBytes.resetBytes(in: 0.. Int32 in - guard let pkPtr = pkBytes.bindMemory(to: UInt8.self).baseAddress else { return -1 } - return seed.withUnsafeBytes { seedBytes -> Int32 in - guard let seedPtr = seedBytes.bindMemory(to: UInt8.self).baseAddress else { - return -1 - } - return derivationPath.withCString { pathCStr in - key_wallet_derive_private_key_from_seed(seedPtr, pathCStr, pkPtr) + var chainCode = Data(count: 32) + defer { chainCode.resetBytes(in: 0.. PlatformWalletFFIResult in + let mnemonicCStr = mnemonicRaw.bindMemory(to: CChar.self).baseAddress + return derivationPath.withCString { pathCStr in + privateKey.withUnsafeMutableBytes { pkRaw in + chainCode.withUnsafeMutableBytes { ccRaw in + platform_wallet_derive_ext_priv_key_from_mnemonic( + mnemonicCStr, + nil, + network.ffiValue, + pathCStr, + pkRaw.bindMemory(to: UInt8.self).baseAddress, + ccRaw.bindMemory(to: UInt8.self).baseAddress, + nil + ) + } } } } - guard rc == 0 else { - print("⚠️ deriveAndStoreIdentityKey: FFI derive failed (rc=\(rc))") - // Zero out any partial write before returning. + let ffi = PlatformWalletResult(deriveResult) + guard ffi.isSuccess else { + print("⚠️ deriveAndStoreIdentityKey: FFI derive failed: \(ffi.message ?? "")") privateKey.resetBytes(in: 0..= totalAmount + fee - } -} - -// MARK: - UTXO Selector - -/// Utility for selecting optimal UTXOs for transaction building -public struct UTXOSelector { - - /// Select UTXOs to cover a target amount plus fees - /// - Parameters: - /// - available: Available UTXOs to choose from - /// - targetAmount: Amount to send (in duffs) - /// - feePerByte: Fee rate in duffs per byte - /// - Returns: UTXO selection if sufficient funds available, nil otherwise - public static func selectUTXOs( - from available: [UTXO], - targetAmount: UInt64, - feePerByte: UInt64 = 1 - ) -> UTXOSelection? { - // Filter to only confirmed UTXOs - let spendable = available.filter { $0.isSpendable } - - // Sort by amount (largest first for now - could implement better algorithms) - let sorted = spendable.sorted { $0.amount > $1.amount } - - var selected: [UTXO] = [] - var totalSelected: UInt64 = 0 - - // Simple selection - take UTXOs until we have enough - for utxo in sorted { - selected.append(utxo) - totalSelected += utxo.amount - - // Estimate fee (simplified - real implementation would be more complex) - let estimatedSize = (selected.count * 148) + (2 * 34) + 10 // inputs + outputs + overhead - let estimatedFee = UInt64(estimatedSize) * feePerByte - - if totalSelected >= targetAmount + estimatedFee { - let change = totalSelected - targetAmount - estimatedFee - return UTXOSelection( - selectedUTXOs: selected, - totalAmount: targetAmount, - fee: estimatedFee, - change: change - ) - } - } - - // Not enough funds - return nil - } -} diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift index 4e6afa228d1..2256198a89e 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Models/DashAddress.swift @@ -1,4 +1,5 @@ import Foundation +import DashSDKFFI import SwiftDashSDK enum DashAddressType: Equatable { @@ -49,8 +50,7 @@ struct DashAddress { } // 2. Try Core address — validate via Rust FFI (Base58Check + network) - let keyWalletNetwork: Network = (network == .mainnet) ? .mainnet : .testnet - if Address.validate(input, network: keyWalletNetwork), + if validateCoreAddress(input, network: network), let script = coreAddressToOutputScript(input) { return DashAddress(type: .core(script), displayString: input) } @@ -81,9 +81,16 @@ struct DashAddress { return Bech32m.encode(hrp: hrp, data: rawBytes) } + /// Validate a Core address for `network` via the Rust FFI. + private static func validateCoreAddress(_ address: String, network: Network) -> Bool { + address.withCString { cstr in + platform_wallet_address_validate(cstr, network.ffiValue) + } + } + /// Convert a base58check Core address to P2PKH output script. /// - /// Caller must validate the address first via `Address.validate()` (Rust FFI) + /// Caller must validate the address first via `validateCoreAddress(_:network:)` (Rust FFI) /// which handles Base58Check decoding, checksum verification, and network matching. /// This method only extracts the pubkey hash and builds the script. static func coreAddressToOutputScript(_ address: String) -> Data? { diff --git a/packages/swift-sdk/SwiftTests/Tests/SwiftDashSDKTests/PlatformWalletTests.swift b/packages/swift-sdk/SwiftTests/Tests/SwiftDashSDKTests/PlatformWalletTests.swift index b1c063db6da..ad2fa23c413 100644 --- a/packages/swift-sdk/SwiftTests/Tests/SwiftDashSDKTests/PlatformWalletTests.swift +++ b/packages/swift-sdk/SwiftTests/Tests/SwiftDashSDKTests/PlatformWalletTests.swift @@ -46,16 +46,6 @@ class PlatformWalletTests: XCTestCase { XCTAssertNotNil(wallet, "Wallet should be created from mnemonic with passphrase") } - func testMnemonicToSeedFromUTF8BytesMatchesStringPath() throws { - let fromString = try Mnemonic.toSeed(mnemonic: testMnemonic, passphrase: "test123") - let fromBytes = try Mnemonic.toSeed( - mnemonicUTF8Bytes: Data(testMnemonic.utf8), - passphrase: "test123" - ) - - XCTAssertEqual(fromBytes, fromString, "Byte-based mnemonic derivation should match the existing string path") - } - // MARK: - Identity Manager Tests func testGetIdentityManager() throws { diff --git a/packages/swift-sdk/build_ios.sh b/packages/swift-sdk/build_ios.sh index ede6ee7105f..a96f8903784 100755 --- a/packages/swift-sdk/build_ios.sh +++ b/packages/swift-sdk/build_ios.sh @@ -129,7 +129,6 @@ inject_modulemap() { #define DASHSDKFFI_H #include "dash-network/dash-network.h" -#include "key-wallet-ffi/key-wallet-ffi.h" #include "rs-sdk-ffi/rs-sdk-ffi.h" #include "platform-wallet-ffi/platform-wallet-ffi.h"