Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions example-apps/dashmint-lab/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
<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>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
/>
</head>
<body>
<div id="root"></div>
Expand Down
6 changes: 5 additions & 1 deletion example-apps/dashmint-lab/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,12 @@ function App() {
}, [refreshBalance]);

// 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.
useEffect(() => {
if (status === "idle") void browseOnly();
if (status !== "idle") return;
const raf = requestAnimationFrame(() => void browseOnly());
return () => cancelAnimationFrame(raf);
}, [status, browseOnly]);

// Default sub-tab: "My" when logged in, otherwise "All".
Expand Down
58 changes: 14 additions & 44 deletions example-apps/dashmint-lab/src/dash/contract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* NFT card data contract schema + ensureContract().
* NFT card data contract schema + registerContract / ensureContract.
*
* WHAT: A Dash Platform "data contract" defines the schema for documents.
* This one describes a single document type (`card`) with four fields
Expand All @@ -11,13 +11,26 @@
* tradeMode: 1 — documents can be priced and purchased (0 to disable)
* creationRestrictionMode: 1 — (1 - only the contract owner can mint; 0 - anyone can mint)
*
* Storage helpers (loadStoredContractId, saveContractId, …) and the owner
* lookup live in contractStorage.ts so they can be imported without
* pulling the @dashevo/evo-sdk runtime into the entry bundle.
*
* SDK methods: new DataContract({ ... }), sdk.contracts.publish(...)
*/
import { DataContract } from "@dashevo/evo-sdk";

import { loadStoredContractId, saveContractId } from "./contractStorage";
import type { Logger } from "./logger";
import type { DashKeyManager, DashSdk } from "./types";

export {
DEFAULT_CONTRACT_ID,
clearStoredContractId,
fetchContractOwnerId,
loadStoredContractId,
saveContractId,
} from "./contractStorage";

