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 (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 |
|---|---|---|
|
|
Connects to the network |
|
|
Derives identity keys and provides signers for write operations |
|
|
Derives platform address keys for address operations |
|
|
DIP-13 path helper and the 5 standard identity key specs |
|
|
Node convenience wrapper — connects and creates key managers from |
|
|
Network and mnemonic configuration, sourced from |
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.
/* 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<string>}
*/
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<EvoSDK>}
*/
export async function createClient(network = 'testnet') {
const factories = /** @type {Record<string, () => 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<string, DerivedKeyEntry>} 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<IdentityKeyManager>}
*/
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<number>} 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<IdentityKeyManager>}
*/
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<string, DerivedKeyEntry>} */ (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<AddressKeyManager>}
*/
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<PlatformAddressInfo | undefined>}
*/
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<PlatformAddressInfo | undefined>}
*/
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.
//
// 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 likegetAuth()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.envfile withNETWORKandPLATFORM_MNEMONIC.setupDashClient()— the main entry point for most tutorials. Connects to the network and creates key managers fromclientConfig, returning{ sdk, keyManager, addressKeyManager }.