From 5ec85f3e9d08176484c56a757f7d90a12fb0de98 Mon Sep 17 00:00:00 2001
From: Quantum Explorer
Date: Thu, 2 Apr 2026 13:25:10 +0300
Subject: [PATCH 1/5] docs(book): add Evo SDK chapters
Add six new chapters covering the JavaScript/TypeScript Evo SDK,
filling gaps not covered by the external API reference at
https://dashpay.github.io/evo-sdk-website/docs.html:
- Overview: architecture diagram, facades table, how WASM/gRPC works
- Getting Started: install, quick start, connection options, logging
- Trusted Mode and Proofs: what trusted mode is, when to use it,
proof variants (WithProof, Unproved)
- State Transitions: identity/document/token/DPNS write operations,
nonce management, signing flow
- Wallet Utilities: mnemonic generation, key derivation (BIP-44,
DIP-9, DIP-13), key pairs, address validation, message signing
- Networks and Environments: testnet/mainnet/local, custom addresses,
browser vs Node.js differences
Rename existing "SDK" section to "Rust SDK" for clarity.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
book/src/SUMMARY.md | 11 +-
book/src/evo-sdk/getting-started.md | 106 ++++++++++++
book/src/evo-sdk/networks-and-environments.md | 74 ++++++++
book/src/evo-sdk/overview.md | 74 ++++++++
book/src/evo-sdk/state-transitions.md | 158 ++++++++++++++++++
book/src/evo-sdk/trusted-mode.md | 66 ++++++++
book/src/evo-sdk/wallet-utilities.md | 124 ++++++++++++++
7 files changed, 612 insertions(+), 1 deletion(-)
create mode 100644 book/src/evo-sdk/getting-started.md
create mode 100644 book/src/evo-sdk/networks-and-environments.md
create mode 100644 book/src/evo-sdk/overview.md
create mode 100644 book/src/evo-sdk/state-transitions.md
create mode 100644 book/src/evo-sdk/trusted-mode.md
create mode 100644 book/src/evo-sdk/wallet-utilities.md
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index f8859d679ae..4539eea3221 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -66,7 +66,16 @@
- [Strategy Tests](testing/strategy-tests.md)
- [Test Configuration](testing/test-configuration.md)
-# SDK
+# Evo SDK (JavaScript/TypeScript)
+
+- [Overview](evo-sdk/overview.md)
+- [Getting Started](evo-sdk/getting-started.md)
+- [Trusted Mode and Proofs](evo-sdk/trusted-mode.md)
+- [State Transitions](evo-sdk/state-transitions.md)
+- [Wallet Utilities](evo-sdk/wallet-utilities.md)
+- [Networks and Environments](evo-sdk/networks-and-environments.md)
+
+# Rust SDK
- [Builder Pattern](sdk/builder-pattern.md)
- [Fetch Traits](sdk/fetch-traits.md)
diff --git a/book/src/evo-sdk/getting-started.md b/book/src/evo-sdk/getting-started.md
new file mode 100644
index 00000000000..b674bc81aa9
--- /dev/null
+++ b/book/src/evo-sdk/getting-started.md
@@ -0,0 +1,106 @@
+# Getting Started
+
+## Installation
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+The package is **ESM-only** (`"type": "module"`). In CommonJS projects use a
+dynamic `import()`:
+
+```js
+const { EvoSDK } = await import('@dashevo/evo-sdk');
+```
+
+Requirements: Node.js ≥ 18.18 or any modern browser with WebAssembly support.
+
+## Quick start
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+// Create a trusted testnet instance and connect
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+// Query the current epoch
+const epoch = await sdk.epoch.current();
+console.log('Current epoch:', epoch.index);
+
+// Fetch an identity by its base58 ID
+const identity = await sdk.identities.fetch('4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF');
+console.log('Balance:', identity?.getBalance());
+```
+
+## Connecting
+
+The SDK must be connected before making any queries. Calling `connect()` is
+asynchronous because it initialises the WASM module and, in trusted mode,
+pre-fetches quorum public keys.
+
+```typescript
+const sdk = new EvoSDK({ network: 'testnet', trusted: true });
+await sdk.connect();
+```
+
+After connecting, all facade properties (`sdk.identities`, `sdk.documents`,
+etc.) are ready to use. Calling `connect()` more than once is a no-op.
+
+### Factory helpers
+
+| Helper | Equivalent |
+|--------|-----------|
+| `EvoSDK.testnet()` | `new EvoSDK({ network: 'testnet' })` |
+| `EvoSDK.mainnet()` | `new EvoSDK({ network: 'mainnet' })` |
+| `EvoSDK.testnetTrusted()` | `new EvoSDK({ network: 'testnet', trusted: true })` |
+| `EvoSDK.mainnetTrusted()` | `new EvoSDK({ network: 'mainnet', trusted: true })` |
+| `EvoSDK.local()` | `new EvoSDK({ network: 'local' })` |
+| `EvoSDK.localTrusted()` | `new EvoSDK({ network: 'local', trusted: true })` |
+
+### Custom addresses
+
+To connect to specific masternodes (useful for testing or private networks):
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ ['https://52.12.176.90:1443'],
+ 'testnet',
+);
+await sdk.connect();
+```
+
+## Connection options
+
+All factory helpers and the constructor accept an optional `ConnectionOptions`
+object:
+
+```typescript
+const sdk = EvoSDK.testnetTrusted({
+ settings: {
+ connectTimeoutMs: 5000,
+ timeoutMs: 10000,
+ retries: 3,
+ banFailedAddress: true,
+ },
+ logs: 'info', // 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
+ proofs: true, // request proofs with every query
+ version: 8, // pin a specific protocol version
+});
+```
+
+### Logging
+
+The SDK delegates logging to the underlying Rust/WASM layer. Pass a simple
+level string or a full `EnvFilter` directive:
+
+```typescript
+// Simple level
+const sdk = EvoSDK.testnetTrusted({ logs: 'debug' });
+
+// Granular filter
+const sdk = EvoSDK.testnetTrusted({ logs: 'wasm_sdk=debug,rs_dapi_client=warn' });
+
+// Change level at runtime (static, affects all instances)
+await EvoSDK.setLogLevel('trace');
+```
diff --git a/book/src/evo-sdk/networks-and-environments.md b/book/src/evo-sdk/networks-and-environments.md
new file mode 100644
index 00000000000..964aa714f06
--- /dev/null
+++ b/book/src/evo-sdk/networks-and-environments.md
@@ -0,0 +1,74 @@
+# Networks and Environments
+
+The Evo SDK supports three built-in network configurations plus custom
+addresses for private or development networks.
+
+## Built-in networks
+
+| Network | Factory | DAPI discovery | Use case |
+|---------|---------|---------------|----------|
+| **Testnet** | `EvoSDK.testnetTrusted()` | Automatic via seed nodes | Development and testing |
+| **Mainnet** | `EvoSDK.mainnetTrusted()` | Automatic via seed nodes | Production applications |
+| **Local** | `EvoSDK.localTrusted()` | `127.0.0.1:1443` | Docker-based local development |
+
+For each network, the SDK discovers DAPI endpoints from seed nodes and rotates
+between them automatically. Failed nodes are temporarily banned so the SDK
+retries against healthy nodes.
+
+## Local development with Docker
+
+When running a local Platform network via
+[dashmate](https://github.com/dashpay/platform/tree/master/packages/dashmate),
+use the `local` network:
+
+```typescript
+const sdk = EvoSDK.localTrusted();
+await sdk.connect();
+```
+
+This connects to `https://127.0.0.1:1443` by default. If your local setup uses
+different ports, use custom addresses:
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ ['https://127.0.0.1:2443'],
+ 'local',
+);
+await sdk.connect();
+```
+
+## Custom masternode addresses
+
+For private devnets, specific nodes, or debugging:
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ [
+ 'https://52.12.176.90:1443',
+ 'https://34.217.100.50:1443',
+ ],
+ 'testnet',
+);
+await sdk.connect();
+```
+
+When custom addresses are provided, the SDK does not perform automatic node
+discovery — it uses only the addresses you supply.
+
+## Browser vs Node.js
+
+The SDK works identically in both environments. The underlying WASM module
+handles platform differences transparently.
+
+**Node.js considerations:**
+
+- Requires Node.js ≥ 18.18 (for WebAssembly and `fetch` support)
+- ESM-only package — use `import`, not `require`
+- No additional polyfills needed
+
+**Browser considerations:**
+
+- Works in any browser with WebAssembly support (all modern browsers)
+- The WASM module is loaded asynchronously on first `connect()` call
+- Total bundle size includes the compiled Rust SDK (~2-4 MB gzipped)
+- gRPC calls use `grpc-web` over HTTPS, compatible with standard CORS
diff --git a/book/src/evo-sdk/overview.md b/book/src/evo-sdk/overview.md
new file mode 100644
index 00000000000..81fc7afcf52
--- /dev/null
+++ b/book/src/evo-sdk/overview.md
@@ -0,0 +1,74 @@
+# Evo SDK Overview
+
+The **Evo SDK** (`@dashevo/evo-sdk`) is the primary JavaScript/TypeScript SDK
+for building applications on Dash Platform. It provides a high-level,
+strongly-typed facade over the WebAssembly-based Rust SDK, working in both
+Node.js (≥ 18.18) and modern browsers.
+
+> **API reference**: For detailed per-method documentation with interactive
+> examples, see the [Evo SDK Docs](https://dashpay.github.io/evo-sdk-website/docs.html).
+
+## How it works
+
+```
+┌──────────────────┐
+│ Your TypeScript │
+│ Application │
+└────────┬─────────┘
+ │ EvoSDK facades (identities, documents, tokens, …)
+┌────────▼─────────┐
+│ @dashevo/ │
+│ evo-sdk │ TypeScript wrapper layer
+└────────┬─────────┘
+ │ calls into compiled WASM module
+┌────────▼─────────┐
+│ @dashevo/ │
+│ wasm-sdk │ Rust SDK compiled to WebAssembly
+└────────┬─────────┘
+ │ gRPC over HTTPS
+┌────────▼─────────┐
+│ DAPI nodes │ Dash Platform's decentralized API
+└──────────────────┘
+```
+
+The Evo SDK does **not** use JSON-RPC or REST. Every request is a gRPC call to
+one of the Platform's DAPI nodes. Responses include cryptographic proofs that
+the SDK verifies against the platform state root, so you do not need to trust
+any single node.
+
+## Facades
+
+The SDK organises its API into domain-specific facades, each accessible as a
+property on the `EvoSDK` instance:
+
+| Facade | Description |
+|--------|-------------|
+| `sdk.identities` | Fetch, create, update, and top up identities |
+| `sdk.contracts` | Fetch, publish, and update data contracts |
+| `sdk.documents` | Query, create, replace, delete, and transfer documents |
+| `sdk.tokens` | Mint, burn, transfer, freeze tokens and query balances |
+| `sdk.dpns` | Register and resolve Dash Platform names |
+| `sdk.addresses` | Query balances, transfer credits, withdraw to L1 |
+| `sdk.epoch` | Query epoch information and evonode proposed blocks |
+| `sdk.protocol` | Protocol version upgrade state and voting |
+| `sdk.stateTransitions` | Broadcast and wait for state transitions |
+| `sdk.system` | System status, quorum info, and total credits |
+| `sdk.group` | Group membership, actions, and contested resources |
+| `sdk.voting` | Contested resource vote states and polls |
+
+A standalone `wallet` namespace is also exported for mnemonic generation, key
+derivation, address validation, and message signing — see the
+[Wallet Utilities](wallet-utilities.md) chapter.
+
+## What it covers
+
+The SDK supports the full set of Platform operations:
+
+- **Queries** (read-only): fetch identities, contracts, documents, token
+ balances, DPNS names, epoch info, vote states, and more. Every query can
+ return a cryptographic proof.
+- **State transitions** (writes): create identities, deploy contracts, manage
+ documents and tokens, register names, cast votes, and transfer credits.
+
+See the [API reference](https://dashpay.github.io/evo-sdk-website/docs.html) for the
+complete list of operations with interactive examples.
diff --git a/book/src/evo-sdk/state-transitions.md b/book/src/evo-sdk/state-transitions.md
new file mode 100644
index 00000000000..4526d7085db
--- /dev/null
+++ b/book/src/evo-sdk/state-transitions.md
@@ -0,0 +1,158 @@
+# State Transitions
+
+State transitions are the write operations of Dash Platform. Unlike queries
+(which are free and instant), state transitions modify on-chain state, cost
+credits, and must be signed with a private key.
+
+> **API reference**: For the full list of state transition methods with
+> parameters and examples, see the
+> [State Transitions section](https://dashpay.github.io/evo-sdk-website/docs.html)
+> of the Evo SDK Docs.
+
+## How state transitions work
+
+1. **Build** — The SDK constructs the transition (e.g., "create identity",
+ "register name") from the parameters you provide.
+2. **Sign** — You provide a private key (WIF format) and the SDK signs the
+ transition.
+3. **Broadcast** — The signed transition is sent to a DAPI node, which
+ propagates it to the Platform chain.
+4. **Wait** — The SDK waits for the transition to be included in a block and
+ returns the result.
+
+## Identity operations
+
+### Create an identity
+
+```typescript
+// Generate keys for the new identity
+const keyPair = await wallet.generateKeyPair('testnet');
+
+const identity = await sdk.identities.create({
+ privateKeyWif: fundingKeyWif, // key with Dash balance for the asset lock
+ identityPublicKeys: [{
+ type: 0, // ECDSA_SECP256K1
+ purpose: 0, // AUTHENTICATION
+ securityLevel: 0, // MASTER
+ publicKeyHex: keyPair.publicKeyHex,
+ }],
+});
+```
+
+### Top up an identity
+
+```typescript
+await sdk.identities.topUp({
+ identityId: 'BxPVr5...',
+ amount: 100000, // credits (1 credit = 1000 duffs)
+ privateKeyWif: fundingKeyWif,
+});
+```
+
+### Transfer credits between identities
+
+```typescript
+await sdk.identities.creditTransfer({
+ identityId: senderIdentityId,
+ recipientId: recipientIdentityId,
+ amount: 50000,
+ privateKeyWif: senderAuthKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(senderIdentityId),
+});
+```
+
+## Document operations
+
+### Create a document
+
+```typescript
+await sdk.documents.create({
+ contractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
+ documentType: 'domain',
+ document: {
+ label: 'my-username',
+ normalizedLabel: 'my-username',
+ normalizedParentDomainName: 'dash',
+ records: { identity: identityId },
+ subdomainRules: { allowSubdomains: false },
+ },
+ identityId,
+ privateKeyWif: authKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(identityId, dpnsContractId),
+});
+```
+
+### Replace, delete, transfer
+
+The `sdk.documents` facade also provides `replace()`, `delete()`,
+`transfer()`, `purchase()`, and `setPrice()` methods. See the
+[API reference](https://dashpay.github.io/evo-sdk-website/docs.html) for
+parameters.
+
+## Token operations
+
+```typescript
+// Mint tokens (requires minting authority)
+await sdk.tokens.mint({
+ tokenId: '...',
+ amount: 1000,
+ recipientId: '...',
+ identityId: minterIdentityId,
+ privateKeyWif: minterKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(minterIdentityId),
+});
+
+// Transfer tokens
+await sdk.tokens.transfer({
+ tokenId: '...',
+ amount: 100,
+ recipientId: '...',
+ identityId: senderIdentityId,
+ privateKeyWif: senderKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(senderIdentityId),
+});
+```
+
+## DPNS name registration
+
+```typescript
+await sdk.dpns.register({
+ name: 'alice',
+ identityId,
+ privateKeyWif: authKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(identityId, dpnsContractId),
+});
+```
+
+## Waiting for results
+
+The `sdk.stateTransitions` facade provides low-level control:
+
+```typescript
+// Broadcast a raw state transition and wait for confirmation
+const result = await sdk.stateTransitions.waitForResult(stateTransitionHash);
+```
+
+## Nonces
+
+Every state transition requires a **nonce** to prevent replay attacks. There
+are two types:
+
+- **Identity nonce** — incremented per identity for identity-level transitions
+ (top-ups, credit transfers, token operations)
+- **Contract nonce** — incremented per identity-contract pair for document and
+ contract transitions
+
+```typescript
+const identityNonce = await sdk.identities.nonce(identityId);
+const contractNonce = await sdk.identities.contractNonce(identityId, contractId);
+```
+
+Always fetch the nonce immediately before broadcasting. If another transition
+lands between your fetch and broadcast, the nonce will be stale and the
+transition will be rejected.
diff --git a/book/src/evo-sdk/trusted-mode.md b/book/src/evo-sdk/trusted-mode.md
new file mode 100644
index 00000000000..998d7517c60
--- /dev/null
+++ b/book/src/evo-sdk/trusted-mode.md
@@ -0,0 +1,66 @@
+# Trusted Mode and Proof Verification
+
+## The problem
+
+Every Platform query response includes a cryptographic proof — a signed hash
+from the current validator quorum that attests the response matches the
+platform state tree. To verify these proofs the SDK needs the **quorum public
+keys** for the active validator set.
+
+On a full node you can look up quorum keys directly from the Core chain. In a
+browser or lightweight environment you cannot. Trusted mode solves this.
+
+## How trusted mode works
+
+When you create an SDK with `trusted: true`, the `connect()` call does an extra
+step before returning: it fetches the current quorum public keys from a
+well-known HTTPS endpoint and caches them in memory.
+
+```typescript
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect(); // fetches quorum keys, then connects to DAPI
+```
+
+The trust model:
+
+- You trust the HTTPS endpoint (operated by Dash Core Group) to return correct
+ quorum public keys.
+- Once the keys are cached, every subsequent query response is verified against
+ them — you do **not** trust individual DAPI nodes for the data itself.
+
+This is a pragmatic trade-off: you trust one endpoint for the validator set,
+but verify all actual data cryptographically.
+
+## When to use trusted mode
+
+| Scenario | Trusted mode? | Why |
+|----------|:---:|-----|
+| Browser app | Yes | No access to Core chain |
+| Node.js script | Yes | Simplest setup |
+| Server with Core RPC | Optional | Can fetch quorum keys from your own node |
+| Local Docker setup | Yes | Use `EvoSDK.localTrusted()` |
+
+If you do not use trusted mode, the SDK still works but cannot verify proofs.
+Queries return data but you are trusting the responding DAPI node.
+
+## Proofs in responses
+
+By default, the SDK verifies proofs internally and returns just the data. To
+inspect the proof metadata yourself, use the `WithProof` variants:
+
+```typescript
+// Standard — proof verified internally, returns data only
+const identity = await sdk.identities.fetch(id);
+
+// With proof — returns both data and proof metadata
+const { data, proof, metadata } = await sdk.identities.fetchWithProof(id);
+console.log('Block height:', metadata.height);
+console.log('Core chain locked height:', metadata.coreChainLockedHeight);
+```
+
+Some methods also offer an `Unproved` variant that skips proof verification
+entirely, useful when you trust the node or want faster responses:
+
+```typescript
+const identity = await sdk.identities.fetchUnproved(id);
+```
diff --git a/book/src/evo-sdk/wallet-utilities.md b/book/src/evo-sdk/wallet-utilities.md
new file mode 100644
index 00000000000..b30ef759563
--- /dev/null
+++ b/book/src/evo-sdk/wallet-utilities.md
@@ -0,0 +1,124 @@
+# Wallet Utilities
+
+The Evo SDK exports a standalone `wallet` namespace with offline cryptographic
+utilities. These functions do **not** require a connected SDK instance — they
+initialise the WASM module on first call and work independently.
+
+```typescript
+import { wallet } from '@dashevo/evo-sdk';
+```
+
+## Mnemonic management
+
+```typescript
+// Generate a new 12-word mnemonic
+const mnemonic = await wallet.generateMnemonic();
+// "abandon ability able about above absent ..."
+
+// Validate an existing mnemonic
+const valid = await wallet.validateMnemonic(mnemonic);
+
+// Convert to seed bytes (with optional passphrase)
+const seed = await wallet.mnemonicToSeed(mnemonic, 'optional-passphrase');
+```
+
+## Key derivation
+
+### From seed phrase
+
+```typescript
+const keyInfo = await wallet.deriveKeyFromSeedPhrase({
+ mnemonic,
+ network: 'testnet',
+ derivationPath: "m/44'/1'/0'/0/0",
+});
+// keyInfo.privateKeyWif, keyInfo.publicKeyHex, keyInfo.address
+```
+
+### From seed with path
+
+```typescript
+const seed = await wallet.mnemonicToSeed(mnemonic);
+const key = await wallet.deriveKeyFromSeedWithPath({
+ seed,
+ network: 'testnet',
+ path: "m/44'/1'/0'/0/0",
+});
+```
+
+### Standard derivation paths
+
+The SDK provides helpers for Dash-specific derivation paths:
+
+```typescript
+// BIP-44 paths
+const bip44 = await wallet.derivationPathBip44Testnet(0, 0, 0);
+// "m/44'/1'/0'/0/0"
+
+// DIP-9 Platform paths (identity authentication keys)
+const dip9 = await wallet.derivationPathDip9Testnet(0, 0, 0);
+
+// DIP-13 DashPay paths (contact encryption keys)
+const dip13 = await wallet.derivationPathDip13Testnet(0);
+```
+
+### Extended public key operations
+
+```typescript
+// Convert xprv to xpub
+const xpub = await wallet.xprvToXpub(xprv);
+
+// Derive child public key
+const childPub = await wallet.deriveChildPublicKey(xpub, 0, false);
+```
+
+## Key pair generation
+
+```typescript
+// Generate a random key pair
+const keyPair = await wallet.generateKeyPair('testnet');
+// keyPair.privateKeyWif, keyPair.publicKeyHex, keyPair.address
+
+// Generate multiple key pairs
+const pairs = await wallet.generateKeyPairs('testnet', 5);
+
+// Import from WIF
+const imported = await wallet.keyPairFromWif('cPrivateKeyWif...');
+
+// Import from hex
+const fromHex = await wallet.keyPairFromHex('abcd1234...', 'testnet');
+```
+
+## Address utilities
+
+```typescript
+// Derive address from public key
+const address = await wallet.pubkeyToAddress(pubkeyHex, 'testnet');
+
+// Validate an address for a network
+const ok = await wallet.validateAddress('yWhatever...', 'testnet');
+```
+
+## Message signing
+
+```typescript
+const signature = await wallet.signMessage(
+ 'Hello Dash Platform',
+ privateKeyWif,
+);
+```
+
+## DashPay contact keys
+
+For DashPay encrypted messaging, derive contact-specific keys:
+
+```typescript
+const contactKey = await wallet.deriveDashpayContactKey({
+ mnemonic,
+ network: 'testnet',
+ senderIdentityId: '...',
+ recipientIdentityId: '...',
+ account: 0,
+ index: 0,
+});
+```
From 14a63d3150826e5d707daad4eda2d1a1a56a0071 Mon Sep 17 00:00:00 2001
From: Quantum Explorer
Date: Thu, 2 Apr 2026 13:34:01 +0300
Subject: [PATCH 2/5] docs(book): add Evo SDK tutorials section
Three step-by-step tutorials demonstrating real-world applications:
- Car Sales Management: data contract design, document CRUD,
querying with filters, reviews system
- Basic Token: token contract deployment, minting, transferring,
balance queries, burning, full lifecycle
- Card Game with Tokens: combining documents (cards, decks, matches)
with an in-game token economy, card packs, player trading,
match recording, and leaderboards
Co-Authored-By: Claude Opus 4.6 (1M context)
---
book/src/SUMMARY.md | 4 +
book/src/evo-sdk/tutorials/basic-token.md | 237 +++++++++++++
book/src/evo-sdk/tutorials/car-sales.md | 223 ++++++++++++
book/src/evo-sdk/tutorials/card-game.md | 396 ++++++++++++++++++++++
4 files changed, 860 insertions(+)
create mode 100644 book/src/evo-sdk/tutorials/basic-token.md
create mode 100644 book/src/evo-sdk/tutorials/car-sales.md
create mode 100644 book/src/evo-sdk/tutorials/card-game.md
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 4539eea3221..4c68cd0018f 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -74,6 +74,10 @@
- [State Transitions](evo-sdk/state-transitions.md)
- [Wallet Utilities](evo-sdk/wallet-utilities.md)
- [Networks and Environments](evo-sdk/networks-and-environments.md)
+- [Tutorials]()
+ - [Car Sales Management](evo-sdk/tutorials/car-sales.md)
+ - [Creating a Basic Token](evo-sdk/tutorials/basic-token.md)
+ - [Card Game with Tokens](evo-sdk/tutorials/card-game.md)
# Rust SDK
diff --git a/book/src/evo-sdk/tutorials/basic-token.md b/book/src/evo-sdk/tutorials/basic-token.md
new file mode 100644
index 00000000000..67a7a9bd9c0
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/basic-token.md
@@ -0,0 +1,237 @@
+# Tutorial: Creating a Basic Token
+
+Create a fungible token on Dash Platform with minting, transferring, and
+balance queries. This tutorial walks through the full lifecycle from contract
+deployment to token operations.
+
+## What you will learn
+
+- Defining a data contract with a token configuration
+- Minting tokens to an identity
+- Transferring tokens between identities
+- Querying balances and supply
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity with enough credits to deploy a contract and
+perform token operations.
+
+## Step 1: Define the token contract
+
+A token is defined as part of a data contract. The contract schema includes a
+`tokens` section alongside the usual document schemas.
+
+```typescript
+import { EvoSDK, wallet } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const identityId = 'YOUR_IDENTITY_ID';
+const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+const signingKeyIndex = 0;
+
+// Define a contract with a token
+const contractSchema = {
+ // Document types (optional — a token-only contract can have none)
+ tokenMetadata: {
+ type: 'object',
+ properties: {
+ tokenName: { type: 'string', maxLength: 64 },
+ description: { type: 'string', maxLength: 256 },
+ },
+ additionalProperties: false,
+ },
+};
+
+// Token configuration is passed separately when publishing
+const tokenConfig = {
+ // Position 0 = first token in this contract
+ conventions: {
+ localizations: {
+ en: {
+ shouldCapitalize: true,
+ singularForm: 'CoffeeCoin',
+ pluralForm: 'CoffeeCoins',
+ },
+ },
+ decimals: 2,
+ },
+ // The contract owner can mint manually
+ manualMinting: {
+ rules: {
+ // Allow the contract owner to mint
+ type: 'ownerOnly',
+ },
+ },
+ // The contract owner can burn their own tokens
+ manualBurning: {
+ rules: {
+ type: 'ownerOnly',
+ },
+ },
+ // Maximum supply (optional)
+ maxSupply: 1_000_000_00, // 1,000,000.00 with 2 decimals
+};
+```
+
+## Step 2: Publish the contract
+
+```typescript
+const contract = await sdk.contracts.publish({
+ identityId,
+ documentSchemas: contractSchema,
+ tokens: [tokenConfig],
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+const contractId = contract.getId().toString();
+console.log('Contract published:', contractId);
+
+// Calculate the token ID (derived from contract ID + position)
+const tokenId = await sdk.tokens.calculateId(contractId, 0);
+console.log('Token ID:', tokenId);
+```
+
+## Step 3: Mint tokens
+
+The contract owner can mint tokens to any identity:
+
+```typescript
+// Mint 10,000.00 CoffeeCoins to yourself
+await sdk.tokens.mint({
+ tokenId,
+ amount: 10_000_00, // 10,000.00 (2 decimal places)
+ recipientId: identityId, // mint to yourself
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Minted 10,000 CoffeeCoins');
+```
+
+### Mint to another identity
+
+```typescript
+await sdk.tokens.mint({
+ tokenId,
+ amount: 500_00, // 500.00 CoffeeCoins
+ recipientId: 'RECIPIENT_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+```
+
+## Step 4: Check balances
+
+```typescript
+// Check your own balance
+const myBalances = await sdk.tokens.identityBalances(identityId, [tokenId]);
+const myBalance = myBalances.get(tokenId) ?? 0n;
+console.log('My balance:', Number(myBalance) / 100, 'CoffeeCoins');
+
+// Check multiple identities at once
+const balances = await sdk.tokens.balances(
+ [identityId, 'OTHER_IDENTITY_ID'],
+ tokenId,
+);
+
+for (const [id, balance] of balances) {
+ console.log(`${id}: ${Number(balance) / 100} CoffeeCoins`);
+}
+```
+
+### Check total supply
+
+```typescript
+const supply = await sdk.tokens.totalSupply(tokenId);
+if (supply) {
+ console.log('Total supply:', Number(supply.totalSupply) / 100, 'CoffeeCoins');
+}
+```
+
+## Step 5: Transfer tokens
+
+```typescript
+await sdk.tokens.transfer({
+ tokenId,
+ amount: 25_00, // 25.00 CoffeeCoins
+ recipientId: 'RECIPIENT_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Transferred 25 CoffeeCoins');
+```
+
+## Step 6: Burn tokens
+
+Reduce the supply by burning tokens you own:
+
+```typescript
+await sdk.tokens.burn({
+ tokenId,
+ amount: 100_00, // 100.00 CoffeeCoins
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Burned 100 CoffeeCoins');
+```
+
+## Full example
+
+Putting it all together as a complete script:
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+async function main() {
+ const sdk = EvoSDK.testnetTrusted();
+ await sdk.connect();
+
+ const identityId = 'YOUR_IDENTITY_ID';
+ const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+ const tokenId = 'YOUR_TOKEN_ID'; // from step 2
+
+ // Check balance
+ const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
+ console.log('Balance:', balances.get(tokenId) ?? 0n);
+
+ // Transfer
+ await sdk.tokens.transfer({
+ tokenId,
+ amount: 10_00,
+ recipientId: 'FRIEND_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(identityId),
+ });
+
+ console.log('Transfer complete!');
+}
+
+main().catch(console.error);
+```
+
+## Next steps
+
+- Add **freeze/unfreeze** capabilities for compliance scenarios
+- Set up a **direct purchase price** so anyone can buy tokens with credits
+- Create a **distribution schedule** for automatic token rewards
+- Use the `tokenMetadata` document type to store on-chain metadata
diff --git a/book/src/evo-sdk/tutorials/car-sales.md b/book/src/evo-sdk/tutorials/car-sales.md
new file mode 100644
index 00000000000..9c8cb5f114c
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/car-sales.md
@@ -0,0 +1,223 @@
+# Tutorial: Car Sales Management
+
+Build a decentralised car listing and sales application on Dash Platform. By
+the end you will have a data contract for vehicle listings, the ability to
+create/query/update listings, and a purchase flow using document transfers.
+
+## What you will learn
+
+- Designing a data contract with multiple document types
+- Publishing a contract to testnet
+- Creating, querying, and updating documents
+- Using document pricing and purchase for a sales flow
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity. See the
+[Getting Started](../getting-started.md) chapter for setup.
+
+## Step 1: Design the data contract
+
+A car sales contract needs two document types: **listings** (vehicles for sale)
+and **reviews** (buyer reviews of sellers).
+
+```typescript
+const carSalesSchema = {
+ listing: {
+ type: 'object',
+ properties: {
+ make: { type: 'string', maxLength: 64 },
+ model: { type: 'string', maxLength: 64 },
+ year: { type: 'integer', minimum: 1900, maximum: 2100 },
+ mileageKm: { type: 'integer', minimum: 0 },
+ priceUsd: { type: 'integer', minimum: 0 },
+ description: { type: 'string', maxLength: 1024 },
+ imageUrl: { type: 'string', maxLength: 512, format: 'uri' },
+ status: { type: 'string', enum: ['available', 'pending', 'sold'] },
+ },
+ required: ['make', 'model', 'year', 'priceUsd', 'status'],
+ additionalProperties: false,
+ },
+ review: {
+ type: 'object',
+ properties: {
+ sellerId: { type: 'string', maxLength: 44 },
+ listingId: { type: 'string', maxLength: 44 },
+ rating: { type: 'integer', minimum: 1, maximum: 5 },
+ comment: { type: 'string', maxLength: 512 },
+ },
+ required: ['sellerId', 'rating'],
+ additionalProperties: false,
+ },
+};
+```
+
+## Step 2: Connect and publish the contract
+
+```typescript
+import { EvoSDK, wallet } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+// Your identity credentials
+const identityId = 'YOUR_IDENTITY_ID';
+const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+const signingKeyIndex = 0;
+
+// Publish the data contract
+const contract = await sdk.contracts.publish({
+ identityId,
+ documentSchemas: carSalesSchema,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+const contractId = contract.getId().toString();
+console.log('Contract published:', contractId);
+```
+
+Save the `contractId` — you will need it for all subsequent operations.
+
+## Step 3: Create a listing
+
+```typescript
+const nonce = await sdk.identities.contractNonce(identityId, contractId);
+
+await sdk.documents.create({
+ contractId,
+ documentType: 'listing',
+ document: {
+ make: 'Toyota',
+ model: 'Camry',
+ year: 2021,
+ mileageKm: 45000,
+ priceUsd: 22500,
+ description: 'Well-maintained, single owner, full service history.',
+ status: 'available',
+ },
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce,
+});
+
+console.log('Listing created!');
+```
+
+## Step 4: Query listings
+
+```typescript
+// Fetch all available listings
+const results = await sdk.documents.query({
+ contractId,
+ documentType: 'listing',
+ where: [['status', '==', 'available']],
+ orderBy: [['priceUsd', 'asc']],
+ limit: 20,
+});
+
+for (const [id, doc] of results) {
+ if (!doc) continue;
+ const data = doc.getData();
+ console.log(`${data.year} ${data.make} ${data.model} — $${data.priceUsd}`);
+ console.log(` ID: ${id}`);
+}
+```
+
+### Search by make
+
+```typescript
+const toyotas = await sdk.documents.query({
+ contractId,
+ documentType: 'listing',
+ where: [
+ ['make', '==', 'Toyota'],
+ ['status', '==', 'available'],
+ ],
+ limit: 10,
+});
+```
+
+## Step 5: Update a listing
+
+Mark a listing as sold:
+
+```typescript
+const listingId = 'THE_LISTING_DOCUMENT_ID';
+
+await sdk.documents.replace({
+ contractId,
+ documentType: 'listing',
+ documentId: listingId,
+ document: {
+ make: 'Toyota',
+ model: 'Camry',
+ year: 2021,
+ mileageKm: 45000,
+ priceUsd: 22500,
+ description: 'Well-maintained, single owner, full service history.',
+ status: 'sold',
+ },
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.contractNonce(identityId, contractId),
+});
+
+console.log('Listing marked as sold');
+```
+
+## Step 6: Leave a review
+
+```typescript
+await sdk.documents.create({
+ contractId,
+ documentType: 'review',
+ document: {
+ sellerId: 'SELLER_IDENTITY_ID',
+ listingId: 'THE_LISTING_DOCUMENT_ID',
+ rating: 5,
+ comment: 'Great seller, car was exactly as described!',
+ },
+ identityId: buyerIdentityId,
+ privateKeyWif: buyerKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(buyerIdentityId, contractId),
+});
+```
+
+### Query reviews for a seller
+
+```typescript
+const reviews = await sdk.documents.query({
+ contractId,
+ documentType: 'review',
+ where: [['sellerId', '==', 'SELLER_IDENTITY_ID']],
+ orderBy: [['rating', 'desc']],
+ limit: 50,
+});
+
+let totalRating = 0;
+let count = 0;
+for (const [, doc] of reviews) {
+ if (!doc) continue;
+ totalRating += doc.getData().rating;
+ count++;
+}
+console.log(`Average rating: ${(totalRating / count).toFixed(1)} (${count} reviews)`);
+```
+
+## Next steps
+
+- Add **indexes** to the contract schema for efficient queries on `make`,
+ `year`, and `priceUsd`
+- Add a `location` field and query by region
+- Use **document pricing** (`sdk.documents.setPrice` / `sdk.documents.purchase`)
+ to let buyers pay for premium listing details
+- Integrate with a frontend framework (React, Vue, etc.) for a full web app
diff --git a/book/src/evo-sdk/tutorials/card-game.md b/book/src/evo-sdk/tutorials/card-game.md
new file mode 100644
index 00000000000..bab4e25351d
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/card-game.md
@@ -0,0 +1,396 @@
+# Tutorial: Card Game with Tokens
+
+Build a collectible card game on Dash Platform where cards are documents that
+can be traded, and an in-game currency token is used for purchases. This
+tutorial combines data contracts, documents, and tokens into a cohesive
+application.
+
+## What you will learn
+
+- Designing a contract with both document types and tokens
+- Using documents as game items (cards) owned by identities
+- Token-based in-game economy (minting rewards, spending on packs)
+- Document transfers for card trading between players
+- Querying collections and leaderboards
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity. This tutorial uses two identities to
+demonstrate trading.
+
+## Step 1: Design the game contract
+
+The contract defines three document types and one token:
+
+- **card** — A collectible card with rarity, power, and element
+- **deck** — A player's active deck configuration
+- **match** — Match result history
+- **GemToken** — In-game currency for buying card packs
+
+```typescript
+const gameSchema = {
+ card: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', maxLength: 64 },
+ element: { type: 'string', enum: ['fire', 'water', 'earth', 'air', 'shadow'] },
+ rarity: { type: 'string', enum: ['common', 'uncommon', 'rare', 'legendary'] },
+ power: { type: 'integer', minimum: 1, maximum: 100 },
+ defense: { type: 'integer', minimum: 1, maximum: 100 },
+ ability: { type: 'string', maxLength: 128 },
+ edition: { type: 'integer', minimum: 1 },
+ },
+ required: ['name', 'element', 'rarity', 'power', 'defense', 'edition'],
+ additionalProperties: false,
+ },
+ deck: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', maxLength: 64 },
+ cardIds: {
+ type: 'array',
+ items: { type: 'string', maxLength: 44 },
+ minItems: 5,
+ maxItems: 10,
+ },
+ },
+ required: ['name', 'cardIds'],
+ additionalProperties: false,
+ },
+ match: {
+ type: 'object',
+ properties: {
+ player1Id: { type: 'string', maxLength: 44 },
+ player2Id: { type: 'string', maxLength: 44 },
+ winnerId: { type: 'string', maxLength: 44 },
+ player1Score: { type: 'integer', minimum: 0 },
+ player2Score: { type: 'integer', minimum: 0 },
+ timestamp: { type: 'integer' },
+ },
+ required: ['player1Id', 'player2Id', 'winnerId', 'timestamp'],
+ additionalProperties: false,
+ },
+};
+
+const gemTokenConfig = {
+ conventions: {
+ localizations: {
+ en: {
+ shouldCapitalize: true,
+ singularForm: 'Gem',
+ pluralForm: 'Gems',
+ },
+ },
+ decimals: 0, // whole numbers only
+ },
+ manualMinting: {
+ rules: { type: 'ownerOnly' },
+ },
+ manualBurning: {
+ rules: { type: 'ownerOnly' },
+ },
+ maxSupply: 10_000_000, // 10 million Gems total
+};
+```
+
+## Step 2: Deploy the contract
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+// Game operator identity
+const operatorId = 'OPERATOR_IDENTITY_ID';
+const operatorKey = 'OPERATOR_PRIVATE_KEY_WIF';
+
+const contract = await sdk.contracts.publish({
+ identityId: operatorId,
+ documentSchemas: gameSchema,
+ tokens: [gemTokenConfig],
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+});
+
+const contractId = contract.getId().toString();
+const gemTokenId = await sdk.tokens.calculateId(contractId, 0);
+
+console.log('Game contract:', contractId);
+console.log('Gem token:', gemTokenId);
+```
+
+## Step 3: Mint starter Gems for a new player
+
+When a player joins, give them starter Gems:
+
+```typescript
+async function onboardPlayer(playerId: string) {
+ // Gift 100 Gems to the new player
+ await sdk.tokens.mint({
+ tokenId: gemTokenId,
+ amount: 100,
+ recipientId: playerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+ });
+
+ console.log(`Welcomed ${playerId} with 100 Gems`);
+}
+```
+
+## Step 4: Create a card pack (operator mints cards)
+
+The operator creates cards as documents. Each card is owned by the operator
+initially, then transferred to players when purchased.
+
+```typescript
+// Define a set of cards for a pack
+const starterPack = [
+ { name: 'Flame Sprite', element: 'fire', rarity: 'common', power: 15, defense: 10, edition: 1 },
+ { name: 'Tidal Guardian', element: 'water', rarity: 'common', power: 10, defense: 20, edition: 1 },
+ { name: 'Stone Golem', element: 'earth', rarity: 'uncommon', power: 25, defense: 30, edition: 1 },
+ { name: 'Wind Dancer', element: 'air', rarity: 'common', power: 20, defense: 12, edition: 1 },
+ { name: 'Shadow Wraith', element: 'shadow', rarity: 'rare', power: 40, defense: 15, edition: 1 },
+];
+
+async function createCards(cards: typeof starterPack) {
+ for (const card of cards) {
+ await sdk.documents.create({
+ contractId,
+ documentType: 'card',
+ document: card,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+ console.log(`Created: ${card.name} (${card.rarity})`);
+ }
+}
+
+await createCards(starterPack);
+```
+
+## Step 5: Player buys a card pack
+
+The purchase flow:
+1. Player spends Gems (transfer to operator)
+2. Operator transfers card documents to the player
+
+```typescript
+const PACK_PRICE = 50; // 50 Gems per pack
+
+async function buyPack(playerId: string, playerKey: string) {
+ // Player pays Gems to the operator
+ await sdk.tokens.transfer({
+ tokenId: gemTokenId,
+ amount: PACK_PRICE,
+ recipientId: operatorId,
+ identityId: playerId,
+ privateKeyWif: playerKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(playerId),
+ });
+ console.log(`Player paid ${PACK_PRICE} Gems`);
+
+ // Operator transfers cards to the player
+ // (In production, select random cards from available pool)
+ const availableCards = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [['$ownerId', '==', operatorId]],
+ limit: 5,
+ });
+
+ for (const [cardId, card] of availableCards) {
+ if (!card) continue;
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: cardId,
+ recipientId: playerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+ console.log(`Transferred ${card.getData().name} to player`);
+ }
+}
+```
+
+## Step 6: Query a player's collection
+
+```typescript
+async function getCollection(playerId: string) {
+ const cards = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [['$ownerId', '==', playerId]],
+ orderBy: [['power', 'desc']],
+ limit: 100,
+ });
+
+ console.log(`\n${playerId}'s collection:`);
+ for (const [id, card] of cards) {
+ if (!card) continue;
+ const d = card.getData();
+ console.log(` [${d.rarity}] ${d.name} — ${d.element} — ATK:${d.power} DEF:${d.defense}`);
+ }
+
+ return cards;
+}
+```
+
+### Filter by rarity
+
+```typescript
+const legendaries = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [
+ ['$ownerId', '==', playerId],
+ ['rarity', '==', 'legendary'],
+ ],
+ limit: 50,
+});
+```
+
+## Step 7: Trade cards between players
+
+Player-to-player trading using document transfers:
+
+```typescript
+async function tradeCards(
+ fromId: string, fromKey: string, fromCardId: string,
+ toId: string, toKey: string, toCardId: string,
+) {
+ // Player A sends their card to Player B
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: fromCardId,
+ recipientId: toId,
+ identityId: fromId,
+ privateKeyWif: fromKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(fromId, contractId),
+ });
+
+ // Player B sends their card to Player A
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: toCardId,
+ recipientId: fromId,
+ identityId: toId,
+ privateKeyWif: toKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(toId, contractId),
+ });
+
+ console.log('Trade complete!');
+}
+```
+
+## Step 8: Record a match result
+
+```typescript
+async function recordMatch(
+ player1Id: string, player2Id: string,
+ winnerId: string,
+ p1Score: number, p2Score: number,
+) {
+ await sdk.documents.create({
+ contractId,
+ documentType: 'match',
+ document: {
+ player1Id,
+ player2Id,
+ winnerId,
+ player1Score: p1Score,
+ player2Score: p2Score,
+ timestamp: Date.now(),
+ },
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+
+ // Reward the winner with Gems
+ await sdk.tokens.mint({
+ tokenId: gemTokenId,
+ amount: 10,
+ recipientId: winnerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+ });
+
+ console.log(`Match recorded. ${winnerId} wins and earns 10 Gems!`);
+}
+```
+
+## Step 9: Leaderboard
+
+Query match history to build a win count:
+
+```typescript
+async function getWinCounts() {
+ const matches = await sdk.documents.query({
+ contractId,
+ documentType: 'match',
+ orderBy: [['timestamp', 'desc']],
+ limit: 100,
+ });
+
+ const wins = new Map();
+ for (const [, doc] of matches) {
+ if (!doc) continue;
+ const winner = doc.getData().winnerId;
+ wins.set(winner, (wins.get(winner) ?? 0) + 1);
+ }
+
+ // Sort by wins descending
+ const sorted = [...wins.entries()].sort((a, b) => b[1] - a[1]);
+ console.log('\nLeaderboard:');
+ sorted.forEach(([id, count], i) => {
+ console.log(` ${i + 1}. ${id.slice(0, 8)}... — ${count} wins`);
+ });
+}
+```
+
+## Architecture recap
+
+```
+┌──────────────────────────────────────────────────┐
+│ Game Contract │
+├──────────────────┬───────────────┬───────────────┤
+│ card (document) │ deck (doc) │ match (doc) │
+│ - name, element │ - cardIds[] │ - players │
+│ - rarity, power │ │ - winner │
+│ - transferable │ │ - scores │
+├──────────────────┴───────────────┴───────────────┤
+│ GemToken (token position 0) │
+│ - in-game currency │
+│ - minted as rewards, spent on packs │
+└──────────────────────────────────────────────────┘
+```
+
+## Next steps
+
+- Add **deck validation** — check that a deck only contains cards the player owns
+- Implement **card pricing** with `sdk.documents.setPrice()` for a marketplace
+- Add **seasonal editions** with different `edition` numbers
+- Build a real-time game client that listens for match results
+- Use **groups** for guild/clan systems with shared card pools
From 6c7034bc0192fb8d3aa8fff1fb5bec53508dd135 Mon Sep 17 00:00:00 2001
From: Quantum Explorer
Date: Thu, 2 Apr 2026 13:36:55 +0300
Subject: [PATCH 3/5] docs(book): add React integration tutorial
Covers SDK initialization with a React context/provider, reusable
query and mutation hooks, components for identity viewing, document
listing, and form submission, plus production considerations for
key management, bundle size, error boundaries, and network switching.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
book/src/SUMMARY.md | 1 +
.../evo-sdk/tutorials/react-integration.md | 535 ++++++++++++++++++
2 files changed, 536 insertions(+)
create mode 100644 book/src/evo-sdk/tutorials/react-integration.md
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 4c68cd0018f..0966c69e631 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -78,6 +78,7 @@
- [Car Sales Management](evo-sdk/tutorials/car-sales.md)
- [Creating a Basic Token](evo-sdk/tutorials/basic-token.md)
- [Card Game with Tokens](evo-sdk/tutorials/card-game.md)
+ - [React Integration](evo-sdk/tutorials/react-integration.md)
# Rust SDK
diff --git a/book/src/evo-sdk/tutorials/react-integration.md b/book/src/evo-sdk/tutorials/react-integration.md
new file mode 100644
index 00000000000..a20b6dc1cd2
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/react-integration.md
@@ -0,0 +1,535 @@
+# Tutorial: React Integration
+
+Build a React application that connects to Dash Platform, queries data, and
+broadcasts state transitions. This tutorial covers SDK initialization in a
+React context, handling async WASM loading, and patterns for queries and
+mutations.
+
+## What you will learn
+
+- Initializing the Evo SDK in a React app with proper lifecycle management
+- Creating a React context/provider for SDK access
+- Building hooks for queries and state transitions
+- Handling loading, error, and connected states
+- Working with the SDK in both development and production builds
+
+## Prerequisites
+
+```sh
+npx create-vite@latest my-dash-app -- --template react-ts
+cd my-dash-app
+npm install @dashevo/evo-sdk
+```
+
+> **Vite** is recommended because it handles WASM imports natively. Create
+> React App (webpack 4) requires additional configuration for WASM — Vite
+> works out of the box.
+
+## Step 1: Create the SDK provider
+
+The SDK must be initialized once and shared across the app. A React context is
+the natural fit.
+
+**`src/DashProvider.tsx`**
+
+```tsx
+import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+interface DashContextValue {
+ sdk: EvoSDK | null;
+ isConnecting: boolean;
+ error: string | null;
+}
+
+const DashContext = createContext({
+ sdk: null,
+ isConnecting: true,
+ error: null,
+});
+
+export function useDash() {
+ return useContext(DashContext);
+}
+
+export function useSDK(): EvoSDK {
+ const { sdk } = useDash();
+ if (!sdk) throw new Error('SDK not connected. Wrap your app in .');
+ return sdk;
+}
+
+interface DashProviderProps {
+ network?: 'testnet' | 'mainnet' | 'local';
+ children: ReactNode;
+}
+
+export function DashProvider({ network = 'testnet', children }: DashProviderProps) {
+ const [sdk, setSdk] = useState(null);
+ const [isConnecting, setIsConnecting] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function connect() {
+ try {
+ setIsConnecting(true);
+ setError(null);
+
+ const instance = new EvoSDK({ network, trusted: true });
+ await instance.connect();
+
+ if (!cancelled) {
+ setSdk(instance);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : 'Failed to connect');
+ }
+ } finally {
+ if (!cancelled) {
+ setIsConnecting(false);
+ }
+ }
+ }
+
+ connect();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [network]);
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+**`src/main.tsx`**
+
+```tsx
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { DashProvider } from './DashProvider';
+import App from './App';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
+```
+
+## Step 2: Build query hooks
+
+Create reusable hooks for common queries. These handle loading and error states
+automatically.
+
+**`src/hooks/useDashQuery.ts`**
+
+```ts
+import { useEffect, useState } from 'react';
+import { useDash } from '../DashProvider';
+import type { EvoSDK } from '@dashevo/evo-sdk';
+
+interface QueryState {
+ data: T | null;
+ isLoading: boolean;
+ error: string | null;
+ refetch: () => void;
+}
+
+export function useDashQuery(
+ queryFn: (sdk: EvoSDK) => Promise,
+ deps: unknown[] = [],
+): QueryState {
+ const { sdk } = useDash();
+ const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [trigger, setTrigger] = useState(0);
+
+ useEffect(() => {
+ if (!sdk) return;
+
+ let cancelled = false;
+ setIsLoading(true);
+
+ queryFn(sdk)
+ .then((result) => {
+ if (!cancelled) setData(result);
+ })
+ .catch((err) => {
+ if (!cancelled) setError(err.message);
+ })
+ .finally(() => {
+ if (!cancelled) setIsLoading(false);
+ });
+
+ return () => { cancelled = true; };
+ }, [sdk, trigger, ...deps]);
+
+ return {
+ data,
+ isLoading,
+ error,
+ refetch: () => setTrigger((n) => n + 1),
+ };
+}
+```
+
+### Specific query hooks
+
+**`src/hooks/useIdentity.ts`**
+
+```ts
+import { useDashQuery } from './useDashQuery';
+
+export function useIdentity(identityId: string) {
+ return useDashQuery(
+ (sdk) => sdk.identities.fetch(identityId),
+ [identityId],
+ );
+}
+```
+
+**`src/hooks/useDocuments.ts`**
+
+```ts
+import { useDashQuery } from './useDashQuery';
+import type { DocumentsQuery } from '@dashevo/evo-sdk';
+
+export function useDocuments(query: DocumentsQuery) {
+ return useDashQuery(
+ (sdk) => sdk.documents.query(query),
+ [JSON.stringify(query)],
+ );
+}
+```
+
+**`src/hooks/useTokenBalance.ts`**
+
+```ts
+import { useDashQuery } from './useDashQuery';
+
+export function useTokenBalance(identityId: string, tokenId: string) {
+ return useDashQuery(
+ async (sdk) => {
+ const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
+ return balances.get(tokenId) ?? 0n;
+ },
+ [identityId, tokenId],
+ );
+}
+```
+
+## Step 3: Build a mutation hook
+
+For state transitions (writes), create a hook that manages submission state:
+
+**`src/hooks/useDashMutation.ts`**
+
+```ts
+import { useState, useCallback } from 'react';
+import { useSDK } from '../DashProvider';
+import type { EvoSDK } from '@dashevo/evo-sdk';
+
+interface MutationState {
+ execute: () => Promise;
+ isSubmitting: boolean;
+ error: string | null;
+ reset: () => void;
+}
+
+export function useDashMutation(
+ mutationFn: (sdk: EvoSDK) => Promise,
+): MutationState {
+ const sdk = useSDK();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const execute = useCallback(async () => {
+ try {
+ setIsSubmitting(true);
+ setError(null);
+ const result = await mutationFn(sdk);
+ return result;
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Transaction failed');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [sdk, mutationFn]);
+
+ return {
+ execute,
+ isSubmitting,
+ error,
+ reset: () => setError(null),
+ };
+}
+```
+
+## Step 4: Build components
+
+### Connection status
+
+**`src/components/ConnectionStatus.tsx`**
+
+```tsx
+import { useDash } from '../DashProvider';
+
+export function ConnectionStatus() {
+ const { sdk, isConnecting, error } = useDash();
+
+ if (isConnecting) return Connecting... ;
+ if (error) return Error: {error} ;
+ if (sdk) return Connected to testnet ;
+ return null;
+}
+```
+
+### Identity viewer
+
+**`src/components/IdentityViewer.tsx`**
+
+```tsx
+import { useState } from 'react';
+import { useIdentity } from '../hooks/useIdentity';
+
+export function IdentityViewer() {
+ const [identityId, setIdentityId] = useState('');
+ const [searchId, setSearchId] = useState('');
+ const { data: identity, isLoading, error } = useIdentity(searchId);
+
+ return (
+
+
Fetch Identity
+
+
+ {isLoading && searchId &&
Loading...
}
+ {error &&
{error}
}
+ {identity && (
+
+
ID: {identity.getId().toString()}
+
Balance: {identity.getBalance().toString()} credits
+
Public keys: {identity.getPublicKeys().length}
+
+ )}
+
+ );
+}
+```
+
+### Document list (e.g., car listings from the previous tutorial)
+
+**`src/components/ListingsList.tsx`**
+
+```tsx
+import { useDocuments } from '../hooks/useDocuments';
+
+const CONTRACT_ID = 'YOUR_CONTRACT_ID';
+
+export function ListingsList() {
+ const { data: results, isLoading, error, refetch } = useDocuments({
+ contractId: CONTRACT_ID,
+ documentType: 'listing',
+ where: [['status', '==', 'available']],
+ orderBy: [['priceUsd', 'asc']],
+ limit: 20,
+ });
+
+ if (isLoading) return Loading listings...
;
+ if (error) return {error}
;
+ if (!results || results.size === 0) return No listings found.
;
+
+ return (
+
+
Available Cars
+
Refresh
+
+ {[...results.entries()].map(([id, doc]) => {
+ if (!doc) return null;
+ const d = doc.getData();
+ return (
+
+ {d.year} {d.make} {d.model} — ${d.priceUsd}
+
+ );
+ })}
+
+
+ );
+}
+```
+
+### Create document form
+
+**`src/components/CreateListing.tsx`**
+
+```tsx
+import { useState, useCallback } from 'react';
+import { useDashMutation } from '../hooks/useDashMutation';
+
+const CONTRACT_ID = 'YOUR_CONTRACT_ID';
+const IDENTITY_ID = 'YOUR_IDENTITY_ID';
+const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_WIF';
+
+export function CreateListing() {
+ const [make, setMake] = useState('');
+ const [model, setModel] = useState('');
+ const [year, setYear] = useState(2024);
+ const [price, setPrice] = useState(0);
+
+ const mutation = useDashMutation(
+ useCallback(
+ (sdk) =>
+ sdk.documents.create({
+ contractId: CONTRACT_ID,
+ documentType: 'listing',
+ document: {
+ make,
+ model,
+ year,
+ priceUsd: price,
+ mileageKm: 0,
+ status: 'available',
+ },
+ identityId: IDENTITY_ID,
+ privateKeyWif: PRIVATE_KEY,
+ signingKeyIndex: 0,
+ nonce: sdk.identities.contractNonce(IDENTITY_ID, CONTRACT_ID),
+ }),
+ [make, model, year, price],
+ ),
+ );
+
+ return (
+
+ );
+}
+```
+
+## Step 5: Assemble the app
+
+**`src/App.tsx`**
+
+```tsx
+import { ConnectionStatus } from './components/ConnectionStatus';
+import { IdentityViewer } from './components/IdentityViewer';
+import { ListingsList } from './components/ListingsList';
+import { CreateListing } from './components/CreateListing';
+import { useDash } from './DashProvider';
+
+export default function App() {
+ const { sdk } = useDash();
+
+ return (
+
+
+
+ {sdk ? (
+
+
+
+
+
+ ) : (
+
Waiting for SDK connection...
+ )}
+
+ );
+}
+```
+
+## Production considerations
+
+### Private key management
+
+The examples above hardcode private keys for clarity. In production:
+
+- **Never** ship private keys in frontend code
+- Use a backend service to sign state transitions, or
+- Prompt the user for their mnemonic/key at runtime and keep it in memory only
+- Consider the `wallet` namespace for key derivation from user-provided mnemonics
+
+```tsx
+import { wallet } from '@dashevo/evo-sdk';
+
+async function signWithUserMnemonic(mnemonic: string) {
+ const keyInfo = await wallet.deriveKeyFromSeedPhrase({
+ mnemonic,
+ network: 'testnet',
+ derivationPath: "m/9'/1'/0'/0/0",
+ });
+ return keyInfo.privateKeyWif;
+}
+```
+
+### Bundle size
+
+The WASM module adds ~2-4 MB (gzipped) to your bundle. To optimise:
+
+- Use **code splitting** — the SDK module only loads when `connect()` is called
+- Vite handles WASM lazy loading automatically
+- Consider loading the SDK only on pages that need it
+
+### Error boundaries
+
+Wrap SDK-dependent components in an error boundary to handle WASM
+initialization failures gracefully:
+
+```tsx
+import { ErrorBoundary } from 'react-error-boundary';
+
+Failed to load Dash SDK
}>
+
+
+
+
+```
+
+### Network switching
+
+To let users switch networks at runtime, key the provider on the network value:
+
+```tsx
+const [network, setNetwork] = useState<'testnet' | 'mainnet'>('testnet');
+
+
+
+
+```
+
+The `key` prop forces React to unmount and remount the provider, creating a
+fresh SDK connection for the new network.
From 4358ffde7f9a15ffcbb13361c5fe8b516e5ad618 Mon Sep 17 00:00:00 2001
From: Quantum Explorer
Date: Thu, 2 Apr 2026 13:39:39 +0300
Subject: [PATCH 4/5] fix(book): add text language identifier to ASCII diagrams
Co-Authored-By: Claude Opus 4.6 (1M context)
---
book/src/evo-sdk/overview.md | 2 +-
book/src/evo-sdk/tutorials/card-game.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/book/src/evo-sdk/overview.md b/book/src/evo-sdk/overview.md
index 81fc7afcf52..646e49ae187 100644
--- a/book/src/evo-sdk/overview.md
+++ b/book/src/evo-sdk/overview.md
@@ -10,7 +10,7 @@ Node.js (≥ 18.18) and modern browsers.
## How it works
-```
+```text
┌──────────────────┐
│ Your TypeScript │
│ Application │
diff --git a/book/src/evo-sdk/tutorials/card-game.md b/book/src/evo-sdk/tutorials/card-game.md
index bab4e25351d..1266d8f602c 100644
--- a/book/src/evo-sdk/tutorials/card-game.md
+++ b/book/src/evo-sdk/tutorials/card-game.md
@@ -372,7 +372,7 @@ async function getWinCounts() {
## Architecture recap
-```
+```text
┌──────────────────────────────────────────────────┐
│ Game Contract │
├──────────────────┬───────────────┬───────────────┤
From 6be2a5525ce12b3b39151962eceeddfb485556e2 Mon Sep 17 00:00:00 2001
From: Quantum Explorer
Date: Thu, 2 Apr 2026 13:44:43 +0300
Subject: [PATCH 5/5] fix(book): address review feedback on Evo SDK docs
- Note that SDK is TypeScript with full type definitions
- Clarify connect() is optional (auto-connects on first facade call)
- Add comment about choosing testnet vs mainnet
- Use an existing identity ID in quick start example
- Mark basic-token as a Node.js tutorial
- Clarify car-sales and card-game: contract deployment via Node.js
with dev identity, then browser app uses published contract ID
Co-Authored-By: Claude Opus 4.6 (1M context)
---
book/src/evo-sdk/getting-started.md | 25 +++++++++++------------
book/src/evo-sdk/tutorials/basic-token.md | 5 +++++
book/src/evo-sdk/tutorials/car-sales.md | 10 +++++++--
book/src/evo-sdk/tutorials/card-game.md | 5 +++++
4 files changed, 30 insertions(+), 15 deletions(-)
diff --git a/book/src/evo-sdk/getting-started.md b/book/src/evo-sdk/getting-started.md
index b674bc81aa9..7dca58e5dda 100644
--- a/book/src/evo-sdk/getting-started.md
+++ b/book/src/evo-sdk/getting-started.md
@@ -6,8 +6,8 @@
npm install @dashevo/evo-sdk
```
-The package is **ESM-only** (`"type": "module"`). In CommonJS projects use a
-dynamic `import()`:
+The package is **ESM-only** (`"type": "module"`) and written in TypeScript with
+full type definitions included. In CommonJS projects use a dynamic `import()`:
```js
const { EvoSDK } = await import('@dashevo/evo-sdk');
@@ -20,32 +20,31 @@ Requirements: Node.js ≥ 18.18 or any modern browser with WebAssembly support.
```typescript
import { EvoSDK } from '@dashevo/evo-sdk';
-// Create a trusted testnet instance and connect
+// Pick your network: testnetTrusted() for development, mainnetTrusted() for production
const sdk = EvoSDK.testnetTrusted();
-await sdk.connect();
-// Query the current epoch
+// Query the current epoch (connect() is called automatically on first use)
const epoch = await sdk.epoch.current();
console.log('Current epoch:', epoch.index);
-// Fetch an identity by its base58 ID
+// Fetch an existing identity by its base58 ID
const identity = await sdk.identities.fetch('4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF');
console.log('Balance:', identity?.getBalance());
```
## Connecting
-The SDK must be connected before making any queries. Calling `connect()` is
-asynchronous because it initialises the WASM module and, in trusted mode,
-pre-fetches quorum public keys.
+Calling `connect()` explicitly is **optional**. The SDK connects automatically
+when you call any facade method for the first time. However, you can call
+`connect()` explicitly if you want to control when the WASM module is
+initialized and quorum keys are prefetched:
```typescript
-const sdk = new EvoSDK({ network: 'testnet', trusted: true });
-await sdk.connect();
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect(); // optional — triggers WASM init and quorum prefetch now
```
-After connecting, all facade properties (`sdk.identities`, `sdk.documents`,
-etc.) are ready to use. Calling `connect()` more than once is a no-op.
+Calling `connect()` more than once is a no-op.
### Factory helpers
diff --git a/book/src/evo-sdk/tutorials/basic-token.md b/book/src/evo-sdk/tutorials/basic-token.md
index 67a7a9bd9c0..13125dd4da9 100644
--- a/book/src/evo-sdk/tutorials/basic-token.md
+++ b/book/src/evo-sdk/tutorials/basic-token.md
@@ -1,5 +1,10 @@
# Tutorial: Creating a Basic Token
+> **Environment:** This tutorial uses **Node.js** scripts to deploy a contract
+> and perform token operations. For browser-based applications, deploy the
+> contract from a Node.js script first, then use the contract ID in your
+> frontend code.
+
Create a fungible token on Dash Platform with minting, transferring, and
balance queries. This tutorial walks through the full lifecycle from contract
deployment to token operations.
diff --git a/book/src/evo-sdk/tutorials/car-sales.md b/book/src/evo-sdk/tutorials/car-sales.md
index 9c8cb5f114c..bcb1a96c919 100644
--- a/book/src/evo-sdk/tutorials/car-sales.md
+++ b/book/src/evo-sdk/tutorials/car-sales.md
@@ -4,11 +4,17 @@ Build a decentralised car listing and sales application on Dash Platform. By
the end you will have a data contract for vehicle listings, the ability to
create/query/update listings, and a purchase flow using document transfers.
+> **How this works in practice:** Data contracts are deployed once using a
+> **Node.js script** with a developer identity. After deployment, your
+> **browser app** uses the published contract ID to create, query, and update
+> documents. Steps 1-2 below are run from Node.js; steps 3 onward can run
+> in either Node.js or the browser.
+
## What you will learn
- Designing a data contract with multiple document types
-- Publishing a contract to testnet
-- Creating, querying, and updating documents
+- Publishing a contract to testnet from a Node.js deployment script
+- Creating, querying, and updating documents (Node.js or browser)
- Using document pricing and purchase for a sales flow
## Prerequisites
diff --git a/book/src/evo-sdk/tutorials/card-game.md b/book/src/evo-sdk/tutorials/card-game.md
index 1266d8f602c..89567f8eee7 100644
--- a/book/src/evo-sdk/tutorials/card-game.md
+++ b/book/src/evo-sdk/tutorials/card-game.md
@@ -5,6 +5,11 @@ can be traded, and an in-game currency token is used for purchases. This
tutorial combines data contracts, documents, and tokens into a cohesive
application.
+> **Environment:** Steps 1-2 (contract design and deployment) are run from a
+> **Node.js script** using a developer/operator identity. Steps 3 onward
+> (minting, trading, querying) can run in either Node.js or a **browser app**
+> using the published contract ID.
+
## What you will learn
- Designing a contract with both document types and tokens