Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e231e6
chore: import dashmint-lab as example app
thephez Apr 22, 2026
d3f3c3d
chore: integrate dashmint-lab with host repo tooling
thephez Apr 22, 2026
685df57
test: add vitest suite for dashmint-lab helpers
thephez Apr 23, 2026
dd9580e
chore(dashmint-lab): add prettier and format app
thephez Apr 23, 2026
557f91e
fix: make header height stable
thephez Apr 23, 2026
0d12cec
fix: tighten typed boundaries and mutation feedback
thephez Apr 23, 2026
5659d7f
test(dashmint-lab): add vitest coverage for app, session, and modals
thephez Apr 23, 2026
e59bb69
test(dashmint-lab): cover CardTile actions and mint-screen gating
thephez Apr 23, 2026
8b0e6e5
style: run npm format
thephez Apr 23, 2026
91b4b6a
feat(dashmint-lab): accept DPNS names as transfer recipients
thephez Apr 23, 2026
30a28e1
refactor(dashmint-lab): extract MAX_QUERY_LIMIT constant
thephez Apr 23, 2026
9702835
feat(dashmint-lab): link card owner to Platform Explorer
thephez Apr 23, 2026
112ce4c
feat(dashmint-lab): show signed-in credit balance in heading
thephez Apr 23, 2026
3419450
feat(dashmint-lab): randomize starter pack from shared card pool
thephez Apr 23, 2026
d4e370e
ci(dashmint-lab): add GitHub Pages deploy workflow with fork-friendly…
thephez Apr 23, 2026
140643c
feat(ui): replace placeholder card art with deterministic SVG themes
thephez Apr 23, 2026
180d052
refactor(dashmint-lab): remove unused fetchBalance helper and test
thephez Apr 23, 2026
a962752
chore: npm format
thephez Apr 23, 2026
7fbde7b
docs: update readme and claude file
thephez Apr 23, 2026
6711310
style: update favicon
thephez Apr 28, 2026
a83e394
fix(dashmint-lab): prevent concurrent mint operations on the same ide…
thephez Apr 28, 2026
eee5fc1
fix(dashmint-lab): give Modal proper dialog semantics and focus manag…
thephez Apr 28, 2026
6c5bcc2
fix(dashmint-lab): reset PurchaseModal state when reopened
thephez Apr 28, 2026
af69f3a
fix(dashmint-lab): reject dotted recipient inputs with disallowed chars
thephez Apr 28, 2026
9934666
fix(dashmint-lab): throw when contract publish returns no id
thephez Apr 28, 2026
f53ec2d
fix(dashmint-lab): close DPNS cache race in concurrent hook instances
thephez Apr 28, 2026
4aeadb0
fix(dashmint-lab): don't cache transient DPNS lookup errors as not-found
thephez Apr 28, 2026
28de79c
style(dashmint-lab): collapse contractId assignment onto one line
thephez Apr 28, 2026
9ab168b
docs: clarify root tsconfig scope and example-apps boundary
thephez Apr 28, 2026
35d3907
fix(dashmint-lab): trim recipient input in useResolvedRecipient
thephez Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example-apps/
63 changes: 63 additions & 0 deletions .github/workflows/deploy-dashmint-lab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Deploy DashMint Lab to GitHub Pages

# Manual-only trigger so forks never auto-publish. Forkers must (1) enable
# Pages with source "GitHub Actions" in repo Settings, then (2) run this
# workflow from the Actions tab. Deploys to:
# https://<owner>.github.io/<repo>/dashmint-lab/
# The base path is derived from github.event.repository.name, so a fork
# works with zero config changes.

on:
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages-dashmint-lab
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
cache: npm
cache-dependency-path: example-apps/dashmint-lab/package-lock.json

- name: Install
run: npm ci
working-directory: example-apps/dashmint-lab