export const CARD_SCHEMAS = {
card: {
type: "object",
Expand Down Expand Up @@ -62,49 +75,6 @@ export const CARD_SCHEMAS = {
},
} as const;

/**
* Fetch the owner identity ID for a given data contract.
*
* SDK method: sdk.contracts.fetch(...)
*/
export async function fetchContractOwnerId({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<string | null> {
const contract = await sdk.contracts.fetch(contractId);
if (!contract) return null;
const json =
typeof contract.toJSON === "function" ? contract.toJSON() : contract;
const ownerId = json.$ownerId ?? json.ownerId ?? null;
return ownerId ? String(ownerId) : null;
}

const STORAGE_KEY = "dashmint-lab.contractId";

/**
* Default contract ID baked into the tutorial so browse-only mode works
* on a fresh machine without any setup. Comes from the original
* HTML tutorial's pre-deployed testnet contract. Users can override it
* in the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
}

export function saveContractId(id: string): void {
localStorage.setItem(STORAGE_KEY, id);
}

export function clearStoredContractId(): void {
localStorage.removeItem(STORAGE_KEY);
}

/**
* Register a fresh NFT card data contract on Platform and persist its ID.
*
Expand Down
46 changes: 46 additions & 0 deletions example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Contract ID persistence + owner lookup. Split from contract.ts so the
* session bootstrap can import these helpers without dragging the
* @dashevo/evo-sdk module (and its WASM bundle) into the entry chunk.
*
* SDK method (fetchContractOwnerId): sdk.contracts.fetch(...)
*/
import type { DashSdk } from "./types";

const STORAGE_KEY = "dashmint-lab.contractId";

/**
* Default contract ID baked into the tutorial so browse-only mode works
* on a fresh machine without any setup. Comes from the original
* HTML tutorial's pre-deployed testnet contract. Users can override it
* in the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
}

export function saveContractId(id: string): void {
localStorage.setItem(STORAGE_KEY, id);
}

export function clearStoredContractId(): void {
localStorage.removeItem(STORAGE_KEY);
}

export async function fetchContractOwnerId({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<string | null> {
const contract = await sdk.contracts.fetch(contractId);
if (!contract) return null;
const json =
typeof contract.toJSON === "function" ? contract.toJSON() : contract;
const ownerId = json.$ownerId ?? json.ownerId ?? null;
return ownerId ? String(ownerId) : null;
}
29 changes: 26 additions & 3 deletions example-apps/dashmint-lab/src/session/SessionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,38 @@ import {
type ReactNode,
} from "react";

import { createClient } from "../dash/client";
import { IdentityKeyManager } from "../dash/keyManager";
import {
clearStoredContractId,
fetchContractOwnerId,
loadStoredContractId,
saveContractId,
} from "../dash/contract";
} from "../dash/contractStorage";
import { errorMessage, type Logger } from "../dash/logger";
import type { DashKeyManager, DashSdk } from "../dash/types";

// The SDK + IdentityKeyManager pull in @dashevo/evo-sdk (and its ~8MB WASM
// bundle), so we load them lazily on first use to keep the app shell off
// the critical path. Cached after first call.
let sdkModulePromise: Promise<{
createClient: (network: string) => Promise<DashSdk>;
IdentityKeyManager: typeof import("../../../../setupDashClient-core.mjs").IdentityKeyManager;
}> | null = null;
function loadSdkModule() {
if (!sdkModulePromise) {
sdkModulePromise = import("../../../../setupDashClient-core.mjs").catch(
(err) => {
// Clear the cache on failure so a subsequent connect/login retry
// can re-attempt the import (e.g., after a transient chunk fetch
// failure). Without this, every retry would await the same
// rejected promise and fail immediately.
sdkModulePromise = null;
throw err;
},
);
}
return sdkModulePromise;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export type SessionStatus =
| "idle"
| "connecting"
Expand Down Expand Up @@ -162,6 +183,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
setStatus("connecting");
setError(null);
log("Connecting to Dash Platform testnet…");
const { createClient } = await loadSdkModule();
const connected = await createClient("testnet");
log("Connected to testnet.", "info");
setSdk(connected);
Expand All @@ -176,6 +198,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
try {
const connected = sdk ?? (await connect());
log("Deriving identity keys from mnemonic…");
const { IdentityKeyManager } = await loadSdkModule();
const km = await IdentityKeyManager.create({
sdk: connected,
mnemonic: trimmed,
Expand Down
4 changes: 3 additions & 1 deletion example-apps/dashmint-lab/src/styles/globals.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap");
/* Google Fonts (Inter + JetBrains Mono) loaded via <link> in index.html
* with rel="preconnect" so the font fetch parallelizes with JS download
* instead of being serialized behind this stylesheet. */
@import "tailwindcss";

/*
Expand Down
9 changes: 4 additions & 5 deletions example-apps/dashmint-lab/test/SessionContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ const {
mockToastError: vi.fn(),
}));

vi.mock("../src/dash/client", () => ({
// SessionContext dynamic-imports the SDK core module directly (not via the
// app's client.ts/keyManager.ts wrappers), so mock that module instead.
vi.mock("../../../setupDashClient-core.mjs", () => ({
createClient: mockCreateClient,
}));

vi.mock("../src/dash/keyManager", () => ({
IdentityKeyManager: {
create: mockIdentityKeyManagerCreate,
},
}));

vi.mock("../src/dash/contract", () => ({
vi.mock("../src/dash/contractStorage", () => ({
DEFAULT_CONTRACT_ID: "default-contract-id",
fetchContractOwnerId: mockFetchContractOwnerId,
loadStoredContractId: mockLoadStoredContractId,
Expand Down
12 changes: 12 additions & 0 deletions example-apps/dashmint-lab/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ export default defineConfig({
},
},
plugins: [react(), tailwindcss()],
build: {
// Vite auto-injects <link rel="modulepreload"> for every dynamic-import
// chunk it discovers at build time. For the ~8MB Evo SDK + WASM chunk
// this defeats the whole point of dynamic-importing it: the browser
// races to fetch the SDK in parallel with the entry chunk, blocking
// FCP. Strip the SDK preload so it only fetches when SessionContext
// actually triggers the dynamic import.
modulePreload: {
resolveDependencies: (_filename, deps) =>
deps.filter((d) => !d.includes("evo-sdk")),
},
},
test: {
environment: "node",
include: ["test/**/*.test.{ts,tsx}"],
Expand Down