Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/js-evo-sdk/src/dpns/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,49 @@ 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)
* Typically use key ID 1 (CRITICAL) or key ID 2 (HIGH)
* @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
*
* @example
* ```javascript
* await sdk.dpns.registerName({
* label: 'myname',
* identityId: 'xxx',
* publicKeyId: 1, // Use key 1 (CRITICAL) or 2 (HIGH), NOT 0 (MASTER)
* privateKeyWif: 'xxx'
* });
* ```
*/
async registerName(args: { label: string; identityId: string; publicKeyId: number; privateKeyWif: string; onPreorder?: Function }): Promise<any> {
const { label, identityId, publicKeyId, privateKeyWif, onPreorder } = args;

// Validate inputs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How this is possible? Typing is not allow it. Oh, when you use it form JS, not TypeScript?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was from JS. Just trying to make things easier for people like me that don't know what they're doing 🤪 You can close the PR if it is a bad approach.

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' +
'Common key IDs:\n' +
' - Key 1: CRITICAL security level\n' +
' - Key 2: HIGH security level\n' +
'Do NOT use Key 0 (MASTER security level).'
);
}

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);
}
Expand Down
87 changes: 87 additions & 0 deletions packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,91 @@ 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');
expect(error.message).to.include('Do NOT use Key 0');
}
});

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');
expect(error.message).to.include('Do NOT use Key 0');
}
});

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();
});
});
});
78 changes: 78 additions & 0 deletions packages/wasm-sdk/src/dpns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,6 +79,81 @@ 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 (usually keys 0-2).",
public_key_id, key_purpose
)));
}

// Check security level
if key_security_level != SecurityLevel::CRITICAL
&& key_security_level != SecurityLevel::HIGH
{
let available_keys: Vec<String> = identity
.public_keys()
.iter()
.filter_map(|(key_id, k)| {
if k.purpose() == Purpose::AUTHENTICATION
&& (k.security_level() == SecurityLevel::CRITICAL
|| k.security_level() == SecurityLevel::HIGH)
{
let level_name = if k.security_level() == SecurityLevel::CRITICAL {
"CRITICAL"
} else {
"HIGH"
};
Some(
String::from(" Key ")
+ &key_id.to_string()
+ ": "
+ level_name
+ " security level",
)
} else {
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<Option<js_sys::Function>> = const { std::cell::RefCell::new(None) };
Expand Down