From d957e43490f70d54bf00bdd2ba621e675babcea3 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 13 Jan 2026 17:11:54 -0600 Subject: [PATCH 1/3] feat(wasm-sdk): auto-generate entropy for document creation when not provided Previously, documentCreate() would throw an error if the Document didn't have entropy set. This was a breaking change for SDKs that create documents via fromObject(), fromJSON(), or fromBytes() which don't auto-generate entropy. Now entropy is auto-generated at the wasm-sdk level when not provided, making it truly optional while maintaining backward compatibility. Co-Authored-By: Claude Opus 4.5 --- .../src/state_transitions/document.rs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/wasm-sdk/src/state_transitions/document.rs b/packages/wasm-sdk/src/state_transitions/document.rs index 44197daf1f9..78d5aaaa4ea 100644 --- a/packages/wasm-sdk/src/state_transitions/document.rs +++ b/packages/wasm-sdk/src/state_transitions/document.rs @@ -51,7 +51,8 @@ export interface DocumentCreateOptions { /** * The document to create. * Use `new Document(...)` or `Document.fromJSON(...)` to construct it. - * Must include dataContractId, documentTypeName, ownerId, and entropy. + * Must include dataContractId, documentTypeName, and ownerId. + * Entropy is optional - if not set, it will be auto-generated. */ document: Document; @@ -108,19 +109,23 @@ impl WasmSdk { let contract_id: Identifier = document_wasm.get_data_contract_id().into(); let document_type_name = document_wasm.get_document_type_name(); - // Get entropy from document - let entropy = document_wasm.get_entropy().ok_or_else(|| { - WasmSdkError::invalid_argument("Document must have entropy set for creation") - })?; - - if entropy.len() != 32 { - return Err(WasmSdkError::invalid_argument( - "Document entropy must be exactly 32 bytes", - )); - } - - let mut entropy_array = [0u8; 32]; - entropy_array.copy_from_slice(&entropy); + // Get entropy from document, or generate if not set + let entropy_array = match document_wasm.get_entropy() { + Some(entropy) if entropy.len() == 32 => { + let mut arr = [0u8; 32]; + arr.copy_from_slice(&entropy); + arr + } + _ => { + // Auto-generate entropy if not provided + use dash_sdk::dpp::util::entropy_generator::{ + DefaultEntropyGenerator, EntropyGenerator, + }; + DefaultEntropyGenerator.generate().map_err(|e| { + WasmSdkError::generic(format!("Failed to generate entropy: {}", e)) + })? + } + }; // Extract identity key from options let identity_key_wasm = From 603ec603fcb256061709d2706b377829b0fcc44f Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 13 Jan 2026 17:38:48 -0600 Subject: [PATCH 2/3] refactor(wasm-sdk): delegate entropy generation to rs-sdk Instead of generating entropy in wasm-sdk, pass None to rs-sdk when entropy is not set. This lets rs-sdk handle both entropy generation and document ID regeneration correctly. Co-Authored-By: Claude Opus 4.5 --- .../src/state_transitions/document.rs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/wasm-sdk/src/state_transitions/document.rs b/packages/wasm-sdk/src/state_transitions/document.rs index 78d5aaaa4ea..253520f0aa4 100644 --- a/packages/wasm-sdk/src/state_transitions/document.rs +++ b/packages/wasm-sdk/src/state_transitions/document.rs @@ -109,23 +109,16 @@ impl WasmSdk { let contract_id: Identifier = document_wasm.get_data_contract_id().into(); let document_type_name = document_wasm.get_document_type_name(); - // Get entropy from document, or generate if not set - let entropy_array = match document_wasm.get_entropy() { - Some(entropy) if entropy.len() == 32 => { + // Get entropy from document if set, otherwise let rs-sdk generate it + let entropy = document_wasm.get_entropy().and_then(|e| { + if e.len() == 32 { let mut arr = [0u8; 32]; - arr.copy_from_slice(&entropy); - arr + arr.copy_from_slice(&e); + Some(arr) + } else { + None } - _ => { - // Auto-generate entropy if not provided - use dash_sdk::dpp::util::entropy_generator::{ - DefaultEntropyGenerator, EntropyGenerator, - }; - DefaultEntropyGenerator.generate().map_err(|e| { - WasmSdkError::generic(format!("Failed to generate entropy: {}", e)) - })? - } - }; + }); // Extract identity key from options let identity_key_wasm = @@ -149,7 +142,7 @@ impl WasmSdk { .put_to_platform_and_wait_for_response( self.inner_sdk(), document_type, - Some(entropy_array), + entropy, identity_key, None, // token_payment_info &signer, From 8aa5644dd88ba4ad0094c18bb352a650ad8c0cc7 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 13 Jan 2026 19:35:33 -0600 Subject: [PATCH 3/3] feat(sdk): add credentials-based API for DPNS registerName Add convenience method that accepts simple credentials (identityId, publicKeyId, privateKey) instead of requiring pre-constructed typed objects (Identity, IdentityPublicKey, IdentitySigner). This simplifies the API for common use cases where the caller has credentials but doesn't need full control over object construction. Changes: - simple-signer: Add from_wif(), from_private_key(), add_key_from_wif() helper methods; update doc to reflect production use - rs-sdk: Add register_dpns_name_with_credentials() convenience method - wasm-sdk: Support both parameter styles in dpnsRegisterName() - js-evo-sdk: Add JSDoc documentation for both styles Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 1 + packages/js-evo-sdk/src/dpns/facade.ts | 32 +++ packages/rs-sdk/Cargo.toml | 3 + .../rs-sdk/src/platform/dpns_usernames/mod.rs | 78 ++++++- packages/simple-signer/src/signer.rs | 71 ++++++- packages/wasm-sdk/src/dpns.rs | 195 +++++++++++++++--- 6 files changed, 353 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8997efbaba4..7f151b68f2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1455,6 +1455,7 @@ dependencies = [ "sanitize-filename", "serde", "serde_json", + "simple-signer", "test-case", "thiserror 2.0.17", "tokio", diff --git a/packages/js-evo-sdk/src/dpns/facade.ts b/packages/js-evo-sdk/src/dpns/facade.ts index 945cf2e6e79..aad232fd671 100644 --- a/packages/js-evo-sdk/src/dpns/facade.ts +++ b/packages/js-evo-sdk/src/dpns/facade.ts @@ -33,6 +33,38 @@ export class DpnsFacade { return w.dpnsResolveName(name); } + /** + * Register a DPNS username on Dash Platform. + * + * Two parameter styles are supported: + * + * **Style A - Typed objects (full control):** + * ```typescript + * const identity = await sdk.identities.fetch(identityId); + * const identityKey = identity.getPublicKeyById(publicKeyId); + * const signer = new IdentitySigner(); + * signer.addKeyFromWif(privateKeyWif); + * + * await sdk.dpns.registerName({ + * label: 'alice', + * identity, + * identityKey, + * signer, + * }); + * ``` + * + * **Style B - Simple credentials (convenience):** + * ```typescript + * await sdk.dpns.registerName({ + * label: 'alice', + * identityId: '...', // Identity ID (string, Identifier, or bytes) + * publicKeyId: 1, // Key index on the identity + * privateKey: bytes, // 32-byte Uint8Array + * }); + * ``` + * + * Style B automatically fetches the identity and retrieves the public key internally. + */ async registerName(options: wasm.DpnsRegisterNameOptions): Promise { const w = await this.sdk.getWasmSdkConnected(); return w.dpnsRegisterName(options); diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 503708e9403..a27efd20237 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -19,6 +19,9 @@ platform-wallet = { path = "../rs-platform-wallet", optional = true } drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } dash-context-provider = { path = "../rs-context-provider", default-features = false } +simple-signer = { path = "../simple-signer", default-features = false, features = [ + "state-transitions", +] } dash-platform-macros = { path = "../rs-dash-platform-macros" } http = { version = "1.1" } rustls-pemfile = { version = "2.0.0" } diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index a801fdc041c..b3a696c4d45 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -15,9 +15,10 @@ use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::{DocumentV0, DocumentV0Getters}; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::signer::Signer; -use dpp::identity::{Identity, IdentityPublicKey}; +use dpp::identity::{Identity, IdentityPublicKey, KeyID}; use dpp::platform_value::{Bytes32, Value}; use dpp::prelude::Identifier; +use simple_signer::signer::SimpleSigner; use std::collections::BTreeMap; use std::sync::Arc; @@ -361,6 +362,81 @@ impl Sdk { }) } + /// Register a DPNS username using simple credentials + /// + /// This is a convenience method that accepts simple credential parameters instead of + /// requiring pre-constructed typed objects. It internally fetches the identity, + /// retrieves the public key, and creates the signer. + /// + /// # Arguments + /// + /// * `label` - The label for the domain (e.g., "alice" for "alice.dash") + /// * `identity_id` - The identifier of the identity that will own the domain + /// * `public_key_id` - The ID of the public key to use for signing + /// * `private_key` - The 32-byte private key corresponding to the public key + /// * `preorder_callback` - Optional callback to be called with the preorder document result + /// + /// # Returns + /// + /// Returns a `RegisterDpnsNameResult` containing both created documents and the full domain name + /// + /// # Errors + /// + /// Returns an error if: + /// - The identity cannot be fetched + /// - The public key is not found on the identity + /// - The DPNS contract cannot be fetched + /// - Document creation or submission fails + /// + /// # Example + /// + /// ```ignore + /// let result = sdk.register_dpns_name_with_credentials( + /// "alice".to_string(), + /// identity_id, + /// 1, // public key ID + /// private_key_bytes, + /// None, + /// ).await?; + /// ``` + pub async fn register_dpns_name_with_credentials( + &self, + label: String, + identity_id: Identifier, + public_key_id: KeyID, + private_key: [u8; 32], + preorder_callback: Option, + ) -> Result { + // Fetch the identity + let identity = Identity::fetch(self, identity_id) + .await? + .ok_or_else(|| Error::Generic(format!("Identity {} not found", identity_id)))?; + + // Get the public key by ID + let identity_public_key = identity + .get_public_key_by_id(public_key_id) + .ok_or_else(|| { + Error::Generic(format!( + "Public key with ID {} not found on identity {}", + public_key_id, identity_id + )) + })? + .clone(); + + // Create the signer with the private key + let signer = SimpleSigner::from_private_key(identity_public_key.clone(), private_key); + + // Call the existing register method + self.register_dpns_name(RegisterDpnsNameInput { + label, + identity, + identity_public_key, + signer, + preorder_callback, + }) + .await + } + /// Check if a DPNS name is available /// /// # Arguments diff --git a/packages/simple-signer/src/signer.rs b/packages/simple-signer/src/signer.rs index 8dd88a20bfe..f230a665000 100644 --- a/packages/simple-signer/src/signer.rs +++ b/packages/simple-signer/src/signer.rs @@ -17,7 +17,10 @@ use dpp::{bls_signatures, dashcore, ed25519_dalek, ProtocolError}; use std::collections::BTreeMap; use std::fmt::{Debug, Formatter}; -/// This simple signer is only to be used in tests +/// A simple signer implementation for signing operations with identity keys. +/// +/// This signer stores private keys in memory and can be used for both testing +/// and production scenarios where convenience methods are preferred. #[derive(Default, Clone, PartialEq, Encode, Decode)] pub struct SimpleSigner { /// Private keys is a map from the public key to the Private key bytes @@ -82,6 +85,72 @@ impl SimpleSigner { self.address_private_keys_in_creation.extend(keys) } + /// Add a key from a WIF-encoded private key string. + /// + /// This method parses the WIF string and adds the key to the signer. + /// + /// # Arguments + /// + /// * `identity_public_key` - The identity public key associated with this private key + /// * `wif` - The WIF-encoded private key string + /// + /// # Returns + /// + /// Returns `Ok(())` if the key was added successfully, or an error if WIF parsing fails. + pub fn add_key_from_wif( + &mut self, + identity_public_key: IdentityPublicKey, + wif: &str, + ) -> Result<(), ProtocolError> { + let private_key = dashcore::PrivateKey::from_wif(wif) + .map_err(|e| ProtocolError::Generic(format!("Invalid WIF private key: {}", e)))?; + self.add_identity_public_key(identity_public_key, private_key.inner.secret_bytes()); + Ok(()) + } + + /// Create a new SimpleSigner with a single key loaded from WIF. + /// + /// This is a convenience method for creating a signer with a single key. + /// + /// # Arguments + /// + /// * `identity_public_key` - The identity public key associated with this private key + /// * `wif` - The WIF-encoded private key string + /// + /// # Returns + /// + /// Returns a new `SimpleSigner` with the key loaded, or an error if WIF parsing fails. + pub fn from_wif( + identity_public_key: IdentityPublicKey, + wif: &str, + ) -> Result { + let mut signer = Self::default(); + signer.add_key_from_wif(identity_public_key, wif)?; + Ok(signer) + } + + /// Create a new SimpleSigner with a single key from raw bytes. + /// + /// This is a convenience method for creating a signer with a single key + /// when you already have the private key as raw bytes. + /// + /// # Arguments + /// + /// * `identity_public_key` - The identity public key associated with this private key + /// * `private_key` - The 32-byte private key + /// + /// # Returns + /// + /// Returns a new `SimpleSigner` with the key loaded. + pub fn from_private_key( + identity_public_key: IdentityPublicKey, + private_key: [u8; 32], + ) -> Self { + let mut signer = Self::default(); + signer.add_identity_public_key(identity_public_key, private_key); + signer + } + /// Commit keys in creation pub fn commit_block_keys(&mut self) { self.private_keys.append(&mut self.private_keys_in_creation); diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index da27f7f9628..7d07752c952 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -100,6 +100,30 @@ fn usernames_from_documents(documents_result: Documents) -> Array { const DPNS_REGISTER_NAME_OPTIONS_TS: &'static str = r#" /** * Options for registering a DPNS username on Dash Platform. + * + * Two parameter styles are supported: + * + * **Style A - Typed objects (current, full control):** + * ```typescript + * await sdk.dpns.registerName({ + * label: 'alice', + * identity, // Pre-fetched Identity object + * identityKey, // IdentityPublicKey from the identity + * signer, // IdentitySigner with private key loaded + * }); + * ``` + * + * **Style B - Simple credentials (convenience):** + * ```typescript + * await sdk.dpns.registerName({ + * label: 'alice', + * identityId: 'abc123...', // Identity ID (string, Identifier, or bytes) + * publicKeyId: 1, // Key index on the identity + * privateKey: privateKeyBytes, // 32-byte Uint8Array + * }); + * ``` + * + * Style B automatically fetches the identity and retrieves the public key internally. */ export interface DpnsRegisterNameOptions { /** @@ -108,23 +132,51 @@ export interface DpnsRegisterNameOptions { */ label: string; + // === Style A: Typed objects (full control) === + /** * The identity that will own the username. * Fetch the identity first using `getIdentity()`. + * Required for Style A. */ - identity: Identity; + identity?: Identity; /** * The identity public key to use for signing the transition. * Get this from the identity's public keys. + * Required for Style A. */ - identityKey: IdentityPublicKey; + identityKey?: IdentityPublicKey; /** * Signer containing the private key that corresponds to the identity key. * Use IdentitySigner to add the private key before calling. + * Required for Style A. */ - signer: IdentitySigner; + signer?: IdentitySigner; + + // === Style B: Simple credentials (convenience) === + + /** + * The identity ID that will own the username. + * Can be a string (base58), Identifier object, or 32-byte Uint8Array. + * Required for Style B. + */ + identityId?: IdentifierLike; + + /** + * The ID/index of the public key on the identity to use for signing. + * Required for Style B. + */ + publicKeyId?: number; + + /** + * The 32-byte private key corresponding to the public key. + * Required for Style B. + */ + privateKey?: Uint8Array; + + // === Common options === /** * Optional callback called after the preorder document is submitted. @@ -223,6 +275,70 @@ fn extract_callback_from_options( Ok(Some(func)) } +/// Extracts an optional u32 field from options. +fn extract_optional_u32_from_options( + options: &JsValue, + field_name: &str, +) -> Result, WasmSdkError> { + let value = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) + .map_err(|_| WasmSdkError::invalid_argument(format!("Failed to get {}", field_name)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let num = value + .as_f64() + .ok_or_else(|| WasmSdkError::invalid_argument(format!("{} must be a number", field_name)))?; + + Ok(Some(num as u32)) +} + +/// Extracts an optional Uint8Array field from options as [u8; 32]. +fn extract_optional_bytes32_from_options( + options: &JsValue, + field_name: &str, +) -> Result, WasmSdkError> { + let value = js_sys::Reflect::get(options, &JsValue::from_str(field_name)) + .map_err(|_| WasmSdkError::invalid_argument(format!("Failed to get {}", field_name)))?; + + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let array = js_sys::Uint8Array::new(&value); + let bytes = array.to_vec(); + + if bytes.len() != 32 { + return Err(WasmSdkError::invalid_argument(format!( + "{} must be exactly 32 bytes, got {}", + field_name, + bytes.len() + ))); + } + + let mut result = [0u8; 32]; + result.copy_from_slice(&bytes); + Ok(Some(result)) +} + +/// Checks if options contains simple credentials (Style B). +fn has_simple_credentials(options: &JsValue) -> bool { + let identity_id = js_sys::Reflect::get(options, &JsValue::from_str("identityId")) + .map(|v| !v.is_undefined() && !v.is_null()) + .unwrap_or(false); + + let public_key_id = js_sys::Reflect::get(options, &JsValue::from_str("publicKeyId")) + .map(|v| !v.is_undefined() && !v.is_null()) + .unwrap_or(false); + + let private_key = js_sys::Reflect::get(options, &JsValue::from_str("privateKey")) + .map(|v| !v.is_undefined() && !v.is_null()) + .unwrap_or(false); + + identity_id && public_key_id && private_key +} + /// DPNS contract ID constant const DPNS_CONTRACT_ID: &str = "GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec"; /// DPNS domain document type @@ -330,7 +446,11 @@ impl WasmSdk { /// 3. Creates and submits the domain document /// 4. Returns the result with both document IDs /// - /// @param options - Registration options including label, identity, key, and signer + /// Supports two parameter styles: + /// - **Style A (typed objects)**: Pass `identity`, `identityKey`, and `signer` + /// - **Style B (simple credentials)**: Pass `identityId`, `publicKeyId`, and `privateKey` + /// + /// @param options - Registration options including label and either typed objects or simple credentials /// @returns Promise that resolves to the registration result #[wasm_bindgen(js_name = "dpnsRegisterName")] pub async fn dpns_register_name( @@ -339,21 +459,10 @@ impl WasmSdk { ) -> Result { let options_value: JsValue = options.into(); - // Extract label from options + // Extract label from options (required for both styles) let label = extract_string_from_options(&options_value, "label")?; - // Extract identity from options - let identity: Identity = IdentityWasm::try_from_options(&options_value, "identity")?.into(); - - // Extract identity key from options - let identity_key_wasm = - IdentityPublicKeyWasm::try_from_options(&options_value, "identityKey")?; - let identity_public_key: IdentityPublicKey = identity_key_wasm.into(); - - // Extract signer from options - let signer = IdentitySignerWasm::try_from_options(&options_value)?; - - // Extract optional preorder callback + // Extract optional preorder callback (common to both styles) let preorder_callback = extract_callback_from_options(&options_value, "preorderCallback")?; // Set up the callback if provided @@ -390,15 +499,51 @@ impl WasmSdk { None }; - let input = RegisterDpnsNameInput { - label, - identity, - identity_public_key, - signer, - preorder_callback: callback_box, - }; + // Check which style is being used and route accordingly + let result = if has_simple_credentials(&options_value) { + // Style B: Simple credentials + let identity_id_value = + js_sys::Reflect::get(&options_value, &JsValue::from_str("identityId")) + .map_err(|_| WasmSdkError::invalid_argument("Failed to get identityId"))?; + let identity_id = identifier_from_js(&identity_id_value, "identityId")?; + + let public_key_id = extract_optional_u32_from_options(&options_value, "publicKeyId")? + .ok_or_else(|| WasmSdkError::invalid_argument("publicKeyId is required"))?; + + let private_key = extract_optional_bytes32_from_options(&options_value, "privateKey")? + .ok_or_else(|| WasmSdkError::invalid_argument("privateKey is required"))?; + + // Use the credentials-based method + self.as_ref() + .register_dpns_name_with_credentials( + label, + identity_id, + public_key_id, + private_key, + callback_box, + ) + .await? + } else { + // Style A: Typed objects + let identity: Identity = + IdentityWasm::try_from_options(&options_value, "identity")?.into(); + + let identity_key_wasm = + IdentityPublicKeyWasm::try_from_options(&options_value, "identityKey")?; + let identity_public_key: IdentityPublicKey = identity_key_wasm.into(); + + let signer = IdentitySignerWasm::try_from_options(&options_value)?; + + let input = RegisterDpnsNameInput { + label, + identity, + identity_public_key, + signer, + preorder_callback: callback_box, + }; - let result = self.as_ref().register_dpns_name(input).await?; + self.as_ref().register_dpns_name(input).await? + }; // Clean up callback PREORDER_CALLBACK.with(|cb| {