# Setup SDK Client In this tutorial we will show how to configure the client and key managers for use in the remaining tutorials. ## Prerequisites - [General prerequisites](../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed) ## Code Save the following two modules side-by-side — `setupDashClient-core.mjs` and `setupDashClient.mjs` — in the same directory. Later Node tutorials import from `setupDashClient.mjs`; browser apps can import from `setupDashClient-core.mjs` directly. | Export | File | Purpose | | ------ | ---- | ------- | | `createClient()` | `setupDashClient-core.mjs` | Connects to the network | | `IdentityKeyManager` | `setupDashClient-core.mjs` | Derives identity keys and provides signers for write operations | | `AddressKeyManager` | `setupDashClient-core.mjs` | Derives platform address keys for address operations | | `dip13KeyPath()` / `KEY_SPECS` | `setupDashClient-core.mjs` | DIP-13 path helper and the 5 standard identity key specs | | `setupDashClient()` | `setupDashClient.mjs` | Node convenience wrapper — connects and creates key managers from `clientConfig` | | `clientConfig` | `setupDashClient.mjs` | Network and mnemonic configuration, sourced from `process.env` | :::{important} After saving, open `setupDashClient.mjs` (the wrapper) and set your mnemonic in the `clientConfig` section near the top of the file — either edit the value directly or create a `.env` file with `PLATFORM_MNEMONIC`. The core file has no configuration to edit. ::: ### Browser-safe core `setupDashClient-core.mjs` holds everything that works in any JS runtime: the SDK client factory, DIP-13 path helper, and the two key manager classes. Browser apps import from this file directly. ```{code-block} javascript :caption: setupDashClient-core.mjs :name: setupDashClient-core.mjs /* eslint-disable max-classes-per-file */ // // Browser-safe core of setupDashClient. // // This file contains the parts of setupDashClient that work in any JS runtime: // - createClient(network) // - dip13KeyPath(network, identityIndex, keyIndex) // - IdentityKeyManager // - AddressKeyManager // - KEY_SPECS // // The Node-only convenience layer (dotenv loading, process.env-driven // clientConfig, and the setupDashClient() wrapper) lives in setupDashClient.mjs // alongside this file. Both Node tutorials and browser apps import the same // IdentityKeyManager from here so there is no copy-paste drift. // import { EvoSDK, IdentityPublicKeyInCreation, IdentitySigner, KeyType, PlatformAddressSigner, PrivateKey, Purpose, SecurityLevel, wallet, } from '@dashevo/evo-sdk'; /** @typedef {import('@dashevo/evo-sdk').Identity} Identity */ /** @typedef {import('@dashevo/evo-sdk').IdentityPublicKey} IdentityPublicKey */ /** @typedef {import('@dashevo/evo-sdk').PlatformAddress} PlatformAddress */ /** @typedef {import('@dashevo/evo-sdk').PlatformAddressInfo} PlatformAddressInfo */ /** @typedef {import('@dashevo/evo-sdk').NetworkLike} NetworkLike */ /** * @typedef {Object} DerivedKeyEntry * @property {number} keyId * @property {string} privateKeyWif * @property {string} [publicKey] - Only present via createForNewIdentity() */ /** * @typedef {Object} AddressEntry * @property {PlatformAddress} address * @property {string} bech32m * @property {string} privateKeyWif * @property {string} path */ // ⚠️ Tutorial helper — holds WIFs in memory for convenience. // Do not use this pattern as-is for production key management. // --------------------------------------------------------------------------- // Browser-safe hex → bytes (replaces Buffer.from(hex, 'hex')) // --------------------------------------------------------------------------- /** * Decode a hex string to a Uint8Array. Browser-safe replacement for * Buffer.from(hex, 'hex'). Throws on odd-length or non-hex input. * * @param {string} hex * @returns {Uint8Array} */ function hexToBytes(hex) { if (typeof hex !== 'string' || hex.length % 2 !== 0) { throw new Error('hexToBytes: expected even-length hex string'); } const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i += 1) { const offset = i * 2; const chunk = hex.slice(offset, offset + 2); if (!/^[0-9A-Fa-f]{2}$/.test(chunk)) { throw new Error(`hexToBytes: invalid hex at offset ${offset}`); } out[i] = parseInt(chunk, 16); } return out; } // --------------------------------------------------------------------------- // DIP-13 key path // --------------------------------------------------------------------------- /** * Build a DIP-13 identity key derivation path. * Returns the full 7-level hardened path: * m/9'/{coin}'/5'/0'/0'/{identityIndex}'/{keyIndex}' * @param {string} network * @param {number} identityIndex * @param {number} keyIndex * @returns {Promise} */ export async function dip13KeyPath(network, identityIndex, keyIndex) { const base = network === 'testnet' ? await wallet.derivationPathDip13Testnet(5) : await wallet.derivationPathDip13Mainnet(5); return `${base.path}/0'/0'/${identityIndex}'/${keyIndex}'`; } // --------------------------------------------------------------------------- // SDK client helpers // --------------------------------------------------------------------------- /** * Create and connect an EvoSDK client for the selected network. * * @param {string} [network='testnet'] * @returns {Promise} */ export async function createClient(network = 'testnet') { const factories = /** @type {Record EvoSDK>} */ ({ testnet: () => EvoSDK.testnetTrusted(), mainnet: () => EvoSDK.mainnetTrusted(), local: () => EvoSDK.localTrusted(), }); const factory = factories[network]; if (!factory) { throw new Error( `Unknown network "${network}". Use: ${Object.keys(factories).join(', ')}`, ); } const sdk = /** @type {EvoSDK} */ (factory()); await sdk.connect(); return sdk; } // --------------------------------------------------------------------------- // IdentityKeyManager // --------------------------------------------------------------------------- /** Key specs for the 5 standard identity keys (DIP-9). */ export const KEY_SPECS = [ { keyId: 0, purpose: Purpose.AUTHENTICATION, securityLevel: SecurityLevel.MASTER, }, { keyId: 1, purpose: Purpose.AUTHENTICATION, securityLevel: SecurityLevel.HIGH, }, { keyId: 2, purpose: Purpose.AUTHENTICATION, securityLevel: SecurityLevel.CRITICAL, }, { keyId: 3, purpose: Purpose.TRANSFER, securityLevel: SecurityLevel.CRITICAL, }, { keyId: 4, purpose: Purpose.ENCRYPTION, securityLevel: SecurityLevel.MEDIUM, }, ]; /** * Manages identity keys and signing for write operations. * * Mirrors the old js-dash-sdk pattern where `setupDashClient()` hid all * wallet/signing config. Construct once, then call getAuth(), getTransfer(), * or getMaster() to get a ready-to-use { identity, identityKey, signer }. * * Keys are derived from a BIP39 mnemonic using standard DIP-9 paths * (compatible with dash-evo-tool / Dash wallets): * Key 0 = MASTER (identity updates) * Key 1 = HIGH auth (documents, names) * Key 2 = CRITICAL auth (contracts, documents, names) * Key 3 = TRANSFER (credit transfers/withdrawals) * Key 4 = ENCRYPTION MEDIUM (encrypted messaging/data) */ class IdentityKeyManager { /** * @param {EvoSDK} sdk * @param {string|null|undefined} identityId * @param {Record} keys * @param {number} identityIndex */ constructor(sdk, identityId, keys, identityIndex) { this.sdk = sdk; this.id = identityId; this.keys = keys; // { master, auth, authHigh, transfer, encryption } this.identityIndex = identityIndex ?? 0; } get identityId() { return this.id; } /** * Create an IdentityKeyManager from a BIP39 mnemonic. * Derives all standard identity keys using DIP-9 paths. * * @param {object} opts * @param {EvoSDK} opts.sdk - Connected EvoSDK instance * @param {string} [opts.identityId] - Identity ID. If omitted, auto-resolved * from the mnemonic by looking up the master key's public key hash on-chain. * @param {string} opts.mnemonic - BIP39 mnemonic * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' * @param {number} [opts.identityIndex=0] - Which identity derived from this mnemonic * @returns {Promise} */ static async create({ sdk, identityId, mnemonic, network = 'testnet', identityIndex = 0, }) { const derive = async (/** @type {number} */ keyIndex) => wallet.deriveKeyFromSeedWithPath({ mnemonic, path: await dip13KeyPath(network, identityIndex, keyIndex), network, }); const [masterKey, authHighKey, authKey, transferKey, encryptionKey] = await Promise.all([ derive(0), // MASTER derive(1), // HIGH auth derive(2), // CRITICAL auth derive(3), // TRANSFER derive(4), // ENCRYPTION MEDIUM ]); let resolvedId = identityId; if (!resolvedId) { const privateKey = PrivateKey.fromWIF(masterKey.toObject().privateKeyWif); const pubKeyHash = privateKey.getPublicKeyHash(); const identity = await sdk.identities.byPublicKeyHash(pubKeyHash); if (!identity) { throw new Error( 'No identity found for the given mnemonic (key 0 public key hash)', ); } resolvedId = identity.id.toString(); } return new IdentityKeyManager( sdk, resolvedId, { master: { keyId: 0, privateKeyWif: masterKey.toObject().privateKeyWif }, authHigh: { keyId: 1, privateKeyWif: authHighKey.toObject().privateKeyWif, }, auth: { keyId: 2, privateKeyWif: authKey.toObject().privateKeyWif }, transfer: { keyId: 3, privateKeyWif: transferKey.toObject().privateKeyWif, }, encryption: { keyId: 4, privateKeyWif: encryptionKey.toObject().privateKeyWif, }, }, identityIndex, ); } /** * Find the first unused DIP-9 identity index for a mnemonic. * Scans indices starting at 0 until no on-chain identity is found. * * @param {EvoSDK} sdk - Connected EvoSDK instance * @param {string} mnemonic - BIP39 mnemonic * @param {string} [network='testnet'] - 'testnet' or 'mainnet' * @returns {Promise} The first unused identity index */ static async findNextIndex(sdk, mnemonic, network = 'testnet') { /* eslint-disable no-await-in-loop */ for (let i = 0; ; i += 1) { const path = await dip13KeyPath(network, i, 0); const key = await wallet.deriveKeyFromSeedWithPath({ mnemonic, path, network, }); const privateKey = PrivateKey.fromWIF(key.toObject().privateKeyWif); const existing = await sdk.identities.byPublicKeyHash( privateKey.getPublicKeyHash(), ); if (!existing) return i; } /* eslint-enable no-await-in-loop */ } /** * Create an IdentityKeyManager for a new (not yet registered) identity. * Derives keys and stores public key data needed for identity creation. * If identityIndex is omitted, auto-selects the next unused index. * * @param {object} opts * @param {EvoSDK} opts.sdk - Connected EvoSDK instance * @param {string} opts.mnemonic - BIP39 mnemonic * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' * @param {number} [opts.identityIndex] - Identity index (auto-scanned if omitted) * @returns {Promise} */ static async createForNewIdentity({ sdk, mnemonic, network = 'testnet', identityIndex, }) { const idx = identityIndex ?? (await IdentityKeyManager.findNextIndex(sdk, mnemonic, network)); const derive = async (/** @type {number} */ keyIndex) => wallet.deriveKeyFromSeedWithPath({ mnemonic, path: await dip13KeyPath(network, idx, keyIndex), network, }); const derivedKeys = await Promise.all( KEY_SPECS.map((spec) => derive(spec.keyId)), ); const keys = { master: { keyId: 0, privateKeyWif: derivedKeys[0].toObject().privateKeyWif, publicKey: derivedKeys[0].toObject().publicKey, }, authHigh: { keyId: 1, privateKeyWif: derivedKeys[1].toObject().privateKeyWif, publicKey: derivedKeys[1].toObject().publicKey, }, auth: { keyId: 2, privateKeyWif: derivedKeys[2].toObject().privateKeyWif, publicKey: derivedKeys[2].toObject().publicKey, }, transfer: { keyId: 3, privateKeyWif: derivedKeys[3].toObject().privateKeyWif, publicKey: derivedKeys[3].toObject().publicKey, }, encryption: { keyId: 4, privateKeyWif: derivedKeys[4].toObject().privateKeyWif, publicKey: derivedKeys[4].toObject().publicKey, }, }; return new IdentityKeyManager(sdk, null, keys, idx); } /** * Build IdentityPublicKeyInCreation objects for all 5 standard keys. * Only works when public key data is available (via createForNewIdentity). * * @returns {IdentityPublicKeyInCreation[]} */ getKeysInCreation() { return KEY_SPECS.map((spec) => { const key = Object.values(this.keys).find((k) => k.keyId === spec.keyId); if (!key?.publicKey) { throw new Error( `Public key data not available for key ${spec.keyId}. Use createForNewIdentity().`, ); } const pubKeyData = hexToBytes(key.publicKey); return new IdentityPublicKeyInCreation({ keyId: spec.keyId, purpose: spec.purpose, securityLevel: spec.securityLevel, keyType: KeyType.ECDSA_SECP256K1, data: pubKeyData, }); }); } /** * Build an IdentitySigner loaded with all 5 key WIFs. * Useful for identity creation where all keys must sign. * * @returns {IdentitySigner} */ getFullSigner() { const signer = new IdentitySigner(); Object.values(this.keys).forEach((key) => { signer.addKeyFromWif(key.privateKeyWif); }); return signer; } /** * Fetch identity and build { identity, identityKey, signer } for a given key. * @param {string} keyName - One of: master, auth, authHigh, transfer, encryption * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getSigner(keyName) { if (!this.id) { throw new Error( 'Identity ID is not set. Use IdentityKeyManager.create() for an existing identity, ' + 'or create/register the identity first and then set the ID.', ); } const key = /** @type {Record} */ (this.keys)[ keyName ]; if (!key) { throw new Error( `Unknown key "${keyName}". Use: ${Object.keys(this.keys).join(', ')}`, ); } const identity = await this.sdk.identities.fetch(this.id); if (!identity) { throw new Error(`Identity "${this.id}" not found on-chain.`); } const identityKey = identity.getPublicKeyById(key.keyId); const signer = new IdentitySigner(); signer.addKeyFromWif(key.privateKeyWif); return { identity, identityKey, signer }; } /** * CRITICAL auth (key 2) — contracts, documents, names. * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getAuth() { return this.getSigner('auth'); } /** * HIGH auth (key 1) — documents, names. * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getAuthHigh() { return this.getSigner('authHigh'); } /** * TRANSFER — credit transfers, withdrawals. * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getTransfer() { return this.getSigner('transfer'); } /** * ENCRYPTION MEDIUM — encrypted messaging/data. * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getEncryption() { return this.getSigner('encryption'); } /** * MASTER — identity updates (add/disable keys). * @param {string[]} [additionalKeyWifs] - WIFs for new keys being added * @returns {Promise<{ identity: Identity, identityKey: IdentityPublicKey | undefined, signer: IdentitySigner }>} */ async getMaster(additionalKeyWifs) { const result = await this.getSigner('master'); if (additionalKeyWifs) { additionalKeyWifs.forEach((wif) => result.signer.addKeyFromWif(wif)); } return result; } } // --------------------------------------------------------------------------- // AddressKeyManager // --------------------------------------------------------------------------- /** * Manages platform address keys and signing for address operations. * * Parallel to IdentityKeyManager but for platform address operations. * Derives BIP44 keys from a mnemonic and provides ready-to-use * PlatformAddressSigner instances. * * Platform addresses are bech32m-encoded L2 addresses (tdash1... on testnet) * that hold credits directly, independent of identities. */ class AddressKeyManager { /** * @param {EvoSDK} sdk * @param {AddressEntry[]} addresses * @param {string} network */ constructor(sdk, addresses, network) { this.sdk = sdk; this.addresses = addresses; // [{ address, bech32m, privateKeyWif, path }] this.network = network; } /** The first derived address (index 0). */ get primaryAddress() { return this.addresses[0]; } /** * Create an AddressKeyManager from a BIP39 mnemonic. * Derives platform address keys using BIP44 paths. * * @param {object} opts * @param {EvoSDK} opts.sdk - Connected EvoSDK instance * @param {string} opts.mnemonic - BIP39 mnemonic * @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet' * @param {number} [opts.count=1] - Number of addresses to derive * @returns {Promise} */ static async create({ sdk, mnemonic, network = 'testnet', count = 1 }) { const addresses = []; /* eslint-disable no-await-in-loop */ for (let i = 0; i < count; i += 1) { const pathInfo = network === 'testnet' ? await wallet.derivationPathBip44Testnet(0, 0, i) : await wallet.derivationPathBip44Mainnet(0, 0, i); const { path } = pathInfo; const keyInfo = await wallet.deriveKeyFromSeedWithPath({ mnemonic, path, network, }); const obj = keyInfo.toObject(); const privateKey = PrivateKey.fromWIF(obj.privateKeyWif); const signer = new PlatformAddressSigner(); const platformAddress = signer.addKey(privateKey); addresses.push({ address: platformAddress, bech32m: platformAddress.toBech32m( /** @type {NetworkLike} */ (network), ), privateKeyWif: obj.privateKeyWif, path, }); } /* eslint-enable no-await-in-loop */ return new AddressKeyManager(sdk, addresses, network); } /** * Create a PlatformAddressSigner with the primary key loaded. * @returns {PlatformAddressSigner} */ getSigner() { const signer = new PlatformAddressSigner(); const privateKey = PrivateKey.fromWIF(this.primaryAddress.privateKeyWif); signer.addKey(privateKey); return signer; } /** * Create a PlatformAddressSigner with all derived keys loaded. * @returns {PlatformAddressSigner} */ getFullSigner() { const signer = new PlatformAddressSigner(); this.addresses.forEach((addr) => { const privateKey = PrivateKey.fromWIF(addr.privateKeyWif); signer.addKey(privateKey); }); return signer; } /** * Fetch current balance and nonce for the primary address. * @returns {Promise} */ async getInfo() { return this.sdk.addresses.get(this.primaryAddress.bech32m); } /** * Fetch current balance and nonce for an address by index. * @param {number} index - Address index * @returns {Promise} */ async getInfoAt(index) { const entry = this.addresses[index]; if (!entry) { throw new Error( `No derived address at index ${index} (count=${this.addresses.length})`, ); } return this.sdk.addresses.get(entry.bech32m); } } export { IdentityKeyManager, AddressKeyManager }; ``` ### Node wrapper `setupDashClient.mjs` is a thin Node-only wrapper that loads `.env`, builds `clientConfig` from `process.env`, exposes the one-call `setupDashClient()` helper, and re-exports everything from the core so Node tutorials can import `IdentityKeyManager`, `AddressKeyManager`, `clientConfig`, and friends from a single module. ```{code-block} javascript :caption: setupDashClient.mjs :name: setupDashClient.mjs // // Node-only convenience wrapper around setupDashClient-core.mjs. // // This file adds the bits that only make sense in Node tutorials: // - dotenv loading (PLATFORM_MNEMONIC / NETWORK from .env) // - clientConfig (network + mnemonic, sourced from process.env) // - setupDashClient() — the one-call wrapper used by tutorial scripts // // Everything browser-safe (createClient, IdentityKeyManager, AddressKeyManager, // dip13KeyPath, KEY_SPECS) is re-exported from setupDashClient-core.mjs so a // Node tutorial that imports from this file sees an identical API surface. // // Browser apps should import from setupDashClient-core.mjs directly. // import { wallet } from '@dashevo/evo-sdk'; import { createClient, dip13KeyPath, KEY_SPECS, IdentityKeyManager, AddressKeyManager, } from './setupDashClient-core.mjs'; // Load .env if dotenv is installed (optional — not needed for tutorials). // Top-level await requires ESM — .mjs extension ensures this. // eslint-disable-next-line import/no-extraneous-dependencies try { const { config } = await import('dotenv'); config(); } catch { /* dotenv not installed */ } /** @typedef {import('@dashevo/evo-sdk').EvoSDK} EvoSDK */ // ⚠️ Tutorial helper — holds WIFs in memory for convenience. // Do not use this pattern as-is for production key management. // ########################################################################### // # CONFIGURATION — edit these values for your environment # // ########################################################################### // Option 1: Edit the values below directly // Option 2: Create a .env file with PLATFORM_MNEMONIC and NETWORK const clientConfig = { // The network to connect to ('testnet' or 'mainnet') network: process.env.NETWORK || 'testnet', // BIP39 mnemonic for wallet operations (identity & address tutorials). // Leave as null for read-only tutorials. mnemonic: process.env.PLATFORM_MNEMONIC || null, // mnemonic: 'your twelve word mnemonic phrase goes here ...', }; // --------------------------------------------------------------------------- // setupDashClient — convenience wrapper // --------------------------------------------------------------------------- /** * @param {{requireIdentity?: boolean, identityIndex?: number}} opts * @returns {Promise<{ sdk: EvoSDK, keyManager: IdentityKeyManager | undefined, addressKeyManager: AddressKeyManager | undefined }>} */ export async function setupDashClient({ requireIdentity = true, identityIndex = undefined, } = {}) { const { network, mnemonic } = clientConfig; if (mnemonic && !(await wallet.validateMnemonic(mnemonic))) { throw new Error( 'PLATFORM_MNEMONIC is not a valid BIP39 mnemonic. ' + 'Run `node create-wallet.mjs` to generate one.', ); } const sdk = await createClient(network); let keyManager; let addressKeyManager; if (mnemonic) { addressKeyManager = await AddressKeyManager.create({ sdk, mnemonic, network, }); if (requireIdentity) { keyManager = await IdentityKeyManager.create({ sdk, mnemonic, network, identityIndex, }); } else { keyManager = await IdentityKeyManager.createForNewIdentity({ sdk, mnemonic, network, identityIndex, }); } } return { sdk, keyManager, addressKeyManager }; } // Re-export everything from the core so existing imports // (e.g. `import { IdentityKeyManager } from './setupDashClient.mjs'`) keep working. export { createClient, dip13KeyPath, KEY_SPECS, IdentityKeyManager, AddressKeyManager, clientConfig, }; ``` ## What's Happening The setup is split across two files so the same code can run under Node and in the browser without copy-paste drift. Node tutorials import from `setupDashClient.mjs` (the wrapper); browser apps import from `setupDashClient-core.mjs` directly. **`setupDashClient-core.mjs` (browser-safe)** provides the substantive pieces: - **`createClient()`** — creates a connected SDK instance for a given network (testnet, mainnet, or local). - **`IdentityKeyManager`** — derives 5 standard identity keys from your mnemonic using DIP-9 key paths. Each key serves a specific purpose (authentication, transfers, encryption). Call methods like `getAuth()` to get `{ identity, identityKey, signer }` — everything the SDK needs to sign and submit a transaction. - **`AddressKeyManager`** — derives platform address keys from your mnemonic using BIP44 paths. These addresses hold credits on the L2 platform layer and are used for identity creation, top-ups, and credit transfers between addresses. - **`dip13KeyPath()`** / **`KEY_SPECS`** — the DIP-13 path helper and the 5-entry spec table used to build identity public keys. **`setupDashClient.mjs` (Node wrapper)** adds the Node-only conveniences and re-exports everything from core: - **`clientConfig`** — your network and mnemonic, defined once. Set values directly or use a `.env` file with `NETWORK` and `PLATFORM_MNEMONIC`. - **`setupDashClient()`** — the main entry point for most tutorials. Connects to the network and creates key managers from `clientConfig`, returning `{ sdk, keyManager, addressKeyManager }`.