- name: Build
run: npm run build
working-directory: example-apps/dashmint-lab
env:
# Self-configuring base path: resolves to /<repo>/dashmint-lab/
# on any fork without editing the workflow.
VITE_BASE_PATH: /${{ github.event.repository.name }}/dashmint-lab/

- name: Assemble site
run: |
mkdir -p _site/dashmint-lab
cp -r example-apps/dashmint-lab/dist/. _site/dashmint-lab/

- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: _site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
node_modules/
.env
.DS_Store

# Example apps (Vite artifacts)
example-apps/*/node_modules/
example-apps/*/dist/
example-apps/*/dist-ssr/
example-apps/*/*.local
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example-apps/
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ Read-only tests skip gracefully when `PLATFORM_MNEMONIC` is unset.
- **`2-Contracts-and-Documents/`** — data contract variants (minimal, indexed, binary, timestamps, history, NFT), document CRUD, NFT operations
- **`test/`** — test runner, assertions, read-only and read-write test suites
- **`docs/`** — HTML/JS interactive tutorial runner (separate from Node tutorials)
- **`example-apps/`** — Standalone applications (Vite + React + TypeScript) that consume the tutorial SDK code. Each has its own `package.json`, tsconfig, and toolchain — the conventions in this file (Node16 modules, `airbnb-base`, etc.) describe the **root** tutorial code only and do not apply inside `example-apps/`. See each app's local `CLAUDE.md` for its conventions.

### Linting / Types

TypeScript (`tsconfig.json`) checks JS files with `strict: true`, `noUnusedLocals: true`, targeting Node16 modules. ESLint uses `airbnb-base`. Prettier uses single quotes, 2-space tabs, trailing commas.
`npm run lint` runs `tsc` against the root `tsconfig.json`, which is scoped narrowly via `include: ["./setupDashClient.mjs"]` and transitively typechecks the tutorial `.mjs` files via `allowJs` + `checkJs`. Settings: `strict: true`, `noUnusedLocals: true`, Node16 module resolution. Apps under `example-apps/` are excluded from this typecheck — they have their own tsconfigs and run their own `tsc -b` via each app's `npm run build`. ESLint uses `airbnb-base`. Prettier uses single quotes, 2-space tabs, trailing commas.
7 changes: 7 additions & 0 deletions example-apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Example Apps

Stand-alone applications built on top of the same `@dashevo/evo-sdk` used by the Node tutorials in the parent repo. Each example is an independent npm project — `cd` into its directory and run `npm install` there.

## Apps

- [dashmint-lab/](./dashmint-lab/) — React + TypeScript + Vite SPA for minting, viewing, transferring, and trading NFT-style collectible cards on Dash Platform testnet. Shares the browser-safe SDK core (`setupDashClient-core.mjs`) with the Node tutorials at the repo root.
24 changes: 24 additions & 0 deletions example-apps/dashmint-lab/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
coverage
1 change: 1 addition & 0 deletions example-apps/dashmint-lab/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Comment thread
thephez marked this conversation as resolved.
52 changes: 52 additions & 0 deletions example-apps/dashmint-lab/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# DashMint Lab — Dash Platform NFTs (Modern React)

React + TypeScript + Vite app for minting, viewing, transferring, and trading NFT-style collectible cards on Dash Platform testnet. Ships with browse-only mode, DPNS-aware recipient input, a themed SVG card-art layer, and a deterministic starter pack.

## Commands

- `npm run dev` — start Vite dev server
- `npm run build` — typecheck (`tsc -b`) then bundle
- `npm run lint` — ESLint
- `npm run test` — Vitest suite in [test/](test/)
- `npm run format` / `format:check` — Prettier
- `npm run preview` — serve production build locally

## Architecture

