Skip to content

Commit 6c0a664

Browse files
committed
feat(dpns): add early validation and helpful error messages for key selection
Add comprehensive validation before signing DPNS registration state transitions to catch common errors early and provide clear, actionable error messages to developers. Fixes issue where users accidentally use MASTER key (ID 0) which is not allowed for DPNS registration, resulting in cryptic error messages only after signing attempt.
1 parent f49390f commit 6c0a664

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

packages/js-evo-sdk/src/dpns/facade.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,49 @@ export class DpnsFacade {
3333
return w.dpnsResolveName(name);
3434
}
3535

36+
/**
37+
* Register a DPNS username
38+
*
39+
* @param args.label - The username label (without .dash suffix)
40+
* @param args.identityId - The identity ID that will own the name
41+
* @param args.publicKeyId - The identity key ID to use for signing
42+
* IMPORTANT: Must be a key with:
43+
* - Purpose: AUTHENTICATION (not TRANSFER)
44+
* - Security Level: CRITICAL or HIGH (NOT MASTER)
45+
* Typically use key ID 1 (CRITICAL) or key ID 2 (HIGH)
46+
* @param args.privateKeyWif - The private key in WIF format matching publicKeyId
47+
* @param args.onPreorder - Optional callback called after preorder succeeds
48+
* @returns Registration result with document IDs
49+
*
50+
* @example
51+
* ```javascript
52+
* await sdk.dpns.registerName({
53+
* label: 'myname',
54+
* identityId: 'xxx',
55+
* publicKeyId: 1, // Use key 1 (CRITICAL) or 2 (HIGH), NOT 0 (MASTER)
56+
* privateKeyWif: 'xxx'
57+
* });
58+
* ```
59+
*/
3660
async registerName(args: { label: string; identityId: string; publicKeyId: number; privateKeyWif: string; onPreorder?: Function }): Promise<any> {
3761
const { label, identityId, publicKeyId, privateKeyWif, onPreorder } = args;
62+
63+
// Validate inputs
64+
if (publicKeyId === undefined || publicKeyId === null) {
65+
throw new Error(
66+
'publicKeyId is required for DPNS registration.\n' +
67+
'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n' +
68+
'Common key IDs:\n' +
69+
' - Key 1: CRITICAL security level\n' +
70+
' - Key 2: HIGH security level\n' +
71+
'Do NOT use Key 0 (MASTER security level).'
72+
);
73+
}
74+
75+
if (typeof publicKeyId !== 'number' || publicKeyId < 0) {
76+
throw new Error(`publicKeyId must be a non-negative number, got: ${publicKeyId}`);
77+
}
78+
3879
const w = await this.sdk.getWasmSdkConnected();
3980
return w.dpnsRegisterName(label, identityId, publicKeyId, privateKeyWif, onPreorder ?? null);
4081
}

packages/wasm-sdk/src/dpns.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use crate::error::WasmSdkError;
22
use crate::sdk::WasmSdk;
33
use dash_sdk::dpp::document::{Document, DocumentV0Getters};
44
use dash_sdk::dpp::identity::accessors::IdentityGettersV0;
5+
use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
6+
use dash_sdk::dpp::identity::signer::Signer;
7+
use dash_sdk::dpp::identity::{Purpose, SecurityLevel};
58
use dash_sdk::dpp::prelude::Identifier;
69
use dash_sdk::platform::dpns_usernames::{
710
convert_to_homograph_safe_chars, is_contested_username, is_valid_username,
@@ -76,6 +79,81 @@ impl WasmSdk {
7679
})?
7780
.clone();
7881

82+
// Validate the key meets DPNS requirements
83+
let key_purpose = identity_public_key.purpose();
84+
let key_security_level = identity_public_key.security_level();
85+
86+
// Check purpose
87+
if key_purpose != Purpose::AUTHENTICATION {
88+
return Err(WasmSdkError::invalid_argument(format!(
89+
"Cannot register DPNS name with key ID {}: key has purpose {:?} but AUTHENTICATION is required.\n\
90+
Use a key with purpose AUTHENTICATION (usually keys 0-2).",
91+
public_key_id, key_purpose
92+
)));
93+
}
94+
95+
// Check security level
96+
if key_security_level != SecurityLevel::CRITICAL
97+
&& key_security_level != SecurityLevel::HIGH
98+
{
99+
let available_keys: Vec<String> = identity
100+
.public_keys()
101+
.iter()
102+
.filter_map(|(key_id, k)| {
103+
if k.purpose() == Purpose::AUTHENTICATION
104+
&& (k.security_level() == SecurityLevel::CRITICAL
105+
|| k.security_level() == SecurityLevel::HIGH)
106+
{
107+
let level_name = if k.security_level() == SecurityLevel::CRITICAL {
108+
"CRITICAL"
109+
} else {
110+
"HIGH"
111+
};
112+
Some(
113+
String::from(" Key ")
114+
+ &key_id.to_string()
115+
+ ": "
116+
+ level_name
117+
+ " security level",
118+
)
119+
} else {
120+
None
121+
}
122+
})
123+
.collect();
124+
125+
let suggestion = if available_keys.is_empty() {
126+
"No suitable keys found in this identity.".to_string()
127+
} else {
128+
format!("Try one of these keys:\n{}", available_keys.join("\n"))
129+
};
130+
131+
return Err(WasmSdkError::invalid_argument(format!(
132+
"Cannot register DPNS name with key ID {}: key has {:?} security level but CRITICAL or HIGH is required.\n\
133+
\n\
134+
DPNS registration requires a key with:\n\
135+
- Purpose: AUTHENTICATION\n\
136+
- Security Level: CRITICAL or HIGH (not MASTER)\n\
137+
\n\
138+
{}",
139+
public_key_id, key_security_level, suggestion
140+
)));
141+
}
142+
143+
// Validate private key matches public key
144+
if !signer.can_sign_with(&identity_public_key) {
145+
return Err(WasmSdkError::invalid_argument(format!(
146+
"The provided private key does not match public key ID {}.\n\
147+
\n\
148+
Public key {} details:\n\
149+
- Security Level: {:?}\n\
150+
- Purpose: {:?}\n\
151+
\n\
152+
Please verify you're using the correct private key (WIF) for this key.",
153+
public_key_id, public_key_id, key_security_level, key_purpose
154+
)));
155+
}
156+
79157
// Store the JS callback in a thread-local variable that we can access from the closure
80158
thread_local! {
81159
static PREORDER_CALLBACK: std::cell::RefCell<Option<js_sys::Function>> = const { std::cell::RefCell::new(None) };

0 commit comments

Comments
 (0)