diff --git a/packages/js-evo-sdk/src/dpns/facade.ts b/packages/js-evo-sdk/src/dpns/facade.ts index 166efc7d145..9ca171dd285 100644 --- a/packages/js-evo-sdk/src/dpns/facade.ts +++ b/packages/js-evo-sdk/src/dpns/facade.ts @@ -33,8 +33,34 @@ export class DpnsFacade { return w.dpnsResolveName(name); } + /** + * Register a DPNS username + * + * @param args.label - The username label (without .dash suffix) + * @param args.identityId - The identity ID that will own the name + * @param args.publicKeyId - The identity key ID to use for signing + * IMPORTANT: Must be a key with: + * - Purpose: AUTHENTICATION (not TRANSFER) + * - Security Level: CRITICAL or HIGH (NOT MASTER) + * @param args.privateKeyWif - The private key in WIF format matching publicKeyId + * @param args.onPreorder - Optional callback called after preorder succeeds + * @returns Registration result with document IDs + */ async registerName(args: { label: string; identityId: string; publicKeyId: number; privateKeyWif: string; onPreorder?: Function }): Promise { const { label, identityId, publicKeyId, privateKeyWif, onPreorder } = args; + + // Validate inputs + if (publicKeyId === undefined || publicKeyId === null) { + throw new Error( + 'publicKeyId is required for DPNS registration.\n' + + 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n', + ); + } + + if (typeof publicKeyId !== 'number' || publicKeyId < 0) { + throw new Error(`publicKeyId must be a non-negative number, got: ${publicKeyId}`); + } + const w = await this.sdk.getWasmSdkConnected(); return w.dpnsRegisterName(label, identityId, publicKeyId, privateKeyWif, onPreorder ?? null); } diff --git a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs index a0b2e0da145..207168ef156 100644 --- a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs @@ -54,4 +54,89 @@ describe('DPNSFacade', () => { expect(wasmSdk.getDpnsUsernameByName).to.be.calledOnceWithExactly('u'); expect(wasmSdk.getDpnsUsernameByNameWithProofInfo).to.be.calledOnceWithExactly('u'); }); + + describe('registerName validation', () => { + it('should throw error when publicKeyId is not provided', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + privateKeyWif: 'someKey', + // publicKeyId intentionally omitted + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + expect(error.message).to.include('CRITICAL or HIGH security level'); + } + }); + + it('should throw error when publicKeyId is undefined', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: undefined, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + expect(error.message).to.include('CRITICAL or HIGH security level'); + } + }); + + it('should throw error when publicKeyId is null', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: null, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + } + }); + + it('should throw error when publicKeyId is negative', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: -1, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('must be a non-negative number'); + expect(error.message).to.include('got: -1'); + } + }); + + it('should throw error when publicKeyId is not a number', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: '1', + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('must be a non-negative number'); + } + }); + + it('should accept valid publicKeyId', async () => { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: 1, + privateKeyWif: 'someKey', + }); + expect(wasmSdk.dpnsRegisterName).to.be.calledOnce(); + }); + }); }); diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index 89945e17695..e4da250aa51 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -2,6 +2,9 @@ use crate::error::WasmSdkError; use crate::sdk::WasmSdk; use dash_sdk::dpp::document::{Document, DocumentV0Getters}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::signer::Signer; +use dash_sdk::dpp::identity::{Purpose, SecurityLevel}; use dash_sdk::dpp::prelude::Identifier; use dash_sdk::platform::dpns_usernames::{ convert_to_homograph_safe_chars, is_contested_username, is_valid_username, @@ -76,6 +79,74 @@ impl WasmSdk { })? .clone(); + // Validate the key meets DPNS requirements + let key_purpose = identity_public_key.purpose(); + let key_security_level = identity_public_key.security_level(); + + // Check purpose + if key_purpose != Purpose::AUTHENTICATION { + return Err(WasmSdkError::invalid_argument(format!( + "Cannot register DPNS name with key ID {}: key has purpose {:?} but AUTHENTICATION is required.\n\ + Use a key with purpose AUTHENTICATION.", + public_key_id, key_purpose + ))); + } + + // Check security level + if key_security_level != SecurityLevel::CRITICAL + && key_security_level != SecurityLevel::HIGH + { + let available_keys: Vec = identity + .public_keys() + .iter() + .filter_map(|(key_id, k)| { + if k.purpose() != Purpose::AUTHENTICATION { + return None; + } + match k.security_level() { + SecurityLevel::CRITICAL => { + Some(format!(" Key {}: CRITICAL security level", key_id)) + } + SecurityLevel::HIGH => { + Some(format!(" Key {}: HIGH security level", key_id)) + } + _ => None, + } + }) + .collect(); + + let suggestion = if available_keys.is_empty() { + "No suitable keys found in this identity.".to_string() + } else { + format!("Try one of these keys:\n{}", available_keys.join("\n")) + }; + + return Err(WasmSdkError::invalid_argument(format!( + "Cannot register DPNS name with key ID {}: key has {:?} security level but CRITICAL or HIGH is required.\n\ + \n\ + DPNS registration requires a key with:\n\ + - Purpose: AUTHENTICATION\n\ + - Security Level: CRITICAL or HIGH (not MASTER)\n\ + \n\ + {}", + public_key_id, key_security_level, suggestion + ))); + } + + // Validate private key matches public key + if !signer.can_sign_with(&identity_public_key) { + return Err(WasmSdkError::invalid_argument(format!( + "The provided private key does not match public key ID {}.\n\ + \n\ + Public key {} details:\n\ + - Security Level: {:?}\n\ + - Purpose: {:?}\n\ + \n\ + Please verify you're using the correct private key (WIF) for this key.", + public_key_id, public_key_id, key_security_level, key_purpose + ))); + } + // Store the JS callback in a thread-local variable that we can access from the closure thread_local! { static PREORDER_CALLBACK: std::cell::RefCell> = const { std::cell::RefCell::new(None) };