Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(dashmint-lab): show mint supply, collapse form when sold out by thephez · Pull Request #89 · dashpay/platform-tutorials · GitHub
Skip to content
Merged
9 changes: 7 additions & 2 deletions example-apps/dashmint-lab/public/dashmint-lite.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,14 +124,19 @@ <h2>Browse cards</h2>
// 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';
const CONTRACT_ID = '5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa';
const DOC_TYPE = 'card';

// Connect to testnet. testnetTrusted() uses the SDK's bundled list of trusted
// nodes — no node URL or config needed. connect() does the gRPC handshake
// + initial sync. No identity or signing is required for read-only queries.
//
// Workaround: pin the platform protocol version for evo-sdk dev.6 so the
// SDK doesn't ask testnet for a newer protocol it can't decode. Mirrors
// PLATFORM_VERSION_OVERRIDE in setupDashClient-core.mjs. Remove once a
// fixed SDK release lands.
async function connectSdk() {
const sdk = EvoSDK.testnetTrusted();
const sdk = EvoSDK.testnetTrusted({ version: 11 });
await sdk.connect();
return sdk;
}
Expand Down
8 changes: 8 additions & 0 deletions example-apps/dashmint-lab/src/App.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,14 @@ function App() {
else if (status === "browsing") setSubTab("all");
}, [status]);

// Re-fetch token balance whenever the Mint tab becomes active. The balance
// effect in SessionContext only runs on login/logout/contract change, so
// without this prompt the value could be stale (read-after-write lag from
// a recent mint elsewhere, or simply a value from many minutes ago).
useEffect(() => {
if (tab === "mint" && status === "authenticated") refreshBalance();
}, [tab, status, refreshBalance]);

// 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
Expand Down
119 changes: 107 additions & 12 deletions example-apps/dashmint-lab/src/components/MintForm.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,23 @@
* Mint-a-card form. Calls src/dash/mintCard directly; session provides
* sdk, keyManager, and the contract ID.
*/
import { useState, type FormEvent } from "react";
import { useEffect, useState, type FormEvent } from "react";
import { drawStarterPack, STARTER_PACK_SIZE } from "../data/starterPack";
import { errorMessage } from "../dash/logger";
import { mintCard } from "../dash/mintCard";
import { DASHMINT_TOKEN_COST } from "../dash/dashMintToken";
import {
DASHMINT_TOKEN_COST,
DASHMINT_TOKEN_SUPPLY,
fetchCardsMintedCount,
} from "../dash/dashMintToken";
import { useSession } from "../session/useSession";
import { OddsTable } from "./OddsTable";

// Module-level cache so the supply count survives tab unmounts. When the
// user navigates back to Mint we render the last known value immediately
// and silently refetch — no "—" placeholder flash.
const mintedCountCache = new Map<string, bigint>();