- **[src/dash/](src/dash/)** — one file per Platform SDK operation. Each exports an async function with a leading JSDoc block. No hooks, no wrappers — the SDK call is the function. Includes the `classifyRecipientInput` / `resolveRecipient` DPNS helpers.
- **Shared SDK core** — [src/dash/client.ts](src/dash/client.ts) and [src/dash/keyManager.ts](src/dash/keyManager.ts) re-export directly from `../../../../setupDashClient-core.mjs` (the canonical browser-safe core at the host repo root). No vendoring, no backport step. The `@dashevo/evo-sdk` bare specifier is aliased to the app's local copy via [vite.config.ts](vite.config.ts).
- **[src/session/](src/session/)** — `SessionContext.tsx` provides the context (SDK, keyManager, identityId, contractId, contractOwnerId, balance, activity log) and calls `sdk.identities.balance` inline; `useSession.ts` is the consumer hook. Mnemonic lives only in the keyManager closure — never in state, never in localStorage.
- **[src/components/](src/components/)** — standard React. Modals call `src/dash/` functions directly. Notable: [HowItWorks.tsx](src/components/HowItWorks.tsx) (static education tab), [CardArt.tsx](src/components/CardArt.tsx) (deterministic themed SVG), [OddsTable.tsx](src/components/OddsTable.tsx).
- **[src/hooks/](src/hooks/)** — `useDpnsName` (identityId → username) and `useResolvedRecipient` (name → identityId). Both use module-level caches so repeated renders don't re-query.
- **[src/data/starterPack.ts](src/data/starterPack.ts)** — shared card pool and `drawStarterPack()` Fisher-Yates shuffle. Injectable RNG for deterministic tests.
- **[src/lib/](src/lib/)** — pure utilities: [rarity.ts](src/lib/rarity.ts) (tier from atk+def), [format.ts](src/lib/format.ts), [explorer.ts](src/lib/explorer.ts) (Platform Explorer URLs), [cardArt.ts](src/lib/cardArt.ts) (theme/palette recipe — presentation only, not Platform-relevant).
- **[src/styles/globals.css](src/styles/globals.css)** — Tailwind v4 import + rarity tokens.
- **[test/](test/)** — Vitest + Testing Library. All test files live here per the `include` pattern in [vite.config.ts](vite.config.ts) and are named after the subject under test (e.g. `CardTile.test.tsx`, `SessionContext.test.tsx`). Default env is `node`; tests that need DOM opt in with `// @vitest-environment jsdom`.

## SDK Patterns

- **Minting**: `sdk.documents.create({ document, identityKey, signer })`
- **Transfer**: `sdk.documents.transfer({ document, recipientId, identityKey, signer })`
- **Set price**: `sdk.documents.setPrice({ document, price: BigInt, identityKey, signer })`
- **Purchase**: `sdk.documents.purchase({ document, buyerId, price: BigInt, identityKey, signer })`
- **Burn**: `sdk.documents.delete({ document: { id, ownerId, dataContractId, documentTypeName }, identityKey, signer })`
- **Query**: `sdk.documents.query({ dataContractId, documentTypeName, where?, limit })`
- **DPNS resolve**: `sdk.dpns.resolveName(fullName)` / `sdk.dpns.username(identityId)`
- **Balance**: `sdk.identities.balance(identityId)` → `bigint` (called directly from `SessionContext`)

All mutations except mint flow through [withAuthedCard.ts](src/dash/withAuthedCard.ts), which fetches the document, bumps its revision, and resolves the auth signer.

## Gotchas

