From 604ee4064b623167bb1403b98b95ec6648dfcc75 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Wed, 19 Nov 2025 20:14:18 +0800 Subject: [PATCH 01/18] v1 --- roll-dice/Cargo.lock | 122 +++- roll-dice/app/app/delegated/page.tsx | 604 ++++++++++++++++++ roll-dice/package.json | 13 +- .../programs/roll-dice-delegated/Cargo.toml | 2 +- .../programs/roll-dice-delegated/src/lib.rs | 20 +- roll-dice/tests/roll-dice-delegated.ts | 169 ++++- roll-dice/yarn.lock | 419 +++++++++++- 7 files changed, 1321 insertions(+), 28 deletions(-) create mode 100644 roll-dice/app/app/delegated/page.tsx diff --git a/roll-dice/Cargo.lock b/roll-dice/Cargo.lock index 1b9b277..fe2dd01 100644 --- a/roll-dice/Cargo.lock +++ b/roll-dice/Cargo.lock @@ -244,6 +244,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -389,6 +395,9 @@ name = "bytemuck" version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +dependencies = [ + "bytemuck_derive", +] [[package]] name = "bytemuck_derive" @@ -539,12 +548,31 @@ checksum = "1567ae90eac4338ad3d87228bd357b540142af5edbc333c73f06c74cd2bf336f" dependencies = [ "anchor-lang", "borsh 0.10.4", - "ephemeral-rollups-sdk-attribute-commit", - "ephemeral-rollups-sdk-attribute-delegate", - "ephemeral-rollups-sdk-attribute-ephemeral", + "ephemeral-rollups-sdk-attribute-commit 0.2.5", + "ephemeral-rollups-sdk-attribute-delegate 0.2.5", + "ephemeral-rollups-sdk-attribute-ephemeral 0.2.5", "solana-program", ] +[[package]] +name = "ephemeral-rollups-sdk" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf068e9dc9c71ba2c0e1c3e1cc14ed633428274589f20483ea6680353cf81c00" +dependencies = [ + "anchor-lang", + "base64ct", + "borsh 1.5.7", + "ephemeral-rollups-sdk-attribute-commit 0.4.1", + "ephemeral-rollups-sdk-attribute-delegate 0.4.1", + "ephemeral-rollups-sdk-attribute-ephemeral 0.4.1", + "getrandom 0.2.16", + "magicblock-delegation-program", + "magicblock-magic-program-api", + "solana-program", + "solana-system-interface", +] + [[package]] name = "ephemeral-rollups-sdk-attribute-commit" version = "0.2.5" @@ -555,6 +583,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ephemeral-rollups-sdk-attribute-commit" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d2622e5f61947543f6f1668434529dc29ac54e9d8512c7bf4f38d67c139485" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" version = "0.2.5" @@ -566,6 +604,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ephemeral-rollups-sdk-attribute-delegate" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540da5e9c176532fa92f60fd74915d09cc694bf2849be0cc079ec30499f94f49" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" version = "0.2.5" @@ -577,6 +626,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ephemeral-rollups-sdk-attribute-ephemeral" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8d85afb0d8559775441afcafe3b121a629f80ef702678e7daac663e25500ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ephemeral-vrf-sdk" version = "0.1.2" @@ -800,6 +860,30 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "magicblock-delegation-program" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e25f9e37194cc27c0f1d3dbc00e83795f7f4012f1a99c20bc557bdeb62e13e" +dependencies = [ + "borsh 1.5.7", + "bytemuck", + "num_enum", + "solana-program", + "static_assertions", +] + +[[package]] +name = "magicblock-magic-program-api" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1076e02cb9260b2400e8b7b21d6c5839e6054a0f5b9d75378811c66fa04d8d40" +dependencies = [ + "bincode", + "serde", + "solana-program", +] + [[package]] name = "memchr" version = "2.7.4" @@ -854,6 +938,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1048,7 +1154,7 @@ name = "roll-dice" version = "0.1.0" dependencies = [ "anchor-lang", - "ephemeral-rollups-sdk", + "ephemeral-rollups-sdk 0.2.5", "ephemeral-vrf-sdk", ] @@ -1057,7 +1163,7 @@ name = "roll-dice-delegated" version = "0.1.0" dependencies = [ "anchor-lang", - "ephemeral-rollups-sdk", + "ephemeral-rollups-sdk 0.4.1", "ephemeral-vrf-sdk", ] @@ -1984,6 +2090,12 @@ dependencies = [ "solana-system-interface", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx new file mode 100644 index 0000000..c1895bf --- /dev/null +++ b/roll-dice/app/app/delegated/page.tsx @@ -0,0 +1,604 @@ +"use client" + +import { useCallback, useEffect, useRef, useState } from "react" +import Dice from "@/components/dice" +import SolanaAddress from "@/components/solana-address" +// @ts-ignore +import * as anchor from "@coral-xyz/anchor" +// @ts-ignore +import { + Connection, + Keypair, + PublicKey, + Transaction, + VersionedTransaction, + SystemProgram, + LAMPORTS_PER_SOL, +} from "@solana/web3.js" +import { createDelegateInstruction, DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" +import { useToast } from "@/hooks/use-toast" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +const PROGRAM_ID = new PublicKey("5bPwgoPWz274NKgThcnPas2Mv4rSknu9JrbxzFVqU5gY") +const PLAYER_SEED = "playerd" +const ORACLE_QUEUE = new PublicKey("5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc") +const BASE_ENDPOINT = "https://rpc.magicblock.app/devnet" +const PLAYER_STORAGE_KEY = "solanaKeypair" +const PAYER_STORAGE_KEY = "delegatePayerKeypair" + +const walletAdapterFrom = (keypair: Keypair) => ({ + publicKey: keypair.publicKey, + async signTransaction(transaction: T): Promise { + // @ts-ignore - Transaction and VersionedTransaction have different sign signatures + transaction.sign(keypair) + return transaction + }, + async signAllTransactions(transactions: T[]): Promise { + // @ts-ignore - Transaction and VersionedTransaction have different sign signatures + transactions.forEach(tx => tx.sign(keypair)) + return transactions + }, +}) + +const derivePlayerPda = (user: PublicKey) => + PublicKey.findProgramAddressSync([Buffer.from(PLAYER_SEED), user.toBuffer()], PROGRAM_ID)[0] + +type RollEntry = { + value: number | null + startTime: number + endTime: number | null + isPending: boolean +} + +export default function DiceRollerDelegated() { + const [diceValue, setDiceValue] = useState(1) + const [isRolling, setIsRolling] = useState(false) + const [isInitialized, setIsInitialized] = useState(false) + const [isDelegated, setIsDelegated] = useState(false) + const [isDelegating, setIsDelegating] = useState(false) + const [rollHistory, setRollHistory] = useState([]) + const previousDiceValueRef = useRef(1) + const previousRollnumRef = useRef(0) + const expectingRollResultRef = useRef(false) + const programRef = useRef(null) + const ephemeralProgramRef = useRef(null) + const connectionRef = useRef(null) + const ephemeralConnectionRef = useRef(null) + const playerPdaRef = useRef(null) + const subscriptionIdRef = useRef(null) + const rollIntervalRef = useRef(null) + const timerIntervalRef = useRef(null) + const timeoutRef = useRef(null) + const blockhashIntervalRef = useRef(null) + const playerKeypairRef = useRef(null) + const payerKeypairRef = useRef(null) + const cachedBaseBlockhashRef = useRef<{ blockhash: string; lastValidBlockHeight: number; timestamp: number } | null>(null) + const cachedEphemeralBlockhashRef = useRef<{ blockhash: string; lastValidBlockHeight: number; timestamp: number } | null>(null) + const { toast } = useToast() + + const clearAllIntervals = useCallback(() => { + if (rollIntervalRef.current) { + clearInterval(rollIntervalRef.current) + rollIntervalRef.current = null + } + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current) + timerIntervalRef.current = null + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + if (blockhashIntervalRef.current) { + clearInterval(blockhashIntervalRef.current) + blockhashIntervalRef.current = null + } + }, []) + + + const ensureFunds = useCallback(async (connection: Connection, keypair: Keypair) => { + const balance = await connection.getBalance(keypair.publicKey) + if (balance < 0.05 * LAMPORTS_PER_SOL) { + const signature = await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL) + await connection.confirmTransaction(signature, "confirmed") + } + }, []) + + const fetchAndCacheBlockhash = useCallback(async (connection: Connection, isEphemeral: boolean) => { + try { + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + const cached = { + blockhash, + lastValidBlockHeight, + timestamp: Date.now(), + } + if (isEphemeral) { + cachedEphemeralBlockhashRef.current = cached + } else { + cachedBaseBlockhashRef.current = cached + } + } catch (error) { + console.error("Failed to fetch blockhash:", error) + } + }, []) + + const getCachedBlockhash = useCallback((connection: Connection, isEphemeral: boolean): string | null => { + const cached = isEphemeral ? cachedEphemeralBlockhashRef.current : cachedBaseBlockhashRef.current + if (!cached) return null + + const age = Date.now() - cached.timestamp + if (age > 30000) { + fetchAndCacheBlockhash(connection, isEphemeral) + } + + return cached.blockhash + }, [fetchAndCacheBlockhash]) + + const sendTransaction = useCallback( + async (connection: Connection, transaction: Transaction, feePayer: Keypair, signers: Keypair[], isEphemeral: boolean = false) => { + let blockhash: string + const cached = getCachedBlockhash(connection, isEphemeral) + if (cached) { + blockhash = cached + } else { + const result = await connection.getLatestBlockhash() + blockhash = result.blockhash + fetchAndCacheBlockhash(connection, isEphemeral) + } + + transaction.recentBlockhash = blockhash + transaction.feePayer = feePayer.publicKey + + const signerMap = new Map() + signerMap.set(feePayer.publicKey.toBase58(), feePayer) + for (const signer of signers) { + signerMap.set(signer.publicKey.toBase58(), signer) + } + + signerMap.forEach(signer => transaction.partialSign(signer)) + + const signature = await connection.sendRawTransaction(transaction.serialize()) + return signature + }, + [getCachedBlockhash, fetchAndCacheBlockhash] + ) + + const refreshDelegationStatus = useCallback(async () => { + if (!connectionRef.current || !playerKeypairRef.current) return false + const accountInfo = await connectionRef.current.getAccountInfo(playerKeypairRef.current.publicKey) + const delegated = !!accountInfo && accountInfo.owner.equals(DELEGATION_PROGRAM_ID) + setIsDelegated(delegated) + return delegated + }, []) + + const loadOrCreateKeypair = useCallback((storageKey: string) => { + if (typeof window === "undefined") return Keypair.generate() + const stored = window.localStorage.getItem(storageKey) + if (stored) { + return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(stored))) + } + const generated = Keypair.generate() + window.localStorage.setItem(storageKey, JSON.stringify(Array.from(generated.secretKey))) + return generated + }, []) + + const initializeProgram = useCallback(async () => { + if (typeof window === "undefined") return + try { + const connection = new Connection(BASE_ENDPOINT, "confirmed") + connectionRef.current = connection + + if (!playerKeypairRef.current) { + playerKeypairRef.current = loadOrCreateKeypair(PLAYER_STORAGE_KEY) + } + if (!payerKeypairRef.current) { + payerKeypairRef.current = loadOrCreateKeypair(PAYER_STORAGE_KEY) + } + + await ensureFunds(connection, playerKeypairRef.current) + await ensureFunds(connection, payerKeypairRef.current) + + const provider = new anchor.AnchorProvider( + connection, + walletAdapterFrom(playerKeypairRef.current), + anchor.AnchorProvider.defaultOptions() + ) + + const idl = await anchor.Program.fetchIdl(PROGRAM_ID, provider) + if (!idl) throw new Error("IDL not found") + + const program = new anchor.Program(idl, provider) + programRef.current = program + + const playerPk = derivePlayerPda(playerKeypairRef.current.publicKey) + playerPdaRef.current = playerPk + + let account = await connection.getAccountInfo(playerPk) + if (!account) { + await program.methods.initialize().rpc() + account = await connection.getAccountInfo(playerPk) + } + if (account) { + try { + const player = program.coder.accounts.decode("player", account.data) + const initialValue = player.lastResult || 1 + const initialRollnum = player.rollnum || 0 + setDiceValue(initialValue) + previousDiceValueRef.current = initialValue + previousRollnumRef.current = Number(initialRollnum) + } catch (error) { + console.error("Failed to decode player on init:", error) + } + } + + const ephemeralEndpoint = process.env.NEXT_PUBLIC_EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet.magicblock.app" + const ephemeralWsEndpoint = process.env.NEXT_PUBLIC_EPHEMERAL_WS_ENDPOINT || "wss://devnet.magicblock.app" + const ephemeralConnection = new Connection(ephemeralEndpoint, { + wsEndpoint: ephemeralWsEndpoint, + commitment: "processed", + }) + ephemeralConnectionRef.current = ephemeralConnection + const ephemeralProvider = new anchor.AnchorProvider( + ephemeralConnection, + walletAdapterFrom(playerKeypairRef.current), + anchor.AnchorProvider.defaultOptions() + ) + ephemeralProgramRef.current = new anchor.Program(idl, ephemeralProvider) + + if (subscriptionIdRef.current !== null && ephemeralConnection) { + await ephemeralConnection.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) + } + if (ephemeralConnection && playerPk) { + subscriptionIdRef.current = ephemeralConnection.onAccountChange( + playerPk, + (accountInfo) => { + if (!ephemeralProgramRef.current || !accountInfo || !accountInfo.data) return + + try { + const player = ephemeralProgramRef.current.coder.accounts.decode("player", accountInfo.data) + const newValue = Number(player.lastResult) + const newRollnum = Number(player.rollnum || 0) + const previousRollnum = previousRollnumRef.current + + if (newValue > 0) { + setDiceValue(newValue) + previousDiceValueRef.current = newValue + } + + if (newRollnum > previousRollnum && expectingRollResultRef.current) { + previousRollnumRef.current = newRollnum + const endTime = Date.now() + setRollHistory(prev => { + const updated = [...prev] + const pendingIndex = updated.findIndex(entry => entry.isPending) + if (pendingIndex !== -1) { + updated[pendingIndex] = { + value: newValue, + startTime: updated[pendingIndex].startTime, + endTime, + isPending: false, + } + } + return updated + }) + expectingRollResultRef.current = false + setIsRolling(false) + clearAllIntervals() + } + } catch (error) { + console.error("Failed to decode player account:", error) + } + }, + { commitment: "processed" } + ) + } + + await refreshDelegationStatus() + + await fetchAndCacheBlockhash(connection, false) + if (ephemeralConnection) { + await fetchAndCacheBlockhash(ephemeralConnection, true) + } + + blockhashIntervalRef.current = setInterval(() => { + if (connectionRef.current) { + fetchAndCacheBlockhash(connectionRef.current, false) + } + if (ephemeralConnectionRef.current) { + fetchAndCacheBlockhash(ephemeralConnectionRef.current, true) + } + }, 20000) + + setIsInitialized(true) + } catch (error) { + console.error("Failed to initialize delegated dice:", error) + setIsInitialized(false) + toast({ + title: "Error", + description: "Failed to initialize delegated dice", + variant: "destructive", + }) + } + }, [ensureFunds, loadOrCreateKeypair, refreshDelegationStatus, toast, fetchAndCacheBlockhash]) + + useEffect(() => { + initializeProgram() + + return () => { + clearAllIntervals() + if (subscriptionIdRef.current !== null && ephemeralConnectionRef.current) { + ephemeralConnectionRef.current.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) + subscriptionIdRef.current = null + } + } + }, [clearAllIntervals, initializeProgram]) + + const handleDelegate = useCallback(async () => { + if ( + !programRef.current || + !connectionRef.current || + !playerKeypairRef.current || + !payerKeypairRef.current || + !playerPdaRef.current + ) + return + if (isDelegated) return + + setIsDelegating(true) + try { + const connection = connectionRef.current + const playerKeypair = playerKeypairRef.current + const payerKeypair = payerKeypairRef.current + + await ensureFunds(connection, playerKeypair) + await ensureFunds(connection, payerKeypair) + + await programRef.current.methods + .delegate() + .accounts({ + user: playerKeypair.publicKey, + player: playerPdaRef.current, + }) + .rpc() + + const ownerInfo = await connection.getAccountInfo(playerKeypair.publicKey) + if (!ownerInfo || !ownerInfo.owner.equals(DELEGATION_PROGRAM_ID)) { + const assignIx = SystemProgram.assign({ + accountPubkey: playerKeypair.publicKey, + programId: DELEGATION_PROGRAM_ID, + }) + await sendTransaction(connection, new Transaction().add(assignIx), payerKeypair, [playerKeypair], false) + } + + const delegateIx = createDelegateInstruction({ + payer: payerKeypair.publicKey, + delegatedAccount: playerKeypair.publicKey, + ownerProgram: SystemProgram.programId, + }) + await sendTransaction(connection, new Transaction().add(delegateIx), payerKeypair, [playerKeypair], false) + + await refreshDelegationStatus() + toast({ + title: "Delegated", + description: "Account delegated successfully", + }) + } catch (error) { + console.error("Delegation failed:", error) + toast({ + title: "Error", + description: "Failed to delegate account", + variant: "destructive", + }) + } finally { + setIsDelegating(false) + } + }, [ensureFunds, isDelegated, refreshDelegationStatus, sendTransaction, toast]) + + const handleRollDice = useCallback(async () => { + if (isRolling || !isInitialized || !isDelegated) return + if (!ephemeralProgramRef.current || !playerKeypairRef.current || !playerPdaRef.current) return + + setIsRolling(true) + expectingRollResultRef.current = true + clearAllIntervals() + + rollIntervalRef.current = setInterval(() => { + setDiceValue(Math.floor(Math.random() * 6) + 1) + }, 100) + + setRollHistory(prev => { + const newEntry = { + value: null, + startTime: Date.now(), + endTime: null, + isPending: true, + } + return [newEntry, ...prev] + }) + + timerIntervalRef.current = setInterval(() => { + setRollHistory(prev => [...prev]) + }, 1) + + timeoutRef.current = setTimeout(() => { + if (expectingRollResultRef.current) { + clearAllIntervals() + expectingRollResultRef.current = false + setIsRolling(false) + setRollHistory(prev => { + const updated = [...prev] + const pendingIndex = updated.findIndex(entry => entry.isPending) + if (pendingIndex !== -1) { + updated[pendingIndex] = { + ...updated[pendingIndex], + isPending: false, + } + } + return updated + }) + toast({ + title: "Notice", + description: "Dice roll is taking longer than expected. Check explorer.", + variant: "destructive", + }) + } + }, 10000) + + try { + const randomValue = Math.floor(Math.random() * 6) + 1 + + const tx = await ephemeralProgramRef.current.methods + .rollDiceDelegated(randomValue) + .accounts({ + payer: playerKeypairRef.current.publicKey, + player: playerPdaRef.current, + oracleQueue: ORACLE_QUEUE, + }) + .transaction() + + const cachedBlockhash = getCachedBlockhash(ephemeralConnectionRef.current!, true) + if (cachedBlockhash) { + tx.recentBlockhash = cachedBlockhash + } else { + const { blockhash } = await ephemeralConnectionRef.current!.getLatestBlockhash() + tx.recentBlockhash = blockhash + } + + tx.feePayer = playerKeypairRef.current.publicKey + tx.sign(playerKeypairRef.current) + + const signature = await ephemeralConnectionRef.current!.sendRawTransaction( + tx.serialize(), + { skipPreflight: true } + ) + + const startTime = Date.now() + setRollHistory(prev => { + const updated = [...prev] + const pendingIndex = updated.findIndex(entry => entry.isPending && entry.value === null) + if (pendingIndex !== -1) { + updated[pendingIndex] = { + ...updated[pendingIndex], + startTime, + } + } + return updated + }) + + toast({ + title: "Dice Rolled", + description: `Result: TX: ${signature.slice(0, 8)}...`, + }) + + if (ephemeralConnectionRef.current) { + fetchAndCacheBlockhash(ephemeralConnectionRef.current, true) + } + } catch (error) { + clearAllIntervals() + console.error("Error rolling dice:", error) + toast({ + title: "Error", + description: "Failed to roll dice", + variant: "destructive", + }) + setIsRolling(false) + expectingRollResultRef.current = false + setRollHistory(prev => { + const updated = [...prev] + const pendingIndex = updated.findIndex(entry => entry.isPending) + if (pendingIndex !== -1) { + updated.splice(pendingIndex, 1) + } + return updated + }) + } + }, [clearAllIntervals, isDelegated, isInitialized, isRolling, toast, getCachedBlockhash, fetchAndCacheBlockhash]) + + return ( +
+
+ +
+ +
+
+ +
+ + {isDelegated ? "Delegated" : "Undelegated"} + + {!isDelegated && ( + + )} +
+ +
+ +
+ + +
+ +
+

Roll History

+
+ + + + Value + Time + + + + {rollHistory.length === 0 ? ( + + + No rolls yet + + + ) : ( + rollHistory.map((entry, index) => { + const elapsed = entry.isPending + ? Date.now() - entry.startTime + : entry.endTime + ? entry.endTime - entry.startTime + : 0 + return ( + + + {entry.value !== null ? entry.value : "-"} + + + {entry.isPending ? `${elapsed}ms...` : `${elapsed}ms`} + + + ) + }) + )} + +
+
+
+
+
+ ) +} diff --git a/roll-dice/package.json b/roll-dice/package.json index 0204ea5..cd20294 100644 --- a/roll-dice/package.json +++ b/roll-dice/package.json @@ -5,17 +5,18 @@ "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { - "@coral-xyz/anchor": "^0.31.0" + "@coral-xyz/anchor": "^0.31.0", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.4.1" }, "devDependencies": { - "chai": "^4.3.4", - "mocha": "^9.0.3", - "ts-mocha": "^10.0.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", - "typescript": "^5.7.3", - "prettier": "^2.6.2" + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^5.7.3" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/roll-dice/programs/roll-dice-delegated/Cargo.toml b/roll-dice/programs/roll-dice-delegated/Cargo.toml index f23db69..71f0ae1 100644 --- a/roll-dice/programs/roll-dice-delegated/Cargo.toml +++ b/roll-dice/programs/roll-dice-delegated/Cargo.toml @@ -19,6 +19,6 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } -ephemeral-rollups-sdk = { version = "0.2.5", features = ["anchor"] } +ephemeral-rollups-sdk = { version = "0.4.1", features = ["anchor"] } ephemeral-vrf-sdk = {version = "0.1.2", features = ["anchor"]} diff --git a/roll-dice/programs/roll-dice-delegated/src/lib.rs b/roll-dice/programs/roll-dice-delegated/src/lib.rs index 476a44f..7329a7c 100644 --- a/roll-dice/programs/roll-dice-delegated/src/lib.rs +++ b/roll-dice/programs/roll-dice-delegated/src/lib.rs @@ -1,12 +1,13 @@ use anchor_lang::prelude::*; use ephemeral_vrf_sdk::anchor::vrf; use ephemeral_vrf_sdk::instructions::{create_request_randomness_ix, RequestRandomnessParams}; +use ephemeral_vrf_sdk::types::SerializableAccountMeta; use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral}; use ephemeral_rollups_sdk::cpi::DelegateConfig; use ephemeral_rollups_sdk::ephem::{commit_and_undelegate_accounts}; -declare_id!("8QudyDCGXZw8jJnV7zAm5Fsr1Suztg6Nu5YCgAf2fuWj"); +declare_id!("5bPwgoPWz274NKgThcnPas2Mv4rSknu9JrbxzFVqU5gY"); pub const PLAYER: &[u8] = b"playerd"; @@ -33,7 +34,11 @@ pub mod random_dice_delegated { callback_program_id: ID, callback_discriminator: instruction::CallbackRollDiceSimple::DISCRIMINATOR.to_vec(), caller_seed: [client_seed; 32], - accounts_metas: None, + accounts_metas: Some(vec![SerializableAccountMeta { + pubkey: ctx.accounts.player.key(), + is_signer: false, + is_writable: true, + }]), ..Default::default() }); ctx.accounts @@ -42,11 +47,15 @@ pub mod random_dice_delegated { } pub fn callback_roll_dice_simple( - _ctx: Context, + ctx: Context, randomness: [u8; 32], ) -> Result<()> { let rnd_u8 = ephemeral_vrf_sdk::rnd::random_u8_with_range(&randomness, 1, 6); msg!("Consuming random number: {:?}", rnd_u8); + player.rollnum = player.rollnum.saturating_add(1); + msg!("Roll number: {:?}", player.rollnum); + let player = &mut ctx.accounts.player; + player.last_result = rnd_u8; Ok(()) } @@ -76,7 +85,7 @@ pub mod random_dice_delegated { pub struct Initialize<'info> { #[account(mut)] pub payer: Signer<'info>, - #[account(init_if_needed, payer = payer, space = 8 + 1, seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)] + #[account(init_if_needed, payer = payer, space = 8 + 2, seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)] pub player: Account<'info, Player>, pub system_program: Program<'info, System>, } @@ -121,6 +130,8 @@ pub struct CallbackRollDiceSimpleCtx<'info> { /// enforcing the callback is executed by the VRF program trough CPI #[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)] pub vrf_program_identity: Signer<'info>, + #[account(mut)] + pub player: Account<'info, Player>, } #[delegate] @@ -145,4 +156,5 @@ pub struct Undelegate<'info> { #[account] pub struct Player { pub last_result: u8, + pub rollnum: u8, } \ No newline at end of file diff --git a/roll-dice/tests/roll-dice-delegated.ts b/roll-dice/tests/roll-dice-delegated.ts index 787631b..02dc07b 100644 --- a/roll-dice/tests/roll-dice-delegated.ts +++ b/roll-dice/tests/roll-dice-delegated.ts @@ -1,7 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; import {Program} from "@coral-xyz/anchor"; -import { LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { LAMPORTS_PER_SOL, Keypair, SystemProgram, Transaction, PublicKey } from "@solana/web3.js"; import { RandomDiceDelegated } from "../target/types/random_dice_delegated"; +import * as crypto from "crypto"; +import { + DELEGATION_PROGRAM_ID, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, + createDelegateInstruction, + MAGIC_CONTEXT_ID, + MAGIC_PROGRAM_ID, +} from "@magicblock-labs/ephemeral-rollups-sdk"; describe("roll-dice-delegated", () => { // Configure the client to use the local cluster. @@ -12,7 +22,7 @@ describe("roll-dice-delegated", () => { const providerEphemeralRollup = new anchor.AnchorProvider( new anchor.web3.Connection( - process.env.PROVIDER_ENDPOINT || "https://devnet.magicblock.app/", + process.env.PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app/", { wsEndpoint: process.env.WS_ENDPOINT || "wss://devnet.magicblock.app/", } @@ -21,33 +31,172 @@ describe("roll-dice-delegated", () => { ); const ephemeralProgram = new Program(program.idl, providerEphemeralRollup); - console.log("Base Layer Connection: ", provider.connection._rpcEndpoint); - console.log("Ephemeral Rollup Connection: ", providerEphemeralRollup.connection._rpcEndpoint); - console.log(`Current SOL Public Key: ${anchor.Wallet.local().publicKey}`) + const payer = anchor.Wallet.local().publicKey; + + const delegatedPayerKeypair = Keypair.generate(); + + const [playerPda] = anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("playerd"), delegatedPayerKeypair.publicKey.toBuffer()], + program.programId + ); + + console.log("Base Layer Connection: ", provider.connection.rpcEndpoint); + console.log("Ephemeral Rollup Connection: ", providerEphemeralRollup.connection.rpcEndpoint); + console.log(`Current SOL Public Key: ${payer}`) + console.log("Player PDA: ", playerPda.toString()); + console.log("Delegated Payer Public Key: ", delegatedPayerKeypair.publicKey.toString()); before(async function () { const balance = await provider.connection.getBalance(anchor.Wallet.local().publicKey) console.log('Current balance is', balance / LAMPORTS_PER_SOL, ' SOL','\n') + + const transferIx = SystemProgram.transfer({ + fromPubkey: provider.wallet.publicKey, + toPubkey: delegatedPayerKeypair.publicKey, + lamports: 0.1 * LAMPORTS_PER_SOL, + }); + const tx = new Transaction().add(transferIx); + await provider.sendAndConfirm(tx); + console.log("Transferred 0.1 SOL to delegated payer keypair"); }) it("Initialized player!", async () => { - const tx = await program.methods.initialize().rpc(); + const tx = await program.methods + .initialize() + .accounts({ + payer: delegatedPayerKeypair.publicKey, + }) + .signers([delegatedPayerKeypair]) + .rpc(); console.log("Your transaction signature", tx); }); it("Delegate Roll Dice!", async () => { - const tx = await program.methods.delegate().rpc(); + const tx = await program.methods + .delegate() + .accounts({ + user: delegatedPayerKeypair.publicKey, + }) + .signers([delegatedPayerKeypair]) + .rpc(); console.log("Your transaction signature", tx); }); - it("Do Roll Dice Delegated!", async () => { - const tx = await ephemeralProgram.methods.rollDiceDelegated(0).rpc(); + it("Delegate on-curve account", async () => { + const delegatedAccount = delegatedPayerKeypair.publicKey; + + const assignIx = SystemProgram.assign({ + accountPubkey: delegatedAccount, + programId: DELEGATION_PROGRAM_ID, + }); + const assignTxHash = await provider.sendAndConfirm(new Transaction().add(assignIx), [delegatedPayerKeypair]); + console.log("Assign transaction signature:", assignTxHash); + + const delegateIx = createDelegateInstruction({ + payer: provider.wallet.publicKey, + delegatedAccount: delegatedAccount, + ownerProgram: SystemProgram.programId, + }); + const delegateTxHash = await provider.sendAndConfirm(new Transaction().add(delegateIx), [provider.wallet.payer,delegatedPayerKeypair]); + console.log("Delegate transaction signature:", delegateTxHash); + + await new Promise(resolve => setTimeout(resolve, 5000)); + }); + + it("Do Roll Dice Delegated with delegated payer!", async () => { + // Create a wallet from the delegated keypair for the ephemeral provider + const delegatedWallet = new anchor.Wallet(delegatedPayerKeypair); + const ephemeralProviderWithDelegatedPayer = new anchor.AnchorProvider( + providerEphemeralRollup.connection, + delegatedWallet, + {} + ); + const ephemeralProgramWithDelegatedPayer = new Program(program.idl, ephemeralProviderWithDelegatedPayer); + + const tx = await ephemeralProgramWithDelegatedPayer.methods + .rollDiceDelegated(1) + .accounts({ + payer: delegatedPayerKeypair.publicKey, + player: playerPda, + }) + .rpc(); console.log("Your transaction signature", tx); + await new Promise(resolve => setTimeout(resolve, 5000)); }); it("Undelegate Roll Dice!", async () => { - const tx = await ephemeralProgram.methods.undelegate().rpc(); + const delegatedWallet = new anchor.Wallet(delegatedPayerKeypair); + const ephemeralProviderWithDelegatedPayer = new anchor.AnchorProvider( + providerEphemeralRollup.connection, + delegatedWallet, + {} + ); + const ephemeralProgramWithDelegatedPayer = new Program(program.idl, ephemeralProviderWithDelegatedPayer); + + const tx = await ephemeralProgramWithDelegatedPayer.methods + .undelegate() + .accounts({ + payer: delegatedPayerKeypair.publicKey, + }) + .rpc(); console.log("Your transaction signature", tx); }); + xit("Undelegate on-curve account", async () => { + const delegatedAccount = delegatedPayerKeypair.publicKey; + const undelegateDiscriminator = crypto + .createHash("sha256") + .update("global:undelegate") + .digest() + .slice(0, 8); + + // For on-curve accounts, seeds is empty vec + const seedsLength = Buffer.allocUnsafe(4); + seedsLength.writeUInt32LE(0, 0); // empty vec length + + const instructionData = Buffer.concat([ + undelegateDiscriminator, + seedsLength, + ]); + + const delegationRecordPda = delegationRecordPdaFromDelegatedAccount(delegatedAccount); + const delegationMetadataPda = delegationMetadataPdaFromDelegatedAccount(delegatedAccount); + + const undelegateAccounts = [ + { pubkey: providerEphemeralRollup.wallet.publicKey, isSigner: true, isWritable: true }, // payer + { pubkey: delegatedAccount, isSigner: false, isWritable: true }, // delegated_account + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // owner_program (system for on-curve) + { pubkey: delegationRecordPda, isSigner: false, isWritable: true }, // delegation_record + { pubkey: delegationMetadataPda, isSigner: false, isWritable: true }, // delegation_metadata + { pubkey: DELEGATION_PROGRAM_ID, isSigner: false, isWritable: false }, // delegation_program + { pubkey: MAGIC_PROGRAM_ID, isSigner: false, isWritable: false }, // magic_program + { pubkey: MAGIC_CONTEXT_ID, isSigner: false, isWritable: true }, // magic_context + ]; + + const undelegateIx = new anchor.web3.TransactionInstruction({ + keys: undelegateAccounts, + programId: DELEGATION_PROGRAM_ID, + data: instructionData, + }); + + const undelegateTx = new Transaction().add(undelegateIx); + undelegateTx.feePayer = providerEphemeralRollup.wallet.publicKey; + undelegateTx.recentBlockhash = (await providerEphemeralRollup.connection.getLatestBlockhash()).blockhash; + const undelegateTxHash = await providerEphemeralRollup.sendAndConfirm(undelegateTx); + console.log("Undelegate transaction signature:", undelegateTxHash); + + // After undelegation, reassign the account back to system program + const reassignIx = SystemProgram.assign({ + accountPubkey: delegatedAccount, + programId: SystemProgram.programId, + }); + + const reassignTx = new Transaction().add(reassignIx); + reassignTx.feePayer = provider.wallet.publicKey; + reassignTx.recentBlockhash = (await provider.connection.getLatestBlockhash()).blockhash; + reassignTx.sign(delegatedPayerKeypair); + const reassignTxHash = await provider.sendAndConfirm(reassignTx, [delegatedPayerKeypair]); + console.log("Reassign transaction signature:", reassignTxHash); + }); + }); diff --git a/roll-dice/yarn.lock b/roll-dice/yarn.lock index fa35083..dccd327 100644 --- a/roll-dice/yarn.lock +++ b/roll-dice/yarn.lock @@ -41,6 +41,28 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@magicblock-labs/ephemeral-rollups-sdk@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.4.1.tgz#2fba50160e6a279c0eaaa02bac50e48ee8781e54" + integrity sha512-Te/8DIddisjci1nhUNM24EtfS2Hk3h4PXjYtGQV/MsIGq5S+Zwd04Sop8q5h05GHYbvzMAG1TqDPIArPBOBzVA== + dependencies: + "@metaplex-foundation/beet" "^0.7.2" + "@phala/dcap-qvl-web" "^0.2.7" + "@solana/web3.js" "^1.98.0" + bs58 "^6.0.0" + rpc-websockets "^9.0.4" + typescript "^5.3.0" + +"@metaplex-foundation/beet@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.2.tgz#fa4726e4cfd4fb6fed6cddc9b5213c1c2a2d0b77" + integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== + dependencies: + ansicolors "^0.3.2" + assert "^2.1.0" + bn.js "^5.2.0" + debug "^4.3.3" + "@noble/curves@^1.4.2": version "1.8.1" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" @@ -53,6 +75,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== +"@phala/dcap-qvl-web@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz#d7a03b059a201355262ca9c1bb6c77a1c22472dd" + integrity sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ== + "@solana/buffer-layout@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -60,6 +87,29 @@ dependencies: buffer "~6.0.3" +"@solana/codecs-core@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.3.0.tgz#6bf2bb565cb1ae880f8018635c92f751465d8695" + integrity sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw== + dependencies: + "@solana/errors" "2.3.0" + +"@solana/codecs-numbers@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz#ac7e7f38aaf7fcd22ce2061fbdcd625e73828dc6" + integrity sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg== + dependencies: + "@solana/codecs-core" "2.3.0" + "@solana/errors" "2.3.0" + +"@solana/errors@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.3.0.tgz#4ac9380343dbeffb9dffbcb77c28d0e457c5fa31" + integrity sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ== + dependencies: + chalk "^5.4.1" + commander "^14.0.0" + "@solana/web3.js@^1.69.0": version "1.98.0" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.0.tgz#21ecfe8198c10831df6f0cfde7f68370d0405917" @@ -81,6 +131,27 @@ rpc-websockets "^9.0.2" superstruct "^2.0.2" +"@solana/web3.js@^1.98.0": + version "1.98.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" + integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@swc/helpers@^0.5.11": version "0.5.15" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" @@ -185,6 +256,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansicolors@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -203,11 +279,29 @@ arrify@^1.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== +assert@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -220,6 +314,11 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -285,6 +384,13 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -310,6 +416,32 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + camelcase@^6.0.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" @@ -336,6 +468,11 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.4.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -379,6 +516,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^14.0.0: + version "14.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.2.tgz#b71fd37fe4069e4c3c7c13925252ada4eba14e8e" + integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== + commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -403,6 +545,13 @@ debug@4.3.3: dependencies: ms "2.1.2" +debug@^4.3.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" @@ -415,6 +564,24 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -430,11 +597,37 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -502,6 +695,13 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -512,6 +712,16 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -522,6 +732,30 @@ get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -541,6 +775,11 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -551,6 +790,32 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -576,11 +841,19 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-arguments@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -588,6 +861,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -598,6 +876,17 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.7: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== + dependencies: + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -605,6 +894,14 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -615,6 +912,23 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-typed-array@^1.1.3: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -699,6 +1013,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + minimatch@4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" @@ -760,7 +1079,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -787,6 +1106,31 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -833,6 +1177,11 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + prettier@^2.6.2: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" @@ -878,11 +1227,36 @@ rpc-websockets@^9.0.2: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^9.0.4: + version "9.3.1" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.3.1.tgz#d817a59d812f68bae1215740a3f78fcdd3813698" + integrity sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + safe-buffer@^5.0.1, safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -890,6 +1264,18 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + source-map-support@^0.5.6: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -1023,6 +1409,11 @@ type-detect@^4.0.0, type-detect@^4.1.0: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== +typescript@^5.3.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + typescript@^5.7.3: version "5.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" @@ -1040,6 +1431,17 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.3.0" +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -1058,6 +1460,19 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which-typed-array@^1.1.16, which-typed-array@^1.1.2: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" From 5e4aefb9f444874fe41ae1f16f8b50c51d708e56 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Wed, 19 Nov 2025 21:08:56 +0800 Subject: [PATCH 02/18] v2 --- roll-dice/app/app/delegated/page.tsx | 160 +++++++------------- roll-dice/app/app/page.tsx | 216 ++++++++++----------------- roll-dice/app/lib/config.ts | 15 ++ roll-dice/app/lib/solana-utils.ts | 77 ++++++++++ roll-dice/app/lib/types.ts | 13 ++ 5 files changed, 240 insertions(+), 241 deletions(-) create mode 100644 roll-dice/app/lib/config.ts create mode 100644 roll-dice/app/lib/solana-utils.ts create mode 100644 roll-dice/app/lib/types.ts diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index c1895bf..f7563ce 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -1,22 +1,18 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import Dice from "@/components/dice" -import SolanaAddress from "@/components/solana-address" -// @ts-ignore import * as anchor from "@coral-xyz/anchor" -// @ts-ignore import { Connection, Keypair, PublicKey, Transaction, - VersionedTransaction, SystemProgram, - LAMPORTS_PER_SOL, } from "@solana/web3.js" import { createDelegateInstruction, DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" import { useToast } from "@/hooks/use-toast" +import Dice from "@/components/dice" +import SolanaAddress from "@/components/solana-address" import { Table, TableBody, @@ -25,38 +21,30 @@ import { TableHeader, TableRow, } from "@/components/ui/table" - -const PROGRAM_ID = new PublicKey("5bPwgoPWz274NKgThcnPas2Mv4rSknu9JrbxzFVqU5gY") -const PLAYER_SEED = "playerd" -const ORACLE_QUEUE = new PublicKey("5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc") -const BASE_ENDPOINT = "https://rpc.magicblock.app/devnet" -const PLAYER_STORAGE_KEY = "solanaKeypair" -const PAYER_STORAGE_KEY = "delegatePayerKeypair" - -const walletAdapterFrom = (keypair: Keypair) => ({ - publicKey: keypair.publicKey, - async signTransaction(transaction: T): Promise { - // @ts-ignore - Transaction and VersionedTransaction have different sign signatures - transaction.sign(keypair) - return transaction - }, - async signAllTransactions(transactions: T[]): Promise { - // @ts-ignore - Transaction and VersionedTransaction have different sign signatures - transactions.forEach(tx => tx.sign(keypair)) - return transactions - }, -}) +import { + PROGRAM_ID, + PLAYER_SEED, + ORACLE_QUEUE, + BASE_ENDPOINT, + PLAYER_STORAGE_KEY, + PAYER_STORAGE_KEY, + BLOCKHASH_REFRESH_INTERVAL_MS, + ROLL_TIMEOUT_MS, + ROLL_ANIMATION_INTERVAL_MS, +} from "@/lib/config" +import { + walletAdapterFrom, + loadOrCreateKeypair, + ensureFunds, + fetchAndCacheBlockhash, + getCachedBlockhash, + checkDelegationStatus, +} from "@/lib/solana-utils" +import type { RollEntry, CachedBlockhash } from "@/lib/types" const derivePlayerPda = (user: PublicKey) => PublicKey.findProgramAddressSync([Buffer.from(PLAYER_SEED), user.toBuffer()], PROGRAM_ID)[0] -type RollEntry = { - value: number | null - startTime: number - endTime: number | null - isPending: boolean -} - export default function DiceRollerDelegated() { const [diceValue, setDiceValue] = useState(1) const [isRolling, setIsRolling] = useState(false) @@ -64,6 +52,7 @@ export default function DiceRollerDelegated() { const [isDelegated, setIsDelegated] = useState(false) const [isDelegating, setIsDelegating] = useState(false) const [rollHistory, setRollHistory] = useState([]) + const previousDiceValueRef = useRef(1) const previousRollnumRef = useRef(0) const expectingRollResultRef = useRef(false) @@ -79,8 +68,9 @@ export default function DiceRollerDelegated() { const blockhashIntervalRef = useRef(null) const playerKeypairRef = useRef(null) const payerKeypairRef = useRef(null) - const cachedBaseBlockhashRef = useRef<{ blockhash: string; lastValidBlockHeight: number; timestamp: number } | null>(null) - const cachedEphemeralBlockhashRef = useRef<{ blockhash: string; lastValidBlockHeight: number; timestamp: number } | null>(null) + const cachedBaseBlockhashRef = useRef(null) + const cachedEphemeralBlockhashRef = useRef(null) + const { toast } = useToast() const clearAllIntervals = useCallback(() => { @@ -103,54 +93,26 @@ export default function DiceRollerDelegated() { }, []) - const ensureFunds = useCallback(async (connection: Connection, keypair: Keypair) => { - const balance = await connection.getBalance(keypair.publicKey) - if (balance < 0.05 * LAMPORTS_PER_SOL) { - const signature = await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL) - await connection.confirmTransaction(signature, "confirmed") - } + const fetchBlockhash = useCallback(async (connection: Connection, isEphemeral: boolean) => { + const cacheRef = isEphemeral ? cachedEphemeralBlockhashRef : cachedBaseBlockhashRef + await fetchAndCacheBlockhash(connection, cacheRef) }, []) - const fetchAndCacheBlockhash = useCallback(async (connection: Connection, isEphemeral: boolean) => { - try { - const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() - const cached = { - blockhash, - lastValidBlockHeight, - timestamp: Date.now(), - } - if (isEphemeral) { - cachedEphemeralBlockhashRef.current = cached - } else { - cachedBaseBlockhashRef.current = cached - } - } catch (error) { - console.error("Failed to fetch blockhash:", error) - } + const getBlockhash = useCallback((connection: Connection, isEphemeral: boolean): string | null => { + const cacheRef = isEphemeral ? cachedEphemeralBlockhashRef : cachedBaseBlockhashRef + return getCachedBlockhash(connection, cacheRef) }, []) - const getCachedBlockhash = useCallback((connection: Connection, isEphemeral: boolean): string | null => { - const cached = isEphemeral ? cachedEphemeralBlockhashRef.current : cachedBaseBlockhashRef.current - if (!cached) return null - - const age = Date.now() - cached.timestamp - if (age > 30000) { - fetchAndCacheBlockhash(connection, isEphemeral) - } - - return cached.blockhash - }, [fetchAndCacheBlockhash]) - const sendTransaction = useCallback( async (connection: Connection, transaction: Transaction, feePayer: Keypair, signers: Keypair[], isEphemeral: boolean = false) => { let blockhash: string - const cached = getCachedBlockhash(connection, isEphemeral) + const cached = getBlockhash(connection, isEphemeral) if (cached) { blockhash = cached } else { const result = await connection.getLatestBlockhash() blockhash = result.blockhash - fetchAndCacheBlockhash(connection, isEphemeral) + await fetchBlockhash(connection, isEphemeral) } transaction.recentBlockhash = blockhash @@ -164,31 +126,18 @@ export default function DiceRollerDelegated() { signerMap.forEach(signer => transaction.partialSign(signer)) - const signature = await connection.sendRawTransaction(transaction.serialize()) - return signature + return await connection.sendRawTransaction(transaction.serialize()) }, - [getCachedBlockhash, fetchAndCacheBlockhash] + [getBlockhash, fetchBlockhash] ) const refreshDelegationStatus = useCallback(async () => { if (!connectionRef.current || !playerKeypairRef.current) return false - const accountInfo = await connectionRef.current.getAccountInfo(playerKeypairRef.current.publicKey) - const delegated = !!accountInfo && accountInfo.owner.equals(DELEGATION_PROGRAM_ID) + const delegated = await checkDelegationStatus(connectionRef.current, playerKeypairRef.current.publicKey) setIsDelegated(delegated) return delegated }, []) - const loadOrCreateKeypair = useCallback((storageKey: string) => { - if (typeof window === "undefined") return Keypair.generate() - const stored = window.localStorage.getItem(storageKey) - if (stored) { - return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(stored))) - } - const generated = Keypair.generate() - window.localStorage.setItem(storageKey, JSON.stringify(Array.from(generated.secretKey))) - return generated - }, []) - const initializeProgram = useCallback(async () => { if (typeof window === "undefined") return try { @@ -302,19 +251,19 @@ export default function DiceRollerDelegated() { await refreshDelegationStatus() - await fetchAndCacheBlockhash(connection, false) + await fetchBlockhash(connection, false) if (ephemeralConnection) { - await fetchAndCacheBlockhash(ephemeralConnection, true) + await fetchBlockhash(ephemeralConnection, true) } blockhashIntervalRef.current = setInterval(() => { if (connectionRef.current) { - fetchAndCacheBlockhash(connectionRef.current, false) + fetchBlockhash(connectionRef.current, false) } if (ephemeralConnectionRef.current) { - fetchAndCacheBlockhash(ephemeralConnectionRef.current, true) + fetchBlockhash(ephemeralConnectionRef.current, true) } - }, 20000) + }, BLOCKHASH_REFRESH_INTERVAL_MS) setIsInitialized(true) } catch (error) { @@ -326,13 +275,14 @@ export default function DiceRollerDelegated() { variant: "destructive", }) } - }, [ensureFunds, loadOrCreateKeypair, refreshDelegationStatus, toast, fetchAndCacheBlockhash]) + }, [refreshDelegationStatus, toast, fetchBlockhash]) useEffect(() => { initializeProgram() return () => { clearAllIntervals() + // Clean up subscription if (subscriptionIdRef.current !== null && ephemeralConnectionRef.current) { ephemeralConnectionRef.current.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) subscriptionIdRef.current = null @@ -377,6 +327,8 @@ export default function DiceRollerDelegated() { await sendTransaction(connection, new Transaction().add(assignIx), payerKeypair, [playerKeypair], false) } + await new Promise(resolve => setTimeout(resolve, 3000)) + const delegateIx = createDelegateInstruction({ payer: payerKeypair.publicKey, delegatedAccount: playerKeypair.publicKey, @@ -411,12 +363,13 @@ export default function DiceRollerDelegated() { rollIntervalRef.current = setInterval(() => { setDiceValue(Math.floor(Math.random() * 6) + 1) - }, 100) + }, ROLL_ANIMATION_INTERVAL_MS) + // Create pending roll history entry (will update startTime after transaction is sent) setRollHistory(prev => { const newEntry = { value: null, - startTime: Date.now(), + startTime: Date.now(), // Temporary, will be updated after send endTime: null, isPending: true, } @@ -449,7 +402,7 @@ export default function DiceRollerDelegated() { variant: "destructive", }) } - }, 10000) + }, ROLL_TIMEOUT_MS) try { const randomValue = Math.floor(Math.random() * 6) + 1 @@ -463,7 +416,7 @@ export default function DiceRollerDelegated() { }) .transaction() - const cachedBlockhash = getCachedBlockhash(ephemeralConnectionRef.current!, true) + const cachedBlockhash = getBlockhash(ephemeralConnectionRef.current!, true) if (cachedBlockhash) { tx.recentBlockhash = cachedBlockhash } else { @@ -474,7 +427,7 @@ export default function DiceRollerDelegated() { tx.feePayer = playerKeypairRef.current.publicKey tx.sign(playerKeypairRef.current) - const signature = await ephemeralConnectionRef.current!.sendRawTransaction( + await ephemeralConnectionRef.current!.sendRawTransaction( tx.serialize(), { skipPreflight: true } ) @@ -492,13 +445,8 @@ export default function DiceRollerDelegated() { return updated }) - toast({ - title: "Dice Rolled", - description: `Result: TX: ${signature.slice(0, 8)}...`, - }) - if (ephemeralConnectionRef.current) { - fetchAndCacheBlockhash(ephemeralConnectionRef.current, true) + await fetchBlockhash(ephemeralConnectionRef.current, true) } } catch (error) { clearAllIntervals() @@ -519,7 +467,7 @@ export default function DiceRollerDelegated() { return updated }) } - }, [clearAllIntervals, isDelegated, isInitialized, isRolling, toast, getCachedBlockhash, fetchAndCacheBlockhash]) + }, [clearAllIntervals, isDelegated, isInitialized, isRolling, toast, getBlockhash, fetchBlockhash]) return (
diff --git a/roll-dice/app/app/page.tsx b/roll-dice/app/app/page.tsx index 4b74f88..ee63f5c 100644 --- a/roll-dice/app/app/page.tsx +++ b/roll-dice/app/app/page.tsx @@ -1,25 +1,25 @@ "use client" import { useState, useCallback, useEffect, useRef } from "react" -import Dice from "@/components/dice" -import SolanaAddress from "@/components/solana-address" -// @ts-ignore import * as anchor from "@coral-xyz/anchor" -// @ts-ignore -import {Connection, Keypair, PublicKey, Transaction, VersionedTransaction} from "@solana/web3.js" +import { Connection, PublicKey } from "@solana/web3.js" import { useToast } from "@/hooks/use-toast" - -// Program ID for the dice game -const PROGRAM_ID = new anchor.web3.PublicKey("8xgZ1hY7TnVZ4Bbh7v552Rs3BZMSq3LisyWckkBsNLP") +import Dice from "@/components/dice" +import SolanaAddress from "@/components/solana-address" +import { PROGRAM_ID_STANDARD, PLAYER_SEED, BASE_ENDPOINT, PLAYER_STORAGE_KEY } from "@/lib/config" +import { walletAdapterFrom, loadOrCreateKeypair } from "@/lib/solana-utils" export default function DiceRoller() { const [diceValue, setDiceValue] = useState(1) const [isRolling, setIsRolling] = useState(false) const [isInitialized, setIsInitialized] = useState(false) - const [key, setKey] = useState(0) // Used to force re-render the component + const [key, setKey] = useState(0) + const programRef = useRef(null) const subscriptionIdRef = useRef(null) const rollIntervalRef = useRef(null) + const timeoutRef = useRef(null) + const { toast } = useToast() // Clear the rolling animation interval @@ -30,91 +30,61 @@ export default function DiceRoller() { } } + const clearAllIntervals = useCallback(() => { + clearRollInterval() + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, []) + const initializeProgram = async () => { try { - // Get or create keypair - let storedKeypair = localStorage.getItem("solanaKeypair") - let keypair: Keypair - - const connection = new Connection("https://rpc.magicblock.app/devnet", "confirmed") + const keypair = loadOrCreateKeypair(PLAYER_STORAGE_KEY) + const connection = new Connection(BASE_ENDPOINT, "confirmed") - if (storedKeypair) { - const secretKey = Uint8Array.from(JSON.parse(storedKeypair)) - keypair = Keypair.fromSecretKey(secretKey) - } else { - keypair = Keypair.generate() - localStorage.setItem("solanaKeypair", JSON.stringify(Array.from(keypair.secretKey))) - } - - // Create the provider const provider = new anchor.AnchorProvider( - connection, - { - publicKey: keypair.publicKey, - signTransaction: async (transaction: T): Promise => { - // @ts-ignore - transaction.sign(keypair) - return transaction - }, - signAllTransactions: async (transactions: T[]): Promise => { - for (const tx of transactions) { - // @ts-ignore - tx.sign(keypair) - } - return transactions - }, - }, - anchor.AnchorProvider.defaultOptions() + connection, + walletAdapterFrom(keypair), + anchor.AnchorProvider.defaultOptions() ) - // User - console.log("User: ", keypair.publicKey.toBase58()) - - // Fetch the IDL - const idl = await anchor.Program.fetchIdl(PROGRAM_ID, provider) + const idl = await anchor.Program.fetchIdl(PROGRAM_ID_STANDARD, provider) if (!idl) throw new Error("IDL not found") - // Create the program instance const program = new anchor.Program(idl, provider) programRef.current = program - console.log("Program instance created successfully: ", program.programId.toBase58()) - - // Initialize the program - const playerPk = PublicKey.findProgramAddressSync([Buffer.from("playerd"), provider.publicKey.toBytes()], program.programId)[0]; - let account = await connection.getAccountInfo(playerPk); - // @ts-ignore - if(!account || !account.data || account.data.length === 0) { - console.log("Player account not found, creating new one...") - const tx = await program.methods.initialize().rpc() - console.log("User initialized with tx:", tx) - }else{ - const ply = program.coder.accounts.decode("player", account.data) - console.log("Player account:", playerPk.toBase58(), "lastResult:", ply.lastResult) - setDiceValue(ply.lastResult) + const playerPk = PublicKey.findProgramAddressSync( + [Buffer.from(PLAYER_SEED), provider.publicKey.toBytes()], + program.programId + )[0] + + let account = await connection.getAccountInfo(playerPk) + if (!account || !account.data || account.data.length === 0) { + await program.methods.initialize().rpc() + } else { + const player = program.coder.accounts.decode("player", account.data) + setDiceValue(player.lastResult) } - // Subscribe to account changes if (subscriptionIdRef.current !== null) { - await connection.removeAccountChangeListener(subscriptionIdRef.current); + await connection.removeAccountChangeListener(subscriptionIdRef.current) } subscriptionIdRef.current = connection.onAccountChange( - playerPk, - // @ts-ignore - (accountInfo) => { - const player = program.coder.accounts.decode("player", accountInfo.data) - console.log("Player account changed:", player) - setDiceValue(player.lastResult) - setIsRolling(false) - clearRollInterval() - }, - {commitment: "processed"} - ); - - // Set initialization as successful - setIsInitialized(true) + playerPk, + (accountInfo) => { + const player = program.coder.accounts.decode("player", accountInfo.data) + const newValue = Number(player.lastResult) + setDiceValue(newValue) + setIsRolling(false) + clearAllIntervals() + }, + { commitment: "processed" } + ) + setIsInitialized(true) } catch (error) { console.error("Failed to initialize program:", error) setIsInitialized(false) @@ -129,30 +99,19 @@ export default function DiceRoller() { useEffect(() => { initializeProgram() - // Cleanup function return () => { - clearRollInterval() - // Clean up subscription + clearAllIntervals() if (subscriptionIdRef.current !== null) { - const connection = new Connection("https://rpc.magicblock.app/devnet", "confirmed") + const connection = new Connection(BASE_ENDPOINT, "confirmed") connection.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) } } - }, [toast, key]) // Add key as dependency to re-run when key changes + }, [toast, key]) const handleBalanceChange = useCallback((newBalance: number) => { - console.log("Balance changed:", newBalance) - - // If not initialized, try to initialize again or force component reload if (!isInitialized) { - console.log("Not initialized, attempting to reinitialize...") - - // Option 1: Call initializeProgram again initializeProgram() - - // Option 2: Force component reload by changing the key setKey(prevKey => prevKey + 1) - toast({ title: "Reinitializing", description: "Balance changed, attempting to reinitialize the program", @@ -161,61 +120,48 @@ export default function DiceRoller() { }, [isInitialized, toast]) const handleRollDice = useCallback(async () => { - if (isRolling || !isInitialized) return; + if (isRolling || !isInitialized) return setIsRolling(true) - - // Clear any existing interval clearRollInterval() - if (programRef.current) { - try { - const tx = await programRef.current.methods.rollDice(Math.floor(Math.random() * 6) + 1).rpc() - console.log("Dice rolled on-chain with tx:", tx) - - toast({ - title: "Dice Rolled", - description: `Result: TX: ${tx.slice(0, 8)}...`, - }) - - // Simulate rolling animation by changing values rapidly - rollIntervalRef.current = setInterval(() => { - setDiceValue(Math.floor(Math.random() * 6) + 1) - }, 100) - - // Add a timeout to stop rolling after 10 seconds if still rolling - setTimeout(() => { - if (isRolling) { - console.log("Rolling timeout reached (10s), stopping animation") - setIsRolling(false) - clearRollInterval() - toast({ - title: "Notice", - description: "Dice roll is taking longer than expected. Check transaction status in explorer.", - variant: "destructive", - }) - } - }, 10000) - - - } catch (error) { - console.error("Error rolling dice:", error) - toast({ - title: "Error", - description: "Failed to roll dice", - variant: "destructive", - }) - setIsRolling(false) - clearRollInterval() - } - } else { - console.error("Program not initialized") + if (!programRef.current) { toast({ title: "Error", description: "Program not initialized", variant: "destructive", }) setIsRolling(false) + return + } + + try { + await programRef.current.methods.rollDice(Math.floor(Math.random() * 6) + 1).rpc() + + rollIntervalRef.current = setInterval(() => { + setDiceValue(Math.floor(Math.random() * 6) + 1) + }, 100) + + setTimeout(() => { + if (isRolling) { + setIsRolling(false) + clearRollInterval() + toast({ + title: "Notice", + description: "Dice roll is taking longer than expected. Check transaction status in explorer.", + variant: "destructive", + }) + } + }, 10000) + } catch (error) { + console.error("Error rolling dice:", error) + toast({ + title: "Error", + description: "Failed to roll dice", + variant: "destructive", + }) + setIsRolling(false) + clearRollInterval() } }, [isRolling, isInitialized, toast]) diff --git a/roll-dice/app/lib/config.ts b/roll-dice/app/lib/config.ts new file mode 100644 index 0000000..f06fc33 --- /dev/null +++ b/roll-dice/app/lib/config.ts @@ -0,0 +1,15 @@ +import { PublicKey } from "@solana/web3.js" + +export const PROGRAM_ID = new PublicKey("5bPwgoPWz274NKgThcnPas2Mv4rSknu9JrbxzFVqU5gY") +export const PROGRAM_ID_STANDARD = new PublicKey("8xgZ1hY7TnVZ4Bbh7v552Rs3BZMSq3LisyWckkBsNLP") +export const PLAYER_SEED = "playerd" +export const ORACLE_QUEUE = new PublicKey("5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc") +export const BASE_ENDPOINT = "https://rpc.magicblock.app/devnet" +export const PLAYER_STORAGE_KEY = "solanaKeypair" +export const PAYER_STORAGE_KEY = "delegatePayerKeypair" +export const MIN_BALANCE_LAMPORTS = 0.05 +export const BLOCKHASH_CACHE_MAX_AGE_MS = 30000 +export const BLOCKHASH_REFRESH_INTERVAL_MS = 20000 +export const ROLL_TIMEOUT_MS = 10000 +export const ROLL_ANIMATION_INTERVAL_MS = 100 + diff --git a/roll-dice/app/lib/solana-utils.ts b/roll-dice/app/lib/solana-utils.ts new file mode 100644 index 0000000..dc05953 --- /dev/null +++ b/roll-dice/app/lib/solana-utils.ts @@ -0,0 +1,77 @@ +import { Keypair, PublicKey, Transaction, VersionedTransaction, Connection, LAMPORTS_PER_SOL } from "@solana/web3.js" +import { DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" +import { MIN_BALANCE_LAMPORTS, BLOCKHASH_CACHE_MAX_AGE_MS } from "./config" +import type { CachedBlockhash } from "./types" + +export const walletAdapterFrom = (keypair: Keypair) => ({ + publicKey: keypair.publicKey, + async signTransaction(transaction: T): Promise { + // @ts-ignore - Transaction and VersionedTransaction have different sign signatures + transaction.sign(keypair) + return transaction + }, + async signAllTransactions(transactions: T[]): Promise { + // @ts-ignore - Transaction and VersionedTransaction have different sign signatures + transactions.forEach(tx => tx.sign(keypair)) + return transactions + }, +}) + +export const loadOrCreateKeypair = (storageKey: string): Keypair => { + if (typeof window === "undefined") return Keypair.generate() + const stored = window.localStorage.getItem(storageKey) + if (stored) { + return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(stored))) + } + const generated = Keypair.generate() + window.localStorage.setItem(storageKey, JSON.stringify(Array.from(generated.secretKey))) + return generated +} + +export const ensureFunds = async (connection: Connection, keypair: Keypair): Promise => { + const balance = await connection.getBalance(keypair.publicKey) + if (balance < MIN_BALANCE_LAMPORTS * LAMPORTS_PER_SOL) { + const signature = await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL) + await connection.confirmTransaction(signature, "confirmed") + } +} + +export const fetchAndCacheBlockhash = async ( + connection: Connection, + cacheRef: { current: CachedBlockhash | null } +): Promise => { + try { + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + cacheRef.current = { + blockhash, + lastValidBlockHeight, + timestamp: Date.now(), + } + } catch (error) { + console.error("Failed to fetch blockhash:", error) + } +} + +export const getCachedBlockhash = ( + connection: Connection, + cacheRef: { current: CachedBlockhash | null } +): string | null => { + const cached = cacheRef.current + if (!cached) return null + + const age = Date.now() - cached.timestamp + if (age > BLOCKHASH_CACHE_MAX_AGE_MS) { + fetchAndCacheBlockhash(connection, cacheRef) + } + + return cached.blockhash +} + +export const checkDelegationStatus = async ( + connection: Connection, + accountPubkey: PublicKey +): Promise => { + const accountInfo = await connection.getAccountInfo(accountPubkey) + return !!accountInfo && accountInfo.owner.equals(DELEGATION_PROGRAM_ID) +} + diff --git a/roll-dice/app/lib/types.ts b/roll-dice/app/lib/types.ts new file mode 100644 index 0000000..cc5d3bb --- /dev/null +++ b/roll-dice/app/lib/types.ts @@ -0,0 +1,13 @@ +export type RollEntry = { + value: number | null + startTime: number + endTime: number | null + isPending: boolean +} + +export type CachedBlockhash = { + blockhash: string + lastValidBlockHeight: number + timestamp: number +} + From accf30f2a456e82df51276db8cf1e98a94948515 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Wed, 19 Nov 2025 21:18:23 +0800 Subject: [PATCH 03/18] v3 --- roll-dice/app/app/delegated/page.tsx | 10 +- roll-dice/app/lib/delegate-instruction.ts | 106 ++++++++++++++++++++++ roll-dice/vercel.json | 5 + 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 roll-dice/app/lib/delegate-instruction.ts create mode 100644 roll-dice/vercel.json diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index f7563ce..9505856 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -9,7 +9,8 @@ import { Transaction, SystemProgram, } from "@solana/web3.js" -import { createDelegateInstruction, DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" +import { DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" +import { createDelegateInstruction } from "@/lib/delegate-instruction" import { useToast } from "@/hooks/use-toast" import Dice from "@/components/dice" import SolanaAddress from "@/components/solana-address" @@ -513,7 +514,7 @@ export default function DiceRollerDelegated() { Value - Time + Time @@ -530,13 +531,14 @@ export default function DiceRollerDelegated() { : entry.endTime ? entry.endTime - entry.startTime : 0 + const formattedTime = `${elapsed.toString().padStart(6, '\u00A0')}ms${entry.isPending ? '...' : ''}` return ( {entry.value !== null ? entry.value : "-"} - - {entry.isPending ? `${elapsed}ms...` : `${elapsed}ms`} + + {formattedTime} ) diff --git a/roll-dice/app/lib/delegate-instruction.ts b/roll-dice/app/lib/delegate-instruction.ts new file mode 100644 index 0000000..5422e54 --- /dev/null +++ b/roll-dice/app/lib/delegate-instruction.ts @@ -0,0 +1,106 @@ +import * as beet from "@metaplex-foundation/beet"; +import * as web3 from "@solana/web3.js"; +import { + DELEGATION_PROGRAM_ID, + delegationRecordPdaFromDelegatedAccount, + delegationMetadataPdaFromDelegatedAccount, + delegateBufferPdaFromDelegatedAccountAndOwnerProgram, +} from "@magicblock-labs/ephemeral-rollups-sdk"; + +const delegateStruct = new beet.FixableBeetArgsStruct( + [ + ["instructionDiscriminator", beet.uniformFixedSizeArray(beet.u8, 8)], + ["commit_frequency_ms", beet.u32], + ["seeds", beet.array(beet.array(beet.u8))], + ["validator", beet.coption(beet.uniformFixedSizeArray(beet.u8, 32))], + ], + "DelegateInstructionArgs" +); + +const delegateInstructionDiscriminator = [0, 0, 0, 0, 0, 0, 0, 0]; + +export interface CreateDelegateInstructionAccounts { + payer: web3.PublicKey; + delegatedAccount: web3.PublicKey; + ownerProgram: web3.PublicKey; + delegationRecord?: web3.PublicKey; + delegationMetadata?: web3.PublicKey; + systemProgram?: web3.PublicKey; + validator?: web3.PublicKey; +} + +export interface CreateDelegateInstructionArgs { + commit_frequency_ms?: number; + seeds?: number[][]; +} + +export function createDelegateInstruction( + accounts: CreateDelegateInstructionAccounts, + args?: CreateDelegateInstructionArgs, + programId: web3.PublicKey = DELEGATION_PROGRAM_ID +): web3.TransactionInstruction { + const delegateBufferPda = delegateBufferPdaFromDelegatedAccountAndOwnerProgram( + accounts.delegatedAccount, + accounts.ownerProgram + ); + const delegationRecordPda = delegationRecordPdaFromDelegatedAccount( + accounts.delegatedAccount + ); + const delegationMetadataPda = delegationMetadataPdaFromDelegatedAccount( + accounts.delegatedAccount + ); + + args = args ?? { + commit_frequency_ms: 4294967295, + seeds: [], + }; + + const keys = [ + { pubkey: accounts.payer, isWritable: false, isSigner: true }, + { pubkey: accounts.delegatedAccount, isWritable: true, isSigner: true }, + { pubkey: accounts.ownerProgram, isWritable: false, isSigner: false }, + { + pubkey: delegateBufferPda, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.delegationRecord ?? delegationRecordPda, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.delegationMetadata ?? delegationMetadataPda, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ...(accounts.validator + ? [ + { + pubkey: accounts.validator, + isWritable: false, + isSigner: false, + }, + ] + : []), + ]; + + const [data] = delegateStruct.serialize({ + instructionDiscriminator: delegateInstructionDiscriminator, + commit_frequency_ms: args.commit_frequency_ms, + seeds: args.seeds.map((seed) => seed.map(Number)), + validator: accounts.validator ? accounts.validator.toBytes() : null, + }); + + return new web3.TransactionInstruction({ + programId, + keys, + data, + }); +} + diff --git a/roll-dice/vercel.json b/roll-dice/vercel.json new file mode 100644 index 0000000..9ffc824 --- /dev/null +++ b/roll-dice/vercel.json @@ -0,0 +1,5 @@ +{ + "buildCommand": "cd app && npm install && npm run build", + "outputDirectory": "app/.next" +} + From 564060588397caed950cdc143df2649db10d7fbe Mon Sep 17 00:00:00 2001 From: fauxfire Date: Wed, 19 Nov 2025 21:23:54 +0800 Subject: [PATCH 04/18] add dependencies --- roll-dice/app/package.json | 4 +- roll-dice/app/yarn.lock | 199 ++++++++++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 4 deletions(-) diff --git a/roll-dice/app/package.json b/roll-dice/app/package.json index 48569e7..cd1a076 100644 --- a/roll-dice/app/package.json +++ b/roll-dice/app/package.json @@ -9,7 +9,10 @@ "lint": "next lint" }, "dependencies": { + "@coral-xyz/anchor": "^0.31.0", "@hookform/resolvers": "^3.9.1", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.4.1", + "@metaplex-foundation/beet": "^0.7.2", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-aspect-ratio": "^1.1.1", @@ -38,7 +41,6 @@ "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "@solana/web3.js": "latest", - "@coral-xyz/anchor": "^0.31.0", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/roll-dice/app/yarn.lock b/roll-dice/app/yarn.lock index 15879bd..134efb3 100644 --- a/roll-dice/app/yarn.lock +++ b/roll-dice/app/yarn.lock @@ -351,6 +351,28 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@magicblock-labs/ephemeral-rollups-sdk@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.4.1.tgz#2fba50160e6a279c0eaaa02bac50e48ee8781e54" + integrity sha512-Te/8DIddisjci1nhUNM24EtfS2Hk3h4PXjYtGQV/MsIGq5S+Zwd04Sop8q5h05GHYbvzMAG1TqDPIArPBOBzVA== + dependencies: + "@metaplex-foundation/beet" "^0.7.2" + "@phala/dcap-qvl-web" "^0.2.7" + "@solana/web3.js" "^1.98.0" + bs58 "^6.0.0" + rpc-websockets "^9.0.4" + typescript "^5.3.0" + +"@metaplex-foundation/beet@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.2.tgz#fa4726e4cfd4fb6fed6cddc9b5213c1c2a2d0b77" + integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== + dependencies: + ansicolors "^0.3.2" + assert "^2.1.0" + bn.js "^5.2.0" + debug "^4.3.3" + "@napi-rs/wasm-runtime@^0.2.7": version "0.2.7" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz#288f03812a408bc53c2c3686c65f38fe90f295eb" @@ -450,6 +472,11 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@phala/dcap-qvl-web@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz#d7a03b059a201355262ca9c1bb6c77a1c22472dd" + integrity sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1057,6 +1084,29 @@ dependencies: buffer "~6.0.3" +"@solana/codecs-core@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.3.0.tgz#6bf2bb565cb1ae880f8018635c92f751465d8695" + integrity sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw== + dependencies: + "@solana/errors" "2.3.0" + +"@solana/codecs-numbers@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz#ac7e7f38aaf7fcd22ce2061fbdcd625e73828dc6" + integrity sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg== + dependencies: + "@solana/codecs-core" "2.3.0" + "@solana/errors" "2.3.0" + +"@solana/errors@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.3.0.tgz#4ac9380343dbeffb9dffbcb77c28d0e457c5fa31" + integrity sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ== + dependencies: + chalk "^5.4.1" + commander "^14.0.0" + "@solana/web3.js@^1.69.0", "@solana/web3.js@latest": version "1.98.0" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.0.tgz#21ecfe8198c10831df6f0cfde7f68370d0405917" @@ -1078,6 +1128,27 @@ rpc-websockets "^9.0.2" superstruct "^2.0.2" +"@solana/web3.js@^1.98.0": + version "1.98.4" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.4.tgz#df51d78be9d865181ec5138b4e699d48e6895bbe" + integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@swc/counter@0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" @@ -1428,6 +1499,11 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +ansicolors@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1552,6 +1628,17 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" +assert@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + ast-types-flow@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" @@ -1603,6 +1690,11 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -1680,6 +1772,13 @@ bs58@^4.0.0, bs58@^4.0.1: dependencies: base-x "^3.0.2" +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + buffer-layout@^1.2.0, buffer-layout@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" @@ -1715,7 +1814,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.7, call-bind@^1.0.8: +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== @@ -1761,6 +1860,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.4.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -1831,6 +1935,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +commander@^14.0.0: + version "14.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.2.tgz#b71fd37fe4069e4c3c7c13925252ada4eba14e8e" + integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== + commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -1994,6 +2103,13 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: dependencies: ms "^2.1.3" +debug@^4.3.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decimal.js-light@^2.4.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -2626,6 +2742,11 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" @@ -2793,6 +2914,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + input-otp@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.4.1.tgz#bc22e68b14b1667219d54adf74243e37ea79cf84" @@ -2812,6 +2938,14 @@ internal-slot@^1.1.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +is-arguments@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" @@ -2922,6 +3056,17 @@ is-generator-function@^1.0.10: has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" +is-generator-function@^1.0.7: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== + dependencies: + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -2934,6 +3079,14 @@ is-map@^2.0.3: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" @@ -2986,7 +3139,7 @@ is-symbol@^1.0.4, is-symbol@^1.1.1: has-symbols "^1.1.0" safe-regex-test "^1.1.0" -is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15, is-typed-array@^1.1.3: version "1.1.15" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== @@ -3333,6 +3486,14 @@ object-inspect@^1.13.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -3778,6 +3939,22 @@ rpc-websockets@^9.0.2: bufferutil "^4.0.1" utf-8-validate "^5.0.2" +rpc-websockets@^9.0.4: + version "9.3.1" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.3.1.tgz#d817a59d812f68bae1215740a3f78fcdd3813698" + integrity sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4319,6 +4496,11 @@ typescript@^5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== +typescript@^5.3.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" @@ -4402,6 +4584,17 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -4487,7 +4680,7 @@ which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.16, which-typed-array@^1.1.18: +which-typed-array@^1.1.16, which-typed-array@^1.1.18, which-typed-array@^1.1.2: version "1.1.19" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== From 85e091212b91dfb4e252003116457366daf79c28 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Wed, 19 Nov 2025 22:55:51 +0800 Subject: [PATCH 05/18] cleaned code --- roll-dice/app/app/delegated/page.tsx | 367 +++++++++++++++++---------- roll-dice/app/app/globals.css | 23 ++ 2 files changed, 252 insertions(+), 138 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index 9505856..a2e9378 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -11,7 +11,6 @@ import { } from "@solana/web3.js" import { DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" import { createDelegateInstruction } from "@/lib/delegate-instruction" -import { useToast } from "@/hooks/use-toast" import Dice from "@/components/dice" import SolanaAddress from "@/components/solana-address" import { @@ -22,6 +21,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" import { PROGRAM_ID, PLAYER_SEED, @@ -46,6 +46,105 @@ import type { RollEntry, CachedBlockhash } from "@/lib/types" const derivePlayerPda = (user: PublicKey) => PublicKey.findProgramAddressSync([Buffer.from(PLAYER_SEED), user.toBuffer()], PROGRAM_ID)[0] +const MiniDice = ({ value }: { value: number | null }) => { + if (value === null) return - + + const safeValue = Math.min(Math.max(1, value), 6) + const dotSize = "w-1.5 h-1.5" + + return ( +
+
+ {safeValue === 1 && ( +
+
+
+ )} + {safeValue === 2 && ( +
+
+
+
+
+
+
+
+ )} + {safeValue === 3 && ( +
+
+
+
+
+
+
+
+
+
+
+ )} + {safeValue === 4 && ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )} + {safeValue === 5 && ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )} + {safeValue === 6 && ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )} +
+
+ ) +} + export default function DiceRollerDelegated() { const [diceValue, setDiceValue] = useState(1) const [isRolling, setIsRolling] = useState(false) @@ -53,10 +152,9 @@ export default function DiceRollerDelegated() { const [isDelegated, setIsDelegated] = useState(false) const [isDelegating, setIsDelegating] = useState(false) const [rollHistory, setRollHistory] = useState([]) + const [timerTick, setTimerTick] = useState(0) const previousDiceValueRef = useRef(1) - const previousRollnumRef = useRef(0) - const expectingRollResultRef = useRef(false) const programRef = useRef(null) const ephemeralProgramRef = useRef(null) const connectionRef = useRef(null) @@ -71,8 +169,6 @@ export default function DiceRollerDelegated() { const payerKeypairRef = useRef(null) const cachedBaseBlockhashRef = useRef(null) const cachedEphemeralBlockhashRef = useRef(null) - - const { toast } = useToast() const clearAllIntervals = useCallback(() => { if (rollIntervalRef.current) { @@ -179,10 +275,8 @@ export default function DiceRollerDelegated() { try { const player = program.coder.accounts.decode("player", account.data) const initialValue = player.lastResult || 1 - const initialRollnum = player.rollnum || 0 setDiceValue(initialValue) previousDiceValueRef.current = initialValue - previousRollnumRef.current = Number(initialRollnum) } catch (error) { console.error("Failed to decode player on init:", error) } @@ -209,41 +303,45 @@ export default function DiceRollerDelegated() { subscriptionIdRef.current = ephemeralConnection.onAccountChange( playerPk, (accountInfo) => { + console.log("[WebSocket] Account change received", { hasData: !!accountInfo?.data }) if (!ephemeralProgramRef.current || !accountInfo || !accountInfo.data) return try { const player = ephemeralProgramRef.current.coder.accounts.decode("player", accountInfo.data) const newValue = Number(player.lastResult) - const newRollnum = Number(player.rollnum || 0) - const previousRollnum = previousRollnumRef.current + + console.log("[WebSocket] Decoded player:", { + newValue + }) if (newValue > 0) { setDiceValue(newValue) previousDiceValueRef.current = newValue } - if (newRollnum > previousRollnum && expectingRollResultRef.current) { - previousRollnumRef.current = newRollnum - const endTime = Date.now() - setRollHistory(prev => { + setRollHistory(prev => { + const pendingIndex = prev.findIndex(entry => entry.isPending) + if (pendingIndex !== -1) { + console.log("[WebSocket] Processing roll completion") + const endTime = Date.now() const updated = [...prev] - const pendingIndex = updated.findIndex(entry => entry.isPending) - if (pendingIndex !== -1) { - updated[pendingIndex] = { - value: newValue, - startTime: updated[pendingIndex].startTime, - endTime, - isPending: false, - } + updated[pendingIndex] = { + value: newValue, + startTime: updated[pendingIndex].startTime, + endTime, + isPending: false, } + setIsRolling(false) + clearAllIntervals() + console.log("[WebSocket] Roll completion processed") return updated - }) - expectingRollResultRef.current = false - setIsRolling(false) - clearAllIntervals() - } + } else { + console.log("[WebSocket] Received update but no pending entry found") + return prev + } + }) } catch (error) { - console.error("Failed to decode player account:", error) + console.error("[WebSocket] Failed to decode player account:", error) } }, { commitment: "processed" } @@ -270,13 +368,8 @@ export default function DiceRollerDelegated() { } catch (error) { console.error("Failed to initialize delegated dice:", error) setIsInitialized(false) - toast({ - title: "Error", - description: "Failed to initialize delegated dice", - variant: "destructive", - }) } - }, [refreshDelegationStatus, toast, fetchBlockhash]) + }, [refreshDelegationStatus, fetchBlockhash]) useEffect(() => { initializeProgram() @@ -338,39 +431,30 @@ export default function DiceRollerDelegated() { await sendTransaction(connection, new Transaction().add(delegateIx), payerKeypair, [playerKeypair], false) await refreshDelegationStatus() - toast({ - title: "Delegated", - description: "Account delegated successfully", - }) } catch (error) { console.error("Delegation failed:", error) - toast({ - title: "Error", - description: "Failed to delegate account", - variant: "destructive", - }) } finally { setIsDelegating(false) } - }, [ensureFunds, isDelegated, refreshDelegationStatus, sendTransaction, toast]) + }, [ensureFunds, isDelegated, refreshDelegationStatus, sendTransaction]) const handleRollDice = useCallback(async () => { if (isRolling || !isInitialized || !isDelegated) return if (!ephemeralProgramRef.current || !playerKeypairRef.current || !playerPdaRef.current) return + console.log("[RollDice] Starting roll") setIsRolling(true) - expectingRollResultRef.current = true clearAllIntervals() rollIntervalRef.current = setInterval(() => { setDiceValue(Math.floor(Math.random() * 6) + 1) }, ROLL_ANIMATION_INTERVAL_MS) - // Create pending roll history entry (will update startTime after transaction is sent) + // Create pending roll history entry (startTime will be set when transaction is sent) setRollHistory(prev => { const newEntry = { value: null, - startTime: Date.now(), // Temporary, will be updated after send + startTime: Date.now(), // Temporary placeholder endTime: null, isPending: true, } @@ -378,15 +462,26 @@ export default function DiceRollerDelegated() { }) timerIntervalRef.current = setInterval(() => { - setRollHistory(prev => [...prev]) - }, 1) + setRollHistory(prev => { + const hasPending = prev.some(entry => entry.isPending) + if (!hasPending) { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current) + timerIntervalRef.current = null + } + return prev + } + setTimerTick(t => t + 1) + return prev + }) + }, 10) timeoutRef.current = setTimeout(() => { - if (expectingRollResultRef.current) { - clearAllIntervals() - expectingRollResultRef.current = false - setIsRolling(false) - setRollHistory(prev => { + setRollHistory(prev => { + const hasPending = prev.some(entry => entry.isPending) + if (hasPending) { + clearAllIntervals() + setIsRolling(false) const updated = [...prev] const pendingIndex = updated.findIndex(entry => entry.isPending) if (pendingIndex !== -1) { @@ -396,13 +491,9 @@ export default function DiceRollerDelegated() { } } return updated - }) - toast({ - title: "Notice", - description: "Dice roll is taking longer than expected. Check explorer.", - variant: "destructive", - }) - } + } + return prev + }) }, ROLL_TIMEOUT_MS) try { @@ -428,23 +519,27 @@ export default function DiceRollerDelegated() { tx.feePayer = playerKeypairRef.current.publicKey tx.sign(playerKeypairRef.current) - await ephemeralConnectionRef.current!.sendRawTransaction( - tx.serialize(), + const serializedTx = tx.serialize() + const transactionStartTime = Date.now() + console.log("[RollDice] Sending transaction") + ephemeralConnectionRef.current!.sendRawTransaction( + serializedTx, { skipPreflight: true } ) - - const startTime = Date.now() + console.log("[RollDice] Transaction sent, waiting for websocket update") + setRollHistory(prev => { const updated = [...prev] const pendingIndex = updated.findIndex(entry => entry.isPending && entry.value === null) if (pendingIndex !== -1) { updated[pendingIndex] = { ...updated[pendingIndex], - startTime, + startTime: transactionStartTime, } } return updated }) + if (ephemeralConnectionRef.current) { await fetchBlockhash(ephemeralConnectionRef.current, true) @@ -452,13 +547,7 @@ export default function DiceRollerDelegated() { } catch (error) { clearAllIntervals() console.error("Error rolling dice:", error) - toast({ - title: "Error", - description: "Failed to roll dice", - variant: "destructive", - }) setIsRolling(false) - expectingRollResultRef.current = false setRollHistory(prev => { const updated = [...prev] const pendingIndex = updated.findIndex(entry => entry.isPending) @@ -468,7 +557,7 @@ export default function DiceRollerDelegated() { return updated }) } - }, [clearAllIntervals, isDelegated, isInitialized, isRolling, toast, getBlockhash, fetchBlockhash]) + }, [clearAllIntervals, isDelegated, isInitialized, isRolling, getBlockhash, fetchBlockhash]) return (
@@ -476,76 +565,78 @@ export default function DiceRollerDelegated() {
-
-
- -
- - {isDelegated ? "Delegated" : "Undelegated"} - - {!isDelegated && ( - - )} +
+
+
+
+ + {isDelegated ? "Delegated" : "Undelegated"} + + {!isDelegated && ( + + )} +
+ +
+ +
+ +
-
- -
- - -
- -
-

Roll History

-
- - - - Value - Time - - - - {rollHistory.length === 0 ? ( - - - No rolls yet - - - ) : ( - rollHistory.map((entry, index) => { - const elapsed = entry.isPending - ? Date.now() - entry.startTime - : entry.endTime - ? entry.endTime - entry.startTime - : 0 - const formattedTime = `${elapsed.toString().padStart(6, '\u00A0')}ms${entry.isPending ? '...' : ''}` - return ( - - - {entry.value !== null ? entry.value : "-"} - - - {formattedTime} +
+
+
+
+ + + Value + Time + + + + {rollHistory.length === 0 ? ( + + + No rolls yet - ) - }) - )} - -
+ ) : ( + rollHistory.slice(0, 10).map((entry, index) => { + const elapsed = entry.isPending + ? Date.now() - entry.startTime + : entry.endTime + ? entry.endTime - entry.startTime + : 0 + const formattedTime = `${elapsed.toString().padStart(6, '\u00A0')}ms${entry.isPending ? '...' : ''}` + return ( + + + + + + {formattedTime} + + + ) + }) + )} + + +
+
diff --git a/roll-dice/app/app/globals.css b/roll-dice/app/app/globals.css index ac68442..ff60b82 100644 --- a/roll-dice/app/app/globals.css +++ b/roll-dice/app/app/globals.css @@ -92,3 +92,26 @@ body { @apply bg-background text-foreground; } } + +.custom-scrollbar::-webkit-scrollbar { + width: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: #888 #f1f1f1; +} From 2ccad3d3d8e5e04d8aeb17bad21a2ccf559a6c5f Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 11:38:54 -0500 Subject: [PATCH 06/18] asdf --- roll-dice/app/app/delegated/page.tsx | 133 +++++++++++++-------------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index a2e9378..a25b671 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -6,11 +6,7 @@ import { Connection, Keypair, PublicKey, - Transaction, - SystemProgram, } from "@solana/web3.js" -import { DELEGATION_PROGRAM_ID } from "@magicblock-labs/ephemeral-rollups-sdk" -import { createDelegateInstruction } from "@/lib/delegate-instruction" import Dice from "@/components/dice" import SolanaAddress from "@/components/solana-address" import { @@ -22,13 +18,14 @@ import { TableRow, } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Copy, Check } from "lucide-react" import { PROGRAM_ID, PLAYER_SEED, ORACLE_QUEUE, BASE_ENDPOINT, PLAYER_STORAGE_KEY, - PAYER_STORAGE_KEY, BLOCKHASH_REFRESH_INTERVAL_MS, ROLL_TIMEOUT_MS, ROLL_ANIMATION_INTERVAL_MS, @@ -153,6 +150,9 @@ export default function DiceRollerDelegated() { const [isDelegating, setIsDelegating] = useState(false) const [rollHistory, setRollHistory] = useState([]) const [timerTick, setTimerTick] = useState(0) + const [playerAccountData, setPlayerAccountData] = useState<{ lastResult: number; rollnum: number } | null>(null) + const [playerPda, setPlayerPda] = useState(null) + const [copied, setCopied] = useState(false) const previousDiceValueRef = useRef(1) const programRef = useRef(null) @@ -166,7 +166,6 @@ export default function DiceRollerDelegated() { const timeoutRef = useRef(null) const blockhashIntervalRef = useRef(null) const playerKeypairRef = useRef(null) - const payerKeypairRef = useRef(null) const cachedBaseBlockhashRef = useRef(null) const cachedEphemeralBlockhashRef = useRef(null) @@ -200,37 +199,9 @@ export default function DiceRollerDelegated() { return getCachedBlockhash(connection, cacheRef) }, []) - const sendTransaction = useCallback( - async (connection: Connection, transaction: Transaction, feePayer: Keypair, signers: Keypair[], isEphemeral: boolean = false) => { - let blockhash: string - const cached = getBlockhash(connection, isEphemeral) - if (cached) { - blockhash = cached - } else { - const result = await connection.getLatestBlockhash() - blockhash = result.blockhash - await fetchBlockhash(connection, isEphemeral) - } - - transaction.recentBlockhash = blockhash - transaction.feePayer = feePayer.publicKey - - const signerMap = new Map() - signerMap.set(feePayer.publicKey.toBase58(), feePayer) - for (const signer of signers) { - signerMap.set(signer.publicKey.toBase58(), signer) - } - - signerMap.forEach(signer => transaction.partialSign(signer)) - - return await connection.sendRawTransaction(transaction.serialize()) - }, - [getBlockhash, fetchBlockhash] - ) - const refreshDelegationStatus = useCallback(async () => { - if (!connectionRef.current || !playerKeypairRef.current) return false - const delegated = await checkDelegationStatus(connectionRef.current, playerKeypairRef.current.publicKey) + if (!connectionRef.current || !playerPdaRef.current) return false + const delegated = await checkDelegationStatus(connectionRef.current, playerPdaRef.current) setIsDelegated(delegated) return delegated }, []) @@ -244,12 +215,8 @@ export default function DiceRollerDelegated() { if (!playerKeypairRef.current) { playerKeypairRef.current = loadOrCreateKeypair(PLAYER_STORAGE_KEY) } - if (!payerKeypairRef.current) { - payerKeypairRef.current = loadOrCreateKeypair(PAYER_STORAGE_KEY) - } await ensureFunds(connection, playerKeypairRef.current) - await ensureFunds(connection, payerKeypairRef.current) const provider = new anchor.AnchorProvider( connection, @@ -265,6 +232,7 @@ export default function DiceRollerDelegated() { const playerPk = derivePlayerPda(playerKeypairRef.current.publicKey) playerPdaRef.current = playerPk + setPlayerPda(playerPk) let account = await connection.getAccountInfo(playerPk) if (!account) { @@ -277,6 +245,10 @@ export default function DiceRollerDelegated() { const initialValue = player.lastResult || 1 setDiceValue(initialValue) previousDiceValueRef.current = initialValue + setPlayerAccountData({ + lastResult: Number(player.lastResult), + rollnum: Number(player.rollnum), + }) } catch (error) { console.error("Failed to decode player on init:", error) } @@ -314,6 +286,11 @@ export default function DiceRollerDelegated() { newValue }) + setPlayerAccountData({ + lastResult: newValue, + rollnum: Number(player.rollnum), + }) + if (newValue > 0) { setDiceValue(newValue) previousDiceValueRef.current = newValue @@ -389,7 +366,6 @@ export default function DiceRollerDelegated() { !programRef.current || !connectionRef.current || !playerKeypairRef.current || - !payerKeypairRef.current || !playerPdaRef.current ) return @@ -399,44 +375,19 @@ export default function DiceRollerDelegated() { try { const connection = connectionRef.current const playerKeypair = playerKeypairRef.current - const payerKeypair = payerKeypairRef.current await ensureFunds(connection, playerKeypair) - await ensureFunds(connection, payerKeypair) - await programRef.current.methods .delegate() - .accounts({ - user: playerKeypair.publicKey, - player: playerPdaRef.current, - }) .rpc() - const ownerInfo = await connection.getAccountInfo(playerKeypair.publicKey) - if (!ownerInfo || !ownerInfo.owner.equals(DELEGATION_PROGRAM_ID)) { - const assignIx = SystemProgram.assign({ - accountPubkey: playerKeypair.publicKey, - programId: DELEGATION_PROGRAM_ID, - }) - await sendTransaction(connection, new Transaction().add(assignIx), payerKeypair, [playerKeypair], false) - } - - await new Promise(resolve => setTimeout(resolve, 3000)) - - const delegateIx = createDelegateInstruction({ - payer: payerKeypair.publicKey, - delegatedAccount: playerKeypair.publicKey, - ownerProgram: SystemProgram.programId, - }) - await sendTransaction(connection, new Transaction().add(delegateIx), payerKeypair, [playerKeypair], false) - await refreshDelegationStatus() } catch (error) { console.error("Delegation failed:", error) } finally { setIsDelegating(false) } - }, [ensureFunds, isDelegated, refreshDelegationStatus, sendTransaction]) + }, [ensureFunds, isDelegated, refreshDelegationStatus]) const handleRollDice = useCallback(async () => { if (isRolling || !isInitialized || !isDelegated) return @@ -559,10 +510,56 @@ export default function DiceRollerDelegated() { } }, [clearAllIntervals, isDelegated, isInitialized, isRolling, getBlockhash, fetchBlockhash]) + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (error) { + console.error("Failed to copy:", error) + } + } + + const formatAddress = (addr: string) => { + if (!addr) return "" + return `${addr.substring(0, 8)}...${addr.substring(addr.length - 8)}` + } + return (
-
+
+ {playerPda && ( + + + Player Account + + +
+
PDA Address
+
copyToClipboard(playerPda.toBase58())} + > + {formatAddress(playerPda.toBase58())} + {copied ? : } +
+
+ {playerAccountData && ( +
+
+
Last Result
+
{playerAccountData.lastResult}
+
+
+
Roll Count
+
{playerAccountData.rollnum}
+
+
+ )} +
+
+ )}
From b5539fd1ebd1eb52f521d048a3fa258e1907acb1 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 11:56:09 -0500 Subject: [PATCH 07/18] improve delegation --- roll-dice/app/app/delegated/page.tsx | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index a25b671..03e3924 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -165,6 +165,7 @@ export default function DiceRollerDelegated() { const timerIntervalRef = useRef(null) const timeoutRef = useRef(null) const blockhashIntervalRef = useRef(null) + const delegationPollIntervalRef = useRef(null) const playerKeypairRef = useRef(null) const cachedBaseBlockhashRef = useRef(null) const cachedEphemeralBlockhashRef = useRef(null) @@ -186,6 +187,10 @@ export default function DiceRollerDelegated() { clearInterval(blockhashIntervalRef.current) blockhashIntervalRef.current = null } + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } }, []) @@ -381,13 +386,30 @@ export default function DiceRollerDelegated() { .delegate() .rpc() - await refreshDelegationStatus() + // Poll every second until delegation succeeds + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + } + + delegationPollIntervalRef.current = setInterval(async () => { + const delegated = await refreshDelegationStatus() + if (delegated) { + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } + setIsDelegating(false) + } + }, 1000) } catch (error) { console.error("Delegation failed:", error) - } finally { + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } setIsDelegating(false) } - }, [ensureFunds, isDelegated, refreshDelegationStatus]) + }, [isDelegated, refreshDelegationStatus]) const handleRollDice = useCallback(async () => { if (isRolling || !isInitialized || !isDelegated) return From 02b33b210f0d1c7acea17ae56aad94f9379c17f0 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 12:01:29 -0500 Subject: [PATCH 08/18] asdf --- roll-dice/app/app/delegated/page.tsx | 20 +++++++++++++------ roll-dice/app/lib/config.ts | 2 +- roll-dice/app/lib/solana-utils.ts | 4 +++- .../programs/roll-dice-delegated/src/lib.rs | 14 +++++++++++-- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index 03e3924..b314e25 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -183,10 +183,7 @@ export default function DiceRollerDelegated() { clearTimeout(timeoutRef.current) timeoutRef.current = null } - if (blockhashIntervalRef.current) { - clearInterval(blockhashIntervalRef.current) - blockhashIntervalRef.current = null - } + // Note: blockhashIntervalRef is NOT cleared here - it should run continuously if (delegationPollIntervalRef.current) { clearInterval(delegationPollIntervalRef.current) delegationPollIntervalRef.current = null @@ -337,12 +334,18 @@ export default function DiceRollerDelegated() { await fetchBlockhash(ephemeralConnection, true) } + // Clear any existing interval before creating a new one + if (blockhashIntervalRef.current) { + clearInterval(blockhashIntervalRef.current) + } + + // Start continuous blockhash refresh - this runs every 20 seconds regardless of other activity blockhashIntervalRef.current = setInterval(() => { if (connectionRef.current) { - fetchBlockhash(connectionRef.current, false) + fetchBlockhash(connectionRef.current, false).catch(console.error) } if (ephemeralConnectionRef.current) { - fetchBlockhash(ephemeralConnectionRef.current, true) + fetchBlockhash(ephemeralConnectionRef.current, true).catch(console.error) } }, BLOCKHASH_REFRESH_INTERVAL_MS) @@ -358,6 +361,11 @@ export default function DiceRollerDelegated() { return () => { clearAllIntervals() + // Clean up blockhash refresh interval on unmount + if (blockhashIntervalRef.current) { + clearInterval(blockhashIntervalRef.current) + blockhashIntervalRef.current = null + } // Clean up subscription if (subscriptionIdRef.current !== null && ephemeralConnectionRef.current) { ephemeralConnectionRef.current.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) diff --git a/roll-dice/app/lib/config.ts b/roll-dice/app/lib/config.ts index f06fc33..d8bd7f3 100644 --- a/roll-dice/app/lib/config.ts +++ b/roll-dice/app/lib/config.ts @@ -8,7 +8,7 @@ export const BASE_ENDPOINT = "https://rpc.magicblock.app/devnet" export const PLAYER_STORAGE_KEY = "solanaKeypair" export const PAYER_STORAGE_KEY = "delegatePayerKeypair" export const MIN_BALANCE_LAMPORTS = 0.05 -export const BLOCKHASH_CACHE_MAX_AGE_MS = 30000 +export const BLOCKHASH_CACHE_MAX_AGE_MS = 25000 export const BLOCKHASH_REFRESH_INTERVAL_MS = 20000 export const ROLL_TIMEOUT_MS = 10000 export const ROLL_ANIMATION_INTERVAL_MS = 100 diff --git a/roll-dice/app/lib/solana-utils.ts b/roll-dice/app/lib/solana-utils.ts index dc05953..82fa6a3 100644 --- a/roll-dice/app/lib/solana-utils.ts +++ b/roll-dice/app/lib/solana-utils.ts @@ -61,7 +61,9 @@ export const getCachedBlockhash = ( const age = Date.now() - cached.timestamp if (age > BLOCKHASH_CACHE_MAX_AGE_MS) { - fetchAndCacheBlockhash(connection, cacheRef) + // Trigger refresh in background but don't return stale blockhash + fetchAndCacheBlockhash(connection, cacheRef).catch(console.error) + return null } return cached.blockhash diff --git a/roll-dice/programs/roll-dice-delegated/src/lib.rs b/roll-dice/programs/roll-dice-delegated/src/lib.rs index 7329a7c..aaf71ce 100644 --- a/roll-dice/programs/roll-dice-delegated/src/lib.rs +++ b/roll-dice/programs/roll-dice-delegated/src/lib.rs @@ -60,11 +60,15 @@ pub mod random_dice_delegated { } // Delegate the player account to use the VRF in the ephemeral rollups - pub fn delegate(ctx: Context) -> Result<()> { + pub fn delegate(ctx: Context, params: DelegateParams) -> Result<()> { + let config = DelegateConfig { + commit_frequency_ms: params.commit_frequency_ms, + validator: params.validator, + }; ctx.accounts.delegate_player( &ctx.accounts.user, &[PLAYER, &ctx.accounts.user.key().to_bytes().as_slice()], - DelegateConfig::default(), + config, )?; Ok(()) } @@ -157,4 +161,10 @@ pub struct Undelegate<'info> { pub struct Player { pub last_result: u8, pub rollnum: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct DelegateParams { + pub commit_frequency_ms: u32, + pub validator: Option, } \ No newline at end of file From 233de4c158ff4b9df3e3d24941594cc00d82a7c6 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 13:28:43 -0500 Subject: [PATCH 09/18] utils --- roll-dice/app/app/delegated/page.tsx | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index b314e25..49d28b5 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -7,6 +7,7 @@ import { Keypair, PublicKey, } from "@solana/web3.js" +import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk" import Dice from "@/components/dice" import SolanaAddress from "@/components/solana-address" import { @@ -153,12 +154,14 @@ export default function DiceRollerDelegated() { const [playerAccountData, setPlayerAccountData] = useState<{ lastResult: number; rollnum: number } | null>(null) const [playerPda, setPlayerPda] = useState(null) const [copied, setCopied] = useState(false) + const [ephemeralEndpoint, setEphemeralEndpoint] = useState(null) const previousDiceValueRef = useRef(1) const programRef = useRef(null) const ephemeralProgramRef = useRef(null) const connectionRef = useRef(null) const ephemeralConnectionRef = useRef(null) + const routerConnectionRef = useRef(null) const playerPdaRef = useRef(null) const subscriptionIdRef = useRef(null) const rollIntervalRef = useRef(null) @@ -256,7 +259,15 @@ export default function DiceRollerDelegated() { } } + const routerEndpoint = process.env.NEXT_PUBLIC_ROUTER_ENDPOINT || "https://devnet-router.magicblock.app" + const routerWsEndpoint = process.env.NEXT_PUBLIC_ROUTER_WS_ENDPOINT || "wss://devnet-router.magicblock.app" + const routerConnection = new ConnectionMagicRouter(routerEndpoint, { + wsEndpoint: routerWsEndpoint, + }) + routerConnectionRef.current = routerConnection + const ephemeralEndpoint = process.env.NEXT_PUBLIC_EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet.magicblock.app" + setEphemeralEndpoint(ephemeralEndpoint) const ephemeralWsEndpoint = process.env.NEXT_PUBLIC_EPHEMERAL_WS_ENDPOINT || "wss://devnet.magicblock.app" const ephemeralConnection = new Connection(ephemeralEndpoint, { wsEndpoint: ephemeralWsEndpoint, @@ -550,6 +561,19 @@ export default function DiceRollerDelegated() { } } + const handleGetClosestValidator = useCallback(async () => { + if (!routerConnectionRef.current) { + console.error("Router connection not initialized") + return + } + try { + const result = await routerConnectionRef.current.getClosestValidator() + console.log("getClosestValidator result:", result) + } catch (error) { + console.error("Failed to get closest validator:", error) + } + }, []) + const formatAddress = (addr: string) => { if (!addr) return "" return `${addr.substring(0, 8)}...${addr.substring(addr.length - 8)}` @@ -575,6 +599,21 @@ export default function DiceRollerDelegated() { {copied ? : }
+ {ephemeralEndpoint && ( +
+
Ephemeral Connection
+
{ephemeralEndpoint}
+
+ )} +
+ +
{playerAccountData && (
From c509148bca335ed5eccc08702d490d01bef8024a Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 13:39:35 -0500 Subject: [PATCH 10/18] v1 --- roll-dice/app/app/delegated/page.tsx | 140 +++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 8 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index 49d28b5..27fccf1 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -211,6 +211,92 @@ export default function DiceRollerDelegated() { return delegated }, []) + const updateEphemeralConnectionToValidator = useCallback(async (validatorFqdn: string) => { + if (!playerKeypairRef.current || !playerPdaRef.current || !programRef.current) return + + // Convert https:// to wss:// for WebSocket endpoint + const ephemeralWsEndpoint = validatorFqdn.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + const newEphemeralConnection = new Connection(validatorFqdn, { + wsEndpoint: ephemeralWsEndpoint, + commitment: "processed", + }) + + // Clean up old subscription + if (subscriptionIdRef.current !== null && ephemeralConnectionRef.current) { + await ephemeralConnectionRef.current.removeAccountChangeListener(subscriptionIdRef.current).catch(console.error) + } + + ephemeralConnectionRef.current = newEphemeralConnection + setEphemeralEndpoint(validatorFqdn) + + // Recreate ephemeral program with new connection + const idl = await anchor.Program.fetchIdl(PROGRAM_ID, programRef.current.provider) + if (!idl) throw new Error("IDL not found") + + const ephemeralProvider = new anchor.AnchorProvider( + newEphemeralConnection, + walletAdapterFrom(playerKeypairRef.current), + anchor.AnchorProvider.defaultOptions() + ) + ephemeralProgramRef.current = new anchor.Program(idl, ephemeralProvider) + + // Recreate subscription with new connection + subscriptionIdRef.current = newEphemeralConnection.onAccountChange( + playerPdaRef.current, + (accountInfo) => { + console.log("[WebSocket] Account change received", { hasData: !!accountInfo?.data }) + if (!ephemeralProgramRef.current || !accountInfo || !accountInfo.data) return + + try { + const player = ephemeralProgramRef.current.coder.accounts.decode("player", accountInfo.data) + const newValue = Number(player.lastResult) + + console.log("[WebSocket] Decoded player:", { + newValue + }) + + setPlayerAccountData({ + lastResult: newValue, + rollnum: Number(player.rollnum), + }) + + if (newValue > 0) { + setDiceValue(newValue) + previousDiceValueRef.current = newValue + } + + setRollHistory(prev => { + const pendingIndex = prev.findIndex(entry => entry.isPending) + if (pendingIndex !== -1) { + console.log("[WebSocket] Processing roll completion") + const endTime = Date.now() + const updated = [...prev] + updated[pendingIndex] = { + value: newValue, + startTime: updated[pendingIndex].startTime, + endTime, + isPending: false, + } + setIsRolling(false) + clearAllIntervals() + console.log("[WebSocket] Roll completion processed") + return updated + } else { + console.log("[WebSocket] Received update but no pending entry found") + return prev + } + }) + } catch (error) { + console.error("[WebSocket] Failed to decode player account:", error) + } + }, + { commitment: "processed" } + ) + + // Fetch blockhash for new connection + await fetchBlockhash(newEphemeralConnection, true) + }, [clearAllIntervals, fetchBlockhash]) + const initializeProgram = useCallback(async () => { if (typeof window === "undefined") return try { @@ -338,11 +424,27 @@ export default function DiceRollerDelegated() { ) } - await refreshDelegationStatus() + const isDelegated = await refreshDelegationStatus() - await fetchBlockhash(connection, false) - if (ephemeralConnection) { - await fetchBlockhash(ephemeralConnection, true) + // If already delegated, update ephemeral connection to nearest validator + if (isDelegated && routerConnectionRef.current) { + try { + const validatorResult = await routerConnectionRef.current.getClosestValidator() + console.log("getClosestValidator result on init:", validatorResult) + + if (validatorResult.fqdn) { + await updateEphemeralConnectionToValidator(validatorResult.fqdn) + } + } catch (error) { + console.error("Failed to update ephemeral connection to nearest validator:", error) + // Continue with default ephemeral connection if update fails + await fetchBlockhash(ephemeralConnection, true) + } + } else { + await fetchBlockhash(connection, false) + if (ephemeralConnection) { + await fetchBlockhash(ephemeralConnection, true) + } } // Clear any existing interval before creating a new one @@ -365,7 +467,7 @@ export default function DiceRollerDelegated() { console.error("Failed to initialize delegated dice:", error) setIsInitialized(false) } - }, [refreshDelegationStatus, fetchBlockhash]) + }, [refreshDelegationStatus, fetchBlockhash, updateEphemeralConnectionToValidator]) useEffect(() => { initializeProgram() @@ -390,7 +492,8 @@ export default function DiceRollerDelegated() { !programRef.current || !connectionRef.current || !playerKeypairRef.current || - !playerPdaRef.current + !playerPdaRef.current || + !routerConnectionRef.current ) return if (isDelegated) return @@ -400,9 +503,30 @@ export default function DiceRollerDelegated() { const connection = connectionRef.current const playerKeypair = playerKeypairRef.current + // Get closest validator + const validatorResult = await routerConnectionRef.current.getClosestValidator() + console.log("getClosestValidator result:", validatorResult) + + const validatorIdentity = validatorResult.identity + const validatorFqdn = validatorResult.fqdn + + if (!validatorIdentity) { + throw new Error("Validator identity not found in getClosestValidator response") + } + + const validatorPubkey = new PublicKey(validatorIdentity) + + // Update ephemeral connection to use the fqdn from getClosestValidator + if (validatorFqdn) { + await updateEphemeralConnectionToValidator(validatorFqdn) + } + await ensureFunds(connection, playerKeypair) await programRef.current.methods - .delegate() + .delegate({ + commitFrequencyMs: 30000, + validator: validatorPubkey, + }) .rpc() // Poll every second until delegation succeeds @@ -428,7 +552,7 @@ export default function DiceRollerDelegated() { } setIsDelegating(false) } - }, [isDelegated, refreshDelegationStatus]) + }, [isDelegated, refreshDelegationStatus, clearAllIntervals, updateEphemeralConnectionToValidator]) const handleRollDice = useCallback(async () => { if (isRolling || !isInitialized || !isDelegated) return From 0b8ad66998fbb9d8d930311ec598b4079cecbe26 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 13:54:17 -0500 Subject: [PATCH 11/18] undelegate all --- roll-dice/app/app/delegated/page.tsx | 183 ++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 20 deletions(-) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index 27fccf1..19d889b 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -149,6 +149,7 @@ export default function DiceRollerDelegated() { const [isInitialized, setIsInitialized] = useState(false) const [isDelegated, setIsDelegated] = useState(false) const [isDelegating, setIsDelegating] = useState(false) + const [isUndelegating, setIsUndelegating] = useState(false) const [rollHistory, setRollHistory] = useState([]) const [timerTick, setTimerTick] = useState(0) const [playerAccountData, setPlayerAccountData] = useState<{ lastResult: number; rollnum: number } | null>(null) @@ -487,13 +488,12 @@ export default function DiceRollerDelegated() { } }, [clearAllIntervals, initializeProgram]) - const handleDelegate = useCallback(async () => { + const handleDelegateToValidator = useCallback(async (validatorIdentity: string, validatorFqdn: string) => { if ( !programRef.current || !connectionRef.current || !playerKeypairRef.current || - !playerPdaRef.current || - !routerConnectionRef.current + !playerPdaRef.current ) return if (isDelegated) return @@ -502,24 +502,11 @@ export default function DiceRollerDelegated() { try { const connection = connectionRef.current const playerKeypair = playerKeypairRef.current - - // Get closest validator - const validatorResult = await routerConnectionRef.current.getClosestValidator() - console.log("getClosestValidator result:", validatorResult) - - const validatorIdentity = validatorResult.identity - const validatorFqdn = validatorResult.fqdn - - if (!validatorIdentity) { - throw new Error("Validator identity not found in getClosestValidator response") - } const validatorPubkey = new PublicKey(validatorIdentity) - // Update ephemeral connection to use the fqdn from getClosestValidator - if (validatorFqdn) { - await updateEphemeralConnectionToValidator(validatorFqdn) - } + // Update ephemeral connection to use the fqdn + await updateEphemeralConnectionToValidator(validatorFqdn) await ensureFunds(connection, playerKeypair) await programRef.current.methods @@ -554,6 +541,139 @@ export default function DiceRollerDelegated() { } }, [isDelegated, refreshDelegationStatus, clearAllIntervals, updateEphemeralConnectionToValidator]) + const handleDelegate = useCallback(async () => { + if ( + !programRef.current || + !connectionRef.current || + !playerKeypairRef.current || + !playerPdaRef.current || + !routerConnectionRef.current + ) + return + if (isDelegated) return + + setIsDelegating(true) + try { + const connection = connectionRef.current + const playerKeypair = playerKeypairRef.current + + // Get closest validator + const validatorResult = await routerConnectionRef.current.getClosestValidator() + console.log("getClosestValidator result:", validatorResult) + + const validatorIdentity = validatorResult.identity + const validatorFqdn = validatorResult.fqdn + + if (!validatorIdentity || !validatorFqdn) { + throw new Error("Validator identity or fqdn not found in getClosestValidator response") + } + + await handleDelegateToValidator(validatorIdentity, validatorFqdn) + } catch (error) { + console.error("Delegation failed:", error) + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } + setIsDelegating(false) + } + }, [isDelegated, handleDelegateToValidator]) + + const handleUndelegate = useCallback(async () => { + if ( + !programRef.current || + !playerKeypairRef.current || + !playerPdaRef.current + ) + return + if (!isDelegated) return + + setIsUndelegating(true) + try { + // Store refs in local variables for TypeScript + const playerKeypair = playerKeypairRef.current + const playerPda = playerPdaRef.current + const program = programRef.current + + if (!playerKeypair || !playerPda || !program) { + throw new Error("Required refs not available") + } + + // List of all known validator endpoints + const validatorEndpoints = [ + "https://devnet-us.magicblock.app", + "https://devnet-as.magicblock.app", + ] + + // Fetch IDL once + const idl = await anchor.Program.fetchIdl(PROGRAM_ID, program.provider) + if (!idl) throw new Error("IDL not found") + + // Send undelegate RPC to all validator endpoints + const undelegatePromises = validatorEndpoints.map(async (endpoint) => { + try { + const wsEndpoint = endpoint.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + const connection = new Connection(endpoint, { + wsEndpoint, + commitment: "processed", + }) + + const provider = new anchor.AnchorProvider( + connection, + walletAdapterFrom(playerKeypair), + anchor.AnchorProvider.defaultOptions() + ) + + const ephemeralProgram = new anchor.Program(idl, provider) + + return await ephemeralProgram.methods + .undelegate() + .accounts({ + payer: playerKeypair.publicKey, + user: playerPda, + }) + .rpc() + } catch (error) { + console.warn(`Undelegation failed for ${endpoint}:`, error) + throw error + } + }) + + // Wait for all attempts (at least one should succeed) + const results = await Promise.allSettled(undelegatePromises) + const successful = results.filter(r => r.status === "fulfilled") + + if (successful.length === 0) { + throw new Error("All undelegation attempts failed") + } + + console.log(`Undelegation succeeded on ${successful.length} endpoint(s)`) + + // Poll every second until undelegation succeeds + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + } + + delegationPollIntervalRef.current = setInterval(async () => { + const delegated = await refreshDelegationStatus() + if (!delegated) { + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } + setIsUndelegating(false) + } + }, 1000) + } catch (error) { + console.error("Undelegation failed:", error) + if (delegationPollIntervalRef.current) { + clearInterval(delegationPollIntervalRef.current) + delegationPollIntervalRef.current = null + } + setIsUndelegating(false) + } + }, [isDelegated, refreshDelegationStatus]) + const handleRollDice = useCallback(async () => { if (isRolling || !isInitialized || !isDelegated) return if (!ephemeralProgramRef.current || !playerKeypairRef.current || !playerPdaRef.current) return @@ -710,7 +830,7 @@ export default function DiceRollerDelegated() { {playerPda && ( - Player Account + Debug
@@ -729,7 +849,7 @@ export default function DiceRollerDelegated() {
{ephemeralEndpoint}
)} -
+
+
+ + +
+
{playerAccountData && (
From bff9063948c869b029ccd00d05d5a197f8a9f9f7 Mon Sep 17 00:00:00 2001 From: fauxfire Date: Thu, 20 Nov 2025 13:56:44 -0500 Subject: [PATCH 12/18] added eu --- roll-dice/app/app/delegated/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/roll-dice/app/app/delegated/page.tsx b/roll-dice/app/app/delegated/page.tsx index 19d889b..0035337 100644 --- a/roll-dice/app/app/delegated/page.tsx +++ b/roll-dice/app/app/delegated/page.tsx @@ -603,6 +603,7 @@ export default function DiceRollerDelegated() { const validatorEndpoints = [ "https://devnet-us.magicblock.app", "https://devnet-as.magicblock.app", + "https://devnet-eu.magicblock.app", ] // Fetch IDL once @@ -872,6 +873,13 @@ export default function DiceRollerDelegated() { > AS +
-
- - - -
- -
- {playerAccountData && ( -
-
-
Last Result
-
{playerAccountData.lastResult}
-
-
-
Roll Count
-
{playerAccountData.rollnum}
-
-
- )} -
+ + + + Debug + + + +
+
PDA Address
+
copyToClipboard(playerPda.toBase58())} + > + {formatAddress(playerPda.toBase58())} + {copied ? : } +
+
+ {ephemeralEndpoint && ( +
+
Ephemeral Connection
+
{ephemeralEndpoint}
+
+ )} +
+ +
+ + + +
+ +
+ {playerAccountData && ( +
+
+
Last Result
+
{playerAccountData.lastResult}
+
+
+
Roll Count
+
{playerAccountData.rollnum}
+
+
+ )} +
+
+
+
)}
@@ -1028,6 +942,14 @@ export default function DiceRollerDelegated() {