diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index f8859d679ae..0966c69e631 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -66,7 +66,21 @@ - [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) +- [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) + - [React Integration](evo-sdk/tutorials/react-integration.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..7dca58e5dda --- /dev/null +++ b/book/src/evo-sdk/getting-started.md @@ -0,0 +1,105 @@ +# Getting Started + +## Installation + +```sh +npm install @dashevo/evo-sdk +``` + +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'); +``` + +Requirements: Node.js ≥ 18.18 or any modern browser with WebAssembly support. + +## Quick start + +```typescript +import { EvoSDK } from '@dashevo/evo-sdk'; + +// Pick your network: testnetTrusted() for development, mainnetTrusted() for production +const sdk = EvoSDK.testnetTrusted(); + +// 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 existing identity by its base58 ID +const identity = await sdk.identities.fetch('4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF'); +console.log('Balance:', identity?.getBalance()); +``` + +## Connecting + +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 = EvoSDK.testnetTrusted(); +await sdk.connect(); // optional — triggers WASM init and quorum prefetch now +``` + +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..646e49ae187 --- /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 + +```text +┌──────────────────┐ +│ 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/tutorials/basic-token.md b/book/src/evo-sdk/tutorials/basic-token.md new file mode 100644 index 00000000000..13125dd4da9 --- /dev/null +++ b/book/src/evo-sdk/tutorials/basic-token.md @@ -0,0 +1,242 @@ +# 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. + +## 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..bcb1a96c919 --- /dev/null +++ b/book/src/evo-sdk/tutorials/car-sales.md @@ -0,0 +1,229 @@ +# 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. + +> **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 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 + +```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..89567f8eee7 --- /dev/null +++ b/book/src/evo-sdk/tutorials/card-game.md @@ -0,0 +1,401 @@ +# 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. + +> **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 +- 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 + +```text +┌──────────────────────────────────────────────────┐ +│ 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 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

+
{ e.preventDefault(); setSearchId(identityId); }}> + setIdentityId(e.target.value)} + placeholder="Enter identity ID" + /> + +
+ + {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

+ +
    + {[...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 ( +
{ + e.preventDefault(); + await mutation.execute(); + }} + > +

Create Listing

+ setMake(e.target.value)} /> + setModel(e.target.value)} /> + setYear(+e.target.value)} /> + setPrice(+e.target.value)} /> + + {mutation.error &&

{mutation.error}

} +
+ ); +} +``` + +## 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 ( +
+
+

Dash Platform App

+ +
+ + {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. 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, +}); +```