- All document mutations (transfer, setPrice, purchase) require fetching the document first and incrementing `document.revision = BigInt(document.revision) + 1n`
- Transfer/trade operations use AUTHENTICATION keys, not TRANSFER purpose keys — the SDK rejects TRANSFER purpose for these state transitions
- Attack/defense are randomly generated (1–10) on mint; rarity is derived client-side in [rarity.ts](src/lib/rarity.ts) (common ≤10, rare 11–14, legendary ≥15) and is not persisted
- Browse-only mode sets `keyManager` to null — `withAuthedCard` guards this; check session status before any write operation
- `listMarketplaceCards` filters client-side for `$price` (no server-side trade-mode index available yet)
- DPNS names are normalized to lowercase + `.dash` suffix before resolving; `classifyRecipientInput` distinguishes identity IDs from names by character set, not length
- Contract ID stored in `localStorage['dashmint-lab.contractId']` (public, safe to persist); clearing falls back to `DEFAULT_CONTRACT_ID` in [contract.ts](src/dash/contract.ts) so browse-only mode always has something queryable
- The mint tab is gated to the contract owner — non-owners see an informative overlay
- The Evo SDK WASM bundle is ~8MB; this is expected and not a build error
- `allowJs: true` in tsconfig so TypeScript can import the JSDoc-typed `.mjs` core at the host repo root
- Deploys to GitHub Pages via `VITE_BASE_PATH`; the workflow lives at the repo root under `.github/workflows/`
- [src/lib/cardArt.ts](src/lib/cardArt.ts) and [src/components/CardArt.tsx](src/components/CardArt.tsx) are presentation-only — don't mistake them for contract or document logic
100 changes: 100 additions & 0 deletions example-apps/dashmint-lab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# DashMint Lab — Dash Platform NFTs (Modern React)

A React + TypeScript + Vite app demonstrating every Dash Platform NFT operation: mint, transfer, price, purchase, burn, and query.

## Prerequisites

- Node >= 20 (developed on v20.19.5, npm 10.8.2)
- A funded Dash Platform testnet identity (BIP-39 mnemonic + identity index)
- (Optional) A second funded identity for testing cross-profile transfer and purchase
- Browse-only mode works without any identity — visitors can explore the marketplace

## Quick start

```bash
npm install
npm run dev
```

Production build: `npm run build && npm run preview`

Other scripts:

```bash
npm run test # Vitest suite
npm run lint # ESLint
npm run format # Prettier (write)
npm run format:check # Prettier (check only)
```

## Known constraints

- The app is built for Dash Platform testnet, not mainnet.
- Write operations require a funded Platform identity; browse-only mode works without login.
- Trading flows are easiest to test with a second funded identity.
- The active contract ID can be swapped or a new one can be registered from Settings.
- Minting is restricted to the contract owner by the schema (`creationRestrictionMode: 1`); non-owners see an overlay on the Mint tab.
- The browser bundle is intentionally heavy because it includes the full `@dashevo/evo-sdk` (~8MB WASM).

## Platform operations at a glance

Every SDK call lives in its own file under [`src/dash/`](src/dash/). Open the file to see the full implementation with a JSDoc header explaining what it does and why.

| Operation | File | SDK method |
| -------------------- | -------------------------------------------------------------- | ------------------------------------------- |
| Connect to testnet | [`src/dash/client.ts`](src/dash/client.ts) | `EvoSDK.testnetTrusted()` + `sdk.connect()` |
| Derive identity keys | [`src/dash/keyManager.ts`](src/dash/keyManager.ts) | `wallet.deriveKeyFromSeedWithPath` |
| Deploy card contract | [`src/dash/contract.ts`](src/dash/contract.ts) | `sdk.contracts.publish` |
| Mint a card | [`src/dash/mintCard.ts`](src/dash/mintCard.ts) | `sdk.documents.create` |
| Transfer a card | [`src/dash/transferCard.ts`](src/dash/transferCard.ts) | `sdk.documents.transfer` |
| Set / remove price | [`src/dash/setPrice.ts`](src/dash/setPrice.ts) | `sdk.documents.setPrice` |
| Purchase a card | [`src/dash/purchaseCard.ts`](src/dash/purchaseCard.ts) | `sdk.documents.purchase` |
| Burn (delete) a card | [`src/dash/burnCard.ts`](src/dash/burnCard.ts) | `sdk.documents.delete` |
| Query cards | [`src/dash/queries.ts`](src/dash/queries.ts) | `sdk.documents.query` |
| Resolve DPNS name | [`src/dash/resolveRecipient.ts`](src/dash/resolveRecipient.ts) | `sdk.dpns.resolveName` |

