Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions examples/ore-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
31 changes: 26 additions & 5 deletions examples/ore-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider>
<HyperstackProvider autoConnect={true}>
<OreDashboard />
</HyperstackProvider>
</ThemeProvider>
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<ThemeProvider>
<HyperstackProvider autoConnect={true}>
<OreDashboard />
</HyperstackProvider>
</ThemeProvider>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}
25 changes: 19 additions & 6 deletions examples/ore-react/src/components/BlockGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,15 +30,21 @@ export function BlockGrid({ round }: BlockGridProps) {
width: 'calc((100vh - 120px - 4 * 0.5rem) / 5 * 5 + 4 * 0.5rem)'
}}
>
{blocks.map((block) => (
<div
{blocks.map((block) => {
const isSelected = selectedSquares.includes(block.id);
return (
<button
key={block.id}
onClick={() => onSquareClick(block.id)}
style={{ aspectRatio: '1' }}
className={`
bg-white dark:bg-stone-800 rounded-2xl p-4 flex flex-col justify-between
relative bg-white dark:bg-stone-800 rounded-2xl p-4 flex flex-col justify-between
transition-all duration-200 hover:shadow-md dark:hover:bg-stone-750
cursor-pointer hover:scale-[1.02] active:scale-[0.98]
${block.isWinner
? 'bg-amber-50 dark:bg-amber-900/30 ring-2 ring-amber-400 shadow-lg'
: isSelected
? 'bg-blue-50 dark:bg-blue-900/30 ring-2 ring-blue-500 shadow-lg'
: 'shadow-sm dark:shadow-none dark:ring-1 dark:ring-stone-700'
}
`}
Expand All @@ -52,8 +60,13 @@ export function BlockGrid({ round }: BlockGridProps) {
<SolanaIcon size={18} />
<span>{Number(block.deployedUi).toFixed(4)}</span>
</div>
</div>
))}
{isSelected && (
<div className="absolute top-2 right-2 w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-bold">
</div>
)}
</button>
)})}
</div>
);
}
258 changes: 258 additions & 0 deletions examples/ore-react/src/components/DeployButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-white dark:bg-stone-800 rounded-2xl p-6 shadow-sm dark:shadow-none dark:ring-1 dark:ring-stone-700">
<h3 className="text-lg font-semibold text-stone-800 dark:text-stone-100 mb-4">Deploy</h3>

{canCheckpointNow && (
<div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg text-xs text-amber-700 dark:text-amber-300">
ℹ️ Round {minerRoundId} is completed and uncheckpointed. It will be checkpointed before deploy.
</div>
)}

{waitingForRoundToEnd && (
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-xs text-blue-700 dark:text-blue-300">
⏳ You have uncheckpointed positions in active round {minerRoundId}. Checkpoint becomes available after this round ends.
</div>
)}

<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-stone-600 dark:text-stone-400 mb-2">
Amount (SOL)
</label>
<input
type="number"
step="0.001"
min="0"
value={amount}
onChange={(e) => 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"
/>
</div>

<div className="text-sm text-stone-600 dark:text-stone-400">
Selected: <span className="text-blue-600 dark:text-blue-400 font-semibold">{selectedSquares.length}</span> square{selectedSquares.length !== 1 ? 's' : ''}
{selectedSquares.length > 0 && (
<span className="ml-2 text-xs text-stone-400 dark:text-stone-500">
({selectedSquares.slice(0, 5).join(', ')}{selectedSquares.length > 5 ? '...' : ''})
</span>
)}
</div>

<button
onClick={handleDeploy}
disabled={!wallet.connected || isProcessing || selectedSquares.length === 0 || !amount}
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700
text-white font-semibold rounded-xl shadow-sm
disabled:bg-stone-300 dark:disabled:bg-stone-700
disabled:text-stone-500 dark:disabled:text-stone-500
disabled:cursor-not-allowed
transition-all duration-200 hover:shadow-md active:scale-[0.98]"
>
{!wallet.connected ? 'Connect Wallet' :
isProcessing ? (canCheckpointNow ? 'Checkpointing + Deploying...' : 'Deploying...') :
`Deploy ${amount} SOL`}
</button>

{isProcessing && (
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl text-blue-700 dark:text-blue-400 text-sm">
⏳ {processingStep === 'checkpoint' ? 'Checkpointing' : 'Deploying'}...
</div>
)}

{result?.status === 'success' && result.deploySignature && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl text-green-700 dark:text-green-400 text-sm">
<div className="font-semibold mb-2">
✅ {result.checkpointSignature ? 'Checkpoint + Deploy' : 'Deploy'} successful!
</div>
{result.checkpointSignature && (
<div className="mb-2">
<div className="text-xs font-medium mb-1">Checkpoint:</div>
<a
href={`https://solscan.io/tx/${result.checkpointSignature}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs underline hover:no-underline break-all"
>
{result.checkpointSignature.slice(0, 8)}...{result.checkpointSignature.slice(-8)} →
</a>
</div>
)}
<div className={result.checkpointSignature ? 'mt-2' : ''}>
<div className="text-xs font-medium mb-1">Deploy:</div>
<a
href={`https://solscan.io/tx/${result.deploySignature}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs underline hover:no-underline break-all"
>
{result.deploySignature.slice(0, 8)}...{result.deploySignature.slice(-8)} →
</a>
</div>
<button
onClick={() => setResult(null)}
className="mt-3 w-full text-xs text-green-700 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 underline"
>
Deploy Again
</button>
</div>
)}

{result?.status === 'error' && result.error && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-700 dark:text-red-400 text-sm">
<div className="font-semibold mb-1">❌ Deploy failed</div>
<div className="text-xs mb-3 opacity-90">{result.error}</div>
<button
onClick={() => setResult(null)}
className="w-full text-xs text-red-700 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 underline"
>
Try Again
</button>
</div>
)}
</div>
</div>
);
}
Loading
Loading