export interface MintFormProps {
contractId: string;
dashMintTokenBalance?: bigint | null;
Expand All@@ -26,17 +35,47 @@ export function MintForm({
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [mintingPack, setMintingPack] = useState(false);
const [mintedCount, setMintedCount] = useState<bigint | null>(
() => mintedCountCache.get(contractId) ?? null,
);
const starterPackTokenCost = BigInt(STARTER_PACK_SIZE);

useEffect(() => {
if (!session.sdk) return;
let cancelled = false;
fetchCardsMintedCount({ sdk: session.sdk, contractId })
.then((count) => {
mintedCountCache.set(contractId, count);
if (!cancelled) setMintedCount(count);
})
.catch(() => {
// Keep the previous (possibly cached) value on failure so the UI
// doesn't regress to "—" just because a refetch hiccuped.
});
return () => {
cancelled = true;
};
}, [session.sdk, contractId, submitting, mintingPack]);
Comment thread
thephez marked this conversation as resolved.
const soldOut = mintedCount !== null && mintedCount >= DASHMINT_TOKEN_SUPPLY;
const starterPackSoldOut =
mintedCount !== null &&
DASHMINT_TOKEN_SUPPLY - mintedCount < starterPackTokenCost;
const hasInsufficientTokensForCard =
dashMintTokenBalance !== null && dashMintTokenBalance < DASHMINT_TOKEN_COST;
const hasInsufficientTokensForStarterPack =
dashMintTokenBalance !== null &&
dashMintTokenBalance < starterPackTokenCost;
const mintedPercent =
mintedCount === null
? 0
: Math.min(100, Number((mintedCount * 100n) / DASHMINT_TOKEN_SUPPLY));

async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForCard) return;
if (submitting || mintingPack || soldOut || hasInsufficientTokensForCard) {
return;
}
setSubmitting(true);
try {
await mintCard({
Expand All@@ -58,7 +97,12 @@ export function MintForm({

async function handleStarterPack() {
if (!session.sdk || !session.keyManager) return;
if (submitting || mintingPack || hasInsufficientTokensForStarterPack) {
if (
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
) {
return;
}
setMintingPack(true);
Expand All@@ -83,6 +127,41 @@ export function MintForm({
}
}

const supplyBlock = (
<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
Supply
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{mintedCount === null
? "—"
: `${mintedCount.toString()} / ${DASHMINT_TOKEN_SUPPLY.toString()} minted`}
</div>
{mintedCount !== null && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full ${soldOut ? "bg-danger" : "bg-accent"}`}
style={{ width: `${mintedPercent}%` }}
/>
</div>
)}
{soldOut && (
<p className="mt-2 rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
All cards have been minted.
</p>
)}
</div>
);

if (soldOut) {
return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
{supplyBlock}
<OddsTable />
</div>
);
}

return (
<div className="flex flex-col gap-5 rounded-xl border border-line bg-surface p-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
Expand All@@ -93,9 +172,11 @@ export function MintForm({
</p>
</div>

{supplyBlock}

<div className="rounded-md border border-line bg-bg px-3 py-2">
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-ink-4">
DashMint tokens
Your DashMint token balance
</div>
<div className="mt-1 font-mono text-[14px] text-ink">
{dashMintTokenBalance === null
Expand DownExpand Up@@ -176,23 +257,37 @@ export function MintForm({
Starter Pack
</h2>
<p className="text-[12px] leading-[1.55] text-ink-3">
Mint a random set of sample cards from the tutorial collection. Costs{" "}
{STARTER_PACK_SIZE} DashMint tokens.
Mint {STARTER_PACK_SIZE} random cards from the tutorial collection.
Costs {STARTER_PACK_SIZE} DashMint tokens (1 per card).
</p>
{hasInsufficientTokensForStarterPack && (
{starterPackSoldOut ? (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter Pack.
Not enough remaining supply for a Starter Pack.
</p>
) : (
hasInsufficientTokensForStarterPack && (
<p className="rounded-md border border-[oklch(30%_0.08_25)] bg-[oklch(22%_0.04_25)] px-3 py-2 text-[12px] font-medium leading-[1.45] text-danger">
You need {STARTER_PACK_SIZE} DashMint tokens to open a Starter
Pack.
</p>
)
)}
<button
type="button"
onClick={handleStarterPack}
disabled={
submitting || mintingPack || hasInsufficientTokensForStarterPack
submitting ||
mintingPack ||
starterPackSoldOut ||
hasInsufficientTokensForStarterPack
}
className="self-start rounded-md border border-line-2 px-4 py-2 text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
className="h-10 rounded-md border border-line-2 px-[18px] text-[13px] font-semibold text-ink transition hover:border-accent-dim hover:text-ink disabled:cursor-not-allowed disabled:border-line disabled:text-ink-4"
>
{mintingPack ? "Minting…" : "Open Starter Pack"}
{mintingPack
? "Minting…"
: starterPackSoldOut
? "Sold out"
: "Open Starter Pack"}
</button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion example-apps/dashmint-lab/src/dash/contractStorage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ const STORAGE_KEY = "dashmint-lab.contractId";
* the Settings modal or register their own.
*/
export const DEFAULT_CONTRACT_ID =
"GDBN1h52Zcs8hSSKBoz67WDyWmUwcRyCSJjXBgKPty94";
"5hK6SMfN4m2vU1t9qhvngUUQjsXeMNwr8MZdFeGBH8Aa";

export function loadStoredContractId(): string | null {
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
Expand Down
18 changes: 18 additions & 0 deletions example-apps/dashmint-lab/src/dash/dashMintToken.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,3 +38,21 @@ export async function fetchDashMintTokenBalance({
const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
return balances.get(tokenId) ?? 0n;
}

// Every mint burns exactly one DashMint token (manual burns/mints are locked
// in the contract), so cards minted = SUPPLY - current circulating supply.
export async function fetchCardsMintedCount({
sdk,
contractId,
}: {
sdk: DashSdk;
contractId: string;
}): Promise<bigint> {
const tokenId = await sdk.tokens.calculateId(
contractId,
DASHMINT_TOKEN_POSITION,
);
const supply = await sdk.tokens.totalSupply(tokenId);
const remaining = supply?.totalSupply ?? DASHMINT_TOKEN_SUPPLY;
return DASHMINT_TOKEN_SUPPLY - remaining;
}
3 changes: 3 additions & 0 deletions example-apps/dashmint-lab/src/dash/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,9 @@ export interface DashSdk {
identityId: string,
tokenIds: string[],
): Promise<Map<string, bigint>>;
totalSupply(
tokenId: string,
): Promise<{ totalSupply: bigint; tokenId: string } | undefined>;
};
dpns: {
username(identityId: string): Promise<string | null | undefined>;
Expand Down
Loading