Balance is fetched inline from `SessionContext` via `sdk.identities.balance(identityId)` — it's a one-liner, so there's no dedicated `src/dash/` file for it.

Supporting files:

- **[`src/dash/withAuthedCard.ts`](src/dash/withAuthedCard.ts)** — shared mutation prelude used by transfer, setPrice, purchase, and burn. Fetches the document, bumps its revision, and resolves the authentication signer.
- **[`src/dash/classifyRecipientInput.ts`](src/dash/classifyRecipientInput.ts)** — decides whether a recipient string looks like a DPNS name or an identity ID by character set.
- **[`src/dash/logger.ts`](src/dash/logger.ts)** — shared `Logger` type so every operation can stream progress messages to the UI.

## Reading this codebase

Recommended order for understanding how the app works:

1. **[`src/dash/`](src/dash/)** — start here. One file per Platform operation, each with a JSDoc block explaining what / why / which SDK method. Read [`mintCard.ts`](src/dash/mintCard.ts) first (simplest create flow), then [`withAuthedCard.ts`](src/dash/withAuthedCard.ts) (shared pattern for mutations that need the current document).

2. **[`src/session/SessionContext.tsx`](src/session/SessionContext.tsx)** — manages the SDK connection, identity, contract ID, contract-owner ID, credit balance, and activity log. The mnemonic never enters React state; it lives only inside the `keyManager` closure and is garbage-collected on logout. The consumer hook lives in [`useSession.ts`](src/session/useSession.ts).

3. **[`src/components/`](src/components/)** — standard React UI. [`CardTile.tsx`](src/components/CardTile.tsx) renders a single card with rarity rail, owner chip, and action buttons. Modals wire directly to the `src/dash/` functions. [`HowItWorks.tsx`](src/components/HowItWorks.tsx) is the in-app education tab.

4. **[`src/hooks/`](src/hooks/)** — [`useDpnsName`](src/hooks/useDpnsName.ts) resolves an identity ID to a DPNS username for display; [`useResolvedRecipient`](src/hooks/useResolvedRecipient.ts) does the reverse for the transfer modal. Both cache across component mounts so lists of cards don't re-query.

5. **[`src/data/starterPack.ts`](src/data/starterPack.ts)** — the shared card pool and `drawStarterPack()` helper behind the "Mint Starter Pack" button.

6. **[`src/lib/`](src/lib/)** — pure utilities. [`rarity.ts`](src/lib/rarity.ts) maps attack + defense to a rarity tier (common / rare / legendary). [`format.ts`](src/lib/format.ts) truncates IDs and formats credits. [`explorer.ts`](src/lib/explorer.ts) builds Platform Explorer URLs. [`cardArt.ts`](src/lib/cardArt.ts) derives a deterministic theme, palette, and composition from each card's name/description/stats — it's presentation-only and has nothing to do with the on-chain schema.

## Tests

[`test/`](test/) uses Vitest + Testing Library, co-located by subject. The default Vitest environment is Node; component tests opt into jsdom per-file with `// @vitest-environment jsdom`. Run with `npm run test`.

## Deploying to GitHub Pages

The project ships with a fork-friendly deploy workflow at the repo root. Pushing the deploy branch triggers a Vite build with `VITE_BASE_PATH` set to the repo name so links resolve under `/<repo>/`. For local previews of that build, run:

```bash
VITE_BASE_PATH=/dashmint-lab/ npm run build && npm run preview
```

## Tech stack

- React 19
- TypeScript
- Vite 8
- Tailwind CSS v4
- Vitest 4 + Testing Library
- `@dashevo/evo-sdk`
- sonner (toasts)
23 changes: 23 additions & 0 deletions example-apps/dashmint-lab/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";

export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
Comment thread
thephez marked this conversation as resolved.
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
]);
13 changes: 13 additions & 0 deletions example-apps/dashmint-lab/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DashMint Lab — Dash Platform NFTs</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading