Setup SDK Client#

In this tutorial we will show how to configure the client and key managers for use in the remaining tutorials.

Prerequisites#

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.

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<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.

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 }.