diff --git a/examples/ore-react/package.json b/examples/ore-react/package.json index 0298d8e..db25033 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 e9d4df5..09b71de 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 2f1bc21..3f521bf 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 0000000..4ad2c81 --- /dev/null +++ b/examples/ore-react/src/components/DeployButton.tsx @@ -0,0 +1,258 @@ +/** + * 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(); + + 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) { + return; + } + + if (!currentRound?.id?.round_address || !currentRound?.entropy?.entropy_var_address || selectedSquares.length === 0) { + return; + } + + setIsProcessing(true); + setProcessingStep(canCheckpointNow ? '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 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 }, + } + ); + 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

+ + {canCheckpointNow && ( +
+ ℹ️ 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. +
+ )} + +
+
+ + 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 769a5f1..10b35d4 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,12 +6,32 @@ 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.state.use( + 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 91858a2..9e7778f 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 9ffcc67..956cd04 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 46975ec..09ad110 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 fdfc723..50590d5 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 97bcd1d..238e7e0 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 ff4f81d..c947f3a 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 0d1e996..7a3b95d 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,