From 52250ddd356e363dcdef225625d39294f7ba5fdc Mon Sep 17 00:00:00 2001 From: Dado Date: Thu, 12 Feb 2026 19:26:49 +0100 Subject: [PATCH 1/7] feat: add initial version of deploy and checkpoint handlers and use them in ore react example --- examples/ore-react/package.json | 14 +- examples/ore-react/src/App.tsx | 31 ++- .../ore-react/src/components/BlockGrid.tsx | 25 +- .../ore-react/src/components/DeployButton.tsx | 239 ++++++++++++++++++ .../ore-react/src/components/OreDashboard.tsx | 42 ++- examples/ore-react/src/components/index.ts | 1 + examples/ore-react/vite.config.ts | 16 +- stacks/sdk/typescript/src/ore/index.ts | 174 +++++++++++++ typescript/core/package-lock.json | 29 +++ typescript/core/package.json | 4 +- typescript/core/src/instructions/pda.ts | 36 +-- typescript/react/src/stack.ts | 12 +- 12 files changed, 585 insertions(+), 38 deletions(-) create mode 100644 examples/ore-react/src/components/DeployButton.tsx diff --git a/examples/ore-react/package.json b/examples/ore-react/package.json index 0298d8ef..db250335 100644 --- a/examples/ore-react/package.json +++ b/examples/ore-react/package.json @@ -9,8 +9,15 @@ "preview": "vite preview" }, "dependencies": { - "hyperstack-react": "^0.5.0", - "hyperstack-stacks": "^0.5.0", + "@solana/wallet-adapter-phantom": "^0.9.24", + "@solana/wallet-adapter-react": "^0.15.39", + "@solana/wallet-adapter-react-ui": "^0.9.39", + "@solana/wallet-adapter-wallets": "^0.19.37", + "@solana/web3.js": "^1.98.4", + "buffer": "^6.0.3", + "hyperstack-react": "file:../../typescript/react", + "hyperstack-stacks": "file:../../stacks/sdk/typescript", + "hyperstack-typescript": "file:../../typescript/core", "react": "^19.0.0", "react-dom": "^19.0.0", "zod": "^3.23.0", @@ -25,6 +32,7 @@ "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "^5.3.0", - "vite": "^5.0.0" + "vite": "^5.0.0", + "vite-plugin-node-polyfills": "^0.25.0" } } diff --git a/examples/ore-react/src/App.tsx b/examples/ore-react/src/App.tsx index e9d4df54..09b71de5 100644 --- a/examples/ore-react/src/App.tsx +++ b/examples/ore-react/src/App.tsx @@ -1,13 +1,34 @@ import { OreDashboard } from './components'; import { HyperstackProvider } from 'hyperstack-react'; import { ThemeProvider } from './hooks/useTheme'; +import { useMemo } from 'react'; +import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'; +import '@solana/wallet-adapter-react-ui/styles.css'; export default function App() { + const endpoint = import.meta.env.VITE_RPC_URL; // add your own RPC URL in a .env file + + // Setup wallet adapters + const wallets = useMemo( + () => [ + new PhantomWalletAdapter(), + ], + [] + ); + return ( - - - - - + + + + + + + + + + + ); } diff --git a/examples/ore-react/src/components/BlockGrid.tsx b/examples/ore-react/src/components/BlockGrid.tsx index 2f1bc212..3f521bff 100644 --- a/examples/ore-react/src/components/BlockGrid.tsx +++ b/examples/ore-react/src/components/BlockGrid.tsx @@ -3,9 +3,11 @@ import { MinerIcon, SolanaIcon } from './icons'; interface BlockGridProps { round: ValidatedOreRound | undefined; + selectedSquares: number[]; + onSquareClick: (squareId: number) => void; } -export function BlockGrid({ round }: BlockGridProps) { +export function BlockGrid({ round, selectedSquares, onSquareClick }: BlockGridProps) { const blocks = round ? round.state.deployed_per_square_ui.map((deployedUi, i) => ({ id: i + 1, @@ -28,15 +30,21 @@ export function BlockGrid({ round }: BlockGridProps) { width: 'calc((100vh - 120px - 4 * 0.5rem) / 5 * 5 + 4 * 0.5rem)' }} > - {blocks.map((block) => ( -
{ + const isSelected = selectedSquares.includes(block.id); + return ( +
- - ))} + {isSelected && ( +
+ ✓ +
+ )} + + )})} ); } diff --git a/examples/ore-react/src/components/DeployButton.tsx b/examples/ore-react/src/components/DeployButton.tsx new file mode 100644 index 00000000..9992c303 --- /dev/null +++ b/examples/ore-react/src/components/DeployButton.tsx @@ -0,0 +1,239 @@ +/** + * Deploy Button with Automatic Checkpoint + */ + +import { useState } from 'react'; +import { useWallet, useConnection } from '@solana/wallet-adapter-react'; +import { useHyperstack } from 'hyperstack-react'; +import { ORE_STREAM_STACK } from 'hyperstack-stacks/ore'; +import { Transaction, PublicKey } from '@solana/web3.js'; + +export function DeployButton({ currentRound, minerData, recentRounds, selectedSquares }: { + currentRound?: any; + minerData?: any; + recentRounds?: any[]; + selectedSquares: number[]; +}) { + const wallet = useWallet(); + const { connection } = useConnection(); + const [amount, setAmount] = useState('0.001'); + const [isProcessing, setIsProcessing] = useState(false); + const [processingStep, setProcessingStep] = useState<'checkpoint' | 'deploy' | null>(null); + const [result, setResult] = useState<{ + status: 'success' | 'error'; + checkpointSignature?: string; + deploySignature?: string; + error?: string; + } | null>(null); + + const stack = useHyperstack(ORE_STREAM_STACK); + const checkpoint = stack.instructions?.checkpoint?.useMutation(); + const deploy = stack.instructions?.deploy?.useMutation(); + + // Check if checkpoint is needed + const needsCheckpoint = + minerData?.state?.round_id != null && + currentRound?.id?.round_id != null && + minerData.state.round_id < currentRound.id.round_id; + + const handleDeploy = async () => { + if (!wallet.connected || !wallet.publicKey) { + return; + } + + if (!currentRound?.id?.round_address || !currentRound?.entropy?.entropy_var_address || selectedSquares.length === 0) { + return; + } + + setIsProcessing(true); + setProcessingStep(needsCheckpoint ? 'checkpoint' : 'deploy'); + setResult(null); + + try { + const amountLamports = BigInt(Math.floor(parseFloat(amount) * 1e9)); + + // TO DO: check + const walletAdapter = { + publicKey: wallet.publicKey!.toBase58(), + signAndSend: async (transaction: any) => { + const tx = new Transaction(); + for (const ix of transaction.instructions) { + tx.add({ + programId: new PublicKey(ix.programId), + keys: ix.keys.map((key: any) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable, + })), + data: Buffer.from(ix.data), + }); + } + return await wallet.sendTransaction!(tx, connection); + } + }; + + let checkpointSig: string | undefined; + + // Call checkpoint first if needed + if (needsCheckpoint) { + const oldRound = recentRounds?.find(r => r.id?.round_id === minerData.state.round_id); + + if (!oldRound?.id?.round_address) { + throw new Error(`Round ${minerData.state.round_id} not available. Cannot checkpoint.`); + } + + const checkpointResult = await checkpoint.submit( + {}, + { + wallet: walletAdapter, + accounts: { round: oldRound.id.round_address }, + } + ); + checkpointSig = checkpointResult.signature; + setProcessingStep('deploy'); + } + + // Then deploy + const deployResult = await deploy.submit( + { + amount: amountLamports, + squares: selectedSquares.length, + }, + { + wallet: walletAdapter, + accounts: { + round: currentRound.id.round_address, + entropyVar: currentRound.entropy.entropy_var_address!, + }, + } + ); + + setResult({ + status: 'success', + checkpointSignature: checkpointSig, + deploySignature: deployResult.signature + }); + + } catch (err: any) { + console.error('Deploy failed:', err); + setResult({ status: 'error', error: err?.message || String(err) }); + } finally { + setIsProcessing(false); + setProcessingStep(null); + } + }; + + return ( +
+

Deploy

+ + {needsCheckpoint && ( +
+ ℹ️ Will checkpoint round {minerData?.state?.round_id} first +
+ )} + +
+
+ + setAmount(e.target.value)} + disabled={isProcessing} + className="w-full px-4 py-2.5 bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-700 rounded-xl + text-stone-800 dark:text-stone-100 placeholder-stone-400 dark:placeholder-stone-500 + focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent + disabled:opacity-50 disabled:cursor-not-allowed transition-all" + placeholder="0.001" + /> +
+ +
+ Selected: {selectedSquares.length} square{selectedSquares.length !== 1 ? 's' : ''} + {selectedSquares.length > 0 && ( + + ({selectedSquares.slice(0, 5).join(', ')}{selectedSquares.length > 5 ? '...' : ''}) + + )} +
+ + + + {isProcessing && ( +
+ ⏳ {processingStep === 'checkpoint' ? 'Checkpointing' : 'Deploying'}... +
+ )} + + {result?.status === 'success' && result.deploySignature && ( +
+
+ ✅ {result.checkpointSignature ? 'Checkpoint + Deploy' : 'Deploy'} successful! +
+ {result.checkpointSignature && ( +
+
Checkpoint:
+ + {result.checkpointSignature.slice(0, 8)}...{result.checkpointSignature.slice(-8)} → + +
+ )} +
+
Deploy:
+ + {result.deploySignature.slice(0, 8)}...{result.deploySignature.slice(-8)} → + +
+ +
+ )} + + {result?.status === 'error' && result.error && ( +
+
❌ Deploy failed
+
{result.error}
+ +
+ )} +
+
+ ); +} diff --git a/examples/ore-react/src/components/OreDashboard.tsx b/examples/ore-react/src/components/OreDashboard.tsx index 769a5f1d..8e5d9666 100644 --- a/examples/ore-react/src/components/OreDashboard.tsx +++ b/examples/ore-react/src/components/OreDashboard.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useHyperstack } from 'hyperstack-react'; import { ORE_STREAM_STACK } from 'hyperstack-stacks/ore'; import { ValidatedOreRoundSchema } from '../schemas/ore-round-validated'; @@ -5,11 +6,31 @@ import { BlockGrid } from './BlockGrid'; import { StatsPanel } from './StatsPanel'; import { ConnectionBadge } from './ConnectionBadge'; import { ThemeToggle } from './ThemeToggle'; +import { DeployButton } from './DeployButton'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { useWallet } from '@solana/wallet-adapter-react'; export function OreDashboard() { + const wallet = useWallet(); const { views, isConnected } = useHyperstack(ORE_STREAM_STACK); const { data: latestRound } = views.OreRound.latest.useOne({ schema: ValidatedOreRoundSchema }); const { data: treasuryData } = views.OreTreasury.list.useOne(); + + const { data: minerData } = views.OreMiner.list.useOne({ + filters: wallet.publicKey ? { authority: wallet.publicKey.toBase58() } : undefined, + }); + + const { data: recentRounds } = views.OreRound.list.use({ take: 10 }); + + const [selectedSquares, setSelectedSquares] = useState([]); + + const handleSquareClick = (squareId: number) => { + setSelectedSquares(prev => + prev.includes(squareId) + ? prev.filter(id => id !== squareId) + : [...prev, squareId] + ); + }; return (
@@ -18,20 +39,33 @@ export function OreDashboard() {

Ore Mining

Live ORE rounds powered by Hyperstack

- +
+ + +
-
+
- +
-
+
+
diff --git a/examples/ore-react/src/components/index.ts b/examples/ore-react/src/components/index.ts index 91858a2b..9e7778f1 100644 --- a/examples/ore-react/src/components/index.ts +++ b/examples/ore-react/src/components/index.ts @@ -2,4 +2,5 @@ export { OreDashboard } from './OreDashboard'; export { BlockGrid } from './BlockGrid'; export { StatsPanel } from './StatsPanel'; export { ConnectionBadge } from './ConnectionBadge'; +export { DeployButton } from './DeployButton'; export * from './icons'; diff --git a/examples/ore-react/vite.config.ts b/examples/ore-react/vite.config.ts index 9ffcc675..956cd048 100644 --- a/examples/ore-react/vite.config.ts +++ b/examples/ore-react/vite.config.ts @@ -1,6 +1,20 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import { nodePolyfills } from 'vite-plugin-node-polyfills' export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + nodePolyfills({ + include: ['buffer', 'process'], + globals: { + Buffer: true, + process: true, + }, + }), + ], + optimizeDeps: { + // exclude local hyperstack packages from pre-bundling + exclude: ['hyperstack-typescript', 'hyperstack-react', 'hyperstack-stacks'], + }, }) diff --git a/stacks/sdk/typescript/src/ore/index.ts b/stacks/sdk/typescript/src/ore/index.ts index 46975eca..09ad110c 100644 --- a/stacks/sdk/typescript/src/ore/index.ts +++ b/stacks/sdk/typescript/src/ore/index.ts @@ -1,4 +1,10 @@ import { z } from 'zod'; +import type { + InstructionHandler, + AccountMeta, + ArgSchema +} from 'hyperstack-typescript'; +import { serializeInstructionData } from 'hyperstack-typescript'; export interface OreRoundEntropy { entropy_end_at?: number | null; @@ -357,6 +363,170 @@ export const OreMinerCompletedSchema = z.object({ automation_snapshot: AutomationSchema, }); +// ============================================================================ +// Instruction Handlers +// ============================================================================ + +const ORE_PROGRAM_ID = "oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv"; +const ENTROPY_PROGRAM_ID = "3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X"; +const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; +const DEPLOY_DISCRIMINATOR = new Uint8Array([6]); +const CHECKPOINT_DISCRIMINATOR = new Uint8Array([2]); + +/* +* Deploy Instruction +*/ +const deployAccounts: AccountMeta[] = [ + { name: "signer", category: "signer", isSigner: true, isWritable: true }, + { name: "authority", category: "signer", isSigner: false, isWritable: true }, + { + name: "automation", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [ + { type: "literal", value: "automation" }, + { type: "accountRef", accountName: "authority" }, + ], + }, + isSigner: false, + isWritable: true, + }, + { + name: "board", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [{ type: "literal", value: "board" }], + }, + isSigner: false, + isWritable: true, + }, + { + name: "config", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [{ type: "literal", value: "config" }], + }, + isSigner: false, + isWritable: true, + }, + { + name: "miner", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [ + { type: "literal", value: "miner" }, + { type: "accountRef", accountName: "authority" }, + ], + }, + isSigner: false, + isWritable: true, + }, + { name: "round", category: "userProvided", isSigner: false, isWritable: true }, + { name: "systemProgram", category: "known", knownAddress: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, + { name: "oreProgram", category: "known", knownAddress: ORE_PROGRAM_ID, isSigner: false, isWritable: false }, + { name: "entropyVar", category: "userProvided", isSigner: false, isWritable: true }, + { name: "entropyProgram", category: "known", knownAddress: ENTROPY_PROGRAM_ID, isSigner: false, isWritable: false }, +]; + +const deployArgsSchema: ArgSchema[] = [ + { name: "amount", type: "u64" }, + { name: "squares", type: "u32" }, +]; + +const deployInstruction: InstructionHandler = { + programId: ORE_PROGRAM_ID, + accounts: deployAccounts, + errors: [ + { code: 0, name: "AmountTooSmall", msg: "Amount too small" }, + { code: 1, name: "NotAuthorized", msg: "Not authorized" }, + ], + build: (args, resolvedAccounts) => { + if (typeof args.amount !== 'bigint' && typeof args.amount !== 'number') { + throw new Error('amount must be a number or bigint'); + } + if (typeof args.squares !== 'number') { + throw new Error('squares must be a number'); + } + + const buffer = serializeInstructionData(DEPLOY_DISCRIMINATOR, args, deployArgsSchema); + const data = new Uint8Array(buffer); + + const keys = deployAccounts.map(meta => ({ + pubkey: resolvedAccounts[meta.name]!, + isSigner: meta.isSigner, + isWritable: meta.isWritable, + })); + + return { programId: ORE_PROGRAM_ID, keys, data }; + }, +}; + +/* +* Checkpoint Instruction +*/ +const checkpointAccounts: AccountMeta[] = [ + { name: "signer", category: "signer", isSigner: true, isWritable: true }, + { + name: "board", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [{ type: "literal", value: "board" }], + }, + isSigner: false, + isWritable: false, + }, + { + name: "miner", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [ + { type: "literal", value: "miner" }, + { type: "accountRef", accountName: "signer" }, + ], + }, + isSigner: false, + isWritable: true, + }, + { name: "round", category: "userProvided", isSigner: false, isWritable: true }, + { + name: "treasury", + category: "pda", + pdaConfig: { + programId: ORE_PROGRAM_ID, + seeds: [{ type: "literal", value: "treasury" }], + }, + isSigner: false, + isWritable: true, + }, + { name: "systemProgram", category: "known", knownAddress: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, +]; + +const checkpointInstruction: InstructionHandler = { + programId: ORE_PROGRAM_ID, + accounts: checkpointAccounts, + errors: [ + { code: 0, name: "AmountTooSmall", msg: "Amount too small" }, + { code: 1, name: "NotAuthorized", msg: "Not authorized" }, + ], + build: (args, resolvedAccounts) => { + const data = CHECKPOINT_DISCRIMINATOR; + + const keys = checkpointAccounts.map(meta => ({ + pubkey: resolvedAccounts[meta.name]!, + isSigner: meta.isSigner, + isWritable: meta.isWritable, + })); + + return { programId: ORE_PROGRAM_ID, keys, data }; + }, +}; + // ============================================================================ // View Definition Types (framework-agnostic) // ============================================================================ @@ -402,6 +572,10 @@ export const ORE_STREAM_STACK = { list: listView('OreMiner/list'), }, }, + instructions: { + checkpoint: checkpointInstruction, + deploy: deployInstruction, + } as const, schemas: { Automation: AutomationSchema, Miner: MinerSchema, diff --git a/typescript/core/package-lock.json b/typescript/core/package-lock.json index fdfc723f..50590d5e 100644 --- a/typescript/core/package-lock.json +++ b/typescript/core/package-lock.json @@ -9,6 +9,8 @@ "version": "0.5.2", "license": "MIT", "dependencies": { + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "pako": "^2.1.0", "zod": "^3.24.1" }, @@ -616,6 +618,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/typescript/core/package.json b/typescript/core/package.json index 97bcd1d6..238e7e02 100644 --- a/typescript/core/package.json +++ b/typescript/core/package.json @@ -47,13 +47,15 @@ "node": ">=16.0.0" }, "dependencies": { + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "pako": "^2.1.0", "zod": "^3.24.1" }, "devDependencies": { - "@types/pako": "^2.0.3", "@rollup/plugin-typescript": "^11.0.0", "@types/node": "^20.0.0", + "@types/pako": "^2.0.3", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.0.0", diff --git a/typescript/core/src/instructions/pda.ts b/typescript/core/src/instructions/pda.ts index ff4f81d9..c947f3a1 100644 --- a/typescript/core/src/instructions/pda.ts +++ b/typescript/core/src/instructions/pda.ts @@ -4,6 +4,9 @@ * Implements Solana's PDA derivation algorithm without depending on @solana/web3.js. */ +import { sha256 } from '@noble/hashes/sha2.js'; +import { ed25519 } from '@noble/curves/ed25519.js'; + // Base58 alphabet (Bitcoin/Solana style) const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; @@ -77,12 +80,10 @@ export function encodeBase58(bytes: Uint8Array): string { } /** - * SHA-256 hash function (synchronous, Node.js). + * SHA-256 hash function (synchronous). */ function sha256Sync(data: Uint8Array): Uint8Array { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createHash } = require('crypto'); - return new Uint8Array(createHash('sha256').update(Buffer.from(data)).digest()); + return sha256(data); } /** @@ -100,18 +101,17 @@ async function sha256Async(data: Uint8Array): Promise { /** * Check if a point is on the ed25519 curve. - * A valid PDA must be OFF the curve. - * - * This is a simplified implementation. - * In practice, most PDAs are valid on first try with bump=255. + * A valid PDA must be OFF the curve to ensure it has no corresponding private key. + * Uses @noble/curves for browser-compatible ed25519 validation. */ -function isOnCurve(_publicKey: Uint8Array): boolean { - // Simplified heuristic: actual curve check requires ed25519 math - // For Solana PDAs, we try bumps from 255 down - // The first bump (255) almost always produces a valid off-curve point - // We return false here to accept the first result - // In production with @solana/web3.js, use PublicKey.isOnCurve() - return false; +function isOnCurve(publicKey: Uint8Array): boolean { + try { + // Try to decode as an ed25519 point - if successful, it's on the curve + ed25519.Point.fromBytes(publicKey); + return true; // Point is on curve - invalid for PDA + } catch { + return false; // Point is off curve - valid for PDA + } } /** @@ -200,7 +200,8 @@ export async function findProgramAddress( const hash = await sha256Async(buffer); if (!isOnCurve(hash)) { - return [encodeBase58(hash), bump]; + const result = encodeBase58(hash); + return [result, bump]; } } @@ -228,7 +229,8 @@ export function findProgramAddressSync( const hash = sha256Sync(buffer); if (!isOnCurve(hash)) { - return [encodeBase58(hash), bump]; + const result = encodeBase58(hash); + return [result, bump]; } } diff --git a/typescript/react/src/stack.ts b/typescript/react/src/stack.ts index 0d1e996b..7a3b95df 100644 --- a/typescript/react/src/stack.ts +++ b/typescript/react/src/stack.ts @@ -159,10 +159,20 @@ export function useHyperstack( useMutation: () => useInstructionMutation(executeFn as InstructionExecutor) }; } + } else if (stack.instructions) { + for (const instructionName of Object.keys(stack.instructions)) { + const placeholderExecutor = () => { + throw new Error(`Cannot execute ${instructionName}: client not connected`); + }; + result[instructionName] = { + execute: placeholderExecutor, + useMutation: () => useInstructionMutation(placeholderExecutor) + }; + } } return result; - }, [client]); + }, [client, stack.instructions]); return { views: views as BuildViewInterface, From c17573b92ebef59d5d008645e887297754e0d905 Mon Sep 17 00:00:00 2001 From: Dado Date: Thu, 12 Feb 2026 23:36:23 +0100 Subject: [PATCH 2/7] fix: miner data and checkpoint instruction call --- .../ore-react/src/components/DeployButton.tsx | 45 +++++++++++++------ .../ore-react/src/components/OreDashboard.tsx | 8 ++-- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/examples/ore-react/src/components/DeployButton.tsx b/examples/ore-react/src/components/DeployButton.tsx index 9992c303..4ad2c817 100644 --- a/examples/ore-react/src/components/DeployButton.tsx +++ b/examples/ore-react/src/components/DeployButton.tsx @@ -30,11 +30,24 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq const checkpoint = stack.instructions?.checkpoint?.useMutation(); const deploy = stack.instructions?.deploy?.useMutation(); - // Check if checkpoint is needed - const needsCheckpoint = - minerData?.state?.round_id != null && - currentRound?.id?.round_id != null && - minerData.state.round_id < currentRound.id.round_id; + const minerRoundId = minerData?.state?.round_id; + const checkpointId = minerData?.state?.checkpoint_id; + const currentRoundId = currentRound?.id?.round_id; + + const hasUncheckpointedRound = + minerRoundId != null && + checkpointId != null && + checkpointId < minerRoundId; + + const canCheckpointNow = + hasUncheckpointedRound && + currentRoundId != null && + minerRoundId < currentRoundId; + + const waitingForRoundToEnd = + hasUncheckpointedRound && + currentRoundId != null && + minerRoundId === currentRoundId; const handleDeploy = async () => { if (!wallet.connected || !wallet.publicKey) { @@ -46,7 +59,7 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq } setIsProcessing(true); - setProcessingStep(needsCheckpoint ? 'checkpoint' : 'deploy'); + setProcessingStep(canCheckpointNow ? 'checkpoint' : 'deploy'); setResult(null); try { @@ -74,12 +87,12 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq let checkpointSig: string | undefined; - // Call checkpoint first if needed - if (needsCheckpoint) { - const oldRound = recentRounds?.find(r => r.id?.round_id === minerData.state.round_id); + // Call checkpoint first only when the miner round is already completed + if (canCheckpointNow) { + const oldRound = recentRounds?.find(r => r.id?.round_id === minerRoundId); if (!oldRound?.id?.round_address) { - throw new Error(`Round ${minerData.state.round_id} not available. Cannot checkpoint.`); + throw new Error(`Round ${minerRoundId} not available. Cannot checkpoint.`); } const checkpointResult = await checkpoint.submit( @@ -127,9 +140,15 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq

Deploy

- {needsCheckpoint && ( + {canCheckpointNow && (
- ℹ️ Will checkpoint round {minerData?.state?.round_id} first + ℹ️ Round {minerRoundId} is completed and uncheckpointed. It will be checkpointed before deploy. +
+ )} + + {waitingForRoundToEnd && ( +
+ ⏳ You have uncheckpointed positions in active round {minerRoundId}. Checkpoint becomes available after this round ends.
)} @@ -173,7 +192,7 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq transition-all duration-200 hover:shadow-md active:scale-[0.98]" > {!wallet.connected ? 'Connect Wallet' : - isProcessing ? (needsCheckpoint ? 'Checkpointing + Deploying...' : 'Deploying...') : + isProcessing ? (canCheckpointNow ? 'Checkpointing + Deploying...' : 'Deploying...') : `Deploy ${amount} SOL`} diff --git a/examples/ore-react/src/components/OreDashboard.tsx b/examples/ore-react/src/components/OreDashboard.tsx index 8e5d9666..10b35d48 100644 --- a/examples/ore-react/src/components/OreDashboard.tsx +++ b/examples/ore-react/src/components/OreDashboard.tsx @@ -15,10 +15,10 @@ export function OreDashboard() { const { views, isConnected } = useHyperstack(ORE_STREAM_STACK); const { data: latestRound } = views.OreRound.latest.useOne({ schema: ValidatedOreRoundSchema }); const { data: treasuryData } = views.OreTreasury.list.useOne(); - - const { data: minerData } = views.OreMiner.list.useOne({ - filters: wallet.publicKey ? { authority: wallet.publicKey.toBase58() } : undefined, - }); + + const { data: minerData } = views.OreMiner.state.use( + wallet.publicKey ? { authority: wallet.publicKey.toBase58() } : undefined + ); const { data: recentRounds } = views.OreRound.list.use({ take: 10 }); From 93d174150f90997cdbeccd181d900f8f10a54a69 Mon Sep 17 00:00:00 2001 From: Dado Date: Sun, 22 Feb 2026 15:49:23 +0100 Subject: [PATCH 3/7] feat: display current or last round winner --- .../ore-react/src/components/OreDashboard.tsx | 79 ++++++++++++++++++- .../ore-react/src/components/StatsPanel.tsx | 16 +++- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/examples/ore-react/src/components/OreDashboard.tsx b/examples/ore-react/src/components/OreDashboard.tsx index 10b35d48..37d950b7 100644 --- a/examples/ore-react/src/components/OreDashboard.tsx +++ b/examples/ore-react/src/components/OreDashboard.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useHyperstack } from 'hyperstack-react'; -import { ORE_STREAM_STACK } from 'hyperstack-stacks/ore'; -import { ValidatedOreRoundSchema } from '../schemas/ore-round-validated'; +import { ORE_STREAM_STACK, type OreRound } from 'hyperstack-stacks/ore'; +import { ValidatedOreRoundSchema, type ValidatedOreRound } from '../schemas/ore-round-validated'; import { BlockGrid } from './BlockGrid'; import { StatsPanel } from './StatsPanel'; import { ConnectionBadge } from './ConnectionBadge'; @@ -10,6 +10,76 @@ import { DeployButton } from './DeployButton'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { useWallet } from '@solana/wallet-adapter-react'; +type WinnerInfo = { + winnerSquare: number | null; + winnerRoundId: number | null; +}; + +function getCurrentRoundWinner(round: ValidatedOreRound | undefined, nowUnix: number): WinnerInfo { + const roundId = round?.id?.round_id; + const finished = + typeof round?.state?.estimated_expires_at_unix === 'number' && + round.state.estimated_expires_at_unix <= nowUnix; + + if (!finished || typeof round?.results?.winning_square !== 'number') { + return { winnerSquare: null, winnerRoundId: null }; + } + + return { + winnerSquare: round.results.winning_square, + winnerRoundId: roundId ?? null, + }; +} + +function getLastCompletedWinner(recentRounds: OreRound[] | undefined, currentRoundId: number | null): WinnerInfo { + const bestRound = recentRounds?.reduce((best, round) => { + const roundId = round?.id?.round_id; + const winner = round?.results?.winning_square; + + if (typeof roundId !== 'number' || typeof winner !== 'number') { + return best; + } + + if (currentRoundId != null && roundId >= currentRoundId) { + return best; + } + + if (!best) { + return round; + } + + const bestRoundId = best.id?.round_id; + if (typeof bestRoundId !== 'number' || roundId > bestRoundId) { + return round; + } + + return best; + }, undefined); + + return { + winnerSquare: + typeof bestRound?.results?.winning_square === 'number' + ? bestRound.results.winning_square + : null, + winnerRoundId: bestRound?.id?.round_id ?? null, + }; +} + +function getStatsWinner( + currentRound: ValidatedOreRound | undefined, + recentRounds: OreRound[] | undefined, + nowUnix: number +): WinnerInfo { + const currentRoundId = currentRound?.id?.round_id ?? null; + const currentWinner = getCurrentRoundWinner(currentRound, nowUnix); + + if (currentWinner.winnerSquare != null) { + return currentWinner; + } + + return getLastCompletedWinner(recentRounds, currentRoundId); +} + export function OreDashboard() { const wallet = useWallet(); const { views, isConnected } = useHyperstack(ORE_STREAM_STACK); @@ -21,6 +91,9 @@ export function OreDashboard() { ); const { data: recentRounds } = views.OreRound.list.use({ take: 10 }); + const nowUnix = Math.floor(Date.now() / 1000); + const { winnerSquare: statsWinnerSquare, winnerRoundId: statsWinnerRoundId } = + getStatsWinner(latestRound, recentRounds, nowUnix); const [selectedSquares, setSelectedSquares] = useState([]); @@ -59,6 +132,8 @@ export function OreDashboard() { round={latestRound} treasuryMotherlode={treasuryData?.state?.motherlode} isConnected={isConnected} + winnerSquare={statsWinnerSquare} + winnerRoundId={statsWinnerRoundId} /> ('00:00'); useEffect(() => { @@ -69,7 +71,17 @@ export function StatsPanel({ round, treasuryMotherlode, isConnected }: StatsPane
-
+
+
Current / Last winner block
+
+ {winnerSquare != null ? `#${winnerSquare + 1}` : '—'} +
+
+ {winnerRoundId != null ? `Round ${winnerRoundId}` : 'Winner pending'} +
+
+ +
Round {round?.id.round_id ?? '–'} {round && ( <> From 644c846e8fb9cb0699d84b5bb4080aec5bb2512f Mon Sep 17 00:00:00 2001 From: Dado Date: Sun, 22 Feb 2026 15:50:16 +0100 Subject: [PATCH 4/7] fix: checpoint manual action --- .../ore-react/src/components/DeployButton.tsx | 141 ++++++++++++------ 1 file changed, 93 insertions(+), 48 deletions(-) diff --git a/examples/ore-react/src/components/DeployButton.tsx b/examples/ore-react/src/components/DeployButton.tsx index 4ad2c817..b546853f 100644 --- a/examples/ore-react/src/components/DeployButton.tsx +++ b/examples/ore-react/src/components/DeployButton.tsx @@ -36,8 +36,7 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq const hasUncheckpointedRound = minerRoundId != null && - checkpointId != null && - checkpointId < minerRoundId; + (checkpointId == null || checkpointId < minerRoundId); const canCheckpointNow = hasUncheckpointedRound && @@ -49,6 +48,70 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq currentRoundId != null && minerRoundId === currentRoundId; + const buildWalletAdapter = () => ({ + publicKey: wallet.publicKey!.toBase58(), + signAndSend: async (transaction: any) => { + const tx = new Transaction(); + for (const ix of transaction.instructions) { + tx.add({ + programId: new PublicKey(ix.programId), + keys: ix.keys.map((key: any) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable, + })), + data: Buffer.from(ix.data), + }); + } + return await wallet.sendTransaction!(tx, connection); + } + }); + + const runCheckpoint = async (walletAdapter: ReturnType) => { + if (!checkpoint) { + throw new Error('Checkpoint instruction is not ready yet.'); + } + + const oldRound = recentRounds?.find(r => r.id?.round_id === minerRoundId); + if (!oldRound?.id?.round_address) { + throw new Error(`Round ${minerRoundId} not available. Cannot checkpoint.`); + } + + return checkpoint.submit( + {}, + { + wallet: walletAdapter, + accounts: { round: oldRound.id.round_address }, + } + ); + }; + + const handleCheckpointNow = async () => { + if (!wallet.connected || !wallet.publicKey || !wallet.sendTransaction || !canCheckpointNow) { + return; + } + + setIsProcessing(true); + setProcessingStep('checkpoint'); + setResult(null); + + try { + const walletAdapter = buildWalletAdapter(); + const checkpointResult = await runCheckpoint(walletAdapter); + + setResult({ + status: 'success', + checkpointSignature: checkpointResult.signature, + }); + } catch (err: any) { + console.error('Checkpoint failed:', err); + setResult({ status: 'error', error: err?.message || String(err) }); + } finally { + setIsProcessing(false); + setProcessingStep(null); + } + }; + const handleDeploy = async () => { if (!wallet.connected || !wallet.publicKey) { return; @@ -64,44 +127,13 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq try { const amountLamports = BigInt(Math.floor(parseFloat(amount) * 1e9)); - - // TO DO: check - const walletAdapter = { - publicKey: wallet.publicKey!.toBase58(), - signAndSend: async (transaction: any) => { - const tx = new Transaction(); - for (const ix of transaction.instructions) { - tx.add({ - programId: new PublicKey(ix.programId), - keys: ix.keys.map((key: any) => ({ - pubkey: new PublicKey(key.pubkey), - isSigner: key.isSigner, - isWritable: key.isWritable, - })), - data: Buffer.from(ix.data), - }); - } - return await wallet.sendTransaction!(tx, connection); - } - }; + const walletAdapter = buildWalletAdapter(); let checkpointSig: string | undefined; // Call checkpoint first only when the miner round is already completed if (canCheckpointNow) { - const oldRound = recentRounds?.find(r => r.id?.round_id === minerRoundId); - - if (!oldRound?.id?.round_address) { - throw new Error(`Round ${minerRoundId} not available. Cannot checkpoint.`); - } - - const checkpointResult = await checkpoint.submit( - {}, - { - wallet: walletAdapter, - accounts: { round: oldRound.id.round_address }, - } - ); + const checkpointResult = await runCheckpoint(walletAdapter); checkpointSig = checkpointResult.signature; setProcessingStep('deploy'); } @@ -143,6 +175,13 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq {canCheckpointNow && (
ℹ️ Round {minerRoundId} is completed and uncheckpointed. It will be checkpointed before deploy. +
)} @@ -202,10 +241,14 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq
)} - {result?.status === 'success' && result.deploySignature && ( + {result?.status === 'success' && (result.deploySignature || result.checkpointSignature) && (
- ✅ {result.checkpointSignature ? 'Checkpoint + Deploy' : 'Deploy'} successful! + ✅ {result.checkpointSignature && result.deploySignature + ? 'Checkpoint + Deploy' + : result.checkpointSignature + ? 'Checkpoint' + : 'Deploy'} successful!
{result.checkpointSignature && (
@@ -220,17 +263,19 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq
)} - + {result.deploySignature && ( + + )}
-
- - {Number(block.deployedUi).toFixed(4)} -
- {isSelected && ( -
- ✓ +
+ + {Number(block.deployedUi).toFixed(4)}
- )} - - )})} + {isWinner && ( +
👑
+ )} + {isDeployed && ( +
+ )} + {!isDeployed && isSelected && ( +
+ )} + + ); + })}
); } diff --git a/examples/ore-react/src/components/DeployButton.tsx b/examples/ore-react/src/components/DeployButton.tsx index 61fb5d5a..ccc03e87 100644 --- a/examples/ore-react/src/components/DeployButton.tsx +++ b/examples/ore-react/src/components/DeployButton.tsx @@ -1,11 +1,7 @@ -/** - * Deploy Button with Automatic Checkpoint - */ - import { useState } from 'react'; import { useWallet, useConnection } from '@solana/wallet-adapter-react'; -import { useHyperstack } from 'hyperstack-react'; -import { ORE_STREAM_STACK, type OreMiner, type OreRound } from 'hyperstack-stacks/ore'; +import type { UseMutationResult } from 'hyperstack-react'; +import { type OreMiner, type OreRound } from 'hyperstack-stacks/ore'; import { Transaction, PublicKey } from '@solana/web3.js'; const LAMPORTS_PER_SOL = 1_000_000_000n; @@ -29,12 +25,15 @@ type DeployButtonProps = { minerData?: OreMiner; recentRounds?: OreRound[]; selectedSquares: number[]; + onClearSquares: () => void; + checkpoint?: UseMutationResult; + deploy?: UseMutationResult; }; -export function DeployButton({ currentRound, minerData, recentRounds, selectedSquares }: DeployButtonProps) { +export function DeployButton({ currentRound, minerData, recentRounds, selectedSquares, onClearSquares, checkpoint, deploy }: DeployButtonProps) { const wallet = useWallet(); const { connection } = useConnection(); - const [amount, setAmount] = useState('0.001'); + const [amount, setAmount] = useState('0.000001'); const [isProcessing, setIsProcessing] = useState(false); const [processingStep, setProcessingStep] = useState<'checkpoint' | 'deploy' | null>(null); const [result, setResult] = useState<{ @@ -44,10 +43,6 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq error?: string; } | null>(null); - const stack = useHyperstack(ORE_STREAM_STACK); - const checkpoint = stack.instructions?.checkpoint?.useMutation(); - const deploy = stack.instructions?.deploy?.useMutation(); - const minerRoundId = minerData?.state?.round_id; const checkpointId = minerData?.state?.checkpoint_id; const currentRoundId = currentRound?.id?.round_id; @@ -134,10 +129,14 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq }; const handleDeploy = async () => { - if (!wallet.connected || !wallet.publicKey) { + if (!wallet.connected || !wallet.publicKey || !wallet.sendTransaction) { return; } + if (!deploy) { + throw new Error('Deploy instruction is not ready yet.'); + } + if (!currentRound?.id?.round_address || !currentRound?.entropy?.entropy_var_address || selectedSquares.length === 0) { return; } @@ -164,10 +163,13 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq } // Then deploy + // Build a bitmask: each selectedSquare is 1-indexed, so square N → bit (N-1). + // The on-chain program treats `squares` as a u32 bitmask where bit i = square i+1. + const squareMask = selectedSquares.reduce((mask, squareId) => mask | (1 << (squareId - 1)), 0); const deployResult = await deploy.submit( { amount: parsedAmountLamports, - squares: selectedSquares.length, + squares: squareMask, }, { wallet: walletAdapter, @@ -183,6 +185,7 @@ export function DeployButton({ currentRound, minerData, recentRounds, selectedSq checkpointSignature: checkpointSig, deploySignature: deployResult.signature }); + onClearSquares(); } catch (err: any) { console.error('Deploy failed:', err); diff --git a/examples/ore-react/src/components/OreDashboard.tsx b/examples/ore-react/src/components/OreDashboard.tsx index 885168a1..853b9ea4 100644 --- a/examples/ore-react/src/components/OreDashboard.tsx +++ b/examples/ore-react/src/components/OreDashboard.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { useHyperstack } from 'hyperstack-react'; +import { useState, useEffect } from 'react'; +import { useHyperstack, type UseMutationResult } from 'hyperstack-react'; import { ORE_STREAM_STACK } from 'hyperstack-stacks/ore'; import { ValidatedOreRoundSchema } from '../schemas/ore-round-validated'; import { BlockGrid } from './BlockGrid'; @@ -10,26 +10,46 @@ import { DeployButton } from './DeployButton'; import { getStatsWinner } from './winner-utils'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { useWallet } from '@solana/wallet-adapter-react'; +import type { Miner } from 'hyperstack-stacks/ore'; + +// The miner_snapshot field arrives at runtime as an account wrapper, but the +// package TypeScript type incorrectly types it as `Miner` directly. +type MinerSnapshot = { data: Miner; account_address: string; signature: string; slot: number; timestamp: number }; export function OreDashboard() { const wallet = useWallet(); - const { views, isConnected } = useHyperstack(ORE_STREAM_STACK); + const { views, isConnected, instructions } = useHyperstack(ORE_STREAM_STACK); + const checkpoint: UseMutationResult | undefined = instructions?.checkpoint?.useMutation(); + const deploy: UseMutationResult | undefined = instructions?.deploy?.useMutation(); const { data: latestRound } = views.OreRound.latest.useOne({ schema: ValidatedOreRoundSchema }); const { data: treasuryData } = views.OreTreasury.list.useOne(); const { data: minerData } = views.OreMiner.state.use( - wallet.publicKey ? { authority: wallet.publicKey.toBase58() } : undefined + { authority: wallet.publicKey?.toBase58() ?? "" }, + { enabled: !!wallet.publicKey } ); + const snapshot = (minerData?.miner_snapshot as MinerSnapshot | null | undefined)?.data; + + const currentRoundId = snapshot?.round_id; + const myDeployed: number[] | undefined = + currentRoundId != null && currentRoundId === latestRound?.id?.round_id + ? snapshot?.deployed + : undefined; + const { data: recentRounds } = views.OreRound.list.use({ take: 10 }); - const nowUnix = Math.floor(Date.now() / 1000); + const [nowUnix, setNowUnix] = useState(() => Math.floor(Date.now() / 1000)); + useEffect(() => { + const interval = setInterval(() => setNowUnix(Math.floor(Date.now() / 1000)), 1000); + return () => clearInterval(interval); + }, []); const { winnerSquare: statsWinnerSquare, winnerRoundId: statsWinnerRoundId } = getStatsWinner(latestRound, recentRounds, nowUnix); const [selectedSquares, setSelectedSquares] = useState([]); const handleSquareClick = (squareId: number) => { - setSelectedSquares(prev => + setSelectedSquares(prev => prev.includes(squareId) ? prev.filter(id => id !== squareId) : [...prev, squareId] @@ -52,9 +72,15 @@ export function OreDashboard() {
(v > 0 ? i : -1)) + .filter((i): i is number => i >= 0) + } />
@@ -65,12 +91,20 @@ export function OreDashboard() { isConnected={isConnected} winnerSquare={statsWinnerSquare} winnerRoundId={statsWinnerRoundId} + minerDeployedThisRoundSol={ + myDeployed != null + ? myDeployed.reduce((a, b) => a + b, 0) / 1_000_000_000 + : undefined + } /> setSelectedSquares([])} + checkpoint={checkpoint} + deploy={deploy} />
diff --git a/examples/ore-react/src/components/StatsPanel.tsx b/examples/ore-react/src/components/StatsPanel.tsx index d8ca79c1..5753e9a1 100644 --- a/examples/ore-react/src/components/StatsPanel.tsx +++ b/examples/ore-react/src/components/StatsPanel.tsx @@ -8,30 +8,31 @@ interface StatsPanelProps { isConnected: boolean; winnerSquare?: number | null; winnerRoundId?: number | null; + minerDeployedThisRoundSol?: number; } -export function StatsPanel({ round, treasuryMotherlode, isConnected, winnerSquare, winnerRoundId }: StatsPanelProps) { - const [timeRemaining, setTimeRemaining] = useState('00:00'); +export function StatsPanel({ round, treasuryMotherlode, isConnected, winnerSquare, winnerRoundId, minerDeployedThisRoundSol }: StatsPanelProps) { + const [timeDisplay, setTimeDisplay] = useState('–'); useEffect(() => { const expiresAtUnix = round?.state.estimated_expires_at_unix; if (!expiresAtUnix) { - setTimeRemaining('00:00'); + setTimeDisplay('-'); return; } const updateTimer = () => { const now = Math.floor(Date.now() / 1000); - const remaining = Math.max(0, expiresAtUnix - now); + const remaining = expiresAtUnix - now; - if (remaining > 300) { - setTimeRemaining('00:00'); + if (remaining <= 0) { + setTimeDisplay('Round expired'); return; } const minutes = Math.floor(remaining / 60); const seconds = remaining % 60; - setTimeRemaining(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); + setTimeDisplay(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`); }; updateTimer(); @@ -50,7 +51,7 @@ export function StatsPanel({ round, treasuryMotherlode, isConnected, winnerSquar
-
{timeRemaining}
+
{timeDisplay}
Time remaining
@@ -65,15 +66,15 @@ export function StatsPanel({ round, treasuryMotherlode, isConnected, winnerSquar
- 0 + {minerDeployedThisRoundSol != null ? minerDeployedThisRoundSol.toFixed(9).replace(/\.?0+$/, '') : '–'}
You deployed
-
Current / Last winner block
-
+
Current / Last winner square
+
{winnerSquare != null ? `#${winnerSquare + 1}` : '—'}
From 37fe068d498d3aa51720bdf119c6459974c735ed Mon Sep 17 00:00:00 2001 From: Dado Date: Tue, 24 Feb 2026 20:57:34 +0100 Subject: [PATCH 7/7] fix: transform esm-only noble packages in jest config --- typescript/react/jest.config.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/typescript/react/jest.config.js b/typescript/react/jest.config.js index e11d9d68..8ecbf332 100644 --- a/typescript/react/jest.config.js +++ b/typescript/react/jest.config.js @@ -12,6 +12,16 @@ export default { useESM: true, }, ], + '^.+\\.js$': [ + 'ts-jest', + { + useESM: true, + }, + ], }, + // Allow Jest to transform ESM-only packages that can't run in CommonJS + transformIgnorePatterns: [ + '/node_modules/(?!(@noble/hashes|@noble/curves)/)', + ], testMatch: ['**/*.test.ts'], };