Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
8 changes: 5 additions & 3 deletions example-apps/dashmint-lab/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ React + TypeScript + Vite app for minting, viewing, transferring, and trading NF

## 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.
- **[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 and [dashMintToken.ts](src/dash/dashMintToken.ts), which holds fixed-supply token constants, token payment metadata, and balance lookup for constrained minting.
- **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).
Expand All @@ -29,7 +29,8 @@ React + TypeScript + Vite app for minting, viewing, transferring, and trading NF

## SDK Patterns

- **Minting**: `sdk.documents.create({ document, identityKey, signer })`
- **Minting**: `sdk.documents.create({ document, identityKey, signer, tokenPaymentInfo })`; `card.tokenCost.create` burns 1 DashMint token, so supply is constrained by the fixed token supply.
- **DashMint token balance**: `sdk.tokens.calculateId(contractId, 0)` + `sdk.tokens.identityBalances(identityId, [tokenId])`
- **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 })`
Expand All @@ -45,11 +46,12 @@ All mutations except mint flow through [withAuthedCard.ts](src/dash/withAuthedCa
- 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
- Mint capacity is real, but rarity is not enforced by the contract: card creation burns one fixed-supply DashMint token, while stats/rarity are still client-generated demo data
- 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 mint tab is login-gated. Any authenticated identity with DashMint tokens can mint; identities without tokens see minting disabled by balance state or rejected by Platform.
- 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/`
Expand Down
10 changes: 6 additions & 4 deletions example-apps/dashmint-lab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ npm run format:check # Prettier (check only)
- 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.
- Minting is gated by a fixed-supply DashMint token. Creating a card burns 1 DashMint token through `card.tokenCost.create`, so any identity with a token can mint.
- The browser bundle is intentionally heavy because it includes the full `@dashevo/evo-sdk` (~8MB WASM).

