diff --git a/example-apps/dashmint-lab/index.html b/example-apps/dashmint-lab/index.html index c4277a3..2cf7c8d 100644 --- a/example-apps/dashmint-lab/index.html +++ b/example-apps/dashmint-lab/index.html @@ -5,6 +5,12 @@ DashMint Lab — Dash Platform NFTs + + +
diff --git a/example-apps/dashmint-lab/src/App.tsx b/example-apps/dashmint-lab/src/App.tsx index 149323f..ac80077 100644 --- a/example-apps/dashmint-lab/src/App.tsx +++ b/example-apps/dashmint-lab/src/App.tsx @@ -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". diff --git a/example-apps/dashmint-lab/src/dash/contract.ts b/example-apps/dashmint-lab/src/dash/contract.ts index ccb2867..d7c79aa 100644 --- a/example-apps/dashmint-lab/src/dash/contract.ts +++ b/example-apps/dashmint-lab/src/dash/contract.ts @@ -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 @@ -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", @@ -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 { - 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. * diff --git a/example-apps/dashmint-lab/src/dash/contractStorage.ts b/example-apps/dashmint-lab/src/dash/contractStorage.ts new file mode 100644 index 0000000..449e380 --- /dev/null +++ b/example-apps/dashmint-lab/src/dash/contractStorage.ts @@ -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 { + 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; +} diff --git a/example-apps/dashmint-lab/src/session/SessionContext.tsx b/example-apps/dashmint-lab/src/session/SessionContext.tsx index 7f74078..cd1f5f1 100644 --- a/example-apps/dashmint-lab/src/session/SessionContext.tsx +++ b/example-apps/dashmint-lab/src/session/SessionContext.tsx @@ -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; + 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; +} + export type SessionStatus = | "idle" | "connecting" @@ -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); @@ -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, diff --git a/example-apps/dashmint-lab/src/styles/globals.css b/example-apps/dashmint-lab/src/styles/globals.css index ff30c04..551994f 100644 --- a/example-apps/dashmint-lab/src/styles/globals.css +++ b/example-apps/dashmint-lab/src/styles/globals.css @@ -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 in index.html + * with rel="preconnect" so the font fetch parallelizes with JS download + * instead of being serialized behind this stylesheet. */ @import "tailwindcss"; /* diff --git a/example-apps/dashmint-lab/test/SessionContext.test.tsx b/example-apps/dashmint-lab/test/SessionContext.test.tsx index 238c5db..3faba2f 100644 --- a/example-apps/dashmint-lab/test/SessionContext.test.tsx +++ b/example-apps/dashmint-lab/test/SessionContext.test.tsx @@ -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, diff --git a/example-apps/dashmint-lab/vite.config.ts b/example-apps/dashmint-lab/vite.config.ts index 274d206..aaabc8e 100644 --- a/example-apps/dashmint-lab/vite.config.ts +++ b/example-apps/dashmint-lab/vite.config.ts @@ -23,6 +23,18 @@ export default defineConfig({ }, }, plugins: [react(), tailwindcss()], + build: { + // Vite auto-injects 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}"],