## Platform operations at a glance
Expand All @@ -45,27 +45,29 @@ Every SDK call lives in its own file under [`src/dash/`](src/dash/). Open the fi
| 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` |
| Token mint capacity | [`src/dash/dashMintToken.ts`](src/dash/dashMintToken.ts) | `sdk.tokens.identityBalances` |
| Mint a card | [`src/dash/mintCard.ts`](src/dash/mintCard.ts) | `sdk.documents.create` + `tokenPaymentInfo` |
| 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.
Credit 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. DashMint token balance lives in [`src/dash/dashMintToken.ts`](src/dash/dashMintToken.ts), because it is part of the token-burn minting flow.

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/dashMintToken.ts`](src/dash/dashMintToken.ts)** — fixed-supply DashMint token constants, token payment metadata, and balance lookup. The contract burns 1 token to create each card document.
- **[`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).
1. **[`src/dash/`](src/dash/)** — start here. One file per Platform operation, each with a JSDoc block explaining what / why / which SDK method. Read [`contract.ts`](src/dash/contract.ts), [`dashMintToken.ts`](src/dash/dashMintToken.ts), and [`mintCard.ts`](src/dash/mintCard.ts) together to see how fixed supply, token burn, and document creation combine. Then read [`withAuthedCard.ts`](src/dash/withAuthedCard.ts) 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).

Expand Down
16 changes: 8 additions & 8 deletions example-apps/dashmint-lab/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"preview": "vite preview"
},
"dependencies": {
"@dashevo/evo-sdk": "3.1.0-dev.1",
"@dashevo/evo-sdk": "3.1.0-dev.6",
"@tailwindcss/vite": "^4.2.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
Expand Down
7 changes: 4 additions & 3 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ <h2>Browse cards</h2>
// identically against the same testnet contract.
import { EvoSDK } from 'https://esm.sh/@dashevo/evo-sdk@3.1.0-dev.1';

// The "card" data contract is already published on testnet by the React app.
// Anyone querying with the same contract id hits the same documents.
const CONTRACT_ID = '4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY';
// The token-enabled "card" data contract is already published on testnet by
// the React app. Anyone querying with the same contract id hits the same
// documents.
const CONTRACT_ID = 'GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
Expand Down
78 changes: 41 additions & 37 deletions example-apps/dashmint-lab/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { useSession } from "./session/useSession";
import { AppShell } from "./components/AppShell";
import { BurnModal } from "./components/BurnModal";
import { CardGrid } from "./components/CardGrid";
import { CollectionToolbar } from "./components/CollectionToolbar";
import {
CollectionToolbar,
RefreshSpinner,
} from "./components/CollectionToolbar";
import { LoginModal } from "./components/LoginModal";
import { MintForm } from "./components/MintForm";
import { PurchaseModal } from "./components/PurchaseModal";
Expand Down Expand Up @@ -41,8 +44,8 @@ function App() {
sdk,
identityId,
contractId,
contractOwnerId,
balance,
dashMintTokenBalance,
refreshBalance,
log,
browseOnly,
Expand All @@ -58,14 +61,19 @@ function App() {

const [sortKey, setSortKey] = useState<SortKey>("rarity");

const [cards, setCards] = useState<Card[]>([]);
const [cardsBySubTab, setCardsBySubTab] = useState<
Partial<Record<CollectionSubTab, Card[]>>
>({});
const [loadingCards, setLoadingCards] = useState(false);
const [refreshNonce, setRefreshNonce] = useState(0);
const refresh = useCallback(() => {
setCardsBySubTab({});
setRefreshNonce((n) => n + 1);
refreshBalance();
}, [refreshBalance]);

const cards = cardsBySubTab[subTab];

// Auto-connect in browse-only mode so read tabs work without login.
// Defer to the next frame so the shell paints before the SDK chunk
// (~8MB WASM) starts downloading.
Expand All @@ -82,11 +90,11 @@ function App() {
}, [status]);

// Load cards for the current sub-tab whenever dependencies change.
// Keeps any previously-cached results visible while refetching so tab
// switches don't tear down the grid — only the first load shows the
// full "Loading…" placeholder.
useEffect(() => {
if (!sdk || !contractId) {
setCards([]);
return;
}
if (!sdk || !contractId) return;
let cancelled = false;
(async () => {
setLoadingCards(true);
Expand All @@ -99,11 +107,13 @@ function App() {
} else {
result = await listAllCards({ sdk, contractId, log });
}
if (!cancelled) setCards(result);
if (!cancelled) {
setCardsBySubTab((prev) => ({ ...prev, [subTab]: result }));
}
} catch (err) {
if (!cancelled) {
log(`Query failed: ${errorMessage(err)}`, "error");
setCards([]);
setCardsBySubTab((prev) => ({ ...prev, [subTab]: [] }));
}
} finally {
if (!cancelled) setLoadingCards(false);
Expand All @@ -116,6 +126,7 @@ function App() {

// Sort cards for the collection grid.
const sortedCards = useMemo(() => {
if (!cards) return [];
if (sortKey === "rarity") {
return [...cards].sort((a, b) => {
const sa = (a.data.attack ?? 0) + (a.data.defense ?? 0);
Expand Down Expand Up @@ -151,7 +162,8 @@ function App() {
},
mint: {
title: "Mint",
subtitle: "Create a new card document on the contract.",
subtitle:
"Create collectible cards on Dash Platform using DashMint tokens.",
},
"how-it-works": {
title: "How it works",
Expand Down Expand Up @@ -203,11 +215,18 @@ function App() {
{tab === "collection" && (
<section>
<div className="flex items-end justify-between">
<SubTabs
value={subTab}
onChange={setSubTab}
showMy={status === "authenticated"}
/>
<div className="relative">
<SubTabs
value={subTab}
onChange={setSubTab}
showMy={status === "authenticated"}
/>
{loadingCards && cards !== undefined && (
<span className="pointer-events-none absolute top-[9px] left-full ml-3 -translate-y-1/2">
<RefreshSpinner />
</span>
)}
</div>
<CollectionToolbar
sortLabel={SORT_LABELS[sortKey]}
onSortClick={() =>
Expand All @@ -219,7 +238,7 @@ function App() {
/>
</div>
<div className="mt-4">
{loadingCards ? (
{loadingCards && cards === undefined ? (
<div className="rounded-lg border border-dashed border-line px-6 py-12 text-center text-ink-4">
Loading…
</div>
Expand Down Expand Up @@ -253,7 +272,7 @@ function App() {
{status !== "authenticated" && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 rounded-lg bg-bg/55 backdrop-blur-sm">
<p className="text-sm text-ink-2">
Login as contract owner to access this feature
Login to burn DashMint tokens and create cards
</p>
<button
type="button"
Expand All @@ -264,28 +283,13 @@ function App() {
</button>
</div>
)}
{/* Overlay: logged in but not the contract owner */}
{status === "authenticated" &&
contractOwnerId &&
identityId !== contractOwnerId && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 rounded-lg bg-bg/55 backdrop-blur-sm">
<p className="max-w-sm text-center text-sm text-ink-2">
Only the contract owner can mint new cards. Register your
own new contract in Settings to try this feature.
</p>
<button
type="button"
onClick={() => setLoginOpen(true)}
className="rounded-md bg-accent px-6 py-2.5 text-sm font-semibold text-bg transition hover:bg-accent-dim"
>
Settings
</button>
</div>
)}

{contractId && (
<div className="mx-auto max-w-[540px]">
<MintForm contractId={contractId} onMinted={refresh} />
<MintForm
contractId={contractId}
dashMintTokenBalance={dashMintTokenBalance}
onMinted={refresh}
/>
</div>
)}
</section>
Expand Down
10 changes: 10 additions & 0 deletions example-apps/dashmint-lab/src/components/CollectionToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ export function CollectionToolbar({
</button>
);
}

export function RefreshSpinner() {
return (
<span
aria-label="Refreshing"
role="status"
className="inline-block h-3 w-3 animate-spin rounded-full border border-ink-4 border-t-transparent"
/>
);
}
Loading