From 081c87d10a587f011f56598c41b0aeaa655ee457 Mon Sep 17 00:00:00 2001 From: Eddie Date: Mon, 13 Apr 2026 16:49:22 -0400 Subject: [PATCH 01/78] clean up stage 1 --- deno.json | 9 +- e2e/deno.json | 11 + .../frontend/deno.json => frontend/deno_.json | 0 {packages/frontend => frontend}/index.html | 0 .../frontend => frontend}/package-lock.json | 16 +- {packages/frontend => frontend}/package.json | 2 +- .../public/game-styles.css | 0 .../public/midnight-prover/bls_midnight_2p10 | Bin .../public/midnight-prover/bls_midnight_2p11 | Bin .../public/midnight-prover/bls_midnight_2p12 | Bin .../public/midnight-prover/bls_midnight_2p13 | Bin .../public/midnight-prover/bls_midnight_2p14 | Bin .../public/midnight-prover/bls_midnight_2p15 | Bin .../public/midnight-prover/bls_midnight_2p16 | Bin .../public/midnight-prover/bls_midnight_2p17 | Bin .../public/midnight-prover/bls_midnight_2p5 | Bin .../public/midnight-prover/bls_midnight_2p6 | Bin .../public/midnight-prover/bls_midnight_2p7 | Bin .../public/midnight-prover/bls_midnight_2p8 | Bin .../public/midnight-prover/bls_midnight_2p9 | Bin .../scripts/fetch-midnight-params.mjs | 0 .../frontend => frontend}/src/GameManager.ts | 0 .../frontend => frontend}/src/UIManager.ts | 0 .../frontend => frontend}/src/apiConfig.ts | 13 +- .../src/components/Card.ts | 0 .../src/effectstreamBridge.ts | 0 .../src/laceWalletBridge.ts | 31 +- {packages/frontend => frontend}/src/main.ts | 0 .../src/midnight-shim.ts | 0 .../src/midnightBridge.ts | 0 .../src/proving/batcher-providers.ts | 62 +- .../src/proving/local-proving.ts | 0 .../src/proving/prover-worker.ts | 0 .../src/proving/worker-types.ts | 0 .../src/screens/GameScreen.ts | 0 .../src/screens/LeaderboardPanel.ts | 0 .../src/screens/LobbyListScreen.ts | 0 .../src/screens/LobbyScreen.ts | 0 .../src/screens/NameEntryScreen.ts | 0 .../src/screens/ResultsScreen.ts | 0 frontend/src/screens/WalletScreen.ts | 96 ++ .../src/services/BatcherMidnightService.ts | 6 +- .../src/services/GoFishContractService.ts | 9 +- .../src/services/GoFishGameService.ts | 0 .../src/services/MidnightOnChainService.ts | 21 +- .../src/services/MidnightService.ts | 35 +- .../src/services/PlayerKeyManager.ts | 0 frontend/src/services/inBrowserWallet.ts | 82 + .../services/midnightBrowserProofProvider.ts | 0 .../midnightInMemoryPrivateStateProvider.ts | 0 .../frontend => frontend}/src/shims/crypto.ts | 0 .../frontend => frontend}/src/shims/level.ts | 0 {packages/frontend => frontend}/src/style.css | 0 .../src/three/InputManager.ts | 0 .../src/three/SceneManager.ts | 0 .../src/three/SoundManager.ts | 0 .../src/three/ThreeApp.ts | 0 .../src/three/animations/CardAnimations.ts | 0 .../src/three/effects/CRTEffect.ts | 0 .../src/three/effects/PostProcessing.ts | 0 .../src/three/objects/BookPile.ts | 0 .../src/three/objects/Card3D.ts | 0 .../src/three/objects/CardHand.ts | 0 .../src/three/objects/Deck3D.ts | 0 .../src/three/objects/OpponentHand.ts | 0 .../src/three/objects/Table.ts | 0 .../src/three/scenes/GameScene.ts | 0 .../src/three/state/AnimationQueue.ts | 0 .../src/three/state/GameStateAdapter.ts | 0 .../src/three/textures/CardTextureAtlas.ts | 0 .../src/three/ui/GameHUD.ts | 0 .../frontend => frontend}/src/vite-env.d.ts | 4 + .../src/workers/midnightProver.worker.ts | 0 .../src/workers/midnightProverMessages.ts | 0 {packages/frontend => frontend}/tsconfig.json | 0 .../frontend => frontend}/vite.config.ts | 31 - packages/client/batcher/.gitignore | 1 + packages/client/batcher/deno.json | 12 +- .../batcher/midnight-level-db/000005.ldb | Bin 309 -> 0 bytes .../batcher/midnight-level-db/000012.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/000015.ldb | Bin 310 -> 0 bytes .../batcher/midnight-level-db/000055.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/000079.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/000195.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/012757.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/014246.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/018622.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/021694.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/022889.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/024795.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/026968.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/027065.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028447.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028499.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028550.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028601.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028652.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028704.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028755.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028806.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/028848.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/030842.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/048945.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/049270.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/049430.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/051422.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/079898.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/083473.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/083807.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/086013.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/088733.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/091978.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/231842.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/231995.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/232335.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/235510.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/247904.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/252072.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/255070.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/255260.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/257960.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/258342.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/276706.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/277756.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/278083.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/278697.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279277.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279351.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279421.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279494.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279576.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/279646.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/280284.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/280371.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/280929.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/281012.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/281099.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/281182.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/281264.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/282234.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/282330.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/282480.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/282783.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/286768.ldb | Bin 439 -> 0 bytes .../batcher/midnight-level-db/286850.ldb | Bin 342 -> 0 bytes .../batcher/midnight-level-db/286857.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/286860.ldb | Bin 343 -> 0 bytes .../batcher/midnight-level-db/286867.ldb | Bin 299 -> 0 bytes .../batcher/midnight-level-db/286955.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/287021.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/287155.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/287192.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/287407.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/287473.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/287768.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/287805.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/287948.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/288010.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/288356.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/288365.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/288734.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/288761.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/288817.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/288947.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/289316.ldb | Bin 472 -> 0 bytes .../batcher/midnight-level-db/289362.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/289372.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/289378.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/289387.ldb | Bin 398 -> 0 bytes .../batcher/midnight-level-db/289396.ldb | Bin 398 -> 0 bytes .../client/batcher/midnight-level-db/CURRENT | 1 - .../client/batcher/midnight-level-db/LOCK | 0 packages/client/batcher/midnight-level-db/LOG | 3 - .../client/batcher/midnight-level-db/LOG.old | 3 - .../batcher/midnight-level-db/MANIFEST-289402 | Bin 18517 -> 0 bytes .../batcher/src/adapter-effectstreaml2.ts | 49 +- .../batcher/src/adapter-midnight-balancing.ts | 371 +---- .../client/batcher/src/adapter-midnight.ts | 911 ----------- packages/client/batcher/src/config.ts | 2 +- packages/client/batcher/src/main.ts | 254 +-- .../vendor/batcher/adapters/adapter.ts | 156 -- .../batcher/adapters/bitcoin-adapter.ts | 482 ------ .../batcher/adapters/evm-contract-adapter.ts | 314 ---- .../batcher/adapters/midnight-adapter.ts | 1042 ------------ .../batcher/adapters/midnight-arg-parser.ts | 315 ---- .../batcher/vendor/batcher/adapters/mod.ts | 34 - .../batcher/adapters/paimal2-adapter.ts | 221 --- .../default-builder-logic.ts | 106 -- .../batch-data-builder/evm-builder-logic.ts | 154 -- .../midnight-builder-logic.ts | 124 -- .../vendor/batcher/core/batch-processor.ts | 215 --- .../vendor/batcher/core/batcher-events.ts | 83 - .../batcher/vendor/batcher/core/batcher.ts | 1443 ----------------- .../batcher/vendor/batcher/core/config.ts | 460 ------ .../client/batcher/vendor/batcher/core/mod.ts | 13 - .../vendor/batcher/core/shutdown-manager.ts | 187 --- .../batcher/vendor/batcher/core/storage.ts | 219 --- .../batcher/vendor/batcher/core/types.ts | 10 - packages/client/batcher/vendor/batcher/mod.ts | 65 - .../vendor/batcher/server/batcher-server.ts | 421 ----- packages/client/database/deno.json | 8 +- packages/client/node/data/game-state.json | 194 --- packages/client/node/data/player-secrets.json | 378 ----- packages/client/node/data/setup-state.json | 534 ------ packages/client/node/deno.json | 22 +- packages/client/node/runtime-config.json | 4 - packages/client/node/scripts/graphql-proxy.ts | 267 --- packages/client/node/scripts/start.dev.ts | 156 +- packages/client/node/src/api.ts | 22 +- packages/client/node/src/midnight-actions.ts | 33 +- packages/frontend/.env.batcher | 16 - packages/frontend/src/screens/WalletScreen.ts | 432 ----- packages/shared/contracts/evm/deno.json | 6 +- packages/shared/contracts/evm/package.json | 2 +- packages/shared/contracts/midnight/deno.json | 8 +- .../contracts/midnight/deploy.midnight.ts | 332 ++-- packages/shared/data-types/deno.json | 10 +- 217 files changed, 843 insertions(+), 9680 deletions(-) create mode 100644 e2e/deno.json rename packages/frontend/deno.json => frontend/deno_.json (100%) rename {packages/frontend => frontend}/index.html (100%) rename {packages/frontend => frontend}/package-lock.json (99%) rename {packages/frontend => frontend}/package.json (97%) rename {packages/frontend => frontend}/public/game-styles.css (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p10 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p11 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p12 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p13 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p14 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p15 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p16 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p17 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p5 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p6 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p7 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p8 (100%) rename {packages/frontend => frontend}/public/midnight-prover/bls_midnight_2p9 (100%) rename {packages/frontend => frontend}/scripts/fetch-midnight-params.mjs (100%) rename {packages/frontend => frontend}/src/GameManager.ts (100%) rename {packages/frontend => frontend}/src/UIManager.ts (100%) rename {packages/frontend => frontend}/src/apiConfig.ts (56%) rename {packages/frontend => frontend}/src/components/Card.ts (100%) rename {packages/frontend => frontend}/src/effectstreamBridge.ts (100%) rename {packages/frontend => frontend}/src/laceWalletBridge.ts (89%) rename {packages/frontend => frontend}/src/main.ts (100%) rename {packages/frontend => frontend}/src/midnight-shim.ts (100%) rename {packages/frontend => frontend}/src/midnightBridge.ts (100%) rename {packages/frontend => frontend}/src/proving/batcher-providers.ts (85%) rename {packages/frontend => frontend}/src/proving/local-proving.ts (100%) rename {packages/frontend => frontend}/src/proving/prover-worker.ts (100%) rename {packages/frontend => frontend}/src/proving/worker-types.ts (100%) rename {packages/frontend => frontend}/src/screens/GameScreen.ts (100%) rename {packages/frontend => frontend}/src/screens/LeaderboardPanel.ts (100%) rename {packages/frontend => frontend}/src/screens/LobbyListScreen.ts (100%) rename {packages/frontend => frontend}/src/screens/LobbyScreen.ts (100%) rename {packages/frontend => frontend}/src/screens/NameEntryScreen.ts (100%) rename {packages/frontend => frontend}/src/screens/ResultsScreen.ts (100%) create mode 100644 frontend/src/screens/WalletScreen.ts rename {packages/frontend => frontend}/src/services/BatcherMidnightService.ts (97%) rename {packages/frontend => frontend}/src/services/GoFishContractService.ts (98%) rename {packages/frontend => frontend}/src/services/GoFishGameService.ts (100%) rename {packages/frontend => frontend}/src/services/MidnightOnChainService.ts (98%) rename {packages/frontend => frontend}/src/services/MidnightService.ts (92%) rename {packages/frontend => frontend}/src/services/PlayerKeyManager.ts (100%) create mode 100644 frontend/src/services/inBrowserWallet.ts rename {packages/frontend => frontend}/src/services/midnightBrowserProofProvider.ts (100%) rename {packages/frontend => frontend}/src/services/midnightInMemoryPrivateStateProvider.ts (100%) rename {packages/frontend => frontend}/src/shims/crypto.ts (100%) rename {packages/frontend => frontend}/src/shims/level.ts (100%) rename {packages/frontend => frontend}/src/style.css (100%) rename {packages/frontend => frontend}/src/three/InputManager.ts (100%) rename {packages/frontend => frontend}/src/three/SceneManager.ts (100%) rename {packages/frontend => frontend}/src/three/SoundManager.ts (100%) rename {packages/frontend => frontend}/src/three/ThreeApp.ts (100%) rename {packages/frontend => frontend}/src/three/animations/CardAnimations.ts (100%) rename {packages/frontend => frontend}/src/three/effects/CRTEffect.ts (100%) rename {packages/frontend => frontend}/src/three/effects/PostProcessing.ts (100%) rename {packages/frontend => frontend}/src/three/objects/BookPile.ts (100%) rename {packages/frontend => frontend}/src/three/objects/Card3D.ts (100%) rename {packages/frontend => frontend}/src/three/objects/CardHand.ts (100%) rename {packages/frontend => frontend}/src/three/objects/Deck3D.ts (100%) rename {packages/frontend => frontend}/src/three/objects/OpponentHand.ts (100%) rename {packages/frontend => frontend}/src/three/objects/Table.ts (100%) rename {packages/frontend => frontend}/src/three/scenes/GameScene.ts (100%) rename {packages/frontend => frontend}/src/three/state/AnimationQueue.ts (100%) rename {packages/frontend => frontend}/src/three/state/GameStateAdapter.ts (100%) rename {packages/frontend => frontend}/src/three/textures/CardTextureAtlas.ts (100%) rename {packages/frontend => frontend}/src/three/ui/GameHUD.ts (100%) rename {packages/frontend => frontend}/src/vite-env.d.ts (88%) rename {packages/frontend => frontend}/src/workers/midnightProver.worker.ts (100%) rename {packages/frontend => frontend}/src/workers/midnightProverMessages.ts (100%) rename {packages/frontend => frontend}/tsconfig.json (100%) rename {packages/frontend => frontend}/vite.config.ts (90%) create mode 100644 packages/client/batcher/.gitignore delete mode 100644 packages/client/batcher/midnight-level-db/000005.ldb delete mode 100644 packages/client/batcher/midnight-level-db/000012.ldb delete mode 100644 packages/client/batcher/midnight-level-db/000015.ldb delete mode 100644 packages/client/batcher/midnight-level-db/000055.ldb delete mode 100644 packages/client/batcher/midnight-level-db/000079.ldb delete mode 100644 packages/client/batcher/midnight-level-db/000195.ldb delete mode 100644 packages/client/batcher/midnight-level-db/012757.ldb delete mode 100644 packages/client/batcher/midnight-level-db/014246.ldb delete mode 100644 packages/client/batcher/midnight-level-db/018622.ldb delete mode 100644 packages/client/batcher/midnight-level-db/021694.ldb delete mode 100644 packages/client/batcher/midnight-level-db/022889.ldb delete mode 100644 packages/client/batcher/midnight-level-db/024795.ldb delete mode 100644 packages/client/batcher/midnight-level-db/026968.ldb delete mode 100644 packages/client/batcher/midnight-level-db/027065.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028447.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028499.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028550.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028601.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028652.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028704.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028755.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028806.ldb delete mode 100644 packages/client/batcher/midnight-level-db/028848.ldb delete mode 100644 packages/client/batcher/midnight-level-db/030842.ldb delete mode 100644 packages/client/batcher/midnight-level-db/048945.ldb delete mode 100644 packages/client/batcher/midnight-level-db/049270.ldb delete mode 100644 packages/client/batcher/midnight-level-db/049430.ldb delete mode 100644 packages/client/batcher/midnight-level-db/051422.ldb delete mode 100644 packages/client/batcher/midnight-level-db/079898.ldb delete mode 100644 packages/client/batcher/midnight-level-db/083473.ldb delete mode 100644 packages/client/batcher/midnight-level-db/083807.ldb delete mode 100644 packages/client/batcher/midnight-level-db/086013.ldb delete mode 100644 packages/client/batcher/midnight-level-db/088733.ldb delete mode 100644 packages/client/batcher/midnight-level-db/091978.ldb delete mode 100644 packages/client/batcher/midnight-level-db/231842.ldb delete mode 100644 packages/client/batcher/midnight-level-db/231995.ldb delete mode 100644 packages/client/batcher/midnight-level-db/232335.ldb delete mode 100644 packages/client/batcher/midnight-level-db/235510.ldb delete mode 100644 packages/client/batcher/midnight-level-db/247904.ldb delete mode 100644 packages/client/batcher/midnight-level-db/252072.ldb delete mode 100644 packages/client/batcher/midnight-level-db/255070.ldb delete mode 100644 packages/client/batcher/midnight-level-db/255260.ldb delete mode 100644 packages/client/batcher/midnight-level-db/257960.ldb delete mode 100644 packages/client/batcher/midnight-level-db/258342.ldb delete mode 100644 packages/client/batcher/midnight-level-db/276706.ldb delete mode 100644 packages/client/batcher/midnight-level-db/277756.ldb delete mode 100644 packages/client/batcher/midnight-level-db/278083.ldb delete mode 100644 packages/client/batcher/midnight-level-db/278697.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279277.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279351.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279421.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279494.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279576.ldb delete mode 100644 packages/client/batcher/midnight-level-db/279646.ldb delete mode 100644 packages/client/batcher/midnight-level-db/280284.ldb delete mode 100644 packages/client/batcher/midnight-level-db/280371.ldb delete mode 100644 packages/client/batcher/midnight-level-db/280929.ldb delete mode 100644 packages/client/batcher/midnight-level-db/281012.ldb delete mode 100644 packages/client/batcher/midnight-level-db/281099.ldb delete mode 100644 packages/client/batcher/midnight-level-db/281182.ldb delete mode 100644 packages/client/batcher/midnight-level-db/281264.ldb delete mode 100644 packages/client/batcher/midnight-level-db/282234.ldb delete mode 100644 packages/client/batcher/midnight-level-db/282330.ldb delete mode 100644 packages/client/batcher/midnight-level-db/282480.ldb delete mode 100644 packages/client/batcher/midnight-level-db/282783.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286768.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286850.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286857.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286860.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286867.ldb delete mode 100644 packages/client/batcher/midnight-level-db/286955.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287021.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287155.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287192.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287407.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287473.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287768.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287805.ldb delete mode 100644 packages/client/batcher/midnight-level-db/287948.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288010.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288356.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288365.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288734.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288761.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288817.ldb delete mode 100644 packages/client/batcher/midnight-level-db/288947.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289316.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289362.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289372.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289378.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289387.ldb delete mode 100644 packages/client/batcher/midnight-level-db/289396.ldb delete mode 100644 packages/client/batcher/midnight-level-db/CURRENT delete mode 100644 packages/client/batcher/midnight-level-db/LOCK delete mode 100644 packages/client/batcher/midnight-level-db/LOG delete mode 100644 packages/client/batcher/midnight-level-db/LOG.old delete mode 100644 packages/client/batcher/midnight-level-db/MANIFEST-289402 delete mode 100644 packages/client/batcher/src/adapter-midnight.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/adapter.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/bitcoin-adapter.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/evm-contract-adapter.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/midnight-adapter.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/midnight-arg-parser.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/mod.ts delete mode 100644 packages/client/batcher/vendor/batcher/adapters/paimal2-adapter.ts delete mode 100644 packages/client/batcher/vendor/batcher/batch-data-builder/default-builder-logic.ts delete mode 100644 packages/client/batcher/vendor/batcher/batch-data-builder/evm-builder-logic.ts delete mode 100644 packages/client/batcher/vendor/batcher/batch-data-builder/midnight-builder-logic.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/batch-processor.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/batcher-events.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/batcher.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/config.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/mod.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/shutdown-manager.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/storage.ts delete mode 100644 packages/client/batcher/vendor/batcher/core/types.ts delete mode 100644 packages/client/batcher/vendor/batcher/mod.ts delete mode 100644 packages/client/batcher/vendor/batcher/server/batcher-server.ts delete mode 100644 packages/client/node/data/game-state.json delete mode 100644 packages/client/node/data/player-secrets.json delete mode 100644 packages/client/node/data/setup-state.json delete mode 100644 packages/client/node/runtime-config.json delete mode 100644 packages/client/node/scripts/graphql-proxy.ts delete mode 100644 packages/frontend/.env.batcher delete mode 100644 packages/frontend/src/screens/WalletScreen.ts diff --git a/deno.json b/deno.json index 89fdb56..17730d6 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,8 @@ { "workspace": [ - "./packages/**/*" + "./packages/client/**", + "./packages/shared/**", + "./e2e" ], "nodeModulesDir": "auto", "tasks": { @@ -25,10 +27,11 @@ "leaderboard:seed": "deno task --filter @go-fish/node leaderboard:seed", "leaderboard:seed:clear": "deno task --filter @go-fish/node leaderboard:seed:clear", - "test": "deno task --filter @go-fish/node test" + "test": "deno task --filter @go-fish/node test", + "test:e2e": "deno task --filter @go-fish/e2e test" }, "imports": { - "@paimaexample/midnight-contracts": "jsr:@paimaexample/midnight-contracts@0.9.0", + "@paimaexample/midnight-contracts": "jsr:@paimaexample/midnight-contracts@0.10.24", "@go-fish/midnight-contracts": "./packages/shared/contracts/midnight/deno.json", "@go-fish/midnight-contracts/midnight-env": "./packages/shared/contracts/midnight/midnight-env.ts" }, diff --git a/e2e/deno.json b/e2e/deno.json new file mode 100644 index 0000000..98a95c9 --- /dev/null +++ b/e2e/deno.json @@ -0,0 +1,11 @@ +{ + "name": "@go-fish/e2e", + "exports": "./game-round.test.ts", + "tasks": { + "test": "deno test -A --no-check --unstable-raw-imports game-round.test.ts" + }, + "imports": { + "ethers": "npm:ethers@5.7.2", + "@midnight-ntwrk/compact-runtime": "npm:@midnight-ntwrk/compact-runtime@0.15.0" + } +} diff --git a/packages/frontend/deno.json b/frontend/deno_.json similarity index 100% rename from packages/frontend/deno.json rename to frontend/deno_.json diff --git a/packages/frontend/index.html b/frontend/index.html similarity index 100% rename from packages/frontend/index.html rename to frontend/index.html diff --git a/packages/frontend/package-lock.json b/frontend/package-lock.json similarity index 99% rename from packages/frontend/package-lock.json rename to frontend/package-lock.json index 6a5379d..7f645b1 100644 --- a/packages/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,7 @@ "@midnight-ntwrk/zswap": "^4.0.0", "@paima/midnight-vm-bindings": "^0.3.1", "@paima/midnight-wasm-prover": "^0.1.0-alpha6", - "@paimaexample/wallets": "^0.7.0", + "@paimaexample/wallets": "^0.10.24", "@types/three": "^0.183.1", "ethers": "^5.8.0", "fp-ts": "^2.16.11", @@ -1721,9 +1721,9 @@ "license": "Apache-2.0" }, "node_modules/@paimaexample/wallets": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@paimaexample/wallets/-/wallets-0.7.1.tgz", - "integrity": "sha512-k6ZtEEZFm6lh7AGqqnirgXH1TV3q4kxMsQ0o/dADXg/B5ZJXFYqMjhIQfuUUl7UhuO+eBj2qRZGlOtOG/pclTQ==", + "version": "0.10.25", + "resolved": "https://registry.npmjs.org/@paimaexample/wallets/-/wallets-0.10.25.tgz", + "integrity": "sha512-IYAOp0oQXIIij05N3yYO4vHI1OVfDbIIy8c2oICS5wI3Mmj/3c5yDPgPAJtSux0ecQT0qD65Ek6pr7VzqTcweA==", "license": "MIT", "dependencies": { "@aurowallet/mina-provider": "^1.0.12", @@ -1731,7 +1731,7 @@ "@coderspirit/nominal": "^4.1.1", "@foxglove/crc": "^1.0.1", "@metamask/providers": "^22.1.0", - "@midnight-ntwrk/dapp-connector-api": "4.0.0", + "@midnight-ntwrk/dapp-connector-api": "4.0.1", "@perawallet/connect": "^1.4.2", "@polkadot/extension-dapp": "^0.58.10", "@polkadot/extension-inject": "^0.58.10", @@ -1767,12 +1767,6 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "license": "MIT" }, - "node_modules/@paimaexample/wallets/node_modules/@midnight-ntwrk/dapp-connector-api": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@midnight-ntwrk/dapp-connector-api/-/dapp-connector-api-4.0.0.tgz", - "integrity": "sha512-XiAesY32uPPDYOk6MXquO1lDsUhe7J/CMbMOIfEgjwDgXfkLvvKFMfNz672O1aSJYkqm1I65o2XjPXNnwloQtA==", - "license": "Apache-2.0" - }, "node_modules/@paimaexample/wallets/node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", diff --git a/packages/frontend/package.json b/frontend/package.json similarity index 97% rename from packages/frontend/package.json rename to frontend/package.json index 462a24a..d3c74a9 100644 --- a/packages/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,7 @@ "@midnight-ntwrk/zswap": "^4.0.0", "@paima/midnight-vm-bindings": "^0.3.1", "@paima/midnight-wasm-prover": "^0.1.0-alpha6", - "@paimaexample/wallets": "^0.7.0", + "@paimaexample/wallets": "^0.10.24", "@types/three": "^0.183.1", "ethers": "^5.8.0", "fp-ts": "^2.16.11", diff --git a/packages/frontend/public/game-styles.css b/frontend/public/game-styles.css similarity index 100% rename from packages/frontend/public/game-styles.css rename to frontend/public/game-styles.css diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p10 b/frontend/public/midnight-prover/bls_midnight_2p10 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p10 rename to frontend/public/midnight-prover/bls_midnight_2p10 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p11 b/frontend/public/midnight-prover/bls_midnight_2p11 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p11 rename to frontend/public/midnight-prover/bls_midnight_2p11 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p12 b/frontend/public/midnight-prover/bls_midnight_2p12 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p12 rename to frontend/public/midnight-prover/bls_midnight_2p12 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p13 b/frontend/public/midnight-prover/bls_midnight_2p13 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p13 rename to frontend/public/midnight-prover/bls_midnight_2p13 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p14 b/frontend/public/midnight-prover/bls_midnight_2p14 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p14 rename to frontend/public/midnight-prover/bls_midnight_2p14 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p15 b/frontend/public/midnight-prover/bls_midnight_2p15 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p15 rename to frontend/public/midnight-prover/bls_midnight_2p15 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p16 b/frontend/public/midnight-prover/bls_midnight_2p16 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p16 rename to frontend/public/midnight-prover/bls_midnight_2p16 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p17 b/frontend/public/midnight-prover/bls_midnight_2p17 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p17 rename to frontend/public/midnight-prover/bls_midnight_2p17 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p5 b/frontend/public/midnight-prover/bls_midnight_2p5 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p5 rename to frontend/public/midnight-prover/bls_midnight_2p5 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p6 b/frontend/public/midnight-prover/bls_midnight_2p6 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p6 rename to frontend/public/midnight-prover/bls_midnight_2p6 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p7 b/frontend/public/midnight-prover/bls_midnight_2p7 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p7 rename to frontend/public/midnight-prover/bls_midnight_2p7 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p8 b/frontend/public/midnight-prover/bls_midnight_2p8 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p8 rename to frontend/public/midnight-prover/bls_midnight_2p8 diff --git a/packages/frontend/public/midnight-prover/bls_midnight_2p9 b/frontend/public/midnight-prover/bls_midnight_2p9 similarity index 100% rename from packages/frontend/public/midnight-prover/bls_midnight_2p9 rename to frontend/public/midnight-prover/bls_midnight_2p9 diff --git a/packages/frontend/scripts/fetch-midnight-params.mjs b/frontend/scripts/fetch-midnight-params.mjs similarity index 100% rename from packages/frontend/scripts/fetch-midnight-params.mjs rename to frontend/scripts/fetch-midnight-params.mjs diff --git a/packages/frontend/src/GameManager.ts b/frontend/src/GameManager.ts similarity index 100% rename from packages/frontend/src/GameManager.ts rename to frontend/src/GameManager.ts diff --git a/packages/frontend/src/UIManager.ts b/frontend/src/UIManager.ts similarity index 100% rename from packages/frontend/src/UIManager.ts rename to frontend/src/UIManager.ts diff --git a/packages/frontend/src/apiConfig.ts b/frontend/src/apiConfig.ts similarity index 56% rename from packages/frontend/src/apiConfig.ts rename to frontend/src/apiConfig.ts index ac0c686..ea77d3a 100644 --- a/packages/frontend/src/apiConfig.ts +++ b/frontend/src/apiConfig.ts @@ -1,17 +1,18 @@ /** * API URL configuration * - * Derives the backend API URL from the current hostname: - * - On localhost -> '' (empty, uses Vite proxy to avoid CORS) - * - On gofish.paimastudios.com -> https://api-gofish.paimastudios.com + * Uses VITE_NODE_API_URL env var to point directly at the Paima node backend. + * For deployed environments, falls back to the api- subdomain convention. */ function getApiBaseUrl(): string { - const hostname = window.location.hostname; + if (import.meta.env.VITE_NODE_API_URL) { + return import.meta.env.VITE_NODE_API_URL.replace(/\/$/, ''); + } + const hostname = window.location.hostname; if (hostname === 'localhost' || hostname === '127.0.0.1') { - // Use same-origin requests; Vite dev server proxies to backend on port 9996 - return ''; + return 'http://localhost:9996'; } // For deployed environments, use the api- subdomain with same protocol diff --git a/packages/frontend/src/components/Card.ts b/frontend/src/components/Card.ts similarity index 100% rename from packages/frontend/src/components/Card.ts rename to frontend/src/components/Card.ts diff --git a/packages/frontend/src/effectstreamBridge.ts b/frontend/src/effectstreamBridge.ts similarity index 100% rename from packages/frontend/src/effectstreamBridge.ts rename to frontend/src/effectstreamBridge.ts diff --git a/packages/frontend/src/laceWalletBridge.ts b/frontend/src/laceWalletBridge.ts similarity index 89% rename from packages/frontend/src/laceWalletBridge.ts rename to frontend/src/laceWalletBridge.ts index 1b7e34d..2b7c5ea 100644 --- a/packages/frontend/src/laceWalletBridge.ts +++ b/frontend/src/laceWalletBridge.ts @@ -9,6 +9,11 @@ import { walletLogin, WalletMode } from "@paimaexample/wallets"; import { setNetworkId, type NetworkId } from "@midnight-ntwrk/midnight-js-network-id"; import type { DAppConnectorWalletAPI } from "@midnight-ntwrk/dapp-connector-api"; import semver from "semver"; +import { + getOrCreateLocalWallet, + getLocalWalletAddress, + getLocalWalletAddressFull, +} from "./services/inBrowserWallet"; // Custom type for the connected wallet API (combines DAppConnectorWalletAPI with shielded address access) // This interface represents the API returned after connecting to the wallet @@ -27,6 +32,9 @@ let connectedAPI: ConnectedAPI | null = null; let contractAddress: string | null = null; let paimaWalletResult: any = null; +// Initialize the in-browser wallet eagerly so it's always available +getOrCreateLocalWallet(); + // Network ID for Midnight network // For local development with the undeployed network, use "undeployed" // For testnet, use "testnet" (Lace Midnight Preview wallet) @@ -145,17 +153,32 @@ export async function laceWalletLogin(): Promise<{ } /** - * Check if Lace wallet is connected + * Check if Lace wallet extension is connected (not the in-browser wallet). */ export function isLaceConnected(): boolean { return connectedAPI !== null && laceWalletAddress !== null; } /** - * Get Lace wallet address + * Check if any wallet is available (Lace or in-browser). + * Always returns true since the in-browser wallet is auto-created. + */ +export function isWalletAvailable(): boolean { + return true; +} + +/** + * Get wallet address — returns Lace address if connected, otherwise in-browser wallet address. */ export function getLaceAddress(): string | null { - return laceWalletAddress; + return laceWalletAddress || getLocalWalletAddress(); +} + +/** + * Get the full (non-truncated) wallet address for identity purposes. + */ +export function getWalletAddressFull(): string | null { + return laceWalletAddress || getLocalWalletAddressFull(); } /** @@ -230,7 +253,9 @@ export function disconnectLaceWallet(): void { export const LaceWalletBridge = { laceWalletLogin, isLaceConnected, + isWalletAvailable, getLaceAddress, + getWalletAddressFull, getDeployedContractAddress, getConnectedAPI, getPaimaWallet, diff --git a/packages/frontend/src/main.ts b/frontend/src/main.ts similarity index 100% rename from packages/frontend/src/main.ts rename to frontend/src/main.ts diff --git a/packages/frontend/src/midnight-shim.ts b/frontend/src/midnight-shim.ts similarity index 100% rename from packages/frontend/src/midnight-shim.ts rename to frontend/src/midnight-shim.ts diff --git a/packages/frontend/src/midnightBridge.ts b/frontend/src/midnightBridge.ts similarity index 100% rename from packages/frontend/src/midnightBridge.ts rename to frontend/src/midnightBridge.ts diff --git a/packages/frontend/src/proving/batcher-providers.ts b/frontend/src/proving/batcher-providers.ts similarity index 85% rename from packages/frontend/src/proving/batcher-providers.ts rename to frontend/src/proving/batcher-providers.ts index 06a580b..bf32093 100644 --- a/packages/frontend/src/proving/batcher-providers.ts +++ b/frontend/src/proving/batcher-providers.ts @@ -32,6 +32,7 @@ import { import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; import type { ProverMessage, ProverResponse } from "./worker-types"; import type { GoFishProviders } from "../services/MidnightOnChainService"; +import { getLocalCoinPublicKey, getLocalEncryptionPublicKey } from "../services/inBrowserWallet"; type WebWorkerPromiseCallbacks = { resolve: ( @@ -164,32 +165,6 @@ class WebWorkerLocalProofServer implements ProofProvider { } } -/** - * Get the batcher's address (coinPublicKey|encryptionPublicKey) - */ -async function getBatcherAddress(): Promise { - const batcherUrl = `${import.meta.env.VITE_BATCHER_URL}/address`; - - console.log(`[BatcherProviders] Fetching batcher address from ${batcherUrl}`); - - const batcherResponse = await withRetries(10, () => - fetch(batcherUrl, { - method: "GET", - headers: { - "Content-Type": "application/text", - }, - }) - ); - - if (batcherResponse.status >= 300) { - throw new Error("Failed to get batcher's address"); - } - - const address = await batcherResponse.text(); - console.log("[BatcherProviders] Got batcher address"); - return address; -} - /** * Convert Uint8Array to hex string */ @@ -266,31 +241,32 @@ async function withRetries( } /** - * Check if batcher mode is enabled via environment variable + * Check if batcher mode is enabled. + * + * With the in-browser wallet, batcher mode is the default — users no longer + * need the Lace extension. Set VITE_BATCHER_MODE_ENABLED=false to explicitly + * disable it (e.g. when testing with the Lace wallet). */ export function isBatcherModeEnabled(): boolean { - return import.meta.env.VITE_BATCHER_MODE_ENABLED === "true"; + return import.meta.env.VITE_BATCHER_MODE_ENABLED !== "false"; } /** - * Initialize providers for batcher mode (no Lace wallet required) + * Initialize providers for batcher mode with in-browser wallet. + * Each user gets their own persistent identity (ZswapSecretKeys stored in localStorage) + * while all transactions are delegated to the batcher for dust balancing and submission. */ export async function initializeBatcherProviders(): Promise { - console.log("[BatcherProviders] Initializing batcher mode providers..."); + console.log("[BatcherProviders] Initializing batcher mode providers (in-browser wallet)..."); // Set the network ID - required before any SDK operations - // Using Undeployed for local dev chain setNetworkId("undeployed"); console.log("[BatcherProviders] Network ID set to undeployed"); - const batcherAddress = await getBatcherAddress(); - const batcherAddressParts = batcherAddress.split("|"); - - if (batcherAddressParts.length !== 2) { - throw new Error(`Invalid batcher address format: ${batcherAddress}`); - } - - const [coinPublicKey, encryptionPublicKey] = batcherAddressParts; + // Use in-browser wallet keys — each user gets their own persistent identity + const coinPublicKey = getLocalCoinPublicKey(); + const encryptionPublicKey = getLocalEncryptionPublicKey(); + console.log("[BatcherProviders] Using in-browser wallet identity"); // Initialize web worker for local proof generation const webWorkerProofProvider = new WebWorkerLocalProofServer(); @@ -306,7 +282,7 @@ export async function initializeBatcherProviders(): Promise { privateStateProvider: levelPrivateStateProvider({ privateStateStoreName: "go-fish-private-state", privateStoragePasswordProvider: async () => "PAIMA_STORAGE_PASSWORD", - accountId: coinPublicKey, + accountId: String(coinPublicKey), }), zkConfigProvider: new FetchZkConfigProvider( window.location.origin, @@ -318,12 +294,11 @@ export async function initializeBatcherProviders(): Promise { indexerWsUrl ), walletProvider: { - // Use the batcher's address since we don't have a wallet getCoinPublicKey(): CoinPublicKey { - return coinPublicKey as unknown as CoinPublicKey; + return coinPublicKey; }, getEncryptionPublicKey(): EncPublicKey { - return encryptionPublicKey as unknown as EncPublicKey; + return encryptionPublicKey; }, balanceTx( tx: UnboundTransaction, @@ -336,7 +311,6 @@ export async function initializeBatcherProviders(): Promise { }, midnightProvider: { submitTx(tx: FinalizedTransaction): Promise { - // Serialize using ledger-v8 (no network ID needed - set globally) const raw = tx.serialize(); return postTxToBatcher(raw); }, diff --git a/packages/frontend/src/proving/local-proving.ts b/frontend/src/proving/local-proving.ts similarity index 100% rename from packages/frontend/src/proving/local-proving.ts rename to frontend/src/proving/local-proving.ts diff --git a/packages/frontend/src/proving/prover-worker.ts b/frontend/src/proving/prover-worker.ts similarity index 100% rename from packages/frontend/src/proving/prover-worker.ts rename to frontend/src/proving/prover-worker.ts diff --git a/packages/frontend/src/proving/worker-types.ts b/frontend/src/proving/worker-types.ts similarity index 100% rename from packages/frontend/src/proving/worker-types.ts rename to frontend/src/proving/worker-types.ts diff --git a/packages/frontend/src/screens/GameScreen.ts b/frontend/src/screens/GameScreen.ts similarity index 100% rename from packages/frontend/src/screens/GameScreen.ts rename to frontend/src/screens/GameScreen.ts diff --git a/packages/frontend/src/screens/LeaderboardPanel.ts b/frontend/src/screens/LeaderboardPanel.ts similarity index 100% rename from packages/frontend/src/screens/LeaderboardPanel.ts rename to frontend/src/screens/LeaderboardPanel.ts diff --git a/packages/frontend/src/screens/LobbyListScreen.ts b/frontend/src/screens/LobbyListScreen.ts similarity index 100% rename from packages/frontend/src/screens/LobbyListScreen.ts rename to frontend/src/screens/LobbyListScreen.ts diff --git a/packages/frontend/src/screens/LobbyScreen.ts b/frontend/src/screens/LobbyScreen.ts similarity index 100% rename from packages/frontend/src/screens/LobbyScreen.ts rename to frontend/src/screens/LobbyScreen.ts diff --git a/packages/frontend/src/screens/NameEntryScreen.ts b/frontend/src/screens/NameEntryScreen.ts similarity index 100% rename from packages/frontend/src/screens/NameEntryScreen.ts rename to frontend/src/screens/NameEntryScreen.ts diff --git a/packages/frontend/src/screens/ResultsScreen.ts b/frontend/src/screens/ResultsScreen.ts similarity index 100% rename from packages/frontend/src/screens/ResultsScreen.ts rename to frontend/src/screens/ResultsScreen.ts diff --git a/frontend/src/screens/WalletScreen.ts b/frontend/src/screens/WalletScreen.ts new file mode 100644 index 0000000..32592ab --- /dev/null +++ b/frontend/src/screens/WalletScreen.ts @@ -0,0 +1,96 @@ +/** + * WalletScreen - Initializes wallets and proceeds to the game. + * + * Both EVM and Midnight wallets are auto-created in the browser: + * - EVM: Local ethers.js wallet connected to Hardhat (lobby operations) + * - Midnight: In-browser ZswapSecretKeys wallet stored in localStorage (ZK operations) + * + * No wallet extension (Lace, MetaMask) is required. All Midnight transactions + * are routed through the batcher for dust balancing and chain submission. + */ + +import * as EffectstreamBridge from '../effectstreamBridge'; +import { MidnightService } from '../services/MidnightService'; + +// Config from backend +interface AppConfig { + useMockedMidnight: boolean; + requiresLaceWallet: boolean; + requiresEvmWallet: boolean; +} + +export class WalletScreen { + private container: HTMLElement; + + // Config from backend + private config: AppConfig = { + useMockedMidnight: false, + requiresLaceWallet: false, + requiresEvmWallet: false, + }; + + constructor(container: HTMLElement) { + this.container = container; + } + + async show() { + // Fetch config from backend to determine which wallets are needed + await this.fetchConfig(); + + // Initialize the local EVM wallet automatically (no user interaction needed) + console.log('[WalletScreen] Initializing local EVM wallet...'); + await EffectstreamBridge.userWalletLogin(); + console.log('[WalletScreen] Local EVM wallet ready'); + + // In-browser Midnight wallet is auto-created (no Lace extension needed). + // Always proceed — the in-browser wallet + batcher handles all Midnight operations. + console.log('[WalletScreen] In-browser wallet ready, proceeding to game...'); + + // Initialize on-chain service in the background (batcher mode with in-browser wallet) + if (!this.config.useMockedMidnight) { + console.log('[WalletScreen] Initializing on-chain service (in-browser wallet + batcher)...'); + MidnightService.tryInitializeOnChain().then((initialized) => { + if (initialized) { + console.log('[WalletScreen] On-chain service initialized'); + } else { + console.log('[WalletScreen] On-chain service not ready yet'); + } + }).catch((error) => { + console.error('[WalletScreen] On-chain initialization error:', error); + }); + } + + this.dispatchNavigate('name-entry'); + } + + hide() { + // No cleanup needed + } + + private async fetchConfig(): Promise { + try { + const { API_BASE_URL } = await import('../apiConfig'); + const response = await fetch(`${API_BASE_URL}/api/config`); + if (response.ok) { + const serverConfig = await response.json(); + // Override EVM requirement - we always use local wallet now + this.config = { + ...serverConfig, + requiresEvmWallet: false, + }; + console.log('[WalletScreen] Config loaded:', this.config); + } + } catch (error) { + console.warn('[WalletScreen] Could not fetch config, using defaults:', error); + // Keep defaults + } + } + + private dispatchNavigate(screen: string, data: any = {}) { + const event = new CustomEvent('navigate', { + detail: { screen, ...data }, + bubbles: true, + }); + this.container.dispatchEvent(event); + } +} diff --git a/packages/frontend/src/services/BatcherMidnightService.ts b/frontend/src/services/BatcherMidnightService.ts similarity index 97% rename from packages/frontend/src/services/BatcherMidnightService.ts rename to frontend/src/services/BatcherMidnightService.ts index 1b7e42b..6ec8790 100644 --- a/packages/frontend/src/services/BatcherMidnightService.ts +++ b/frontend/src/services/BatcherMidnightService.ts @@ -16,8 +16,8 @@ import { PlayerKeyManager } from './PlayerKeyManager'; import { API_BASE_URL } from '../apiConfig'; -// Batcher query server (proxied via /batcher-query prefix to avoid CORS). -const BATCHER_QUERY_URL = import.meta.env.VITE_BATCHER_QUERY_URL || ""; +// Batcher query server URL (direct connection, no proxy). +const BATCHER_QUERY_URL = import.meta.env.VITE_BATCHER_QUERY_URL || "http://localhost:9997"; // ============================================================================ // Secret Registration @@ -122,7 +122,7 @@ export async function queryHandFromBatcher( } try { - const response = await fetch(`${BATCHER_QUERY_URL}/batcher-query/query-hand`, { + const response = await fetch(`${BATCHER_QUERY_URL}/query-hand`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/packages/frontend/src/services/GoFishContractService.ts b/frontend/src/services/GoFishContractService.ts similarity index 98% rename from packages/frontend/src/services/GoFishContractService.ts rename to frontend/src/services/GoFishContractService.ts index 4fc702f..303e767 100644 --- a/packages/frontend/src/services/GoFishContractService.ts +++ b/frontend/src/services/GoFishContractService.ts @@ -31,6 +31,7 @@ import { Contract as GoFishContractClass, } from "../../../shared/contracts/midnight/go-fish-contract/src/managed/contract/index.js"; import { PlayerKeyManager } from "./PlayerKeyManager"; +import { getLocalCoinPublicKey, getLocalEncryptionPublicKey } from "./inBrowserWallet"; import { API_BASE_URL } from "../apiConfig"; // --------------------------------------------------------------------------- @@ -41,7 +42,7 @@ const BASE_URL_MIDNIGHT_INDEXER_API = import.meta.env.VITE_INDEXER_HTTP_URL || "http://127.0.0.1:8089/api/v1/graphql"; const BASE_URL_MIDNIGHT_INDEXER_WS = import.meta.env.VITE_INDEXER_WS_URL || "ws://127.0.0.1:8089/api/v1/graphql/ws"; -const BATCHER_URL = import.meta.env.VITE_BATCHER_URL || ""; +const BATCHER_URL = import.meta.env.VITE_BATCHER_URL || "http://localhost:3334"; /** Sentinel thrown by the balanceTx hook to abort the SDK pipeline after delegation. */ const DELEGATED_SENTINEL = "GoFish: delegated to midnight_balancing batcher"; @@ -142,9 +143,9 @@ function normalizeAddress(addr: string): string { */ function makeProvider(): any { const provider: any = { - // Dummy coin keys — the batcher wallet covers dust balancing - getCoinPublicKey() { return "00".repeat(32); }, - getEncryptionPublicKey() { return "00".repeat(32); }, + // Use in-browser wallet keys so each user has their own identity + getCoinPublicKey() { return getLocalCoinPublicKey(); }, + getEncryptionPublicKey() { return getLocalEncryptionPublicKey(); }, async balanceTx(tx: any) { if (typeof provider.__delegatedBalanceHook === "function") { diff --git a/packages/frontend/src/services/GoFishGameService.ts b/frontend/src/services/GoFishGameService.ts similarity index 100% rename from packages/frontend/src/services/GoFishGameService.ts rename to frontend/src/services/GoFishGameService.ts diff --git a/packages/frontend/src/services/MidnightOnChainService.ts b/frontend/src/services/MidnightOnChainService.ts similarity index 98% rename from packages/frontend/src/services/MidnightOnChainService.ts rename to frontend/src/services/MidnightOnChainService.ts index 887e8c0..35d1933 100644 --- a/packages/frontend/src/services/MidnightOnChainService.ts +++ b/frontend/src/services/MidnightOnChainService.ts @@ -15,6 +15,7 @@ import { getConnectedAPI, isLaceConnected } from "../laceWalletBridge"; import { isBatcherModeEnabled } from "../proving/batcher-providers"; +import { getLocalCoinPublicKey, getLocalEncryptionPublicKey } from "./inBrowserWallet"; import * as BatcherMidnightService from "./BatcherMidnightService"; import * as GoFishContractService from "./GoFishContractService"; @@ -537,10 +538,14 @@ async function joinContract( } /** - * Initialize the on-chain service - * Supports two modes: - * 1. Wallet mode (default): Uses Lace wallet for transaction signing - * 2. Batcher mode: Uses batcher service for transaction submission (no wallet required) + * Initialize the on-chain service. + * + * Default mode: In-browser wallet + batcher (GoFishContractService with WASM proving). + * No Lace extension required — the in-browser wallet provides the user's identity + * and all transactions are delegated to the batcher for dust balancing and submission. + * + * Lace wallet mode is still supported when VITE_BATCHER_MODE_ENABLED=false and Lace + * is connected, for advanced users who want direct wallet control. */ export async function initializeOnChainService(): Promise { if (isInitialized) { @@ -548,20 +553,20 @@ export async function initializeOnChainService(): Promise { return true; } - // Check if batcher mode is enabled + // Check if batcher mode is enabled (default: true with in-browser wallet) batcherModeActive = isBatcherModeEnabled(); if (batcherModeActive) { - console.log("[MidnightOnChain] Batcher mode enabled - using GoFishContractService (WASM proving)"); + console.log("[MidnightOnChain] In-browser wallet + batcher mode - using GoFishContractService (WASM proving)"); console.log("[MidnightOnChain] No Lace wallet required - proven txs delegated to midnight_balancing batcher"); await GoFishContractService.initializeMidnight(); isInitialized = true; return true; } - // Wallet mode requires Lace connection + // Explicit Lace wallet mode (VITE_BATCHER_MODE_ENABLED=false) if (!isLaceConnected()) { - console.warn("[MidnightOnChain] Lace wallet not connected"); + console.warn("[MidnightOnChain] Lace wallet not connected (batcher mode disabled)"); return false; } diff --git a/packages/frontend/src/services/MidnightService.ts b/frontend/src/services/MidnightService.ts similarity index 92% rename from packages/frontend/src/services/MidnightService.ts rename to frontend/src/services/MidnightService.ts index 6d2552e..990ed3e 100644 --- a/packages/frontend/src/services/MidnightService.ts +++ b/frontend/src/services/MidnightService.ts @@ -20,7 +20,6 @@ */ import MidnightOnChainService, { isOnChainReady, initializeOnChainService, isBatcherMode } from "./MidnightOnChainService"; -import { isLaceConnected } from "../laceWalletBridge"; import { isBatcherModeEnabled } from "../proving/batcher-providers"; import { API_BASE_URL } from "../apiConfig"; @@ -81,24 +80,20 @@ export function getConfig(): AppConfig { } /** - * Initialize the Midnight service - * In production mode, this initializes the on-chain service + * Initialize the Midnight service. + * In production mode, this initializes the on-chain service using the + * in-browser wallet + batcher (no Lace extension required). */ export async function initialize(): Promise { await loadConfig(); if (!appConfig.useMockedMidnight) { - // Production mode - try to initialize on-chain service if Lace is connected - if (isLaceConnected()) { - console.log("[MidnightService] Initializing on-chain mode..."); - onChainInitialized = await initializeOnChainService(); - if (!onChainInitialized) { - console.warn("[MidnightService] On-chain service not ready - will fall back to backend for now"); - } else { - console.log("[MidnightService] On-chain mode active"); - } + console.log("[MidnightService] Initializing on-chain mode (in-browser wallet + batcher)..."); + onChainInitialized = await initializeOnChainService(); + if (!onChainInitialized) { + console.warn("[MidnightService] On-chain service not ready - will fall back to backend for now"); } else { - console.log("[MidnightService] Lace wallet not connected yet - on-chain service will initialize when connected"); + console.log("[MidnightService] On-chain mode active"); } } else { // Mock mode - backend handles everything @@ -125,7 +120,8 @@ export function isOnChainModeActive(): boolean { } /** - * Try to initialize on-chain service (call after Lace wallet connects or in batcher mode) + * Try to initialize on-chain service. + * With the in-browser wallet, this always proceeds (no Lace required). */ export async function tryInitializeOnChain(): Promise { if (appConfig.useMockedMidnight) { @@ -137,16 +133,7 @@ export async function tryInitializeOnChain(): Promise { return true; } - // Check if batcher mode is enabled - const batcherMode = isBatcherModeEnabled(); - - // In batcher mode, we don't need Lace wallet - if (!batcherMode && !isLaceConnected()) { - console.warn("[MidnightService] Cannot initialize on-chain - Lace wallet not connected (and batcher mode not enabled)"); - return false; - } - - console.log("[MidnightService] Attempting on-chain initialization...", batcherMode ? "(batcher mode)" : "(wallet mode)"); + console.log("[MidnightService] Attempting on-chain initialization (in-browser wallet + batcher)..."); try { onChainInitialized = await initializeOnChainService(); console.log("[MidnightService] On-chain initialization result:", onChainInitialized, "isOnChainReady:", isOnChainReady()); diff --git a/packages/frontend/src/services/PlayerKeyManager.ts b/frontend/src/services/PlayerKeyManager.ts similarity index 100% rename from packages/frontend/src/services/PlayerKeyManager.ts rename to frontend/src/services/PlayerKeyManager.ts diff --git a/frontend/src/services/inBrowserWallet.ts b/frontend/src/services/inBrowserWallet.ts new file mode 100644 index 0000000..66caea9 --- /dev/null +++ b/frontend/src/services/inBrowserWallet.ts @@ -0,0 +1,82 @@ +/** + * In-Browser Wallet — Creates and persists a Midnight wallet locally in the browser. + * + * Uses the same pattern as pvp-v2: a random 32-byte seed is generated once, + * stored in localStorage, and used to derive ZswapSecretKeys. This gives each + * browser tab/user a persistent Midnight identity without requiring the Lace + * wallet extension. All transactions are routed through the batcher for dust + * balancing and chain submission. + */ + +import { + ZswapSecretKeys, + type CoinPublicKey, + type EncPublicKey, +} from "@midnight-ntwrk/ledger-v8"; + +const LOCAL_ZSWAP_SEED_KEY = "go-fish-local-zswap-seed"; + +const toHex = (data: Uint8Array): string => + Array.from(data) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + +const fromHex = (hex: string): Uint8Array => { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + const match = clean.match(/.{1,2}/g); + return new Uint8Array(match ? match.map((b) => parseInt(b, 16)) : []); +}; + +let _keys: ZswapSecretKeys | null = null; + +/** + * Get or create the local in-browser Midnight wallet. + * The seed is persisted in localStorage so the identity survives page reloads. + */ +export function getOrCreateLocalWallet(): ZswapSecretKeys { + if (_keys) return _keys; + + const existingSeed = localStorage.getItem(LOCAL_ZSWAP_SEED_KEY); + if (existingSeed) { + _keys = ZswapSecretKeys.fromSeed(fromHex(existingSeed)); + console.log("[InBrowserWallet] Restored wallet from localStorage"); + } else { + const seed = crypto.getRandomValues(new Uint8Array(32)); + localStorage.setItem(LOCAL_ZSWAP_SEED_KEY, toHex(seed)); + _keys = ZswapSecretKeys.fromSeed(seed); + console.log("[InBrowserWallet] Created new wallet, seed saved to localStorage"); + } + + return _keys; +} + +/** Get the wallet's coin public key (used for walletProvider). */ +export function getLocalCoinPublicKey(): CoinPublicKey { + return getOrCreateLocalWallet().coinPublicKey; +} + +/** Get the wallet's encryption public key (used for walletProvider). */ +export function getLocalEncryptionPublicKey(): EncPublicKey { + return getOrCreateLocalWallet().encryptionPublicKey; +} + +/** + * Get a display-friendly address string for the in-browser wallet. + * Returns a truncated hex representation of the coin public key. + */ +export function getLocalWalletAddress(): string { + const cpk = getLocalCoinPublicKey(); + // CoinPublicKey is opaque — convert to string for display + const raw = String(cpk); + if (raw.length > 16) { + return raw.slice(0, 8) + "..." + raw.slice(-8); + } + return raw; +} + +/** + * Get the full (non-truncated) coin public key as a string for identity purposes. + */ +export function getLocalWalletAddressFull(): string { + return String(getLocalCoinPublicKey()); +} diff --git a/packages/frontend/src/services/midnightBrowserProofProvider.ts b/frontend/src/services/midnightBrowserProofProvider.ts similarity index 100% rename from packages/frontend/src/services/midnightBrowserProofProvider.ts rename to frontend/src/services/midnightBrowserProofProvider.ts diff --git a/packages/frontend/src/services/midnightInMemoryPrivateStateProvider.ts b/frontend/src/services/midnightInMemoryPrivateStateProvider.ts similarity index 100% rename from packages/frontend/src/services/midnightInMemoryPrivateStateProvider.ts rename to frontend/src/services/midnightInMemoryPrivateStateProvider.ts diff --git a/packages/frontend/src/shims/crypto.ts b/frontend/src/shims/crypto.ts similarity index 100% rename from packages/frontend/src/shims/crypto.ts rename to frontend/src/shims/crypto.ts diff --git a/packages/frontend/src/shims/level.ts b/frontend/src/shims/level.ts similarity index 100% rename from packages/frontend/src/shims/level.ts rename to frontend/src/shims/level.ts diff --git a/packages/frontend/src/style.css b/frontend/src/style.css similarity index 100% rename from packages/frontend/src/style.css rename to frontend/src/style.css diff --git a/packages/frontend/src/three/InputManager.ts b/frontend/src/three/InputManager.ts similarity index 100% rename from packages/frontend/src/three/InputManager.ts rename to frontend/src/three/InputManager.ts diff --git a/packages/frontend/src/three/SceneManager.ts b/frontend/src/three/SceneManager.ts similarity index 100% rename from packages/frontend/src/three/SceneManager.ts rename to frontend/src/three/SceneManager.ts diff --git a/packages/frontend/src/three/SoundManager.ts b/frontend/src/three/SoundManager.ts similarity index 100% rename from packages/frontend/src/three/SoundManager.ts rename to frontend/src/three/SoundManager.ts diff --git a/packages/frontend/src/three/ThreeApp.ts b/frontend/src/three/ThreeApp.ts similarity index 100% rename from packages/frontend/src/three/ThreeApp.ts rename to frontend/src/three/ThreeApp.ts diff --git a/packages/frontend/src/three/animations/CardAnimations.ts b/frontend/src/three/animations/CardAnimations.ts similarity index 100% rename from packages/frontend/src/three/animations/CardAnimations.ts rename to frontend/src/three/animations/CardAnimations.ts diff --git a/packages/frontend/src/three/effects/CRTEffect.ts b/frontend/src/three/effects/CRTEffect.ts similarity index 100% rename from packages/frontend/src/three/effects/CRTEffect.ts rename to frontend/src/three/effects/CRTEffect.ts diff --git a/packages/frontend/src/three/effects/PostProcessing.ts b/frontend/src/three/effects/PostProcessing.ts similarity index 100% rename from packages/frontend/src/three/effects/PostProcessing.ts rename to frontend/src/three/effects/PostProcessing.ts diff --git a/packages/frontend/src/three/objects/BookPile.ts b/frontend/src/three/objects/BookPile.ts similarity index 100% rename from packages/frontend/src/three/objects/BookPile.ts rename to frontend/src/three/objects/BookPile.ts diff --git a/packages/frontend/src/three/objects/Card3D.ts b/frontend/src/three/objects/Card3D.ts similarity index 100% rename from packages/frontend/src/three/objects/Card3D.ts rename to frontend/src/three/objects/Card3D.ts diff --git a/packages/frontend/src/three/objects/CardHand.ts b/frontend/src/three/objects/CardHand.ts similarity index 100% rename from packages/frontend/src/three/objects/CardHand.ts rename to frontend/src/three/objects/CardHand.ts diff --git a/packages/frontend/src/three/objects/Deck3D.ts b/frontend/src/three/objects/Deck3D.ts similarity index 100% rename from packages/frontend/src/three/objects/Deck3D.ts rename to frontend/src/three/objects/Deck3D.ts diff --git a/packages/frontend/src/three/objects/OpponentHand.ts b/frontend/src/three/objects/OpponentHand.ts similarity index 100% rename from packages/frontend/src/three/objects/OpponentHand.ts rename to frontend/src/three/objects/OpponentHand.ts diff --git a/packages/frontend/src/three/objects/Table.ts b/frontend/src/three/objects/Table.ts similarity index 100% rename from packages/frontend/src/three/objects/Table.ts rename to frontend/src/three/objects/Table.ts diff --git a/packages/frontend/src/three/scenes/GameScene.ts b/frontend/src/three/scenes/GameScene.ts similarity index 100% rename from packages/frontend/src/three/scenes/GameScene.ts rename to frontend/src/three/scenes/GameScene.ts diff --git a/packages/frontend/src/three/state/AnimationQueue.ts b/frontend/src/three/state/AnimationQueue.ts similarity index 100% rename from packages/frontend/src/three/state/AnimationQueue.ts rename to frontend/src/three/state/AnimationQueue.ts diff --git a/packages/frontend/src/three/state/GameStateAdapter.ts b/frontend/src/three/state/GameStateAdapter.ts similarity index 100% rename from packages/frontend/src/three/state/GameStateAdapter.ts rename to frontend/src/three/state/GameStateAdapter.ts diff --git a/packages/frontend/src/three/textures/CardTextureAtlas.ts b/frontend/src/three/textures/CardTextureAtlas.ts similarity index 100% rename from packages/frontend/src/three/textures/CardTextureAtlas.ts rename to frontend/src/three/textures/CardTextureAtlas.ts diff --git a/packages/frontend/src/three/ui/GameHUD.ts b/frontend/src/three/ui/GameHUD.ts similarity index 100% rename from packages/frontend/src/three/ui/GameHUD.ts rename to frontend/src/three/ui/GameHUD.ts diff --git a/packages/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts similarity index 88% rename from packages/frontend/src/vite-env.d.ts rename to frontend/src/vite-env.d.ts index 12b9fd8..60ab0a0 100644 --- a/packages/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -21,8 +21,12 @@ declare module "browser-level" { interface ImportMetaEnv { /** Enable batcher mode (set to "true" to enable) */ readonly VITE_BATCHER_MODE_ENABLED?: string; + /** Paima node API URL */ + readonly VITE_NODE_API_URL?: string; /** Batcher service URL */ readonly VITE_BATCHER_URL?: string; + /** Batcher query server URL */ + readonly VITE_BATCHER_QUERY_URL?: string; /** Indexer HTTP URL */ readonly VITE_INDEXER_HTTP_URL?: string; /** Indexer WebSocket URL */ diff --git a/packages/frontend/src/workers/midnightProver.worker.ts b/frontend/src/workers/midnightProver.worker.ts similarity index 100% rename from packages/frontend/src/workers/midnightProver.worker.ts rename to frontend/src/workers/midnightProver.worker.ts diff --git a/packages/frontend/src/workers/midnightProverMessages.ts b/frontend/src/workers/midnightProverMessages.ts similarity index 100% rename from packages/frontend/src/workers/midnightProverMessages.ts rename to frontend/src/workers/midnightProverMessages.ts diff --git a/packages/frontend/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from packages/frontend/tsconfig.json rename to frontend/tsconfig.json diff --git a/packages/frontend/vite.config.ts b/frontend/vite.config.ts similarity index 90% rename from packages/frontend/vite.config.ts rename to frontend/vite.config.ts index 8e15509..2547475 100644 --- a/packages/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -171,37 +171,6 @@ export default defineConfig({ 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', }, - proxy: { - '/api': { - target: 'http://localhost:9996', - changeOrigin: true, - }, - '/open_lobbies': { - target: 'http://localhost:9996', - changeOrigin: true, - }, - '/lobby_state': { - target: 'http://localhost:9996', - changeOrigin: true, - }, - '/user_lobbies': { - target: 'http://localhost:9996', - changeOrigin: true, - }, - '/game_state': { - target: 'http://localhost:9996', - changeOrigin: true, - }, - '/send-input': { - target: 'http://localhost:3336', - changeOrigin: true, - }, - '/batcher-query': { - target: 'http://localhost:9997', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/batcher-query/, ''), - }, - }, }, preview: { headers: { diff --git a/packages/client/batcher/.gitignore b/packages/client/batcher/.gitignore new file mode 100644 index 0000000..01d34f6 --- /dev/null +++ b/packages/client/batcher/.gitignore @@ -0,0 +1 @@ +midnight-level-db/ \ No newline at end of file diff --git a/packages/client/batcher/deno.json b/packages/client/batcher/deno.json index 607a0c2..47b35b3 100644 --- a/packages/client/batcher/deno.json +++ b/packages/client/batcher/deno.json @@ -9,12 +9,12 @@ "dev": "deno run -A --unstable-detect-cjs src/main.ts" }, "imports": { - "@paimaexample/batcher": "jsr:@paimaexample/batcher@0.10.7", - "@paimaexample/utils": "jsr:@paimaexample/utils@0.10.7", - "@paimaexample/concise": "jsr:@paimaexample/concise@0.10.7", - "@paimaexample/coroutine": "jsr:@paimaexample/coroutine@0.10.7", - "@paimaexample/crypto": "jsr:@paimaexample/crypto@0.10.7", - "@paimaexample/midnight-contracts": "jsr:@paimaexample/midnight-contracts@0.10.7", + "@paimaexample/batcher": "jsr:@paimaexample/batcher@0.10.25", + "@paimaexample/utils": "jsr:@paimaexample/utils@0.10.24", + "@paimaexample/concise": "jsr:@paimaexample/concise@0.10.24", + "@paimaexample/coroutine": "jsr:@paimaexample/coroutine@0.10.24", + "@paimaexample/crypto": "jsr:@paimaexample/crypto@0.10.24", + "@paimaexample/midnight-contracts": "jsr:@paimaexample/midnight-contracts@0.10.24", "effection": "npm:effection@^3.5.0", "@std/path": "jsr:@std/path@^1.1.3", "@std/fs": "jsr:@std/fs@^1.0.19", diff --git a/packages/client/batcher/midnight-level-db/000005.ldb b/packages/client/batcher/midnight-level-db/000005.ldb deleted file mode 100644 index a6b100466ed782c89cd2e06fc2e1b20b58605274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 309 zcmZQD3s)@8OwY^AOV`a#tt?iIkI&6a$;(X7D2Y$aOD?J`D9Oyvi_c9hNlZyBNsNzY zWMlw?YNg`DoDwA~rDV%g!=$8SBl9E^)3ns2q%^aX6f+CURAXbyBm)EE6w@Sgqr?=G zBqQTw14DyE6N6N<6ay2>WaH#ylcZz|6D1v`vecqtpe;&PhP5EG!8V*}G}+0}(80i< zpv1ri66FM9ZiZQ`KZO~TnA0+IN>YpTa#G7ub5fG@oJxWG%si)@{QO)uu$0kUMj?XxB_u&gqBj;4n*5Xc?jVV3x$o_-h0~uEstW0le>c^ZZ2$o z1mhRb(bd6^;74%Kw(kCO&PmSiJaTe~!Al~$wynx(tgy#f<5z4g4i9qKk ztjGyE2!X)~Jfj;BAjq=_g+PpufjF86IxLo0#3o20mZUr8MriVVROUsGaUHt@q;m%7 zOQzKjq~1c}Jh|x$L2Fx8J}*92~fM#RF}9H zsH`<@_nmEP4fTS1!7O|{*8HHMjg|Reglv6+suVk1nvGJigQteAGWHyE%JsHR%g(q9 zSz86Iy5I$7jaq3YP|=(-RSWyLQi+aFMB(Z5?VTVqp^=%r2&6*aCwA$(10t71*35&e z{0v^fnE~?hCd@5Ip1JN^iA%*Dc^FN_yI#+<4qtyG+tQ=(+0WS(ejVPI}yoMM=iVrFiUmS|*Yl9Xa$W@%z>VPs*FYH4DeY?f@4 znrvcdXqlRnY>}91WM*k*WNvC`nr2{@s-&Y-mReK{v_;9vuoh%CBZ#;sGg(oAfvbUm zgMo<+B)|#8+ze|{FSIczF{fqbl%y8v<)oIS=A~Y}8xBYRz{k3Oq&i0YM14 z9#wz}u!jirAtHc4bq8XNDToo`1&{#(5Oy`PLov}(bJ7&u6O93N)KWlHtq)?+JaYAY z;Cda9&r-eE)^Xo(F6l5oG9AcFONI>^M(5U$+C}Gl7;$6o)S@Ojsa*lh%5LJWIp`eN z2CU1X7s#?wtoycHvMc3?#{%dTdKsDhPA+eaSa?`g<5ISf521TZtA&=w58CP;$3Cz^ z!;z1h!nop~TF4prkO-%9%H`Gwwf7F#N6U+V$4rwLGm8ny3+bg z5b{3jZgY0QxbJQogNY7>!Q;4PO(S@pUP`W}UC){7i!?v8p8Ebpx=a6Mw>QhqV_Nx4 QuO!z0RKCX78w30G2OI@e!a*d&{E9u-1#G)Qzb zh?0s9@dJDVAU=Ulpny2^9BD=~_iC<2IDJX2Gp`kTVQV{}W44Z55jRyV5rPy-B)3T% z1T$DxR0x5BGqTjo5+u5gqsW9dT__P*1^@!rp%R61YDt)(IboY4Tdlf^njMHy=$Ei| zAY!K{7IH|doNMwxw})s{7#~ur-6+>tFUxSh?^*t2;vYlM2<&mNTg(~tky?&3pk|c} z?pGo3D+Z^fp~esLg}huc;uhguZ1@wv^$OpLMMsR06=4)jY{rz}B+GQ$EV2kQsjZuZ zFjpUq^3||s9-2{i7$OG5eS;a%96 zbG^=h_IElorX7NJbUxj^Q13)h*PL1KKE0e=&9Ywi8*jqm+(zo>58*D!{qt^bR_y0A S_)4!P(*M-IFRnMa_4x~XBYZOe diff --git a/packages/client/batcher/midnight-level-db/000195.ldb b/packages/client/batcher/midnight-level-db/000195.ldb deleted file mode 100644 index 33cc1f28e819a58415495814597aae1156a363ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJ5R!36ov~VCdAD_T`_J%=;ixLI}mN<5(|nHD3`djU%!?@X@SxUo4bpNn~Rfw z!RQb0H#oSsyE^Hh6cb0E zVE6*0Xclrb2BgklP2&Iuuu1}G=-4ncj8q5^k93_&>=J7PL;AgoChHn?VR?T+DXq2VPFl1QR<~u#1Sd5=f#O?F2_(2e*QfW1B#6o&io5a(_+dZ+D zGAw(6=Q$e(*w(e2=&yFm5pCCs!x9y%!>#+Dm-@Lfj_*yqoK7=^;tJ RCwF4$f4X0nSNneT^#`7F`vB*hUJgZuHNsqHcPTX|C%%7?9+H3F-R-jVk_4a0 QmALgk?$`Cr`oR470crepqyPW_ diff --git a/packages/client/batcher/midnight-level-db/014246.ldb b/packages/client/batcher/midnight-level-db/014246.ldb deleted file mode 100644 index 1544a97b1c4ec9d08128cc383a535c2216e64038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmyH4Xk6o#E3trTrX0ora8ZHGu^d>Ky)By5}*CK1cVtb^(BcpRII?Kl_gq@t%m zLNqkI#cE$*3!Z>t-(dF23rBGnk9NmwIjj33^eB=g)n}dv zsIMXlMTucVMM+d;iU4KI7paSoEVq5+%D}_Q1#Tk@J*41DT`19L94s(V2R>5UbC|@J z-(AgsE=^(o2x~tknljf(hV%Zi6PwAQb07_rNVQsaFaPO^?t;MZVweMS6{Y=@97)HZ zW#qPF1ub9<^$0_)OM($UbTU&X%E>tKf^mXX$Let7#Ipc}lT^wkr`_|_vdu9W1`_52 zV5ZCsEon99_H<&XXNDb*dO=Q{DC_NHdwa$6l_LGS-~3W7ny-zSl3Odi~iydlqUNjmqmg!b9m?YInC=|MCrZ#ye1^Jf@0+(HK9Pa-b-5wr3Fg)aB`N##LdNj zVf+LB1_x&+e}aQjOdNfZbCUDEdEXqMza%oD>-etk9QCb5n4xS3V!?zQ%iEA(0W&Lo@DIyX?Jmy%*CMHj9#EJsLn@r*mtdXhTjU-jPwgnMSVBq)aL@q)? zN3e1!$=XonCyF|g=9NVWD~%RM35Q%VyK1$y@jwzIS)ygbR~Qubf}Sh%FIia38Col9 zw6DV4g*$fyGhb_%x_j27jbdKYgRbr9RdaEk*VR562D#E`%1b$=YP6Z+&9LTZeO?ZY z?!Z$zJ(%m^c+(wE$5CD-BV#Zt6yoO-@e=iqCD^zH+aR^S84KyvYyqcqcLK60 z+w~^a_}H_ime(Q2O0>FNU-5!qaK5(CQ*tLxt-D@NTW=tp$|k;l0guT)@8Nz|eNHl; R$-P+mpXS%?@*ui+{{iiCcfbGu diff --git a/packages/client/batcher/midnight-level-db/021694.ldb b/packages/client/batcher/midnight-level-db/021694.ldb deleted file mode 100644 index 717e07c1de5b216d1c81e7ae4480ebb7cb9c8118..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJ5Iwu6h-ZT5QvTfT8QWn!JhGqX9^_jB*d8Dd;mM7!{3;g$;WX>x^xsYNOVvj zwm^Z{0BfLN39JB701gr*M|w|s_v&6f&V1(A;xJ$#3pU%{I9`*mYzq_sDmWf?s1Fcz z2oWG8Sdwce%X?6TRDuJjcRK@KSZt&$XZ+vbt_+JutYTvk}C28 zpfy8#(1W;P)VrEDwrbEaw76Kq{e!mF8HQWBRc%4@C@7fC(QzfVdOE8_h5?x=ZRlFx zib_IE-SWw?t-`G{S14&sNJxFxIApc$?euv*U6+p!S#EZfP@D4ZX;W_veS6w7G|>i*Du^{=FhviUe=#7 S!h2>VmHx;2oLsFZrMGW!Zg%eg diff --git a/packages/client/batcher/midnight-level-db/022889.ldb b/packages/client/batcher/midnight-level-db/022889.ldb deleted file mode 100644 index 67b52d9b8a6657fd336099be422e82b2b5de441e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJx{_=6oyMlOo*F<&JZ^u>1`>u9Z2BHmxYE8313_9?JZPFOA9TO$> zpc5I22GMm)H~_LaO_nGWjGa!hIVcht^Ar+ZmpERF0cKB;*b`q;U}qO4FM(s zb)>3T;9=keUC8;481$||ZlrEddWJZE+0m5gyqb3T_8j$8&_!Np(bnux8)@C{DqPUA zFN8JQb=8j9*QS?sY!F%2#^n5HDTa~;Om_r>!LSom)Ou*i~GpV>?T1)C> zEE-Tt#~I}BcGE_d3{m) Kx7%~S`uPj=|9eyb diff --git a/packages/client/batcher/midnight-level-db/024795.ldb b/packages/client/batcher/midnight-level-db/024795.ldb deleted file mode 100644 index 08ac8b9d8913c1f4d6b9a89540cd8a31ee7789bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJx{_=6oyMRM&n`}bXDTuqT%-I-aDAk@(n5lY>RH(+goTUEdu4k$=$`o&6v12 zI~xChzrn$u;7@Qsi;1I8a!zvIH}9LnnXif6q1z7JpncF~v*9iju_OS(WI$6Q5hWzs zB4&_@iUec?Fv2#r0R}C>7Uh;LQ33%`AOQpsr`19UgR%o|S4^Q_j%*pb-gPr_(S~IzH8v?3F3`6;d<| zPcLDp4Jw8%cmpVdLcvEatI}?-9~FfY-SR=z=#I_anRXhs8!2sUG%YhCMaK{To3Pw? z>ImkAjU{T~ikQ)d`Ou=Sr?|PiQad_|pHIZ=c`}*v%c~nZX^vZog|(kq=N|e``&@F# zas3e+9Qtg`e489<(a-G$njeO}{K66*`OP@B=z2>x-?_D=WMcjcchCQMceh*R7e4jL QZ^zRASYOvSdr{%z8^9lTssI20 diff --git a/packages/client/batcher/midnight-level-db/026968.ldb b/packages/client/batcher/midnight-level-db/026968.ldb deleted file mode 100644 index c193ca8224d9764155d92ba2a0b7fe85d6834d1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmzfQtX6vkUHMq*qXbXDT2wAb6-Vq!x1Q?V5VwEWw8dwXkXxhND0OztiwZZ5uq zgK;)7K7xaf;3GJw#l+EHa!zu-FW;BLHJx!b-o9c~fE1R{6WjhE^Bq#(ZGK5Hyp=<#KQv#4cMTN@z9ydp|+4)G(QJu;t8;X6_J;7>G z#P+3lbf~Dc3snm9dZbK_rlqPE1=6(EEtmrwn&V~$lp-*k=mQU$;i2mrvtq?*y5}lG zMi-Hc#kwY|Xbx)${s>Hj8Z2va(9=wUwK*@cs?WuzCXk diff --git a/packages/client/batcher/midnight-level-db/027065.ldb b/packages/client/batcher/midnight-level-db/027065.ldb deleted file mode 100644 index f02079df62a3c1fe89d245ccaf7f32dbf9260b02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmZPw+rg+QVTEhpfodc%M!!< zye$7DM;F7SG?RKqL1`vk;%`q)?ZP zf~vd%zoPI&505PKvaqC3$KoO*b5qaM++-i;kO+T6H~CiSa)KFwA0t%T1mj=CzH< U;5CytNC-a&|J~57QtEyi0K!Xn!2kdN diff --git a/packages/client/batcher/midnight-level-db/028447.ldb b/packages/client/batcher/midnight-level-db/028447.ldb deleted file mode 100644 index cffeb6d0f17de67ba139dca8f13a71cf154cf15a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmy-vbV6vx||7>$c@&{c>VQR(g1WgwwgKx*Y%DBW&*OSyvmfL6rG-NnSsMF$h# z!1w~Lt`5F{i4Wl5pcE5Ff5|z?`TufG4%L356Jz4I#C5g@_H>*Oc_ayG9t((PAcF`RJ(*jPQgpfN+}>{q3YC+d)sPl!BY8{*o}#alP+#P zg8Bl+M=-`m@DUsoFmd!ZoaCI7|H+?fJ!jUEz>9*&+c?F;WQ`|0;0ecDmWzqtAjtxj zl!p*Nkrz3_3Y>s!+qHosh`j3p0 zVGo~ghWG-9uDz+G6CXrM7{_O&S;gJcOVg{_*2}i}MlED>neShyJNmD?xn9;D>HH_X PGL8PH{WZQ^Pt}hfYHWCN diff --git a/packages/client/batcher/midnight-level-db/028601.ldb b/packages/client/batcher/midnight-level-db/028601.ldb deleted file mode 100644 index 25eed7b18c0369635166e1e47ac5532ee7fda2dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmzfQtX6vl5MF)?lqI%C|3+JCpV9ZYOvt+oUKr6_UnUT%R46le>SP7Z7?CJrVn zzJ>7xd<5g*BlrlKD8r|rY4=5g;qTo4)p6PoQ?&j2bp!os-3_#v3uFEm?Z2Cq$Ay} zmWUH#$22i?ilj!$VbqrmcicEMoQqHk92O|7kl2+((_3}5e#}o|F<-HUjQRN>PMWY+ z49;1?Mhz>jC%vdRs0r?gYn4lRsiNrn`|0ysy1pK2b1(zI03f%L3ag7*0}#H>z*g34 zcOx3?cIlXQ8+g|kE$+5&bo>3@(b5thGwW$;+4XX8@dj42Tio|AV4nH&?(a72rwsp* Q*-WMXseRtuZjY?@AI@QVivR!s diff --git a/packages/client/batcher/midnight-level-db/028652.ldb b/packages/client/batcher/midnight-level-db/028652.ldb deleted file mode 100644 index 31ff87344faceeaa710e95f4ab2362a9d7e662e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKm%}&BV6orRkNQfKbLKm(e?ugjw&~{jm*h)nz@~iT*vNN4le%d0XKwatDjfsg1 zH@=1O1$+hD1!pg@l;5}AzH9FcS?H$>!VH9ohzY`&nhX(x z&zY7e3m7wjA}bFC9%3jn1ao=JQvwBIic-#?29+%uBVdU_-HQP0a?_IJ{1kfbphlD; zB=izi%2;WP6>+NSbG)d8+PFJ+>Q!Hxw2NZS->hkO^ZIr>D7}9JNd0;# diff --git a/packages/client/batcher/midnight-level-db/028704.ldb b/packages/client/batcher/midnight-level-db/028704.ldb deleted file mode 100644 index 713801d2b961a1649fcb0640b03d424e7c5b1187..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmzfQtX6vhh@6XK!{Iz!xu(n8yNJCN9`l~OPT3IE*M-diY?7TR)ECU+MTHy7W) zP2=L|BNzuC!8g!BDJG8ol5=v-_nmWcnD$d56FPm*@%p+kk>7i4rRq7@C+D z045gTA#1U=K>#imB*0lvGKfex7E9Q$Z73Ca!f__%_{4x&B3s^I1~qVEi$IDv-y7XYZbmk3J zM+imJiqaJIvZ9zipz5i#m>T!;p%=7NzU8ZduIDrzb^3#@TLt#;tP+Zl@&<_-^kAM> zCIdV-i*$xSHB=%Mw#HQG6-W6Xr_p|?6hEJc)BUIT5Em(VJ+&GO=|!wDrH$uTCc9!g zZbX7Tmr&wja}UiHz2hO*_s2&|t8kaxjJKAzUiQ0hOnN1o`2K~tP5yZ|*IU|SlKn_- P$I}0FzAmqJX65%E%aM8m diff --git a/packages/client/batcher/midnight-level-db/028755.ldb b/packages/client/batcher/midnight-level-db/028755.ldb deleted file mode 100644 index 92bfb9a124885562d6cd35e8eff1935b3e98d6dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKm%}T>S6osd)2qJ>G&~?R~Xp^*=%q#?x{-lXj@uyAOT{1Ii(#DK2Nwu!ryAj;E zaqSC;FW?&pE_?)^KwPM);L;E7g?kR?a6#uOSB(8Z=!b)i5u3zCTXAf=Vv7oMMO-G5 ziv^6zklB!7DiB1W43X#vG7@D83yv&Q1R^3^VGg0BjB_EFqLR={k*OTIP{E1F!{NT8 z)!z)bBBPel?$O|q_o@}0E5qs})#P*D;~mo5N$MpJ5+yRMfSxoXZZ(Y# zY!IV1=Cs{~>#3G_2h|c{v3uBWLmB(()>ig>E?W=L#}l|(1R4Ov*-V)GnR&4AGV=^p z3$7m|>})e&V-`?nQ%iqtKhy%wPa0E8y3a3VscF~qcJ~#`6;^X!zks{^pLct+Y(D0N R_xwsG{g3r|b-kAAZ{J%{dG`PS diff --git a/packages/client/batcher/midnight-level-db/028806.ldb b/packages/client/batcher/midnight-level-db/028806.ldb deleted file mode 100644 index 9a05231f055962a7e76f197c089775ba160000b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJx{_=6ov~VCdSP{XN;RsdV6Vm??B=$w0sCc`6z|0w_g;n6)2yRi<2=iadUC< zFBt!Tzrn%T$)Dh$6cb0E6eREC@qrJqE6VLTM-#s4K%Sn>uScl~u2u<5U0&hVR zBEc4sfeZt)7_$~OY@6dK01l53Fa!+K0)&p}@PJ}27BU-%>G!6nB-R~4#32a$ewC6O zpymjai-@R~gb$_C94*RAc@oZza(Pmadupm8)0W-@vJrac?BxaDbft!>iQcr==Z;X7 z@&xq?TJ4Csq{uZixoMfG8z@d~(da;uUe&vW{Ay9xh43u!N8@T*DPm<{suYlvX1cni z^l?SWRcGU@ie}clEoJ-JO0kt9ofdR6ndtdgB>MI3)4J7KYuSp7?RD)ii_-1~bC7Vn zVQ7y}hW5-JTJ%Jku4m7ehCy(lY$EP4z8h_AZoQuBZ_IY$Aol$W^AP{b?(g@Fs5E{w+2 z!RQ0{3=Temk6?^iOdS0s=OpL*@_jj6_A|Eu>G!CbLIDzW}sWwp6Piwc(MoVbkmD zdP9ZeymaW5)0*NQkBS#{aoVw5sdgbo`}^_pe7qi#cc06v+`)|Fg_T%H{LC8nF~5K2 zl1t2K2XwI8rX$+6%-vl0b31;n-R*XY3rm^{>v3w)^>U!Ua*3rR|Md%ZEBtvk*Bk1i T0N#boSo)vl$Lwl5EWLdLo;Q3) diff --git a/packages/client/batcher/midnight-level-db/030842.ldb b/packages/client/batcher/midnight-level-db/030842.ldb deleted file mode 100644 index 2a204bf2357dd7df1bd82829be6bbb909b898550..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmF;Buk7={ltM&jn6v&L1qgSK}LCblUkr3wW_bi3e}bczW2@h<^!F_+{(~%LoamKeUT1V93m)@go7j~sG?wrmI*0i zC`*i%Aw!#zLotRJagHRyY|OF3A%s{6ZgUEAmtcx&hrO5>xaBB#bA+Psw82yzF?)c_ z8d0q?RUT=^m?UOeIfuif7wejnxSm|PuxMO5u^>LIkJ8wwo#3i9dI+j7hQO6_fpe&kk#QR@2p)Jn1Gz-RBpx)U@k)yZZ_XGpo6;U%*}d&%3=@G9L2qJ-?hu O|8w+tb-fnvynO>mIeN$d diff --git a/packages/client/batcher/midnight-level-db/048945.ldb b/packages/client/batcher/midnight-level-db/048945.ldb deleted file mode 100644 index 941dc90c8c57b3905142090d99bd25647d43d1f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJ5Iwu5Qf)5LW-cGfI5fqkA|?qk#>k;qmMBvch-GHk7R1yQF%_}M-GBvz2F-z@%DOAbdlBpg?Xpdd zpiQr#R!~T_M{tx?N6J{68@wOzazndMw&=w<6B%at1a>e!EjQtjB(jp#%_A*FtXG<$PZcHpp9 zAoX#L7Y{2cs&x)ET{CpE(sLt^4>fd7_V?rGxwxg?U!*Aqt^imC%W;sL*9!QYmAAp> zlH2klr@!kvL&s31VK`m<(Ps RiGzgjgYe%C-72N-w*h)kdy)VE diff --git a/packages/client/batcher/midnight-level-db/049430.ldb b/packages/client/batcher/midnight-level-db/049430.ldb deleted file mode 100644 index a7ca910a28627f2d09d58d36e7b724876ad98205..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJ5Iwu6h-YIAt9PT0X2v&A~-Y2c&3PiosWrak^sR(>9EI>Og?u0wy6*;4H6v< zJqti=fHhEL2`qsE;vi9Sr1zwIukOA3?>6Psf4b02brO&yc&n?bx>{Rv$d(~OcZjDYg$zqih zRYMn?*(XKoq(Yh5&~UXfD2yQQRKw_K-mYqDS2iwS3rK~6;cK~47}d2F#Z9HmjB&S) znzc?5h22J|(C7tLip+z2K7L+|S7Uhhl1wI-N)*>)V`Dj+!eUFy3;Sz~ z_owbS-FN41pW114w$$8H`@TOYuOjfA*p6GPTkprs4`E|%U;O?>cuM?bj}JTgYeM== Q?8esrTz%c%AI!AH54;<9H~;_u diff --git a/packages/client/batcher/midnight-level-db/051422.ldb b/packages/client/batcher/midnight-level-db/051422.ldb deleted file mode 100644 index 16f2997a94806a9693262bf21ca0a5446f0d6cec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmzfQtX6vhiACd3H`oh5EY(%akiwu6bSU}=NW5-etk>`m+}+(x{}(Hw082=IvKvBg*b ziVS8EWLS~51ZWW!5r7cQ2!sHPkp*}>G7*HB6G`EQS|MToA{I2%Av_Wz5cuP^Db<0A zFF{KcBx5FVk*uY{qLnoqRW^_16ho)qmFlNJnG(9!yVQqdH+N7l(Z14mYpsqXNpE7>G>;}tn~pknB8Ui1 z9QY675BLoZ1b@I!aG*^Ehb}k=&fe_J0cS6YMpi_(;zRl__wXHej~4lVI?+JhCzQAQ&d*5}2ZM{2!MC4>VJHm03m zUPZwbUr-92yfxF(u$lK-vd&qjvgq4c*B`c;cDh^|r#YwQ7BBpE<*bw!gs`h)Ql8Ig zuT2G|Mr9cKW1(aRh5E74g-)kkcW^kHTCH3zdOjAd5A!|=5_@260k)Enu)drPFuUA* z0{aQgFnu!4m?R)(U&)A*bl+fi!O^?G#v2G$e%vF~5NL;TOXz1fkT UR diff --git a/packages/client/batcher/midnight-level-db/083473.ldb b/packages/client/batcher/midnight-level-db/083473.ldb deleted file mode 100644 index fb44119dc57ad62cd4dd0da0625d5b4494667110..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKm%}#?r6or8r6JuhG3*B1duCaw-VCbU8VgaRSf2{p+V>-i7sYri-)^_FIg(mLo z!nZKKfY0ED@h>99L{&#d*-+^D-4f0h z1WX2kb*SVk{k#;a=2*G5CYt3|rv==fm;{ahY#fx@zNSt4pl1bqDnQll_|gtK1GVgD z28ISKilS=WG7HqLWy=j*sto1BU=$vutXh$GhSI3qby}HBbE;bPv(RvBz%UPVtyzG_ zz!=nkUYw|Kthy}^XGrC`A|BQEQz_Lx9p`fK^K88CbUeFZ7B(4{TV0HWrJq@5PFBu$ znXQE5dLbRAJvySEjniuMbGyFk^?FzO+yYPBTAZ49z3w&Nn5D!P`}GU+$o+W__v_{} T$A54evGhNU&zsxr$awz-)^T@K diff --git a/packages/client/batcher/midnight-level-db/083807.ldb b/packages/client/batcher/midnight-level-db/083807.ldb deleted file mode 100644 index 9a1779517f7117972e9fbe4b31549d22a570b010..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJ5Rz;7>3IwCdSP{XNaq`r`N+kLd&%UL?BRMF+J@83Wrl}<>KUM;$q_FVjN8T z1;#(%Z*Xw%2lx{llw#uOn|yEby-&V3AJcjX#b$1gy0mvZ#Oqm1QeN7rakKv{=sZLCUj%~--!d7YEn?Rv{8sYy&wd6eOJO{a!m%?zokdkPsBMak(9bEpEh zK{>9%7ExO-O!6zXW(ciTBVUm^a&Iu;N)y&&rx(iwE$hv((l_TWi`KlU4xrm+*;&B{ zm6KCH56xAb&v>NM)V!%Qq+Zn_Fq>N~Q>ozjP+%YDPZ=hfX0jXG34-0t>@io%=pz%4 zbX_vX(*(f_OdOQZ{LSuCjdcD<(0-k9A;JoNo5<}UolZf_2Z=P>sf QJ`AG&Wqw^=ANhs%A5&>~!~g&Q diff --git a/packages/client/batcher/midnight-level-db/086013.ldb b/packages/client/batcher/midnight-level-db/086013.ldb deleted file mode 100644 index 49dbf3ad33823d63c3c8486a484f13971e659d96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmJx{_=6oyMlOo*FkJiL z$O9csfn0_{eIhWvC@tVpUKK2(u9mdEsH_)5H6!aZmkLs;3S)OFlNMm6wqkm`HMT}t z&UdCBFZ7H8^n;?XY*SKYR6FhZ0smfg{?kJ1%Z5`hnaFQ`=iFC+Dw3ESijb|BARL|Gb;)ed&p$ SKFNa+{jbi~#noX@diw!Bb9r_E diff --git a/packages/client/batcher/midnight-level-db/088733.ldb b/packages/client/batcher/midnight-level-db/088733.ldb deleted file mode 100644 index 4b870b2bee1b4d1ca00e82a714e1223813939d99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnzfQtX6vj(QOo*FeJVABRVZ_pDhk##z_IE`ka4ULP2o1PZJv1%7U(qUC%BF*t|1bGQ4}0FdIdQ2 z6f|pCZ;y2t8}&1M-b|~|)l+gbp7bmR>%fqx(K@Wq0RcV_wW#Y@sI1ye(~y(KxeCWx z!?jwKI7krP^fG0Xih zW|OUu%~y3>6Va@k_$>lfi({PXT^R_l+V^dYXz PrT;npyu4aZcHh4NOzwA5 diff --git a/packages/client/batcher/midnight-level-db/091978.ldb b/packages/client/batcher/midnight-level-db/091978.ldb deleted file mode 100644 index 116e0407697ce3e79488f962f69c97932ccea4e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaJ*yH3Jj6fTgM5EtU0Glb1Z`uE-eW6Pz0$ejY+`u768&{Al*Yz}TNCN9Rsw=fK@ zK7)xqf{&nywsrSQ&X;rYU2+g{pUAF^sbe_PGfUg8vJ^qG7^Blbrd36OtgKQPqmm4( zurvrYf>33GB@{*m7)7%Lt?Mez(vZ|N9N;pQct>JziZctC2Hn#`mk(UWY(uULpyGjM zo#DC*j`aD)hS@fE6%p$Sy%{!X!=;YHj!a1XPF27HZU?GfsW!HPQ-3a+TA)+S(uNl& z&blYamQWv}z%A7r72>81wxr%_;RA7Gk|Vxd_Fx-~aep}Ec@(7tiJ3|}7FIoBb5$?N zQm4PJY+MR67U)3rz~z`*qZ6)JjGj+Kw9jX(>`f*a?FSL1!#PC0&tD#pTuL`=Ut1Pz zZLQfVEbu{i8xC)~?z|dH*i-T-O2u8T7vnc1oysMC{(wBjwXt@8ciebRqMyl=$o@Y@ LU)Q(!K=}9tp)z;t diff --git a/packages/client/batcher/midnight-level-db/231842.ldb b/packages/client/batcher/midnight-level-db/231842.ldb deleted file mode 100644 index 45b6df43781a4aa4333f9cb46b73836927a6cb96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKm%}T>S6or!tg1Au^x>np3btY+==|XU#ZEft2@vpVI=_HvZHl4I-G8%B<-i;u* zaN}Euh+DxoaNz?e_y{hvso>HN?tyy__i%-S2XTJjwwcS?OC38N%(p-W*ez3{8AU=C zMH#9<0?izeDFHx5#3ZN%WU5+OU@1^Wifls*g6>E-4bslJ)P8mY;u+-Z(TwXFNyYimO!iZz4 zH}=vr-5=Q7Eyu@&-Jq!{c&kE-n-bgH=vHvmXoftl>&9K@Ss`m7x3z8^1PVH%g5ufFfB}pLm5wuiQ!BN=Ql@B z!onDJz0eMpJ)7H}MV7T_xVudAe7{>5MbcerCb=4QJ+C!hg^95R@%tCyHuaZXU(M?G TDe#$^ORWE?e_dQIM#Z-uI(mAc diff --git a/packages/client/batcher/midnight-level-db/231995.ldb b/packages/client/batcher/midnight-level-db/231995.ldb deleted file mode 100644 index 5333f61836f48454394d5dc70bc6a15d3ab9a7b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKmy-LGS6vxvFf(Q-{Ivd=Iwz>JZxr1QR)@qE}*489$xw*Oa#x%7yNv)H+i=dN> z_!8m^_zVsXK7fgqVH`dm&=W|(_tKWZQmFIbGI%{UeG0VNaJ2xCZ zMOhg8r>mi#`XRxqdNQ6_q=&s;cVl9KyZn5%HM#Y)Za;G~Q+)3G7w$Iy=Urbd7!Uc< RTYfQ<{>S>dxLi(ZFF$+hdD8#@ diff --git a/packages/client/batcher/midnight-level-db/232335.ldb b/packages/client/batcher/midnight-level-db/232335.ldb deleted file mode 100644 index 7266788e006bb2a4c59921f873e0c73716096a73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmZPw+rg+OciYHpBd zk!WaJsb`Y37?+S*e{AWszbWP?T(unH&;S6zOK+ z>u8YX8j@}t5|I^>ZcrW&;pOh1oRMW=ROymY>KWjhROud)WA0|^>{}k5T5e{VQQ@AP zo)}RU5L}evU2ba&ay!@u9qaDjsuGn$IKz(gil_s6)hc22N%r#{Uq&FpCK;H+h1X*ES}D T*G%FdA^afxcSE;IsrzjJmC<+U diff --git a/packages/client/batcher/midnight-level-db/235510.ldb b/packages/client/batcher/midnight-level-db/235510.ldb deleted file mode 100644 index 8bdb459124db43a857a98c450ede30e1739141ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJ5Iwu6h+65#78uUB6LVZbcirB{uhyOFd^6>Ur0hak3G)Mb^^9>LPbGGgG5Ke zDnM+2h7C}#01B2s0XRsM9O)JBUfrkrK=X-N5Bgod?{~L`bQ-K9-Xcgwf}|*>=x{d0 zrsZIpld%PDLMbl@)UqLhP!=3akRsXwB8r5Og-pUZ%o+m{RLk#0vM$vfQBp!Q@&^^7 zYKT~;sB|E!wXrIMc|DdVrRiS%Y#7r@qeyG*Z9@us$Gl&OoF1kFQgm^(scRDgJ=Cgs zB6bU8j5|$};;!a#oMsG%NDVsiNmYfrHjI>rD~l(#Z-r6RmdBF@GCE~}4!n9t_p7Iq zP0AbNR`aOVog!Q`O7b2&3J#$Zms@5#&gGKsvkx9#;5)bi%Pd%6<`Q-BXO_T4IyDb6 zDW~s-^epSrbL!e8n~#3>zMpqTBd;*C@IAYdq-M9CciOLDF_mGyegSvvpLctc)*o5; R!LBCK|Fk|YuQ#Iow{JXwcmMzZ diff --git a/packages/client/batcher/midnight-level-db/247904.ldb b/packages/client/batcher/midnight-level-db/247904.ldb deleted file mode 100644 index dd388941c4dd8dd95f6e14e410074ac777ead8cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJx_yB6ozlx#H6~JsI$hn7(ckc{TP}El~yPbt0`7DE+2)8AT1QTxx1LSxi~wz z7#Bx>fRlqi!NDKkpi&b@pX3bZee<5Y2bhng#bMBkf~dE4;*N)lm^p&S^P){0Vl%jj zJWL%3Ic!7Tqk@Ya1|e~H9ud(Kn9Uesyh9LW7}^3trIivWknG6Mh=%t)glWpfQQu(F zHplFQ(^Vq1&m=LGwVa&mV@2FPIjsAZvf4LjcK|n(9nxJ4wpMCesyBKO=z@`km|)*U3TF(ml&%A=gUv zmYU}Q(_+I)j6B>-oE2vhMN?*T#oARG`eKoH=lsDhY Q3x)JQho2Xh%USd78-LMxhyVZp diff --git a/packages/client/batcher/midnight-level-db/252072.ldb b/packages/client/batcher/midnight-level-db/252072.ldb deleted file mode 100644 index 7fd14ca2c9d3a3b84d7d1ce34bbb3246279c41f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJx{_=6oxO5m=HGyb;USo1a5oj$3Wt(wtNVpC<2@9?Y&T1THx{_o!nha++6$@ z#=+G;;NsvX^p z5L#piDE5Kq8wykF{pxO}bbg}r_2Xt^q&or*{b^%_ zIp?Z3vx|~;D7V|L$WVb3yG2j2?;iDL`FzxU`9Xc<`4e0Mun9n7B~p`%Sp_@y+pl0N z!5nW)M`@2vsb^bhZL-)MU-N>XZ!9h0A-)!+mbYH^TW=tl*ouAs0`B8~-renb`7xgT Rh;KyF|1`g@Z?-3e_aE$odH?_b diff --git a/packages/client/batcher/midnight-level-db/255070.ldb b/packages/client/batcher/midnight-level-db/255070.ldb deleted file mode 100644 index a875fa168176496bd6044faf40597ff584f59647..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJx{_=6ov~%6XT{vXN`lwh`sH-{TN8Rm5)#nB7#V_-rIXip%f?u?B?!b;^yM& z;A|XR{S6NO1b>2qT1*^$l9QA7yg6^q5ze1eTVu}+yujV>v3R@%RLX6DWEoJxbwnk` zB~@aM$ZU;>jx00IDCdeJQ^n>`M=GQWU=Be@22AIvW=crFH0Vq;Q>}AF)gv$nx;29H zfY1@Blr?-h#3(Y%nRZ!;3o3GLv()QUkkcrLMyXQ^o9*b-8z0R`7mmZ@79NGD+di8e z7`PJ^r;dz|!u_lR3Gu>}g3 zRzI)DC$Lm)hsbDDMH*XENo_*EOkxM;a>?!U4<2`4U?PNvjIfmS<)2y+)-vmh!uA69 z{D_5FpG}$XkgPHJ(OzKqgF(MIx1{^@YO*!YdOf^&6P6dYQ(wObcj-Ux_GZI;N{b)q Q&1CdHtR diff --git a/packages/client/batcher/midnight-level-db/255260.ldb b/packages/client/batcher/midnight-level-db/255260.ldb deleted file mode 100644 index ec79efae072bc8959cd391a027c48ee870c00add..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnKTpC?6vZD9O^ll{IvS0uO`E@u1fgQo%JQ+N0n-#AnUky22o} zSMO-8I7zxjDQ*zGrB^j~SRX_ju9fA3gS7kNgZb9}3jpf?EP>{7s;>OZDwuu1d$5zE zUNCZo`M?=F0kQK+^0RxP5=2q2ys+4Z>{^;y+Ur{OB8eiB`WKc!*O0CL}H=Sq(tE}EI4D3>G zc;56hO_X%K-#LwFk)IdqQBihbL<`n5Tnu5?ad|+4?r~qK%C*I`iF&#f>Std{nn@nEZz0 z(tunZQgTJ8Z5=A%YIl8wP9|P$ZE=t3og}rs^>WmIW3n6h)b}sUL;BCVzuVQG((Gq? QFOmMI_qDv;4^Q5I0E(}99RL6T diff --git a/packages/client/batcher/midnight-level-db/258342.ldb b/packages/client/batcher/midnight-level-db/258342.ldb deleted file mode 100644 index 4f64be502ede7c6de17c427f14f55017e33246b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnzfQtX6o)U>7>$z->Z)-uM5Lwdy&X&}6#rmR5N(yM_qMl|mX-nqN=#&Mb1`vq z@hyxm;O6e&Be=LbD84i3!IcNI6rYg2+OX zAk)GSBTTa#<#-;C4HgWN6HQ_m$jT9uN0uN`gJdO=x=w)vnfR9xD!~&fLaECJe&ZO+ z3X4g|ss~7}wq&83FZIztjf*|chR|UvSmrMdb(`$h^=eN(I}QDzJ0Pr&x@r&yay^V| zeB8u^T0?>@LpoE`vP}(GrcqiwZR#CYJZc-Ft%rhW6&sRgqB*&u8mdw*b72uR zqOhb62U^ujxA?xP^f)Z?d_k*AvF`S_x0Bl^AFLEUuK)w4N3&pICb4tlng{8Pn^&+l zMIEnewKJa8vpf@L^3k|={Ja+g&D}}FJu-{Q)MVGo&iNadn_5eK{{kMEzwG{Qsr1BT SKbYl2{g3`Nx?PX<-hTiV40}HS diff --git a/packages/client/batcher/midnight-level-db/276706.ldb b/packages/client/batcher/midnight-level-db/276706.ldb deleted file mode 100644 index 0c8aba1b8cb5be959032cddb80199b1331f3768b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnKTpD75QhsU2IJaB=q|I4H%$(I>g#o?q^A$Fv_~$)RI;j%RHik;yQLGGJy{ngxReK?lfE zj`Bbd!dN$niA9$1g1}KdGjp71P{;w#WpRc@IfKPw>^jCoNQP$zVo7LF5OO1K;Pq-) zQaEfJbCo?&s{0Z@l1rg@TA8S-R%%%gj1LcinC!l3gR8YQw^`5k0^bn`(fYWk)eFOB zRX6bwwSjt?wc8_gi=k*Z?`_CQHYq^S-D4%bmsEzDqG#C`*u zAyk8)kPL=pxX7p;odi9l+$IpV`@6eo_GN?Xn>iN*Q4|-#YO3ZIvnD*vJ~csHVSYHK z=Q|;t(9k10rFgOXyc9;!sJ66}d3HU$TK;<7>%9rN6*2Sui|~;B^X~69tmmxsk=;zC N|LJ~R-EPI^`wx(?d1U|q diff --git a/packages/client/batcher/midnight-level-db/278083.ldb b/packages/client/batcher/midnight-level-db/278083.ldb deleted file mode 100644 index 3592f7c7d941d0b1ab764f1d9691244a6c600036..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnF;Buk6vqoDCdSP{N275gVy~sWI};OIP~ZS9pcdV@_6}OgAy8157)@ME+*q6) zU5taPAHg{I5&Q@aYB6#2m%Nw!-#_o4_oS1j*jnFf2v0ONTWr){gQ|ltb6u)xP)`92 zS*os4tf;Q8azImcu0f2L0$r7IMFWguS;MOCV8%e*)ngAaDZ+3>bASk}a2?_hbcBCM zjSQep7ZmfzD4!czXp#$bSsd++yR5QbDaa)r8iH26T#sAjaX~w9yH-{N!vrz{EFbv+ zsqK|~5|UGe^n6O}tlaO_YFjqw9D8<9rNiSBp_bUlMm(XZjOGM7RZI)!6S?%oup+me zRw0WZEH$f^@H1mbWOj!0N9hc;Oc{E{&Q5gu?1Sz5Dr4A_tbE=`()AMFV<=yclqGfTOPFGX9kte4%|o3t>u9{c)5n#TXU+nZ(b7?(fd QE79nG?9a*d#vuFt4X_A!l>h($ diff --git a/packages/client/batcher/midnight-level-db/278697.ldb b/packages/client/batcher/midnight-level-db/278697.ldb deleted file mode 100644 index 02c67203b521289fd3e0ed45a7c38eafe84d31ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnzfZzY5XTE7CdSP{2Pcgik@nHoUxSG)6r_M+DFxhkkG9m7hd_bS$+)_hxViXW z7+u_4T^alnT-+U$V&drMa>?EIo4edm=2K#K>^j6H&Vgsm$GZ&Tx*R7$R$v8aSsVrc zAO^D>!eWQ9fdpZPg**Z_k60cEShNKQBo+Z|L(pLo&j~6<#-uk9H9@y|K?=ZtT$~~~ z4-g)LN=cN@E@duIw3!%I=4ud%re`{JraiQ>vs?kS3nkj=VW(CH#3)ypywW%9d0G$k zqB^GQ>6u>x(bQ?FWm5yPLbM6*{&a0 zql|A&Ex(I0%4FHQMDYiMeqm*0ACjB#)N0r3QENf1r}h%xzfkwdKkx2#OM6T*AIa@l O`k&_4)y@8-`2GVbVR)nf diff --git a/packages/client/batcher/midnight-level-db/279277.ldb b/packages/client/batcher/midnight-level-db/279277.ldb deleted file mode 100644 index 7ffe8835e14c39bcc85aea7a4e944536e7f97d5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJx{_=6ov~VM&hQ4&JZ^urMDmVb|A4WRSJ~~g0Rw$TS_lQgn~FRlDL={9bBCK z1I8bq&TbC=1UFX)rIYPI&@POf{JleP+GYOU02zRa@aU@ zRIvgKLhM!*GeaD*?m@BE5X*Lhm_$)DPQz}G8O!>);e(2=Of12&3v|CylIrzLw<>Dk zK+JG8Xt@U+X!%4}L!(pPADnT3MY;@BDW8vSUw-g;eg8CHjpS^;61C)_)~K6}*EE$* zV2_NP(=2hu4zbOw94@pM$fV!z?JX^E7hjLImRZlctv4!}NXNc^p>E@U-qq!1=^>u` Rh;K!s|7m{BF1Ew{_aE1acxnIu diff --git a/packages/client/batcher/midnight-level-db/279351.ldb b/packages/client/batcher/midnight-level-db/279351.ldb deleted file mode 100644 index 030c308da86b80d6a65c3c8b84e08d2e608c7268..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnKTE?<6vbbn2*u4oS1V2iHGf{-O9!DTHfclsUz*_dUVLezY0|WbO*6T>2yQNZ z2@$`5tKj6|2XJt3*FoC~j{e{d=N#_k9?*K=iUYqL`eA$hjEx3G;xgzW5fPVq3Z`Qu zf*>Oh5iBFu^)y*jOLB=4swx<%6d}Y!k77|m%tMTO;=lym4Lhl3;X@DMQX)m+=^oX$ zCF;gfqps-(JzY&I)=)GS`>C%Y?n1m zVy(6ZZLepWNwInXFKg+p!(ATKTP4;QbVRAXk3sAkYk<&w-BCqe-*lQ09t}pvX)mVb zA#9*tw|g3x^sqr-ta&g1yV2%h4OvYiO;phFE3LAq`N(C%dJIp}OJDTtC`FZnp;_Mo z%sb~nB2G#kFRMqcOAP8Hh*Pgn$Zj8h@Nw;J?@025GPNX4X6p2)W~IgZnS!*E<86_! z^9{j3u1prb!=BSK3#?df1*3yQ4;Es}z4oS&A;q2;G2F2+&gV&djv zoV)|$4R{R>Cf9 zfiOxa#ww+VxOrI?D8VvjF3BlKMIuD4yd=q#FpL!>w3Nhq0>o$}XpfMoR9#t7BQXq) z_pw$KG3kqzfi%0P!ALhx(bziMhH{&_a9F?x2kFzpkrj3K>s1}@hCw@<&e?mklP7l1 zYASZRW}>oOi_~0YZ(t9g6GF$~R@$s-1J{Sp+TlSXLXqg?Ryq4$Tk= z+Y+&O!4VM$Hjy|Y5`=i-xY)J;Tudlff|LOs0WW~azOzh>Od@}TRj5!Aq#@Udnz}73 zoK1RMxrF7Tt}G05>M0(VCwV~!7!!NkDrT*&x^ESg1HTXZZQra`s*2I7 zQxvFXt*%!xP_c~0F=ya>bmn`Y9+n&0R*3Rd(`)+tR&&Eg*+>D}kWYJ>+E$zr4Cs*- zWvh}?)4gI&D(RllSHLKk?ci+0bM-Nmx3}Z&vk%s_+ZSd>naV|WK6aDSSztcj9`Bgc z91X&u+fRq?i5oh0Iyai?Aj*ZEPP;IR&`n|~-kRNd+NnP?$+;B!{R?xQ_{**?m(}}3 T<|DBZTmNHxonNet_FjJg1TcA= diff --git a/packages/client/batcher/midnight-level-db/279646.ldb b/packages/client/batcher/midnight-level-db/279646.ldb deleted file mode 100644 index ec2bf8fc9a4069e5b5070ca47e0ed65f327d35a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmZPw+rg+VP<4*lx%8YW|CrRU}~OZVPS4$YH4U@V3BH;$oP(t0R$YA^D;^;yv&2r%*-uI z4D<7{{F5AA43m-z4ShT<90Lm+O-r1-$}K8r=4N2-uW@5gVouABT z%SkOu%}Ghpb1DV$GxMBs^7C`uz*0u@nS?;Pz-AqFh`7$c$*jcq9|9O=F~Q{~PZ0Ck W#$@oCNgO1EAB6vI=vFCpzYPGO6n60d diff --git a/packages/client/batcher/midnight-level-db/280284.ldb b/packages/client/batcher/midnight-level-db/280284.ldb deleted file mode 100644 index cdfc850a5b013a7eedacba33271dac2622aca665..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmZPw+rg+ZfRm*m~4`0VU%i? zoN8%kW}IY{YHpsEmS~WYW{_rLYy<>pNhy}bDW=Jc-xwJ{z%e;5qtwF7JSfe~+_Jk>5#?W1RHz?nWS;3+kZGA@;^JXm?vd=`93Ezz8DSXWmErDD9OfVF z<{X*m<(%Xdl^o>~?&KJj=2YepkYntaX_}Pn?QH025*d{3;aX-MQCR8|kyI5{Ss79s zWf_(m?B)>~Xln~{JJ<)S^d-C)8h`*uuz^H5ftZ`&#_8SW3`)#tnK>n?MS3}@WvMwS zNqSDDKz?SPQ%-({6!z?Da+~f&jUfY-qUNeb< Pgz$s#-woX=rS7)@X(@Ww diff --git a/packages/client/batcher/midnight-level-db/280371.ldb b/packages/client/batcher/midnight-level-db/280371.ldb deleted file mode 100644 index e55c856978211565591d695908b4144e7358f13c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKnJx;?w5QR5JLPB&D&_YB-M2PKp*DHv`Iw1)r79=P?N@si5#Kw3X{1Zq;PlH5* zXgB~p;sTt3f+J9H01CiCqU4jNnD_KXZ$R%kw-&i4fg7A`p0dkmO-2|yGNY0#+f>C0 zW+IZPO>G-e8J88)M7SiH3Z@K~%@V>Egh+x2QYMuh3UgmMkU{KVl&CuDI1u4jQQGtYoi8eACWRwouJx;bS*II8IlZ&Imb-De*>e(anHAP-grusWZq3qP|6-nTdI zKz`0~{g|B>eU>txT174S+1)_%IX|n8 zA&f8JE9k)P;sZE1D8=1ha!zv2cfOO8L$qEZNyqHkrrkXm;J`^TBw#6!N00?710jZ* zPE&@Cb*v(q0xV0jh$9ie7ytuM48mB&B*uu;b0lYg$ZnJf;WfKA<0Q6eKsN7EBfEd0 z@?}cZCR9!2_{NxL+>+#T^IA|5k;!;j15T@W*tR6CJl7S;vlPEOt1x}aH^zak8S1Hc z)h;ixwlGDFVP2Rjf}@WijqGZG0^!XCDCHB*|cYtt@SqtQ^w4 z#vd?o<<^Ct;3v3HihEBolQ}taCNo2IUPAGy)psnXe=;PiX&iFAn!!k;bq;b6sRV;8 zL_mX>03N7>#ykVKEX2I3vkZd)t7$4tt5DUU!5}6S3sV6E-7$S6!G^&>enz{_psEQX zt?6T0E+e7t3E5drTA-!8DiGOrMx7C0E1+7+6f1f%%{IN7V)lAdW1u(ds+e2M70LJV z-L>VECgL=0SMupLX^INu2Ca6oT555{VOOw04=$Qw0V07xHFGKYas%Y?yaVS@5nbb= zWT&MvQ)BT2SSv}s?0SVvo;SgGEoPPa>1nWgC`i=t!(MGuJ#FsU33zl9yVU;u+dGwr z7?wRFlax*7#Ku}G=Wn>>CKBLFD81? zi)UZJ_yRtI2OmM-z!)uf_cxhIX8tpi$uR8~Hh(tk`$NCKKc@4uya*i3iwFTM0UjXB z6-A1WNFXGf1A!y)L_(5<1&ArM915w;S;R$_Kn2IO3A>$RQWV=C#JG$au7uSE447siG01Hw_O>Gfv_*+$+|{Ew3BHSu4Vd(AD6mFZjGz zSLaT>skiF_S0Y#Q#ip03uIKU+=i+t#-UJy)*nZ!Tl*3+qM*XvRDjagaRVSoN$9{>gw52dg6ezY7h?s;J$~%lQ6(W^7SdlS=qRa9`*hzpGjqsZYnX1hc6%S#+Z%-Vp z2p!Ug)*;gBJxw0!<{cValafiI%t)DPdgVS3aINbQuHW#mVdsziTjT89CMA}o=)9WiMop;M(`N1vMTXl5>Q`8c z^rBjX6FR`BQ)9@f0z)_mJfj(jwT4zG#J3A^T6zvj;2MB6FcX&J#>#K3f~|RT38a(E z?+o3+UdJ7|9qR1q(QoZ@y%Pl8@*RvFmOB@*S)s)57;3;BoP!h4wJtFkcel SXJRuR|DVR!?0!3{eEa}y%Xdlu diff --git a/packages/client/batcher/midnight-level-db/281264.ldb b/packages/client/batcher/midnight-level-db/281264.ldb deleted file mode 100644 index 7ce81918a3ce290cd0e4c602839fed48909566a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKoJx{`55XW0cOo$6{&>7-Jgr_f0pNE0O@={7c2q15p?b8;DrIpf_m&x75#Lbxa z5p*(s0Y8I-AHl`lK`HM3lDpj9|L$@*s{cx-7S`CYobkz}v0kL0s)UepT5}{vImUz`zS61@InqW+(O*NcMQMotf2i;=R&3g@1bxOAG&r4x-oEZW& z(_`y7E>PsnY%a4N5j)+od0rmMdBxX!F5@p}c5Mngdo*N%Zod@PwPs%`sook(wE=8y z0zL%Nnbenn4eNE08|BPV+h{p~xNZqzp%C3pNABsN!coa&lA>vPFDfLqwohG*zMiRc z+_Y@p@UpfM8n&)wi@`RxoTBZzGifKWkFkSjYPaj{bofps;%WNl59)sRq@8wmd#Jp` Tz-R0zYX6_X_w~(jQ2qD?%|mz# diff --git a/packages/client/batcher/midnight-level-db/282234.ldb b/packages/client/batcher/midnight-level-db/282234.ldb deleted file mode 100644 index 2b4e22d46d042f4e0de1df5b183b4dd30903e8ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKo%}&BV5XVbNjK-S>y!v$&+U|C{J($=E7)l8r#){ln+QOFFQtU$EV7MC-6K`I8 z3**77&)~sF&^Pd)6z~3$nPmR|naN~0>m{>$P5S{2`iDbjcD)M;ln}BJP~5K56BO4b z1OoxdT}M>}r~r4VD-MQOl4S%X8M=ZE0a6j@0vW@M%5y1NFBmXvC@mK$YAl9G3=WH;nduM7jhES?q4x)l)N2XiHC>r}rv&xNd-fnfmd9fNb{TUWD zmw>B;;46n*7OM2qB(y7(1x8wFZw~^h;{QSW^uAa2=9`19-GcSDd Q+iCm%TzoHX57_C)FFq7{5dZ)H diff --git a/packages/client/batcher/midnight-level-db/282330.ldb b/packages/client/batcher/midnight-level-db/282330.ldb deleted file mode 100644 index 51fbdd289ce42080d57246d9f19aaf333c372b38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmaKoJx{_=6oyMp493ktXVHx)y|>Vh!Nl@agyIzg0&czSz0gu_vCvvJc4K1V=HgFq zGtMS1{s9Mf7k`3-THJk-bCPr3^PZd>?(8YGJ#_}mVS~L9nNPPNBqk-;vLsRvB>?~x zm^PFPm@29%L1>E#%}b(fDgsoYoQI}J0T8L02bO}dlq&IDf@ZN{sMO_CD#_{$_{>FE zD+6p@0OLr}S`)1>E7mWS*qB#N=0?o}mdPesgaT^&deDq?)E`OeDRe6+I~G{6BB z$$XEYNiK|Or`p!rCERmL4a3QyV^HfLU)1`IEOLUQ(z!7XBE6!<+}|)O0YLH@K zWNe(4W@em{WNBn*lxmu2VwPl-mYA4kn4D~2YMg3l#u&uJ00NH5c^Rb^Ugkk*X6BY9 zhWU9}{z;B5hDphVhCZGaj)4V^rX@~Z+*D3JgqaAOTJw=4OZu*}j26i8(DZrzEvV zFDJDuH76xWs1&&+em$uj%C$kdce+Xch1r8*z+~f&j YUfY-qUNebgYIYp&aiUOL(=_=R|nx+E) z7>XsJPSc9Qsc>U4Ng6<$0yv}T6v>cKF=-Q^Qw&MKYwjRoMc6hO$c4D)4zEXC#$38Brf2l*yf>EvzEDVRN0YW*e|%yG$j&jcSKLjA^rrTa%$K!7vMJMc zLc`BF#>{YZHCNi~4{f*Pc-~mpO2iX(m`rW6UW@V@l1^pOpFbdv+b31PQ);=bEy(SRAmaSBlNULf6gWWMVFL+p0x>s(oa$d*1|{aS%$$$v^+|)^fd6JY` z8J%CX37WL-ZdzTLq{LPz(TXIoo@_GUmNuejX+)Wq0VoyfNBn*Nc(Pf4{sJZiR;2&{ diff --git a/packages/client/batcher/midnight-level-db/286857.ldb b/packages/client/batcher/midnight-level-db/286857.ldb deleted file mode 100644 index 2c9abe7c004263c1caeaccb60d25d7470d47db90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaKo&2G~`5XUzOLaMl+Ubwauw?ezS_O91RmFi8Jx|Ioy8@p+^;C0rni9cF9u?Z4V z)mue-<6)4v@D?0+08YL1REYz`;m%K*(aij3Mxz1M*T#Agg=v(AFDG(dyrLeVQWZM( zWS|5pQhi-gMirHgkq>>g9zZ0ShN1K@4v55p5rK7$E&`?rj|7DI82O$Ql4y#eVB#UB zXhcxtVGYy9Z4=ZO?=3hOBxhOfzL~~$mv_X(E(r?ThE8#WAxv_9G2(PP?p2U~tTu;( zgK6IwhREhK%pGDM@a@^LH9Vb*P66k7M3T|DldD6Me^70+t&B_Q#%=j}a@6f9;l*C$ zAU7^7t12tA-r7#^JGdN0}I*6Fda zvs2&RsNI)-cn7Z5!0yvW;Bj48S=bYhJ$!l#Hd;XxPi4Lp%TmT;vBhVLJW6?-Wl47_ y;qT4WdTP0=x_;q;mDWb%&p*Jo<&&1${V(?JM-$$(p4ILD=j8Y0=jXHipML>9bb!tRHjiDPHO9XB2*xN&m&(!%2G~w5`mC{aiM8K5;=_H z+~~sG-q{$6q`F^NQxm9%=#*0u$1gG?0R)((yFW zNkRfesc?8mwF*Hq#(5A(KVnh92_E4Fs@i?Q6S`{AUTZs-U(aYCoqFgRZMVU|)h-GT zwvG|*rzW#djWcK<)AR9k>3UO4v$D+3x{?3x?X_1OYr`K$Xwb*r&!5m+7kBo0^>Xpy Tk#+B|UH@OR@2B6UJu zNuI$*rg=q4CHd*@&e^(4=fBO b65krXU&Ux}pHUp796t#E-O#O4>V6vlacobu diff --git a/packages/client/batcher/midnight-level-db/286955.ldb b/packages/client/batcher/midnight-level-db/286955.ldb deleted file mode 100644 index 39c94151177d4a9ded7d52b7124283972c8038c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 zifIOhsTSr&2C0cgMro$0MoAW?Nk*3DW597F)PE>Jv1%QJkP`^J>M-dDj>_TD$v;1DbY3D z!_nNbK--`y$SvGC+ceBE%G<|GJJdZr%F@Xxs4P3kyCTphATTY*#5AJN-yq-LzrxqW zH^VI7*uo>Ctk5g8C^E+jWCtUNSjF{a28d<>Vm1(i6NtGPeqG%j$DqWVmYGwMTBMhg zT9%rVlBDNU3glK~G%UPlzvr6xNAeOo3KJWu z+KoSj1&M`!fdvL6R(4hfsKd@jx|8m{_dT7?Sba2i7Fk+mW%_6;*NexL6DqM_kV}OW zws6cObtu9#v0@NAJ46B-FhGO?2@#GWBoQ)^AdZENAjJ$Qg)AtUtr9{YKu913tXSFv zNXn%7zGJkwuZJKEi_1z!&zJeB?;VQiX+x~^smmzLAOMB-<`a+Z=Oax*EUT*0XGn$3 zc%(JCEbINlpd3wR{m8<6UDV-mc^2rD+ulWChv+=g{!mq5&M`bWX=;R{@v~vTUbw3P z7fv+q*F50UH)t7Ltlgn+El@K(>aVpIjUxyO^{_STkRJ~8gJjoxkCHM;LUd@**rD$AEr@8z{?wtjEr7)*ZlJjcRKFv7CaH diff --git a/packages/client/batcher/midnight-level-db/287155.ldb b/packages/client/batcher/midnight-level-db/287155.ldb deleted file mode 100644 index 44f85d71d0c68553e4c5228ddf871c1f5edd5529..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaJ-yHeXg6qRj;2}8rA2`vmWG=Nu+U9Fg*@Y=`&j$p7Y5v$5-wJXb(Wo#h{Q$fkV zSL6rMCX*(ALBR*mr%eHp)8`cTan8MG?p!JVQQ1nuAP(bTf6N!jA)*j*%`+6rg(@^z zV+KbUsT%TS1{mJbu zdW#mQYa!=&azc|{gQT+Ey6J^ZfHOb0?NK`N^z<^ivCa&qX}DKruOT|O&HU)7yuDI} z_3w+#`yy|ASgEd-#9B#8KmB-D+OCN(y5{qPh-Wv5CYA|sg~wxDW;}I2FAuo z#%TtI$*Jb12B~J2sa8>r)Q^P7h^MX`;12gS% z@1nfy!l<%r3tyi|57Xj&N01$iAc9-{@_wLo0xUowHjn@(NRVNR;sGxPCFZouoRZWc zy`0pt)SQ$gJ*QG2KQqrMCqF;e4J>8!jZp}s3vAZ5msy_}IGL3g|3d)7N3bxMc-VJ8 XqL<0w5R*7aIerlSyP;d9)crO9EtqV8 diff --git a/packages/client/batcher/midnight-level-db/287407.ldb b/packages/client/batcher/midnight-level-db/287407.ldb deleted file mode 100644 index 44dee3a2e5251ca8dedc674783d884e78177661e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 ziY6B3Ny(-usmY1SY32q7K!!nTvau;p(lo``G|j>^+1Sk7BGuB=BE=#xE!EgO)gak4 zHO(AoK%$|Ag@F~+zF?4bj9Zx)K)^98!__@BEzmsA#3()AEix(~%dslZ*w-o1HQd9| z+_FI1peo2M+&SAc%+bd{KieoJz~9+BJk7B@#MnL6sUSO~(lE`YpT za#G7ub5fG@oJxWG%si)@{QO)uu$0j^Mj?OyVHr_(AyZhHjNo_uBwa{B6(x diff --git a/packages/client/batcher/midnight-level-db/287473.ldb b/packages/client/batcher/midnight-level-db/287473.ldb deleted file mode 100644 index c3aad4aaf9b6c0a76a861c18d9bfe539c9c07423..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaKo&2G~`5XbFALaI2G3+Gnc49)J^yYV7b!fr?nN#o$8anlp)^=|6ev5Vu6gahJ= z;CT=d7hZrH5(gfDJ68@6hdV!MMl&Jv)B8-3{CZsM6 zXmBcsNpypRkOOWWh0GK>#85)WOpLJL1_1;!i3npR5uuP>!U2|&bEIRgheik~r9n3U zIu%I$*iss-e?oqWW-+U+Y%1E?ds+- z8v0i=Ey%|k=UMNf>^SMDV$MAF*(APzAdN;*K`)1uKcBmU+dOlw4WrVRnl1lJj z+PLZH&GRR{U2v!>cbdY%&h9GXuip-p)|Mr%#JIgWQn{QB|(4iM%rbgY02TR;;= z4#yPR5Hm9Z5#5SVNKE1oJB%XE1u+o|8RHa+Lc|yeAP6Whso>la3tz%3IZ4jQX&^uP%^~af zDY@wmxXrwNO{zsWYRV;0*=(@PQ~SoMbv$R{%{W3QhoYrKL&h%5h-(0KzQ133*UR+0 z`CZeCN-yxH9IO?wu6Yl?+S+ziNOi@hdx{sfSh(lTie8Rgm1XJCin1OnZ%ftc*5d1q nudP+L^?$F>?pIG*aesc(&p#^QuDVgK{~!IoUvA&c8b4nEE^C6o diff --git a/packages/client/batcher/midnight-level-db/287805.ldb b/packages/client/batcher/midnight-level-db/287805.ldb deleted file mode 100644 index 937f0d3209479f27fca7262571c5abaac6d4310c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 ziYbW}sm6w;hG~X|i58|trm4v$W(H=aiD^b=iHR0wsVS*x=4QzTrfF%($wtPe$;K%r zDTbDomPzJG2F8}DMpjV!f;Rwqkl-q#LdVQVqbyISOheD$a)Yb@Ur)!<%%lKc zqY^X2JfD20^6;V(FF*720FWJwAY$Jt|0oUy2xJ2ZZ~`$m!|hYw6Bv}3(=u~PQj7F* zQp-|vQj+wXN`d^$Jg1!e{9HG%l+iavA&@SxS=(M_ePZBbR$}}Q0Sq6(!eHWI-~EVQ VCWAvv;vnVtLHO^6Zk1B^+W@B4ZA<_F diff --git a/packages/client/batcher/midnight-level-db/287948.ldb b/packages/client/batcher/midnight-level-db/287948.ldb deleted file mode 100644 index 6b2e48e40b96929cfe0da2a38de9e854728a4253..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 zipi!)=7#2$=Eh0M$*HMn=7y#zNhv9*7A9#xO(`kni6#b#76yhEX{o73X(IVDQ2mrMrI}{R#5wbLDn(eVPXIQ$E*xj_t3OJ^E?xy^nAC-sDLcTsz763r$pCq z4@Yy$0&Rn;Ah&SmY|}8ud^3Z@%Baw=v_j)T50fZwqf+0rJkwCuP|L)SQr`+Q?E<&p zDqj$!QoM zrKR6xrJ#P%Cf#n*&qlq@SbC1#6a(*4SEEMV;xwj&BqMuxX7ieKBk42@NAP-QmUiQy z@o^@zKHEM>1bPEH^-p|A_6+x+7q=W|7{R#HZzjPZxNOqA%1_6leFf=yHk{SBwu;+J zg}ble_v%uS*{_z1u<~NBl^-{MzbR{#FqUH+ZAiRC8Hf#g@>0jCEwe0f781QJzbU2` ryQZJt_LP;%TIuN@%Jt$&3+>^Ld45+WUn{Fc`~Nw9{Pbmg^8VK|qTPhJ diff --git a/packages/client/batcher/midnight-level-db/288356.ldb b/packages/client/batcher/midnight-level-db/288356.ldb deleted file mode 100644 index 5640082349a3af0ea59865668971d4de39538baa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaKo%}(1u5XU!(+6wi?rDqVgwsd#ByY(U=72_lgAps?po#cdfy=$B}&XEj6b?a6y<7+7!~4I0(2f5JZe%4pCqtrVI*wh7_kzYbw4H zsxb`;4@Ho(*FS`gGdvwl-J>*aRV-T!T62oR{XNcbgdrSGtx3;fU*hI>VPj=`+K|FE z>m8Td0%IMm@`=r)oj8y=upyM53NC%zYtoR5dSYlM5;H<3Ns1TQ2=WOr7 z^2C4?uf*(9uOi1Xi(s>IFOVIKAc9SG-*W~=CKe6`hXyu~04ET0GuWNlzl%YMIW04% zB(+E{C$%g!CnZVGsT9c1%yY`g&(C!OOBsD*6awi2o3-s_)+Yu|W+le|5Ww&eEDRUpv_W8q-NPMAJ%GH=u}8yyO33oG@wz13`AQF_oE4b1-j1bOk8+jOp-#N9!o@WV+X z?c2yzyy+=J6UWNFDUfBy?r`}|*J`z0J<40Pq!6uz;((*h#bV3u2ff3nrWYem%y)Oo z+vT#{{KgJ diff --git a/packages/client/batcher/midnight-level-db/288761.ldb b/packages/client/batcher/midnight-level-db/288761.ldb deleted file mode 100644 index 5bb4ba3f2031118571409a9e459daee2eb5e0dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmaKlKTpCy7{;$Qnh+g~i!;Ox#s0ZFdTgywh>1cWP;jEx-eIA%1PU=sEKWv0jvv6) z_z4`0qoWQk4oa)DzvO+A_j&(3;9BTsCsTjn#O!DmueF37gc+nnaTr%Ql%Y!)*02h( zC%clXWi_Ny?s9@CB@|*txkg>bRXmSSRhDE76(Y%=CS#@|$-y!sNa7sPB1Bw4UggdPyxE$8GW2seN z>E&U=p47?%y)|mjeSa3}ZQJYQyXKWSj%&?RKX4)qCmBK_kB8Q7AAlXO62MksWY)C} zHjVQqkQ2Ed#B5p!*n$NfEg184?}tVZMPoA^DWAe#a+UtNe;a;)jF=O?e*y2QoyzOo WOZRp|df3=c*8j);THPGXtIt0|`D--* diff --git a/packages/client/batcher/midnight-level-db/288817.ldb b/packages/client/batcher/midnight-level-db/288817.ldb deleted file mode 100644 index 441cd9f731e064f4f3a01ad01214273ff5fb6f3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 ziWU}WDP{&KW~piB7DmRYX(=XYM&`+;DTb*Q2B`*?iI!%jmZql0$*C!c7D;Aii3Ujq ziDsro7RISYCgui-Nmfw%f+&hc}_X``MGXjDWh+ULLgmWv$nm=`ozG=ti<>q0vJAmg~7zb ZzWWiqOa_OT#6imOgYe%C-72N-w*j}WYODYN diff --git a/packages/client/batcher/midnight-level-db/288947.ldb b/packages/client/batcher/midnight-level-db/288947.ldb deleted file mode 100644 index bebb624e6ca5de5626906d051ac759125142f3cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaKm&1%~~6otn&1VSK`ZUWgkWal)Qkw&ALf)^UQPU9HI82`nqN~4i1S(dS+Sdl0+ z+mOCS-XYLUNgkrmJV5r{cF{%&blDH?g?kR?a6x)ks>YrhdZGJnF0=SOF*PEw#S~LI zs)G?0OcF{|Ol%~epjBcbO*0vySfz-W1nHJ87#30|j1rq-9cuzfo7#|CP+AHkgfPcq zP}L-(qO@HB1%}^&gTZ39j7Dck&~NdkHSe!Ag$HUxTX0^u3n7eaj7)sZ6KX#+ei8Si< z;`dUq?jOE<2A~SA-T=N)2%FEd1%7Vd?t;CF;{}O~4gzA52P3dq*(?2d1$-<2^L{e@B! ziY<*;V)S91G;C6M5&9!k|Vt*y?b@9o>5=6Hc#Uyi?ir;D$h@MFmtf9IYmq= zOWDvCRAPcH8;4K;K{gdq3?Ua1mvDz#p9Gx2TBl^ z6C|laS}!|BLoXSD>2$ryi(p*pN#EP&>Ez6Tk>8^j#R!0P;hii!Z0fglw-_vDt6YmQ zi^^$oFe-%+VZVp_v$a`7UVz5|*F`zu0HkJbUL~XR?g5hK@oFL#x>U1SaB`IT zB%J%o&X!5Cc;{vNzEvF`l@~OJgW)jZNXcPR`H;CNB!tt-vAQ4PHzkC1bWzv4yUp*d zW_`VRacSIpW<1zyKf2!t8@Kb&z`ysN8e1I|>rximTAoQA@@=oWz2nT&dA{!7S?GKF tag)01sz1F7jE&A#>*g=UxAs5p=QVl%qYb`vo;1?`ng9Cu`FSDHbUP7D)!FNk#@{ zW@eUYNtOo2rm2<|$yQMNf#qe^F+#eyNLoR=GiOMr5Rck9%>ruYOu;ZjOmhQKp+w zg^WPRq^K;$6Qbykxg+RK%W^H?!^@)L#S&8vK1TcIA3xkP= aefJ}JnG6mwiG!5m2jRaPx>ZWuZvz1C0BhX< diff --git a/packages/client/batcher/midnight-level-db/289372.ldb b/packages/client/batcher/midnight-level-db/289372.ldb deleted file mode 100644 index faaa94424acb3f3d73a01c24c55cc33787247045..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmaKlJxjwt7{@Oy2*p8gaT8kHitXj{a+iWbqJ0y@R;jU_yi4v((lo6#snE&AN%VvG z0sH`d4F_>_QwIkfw299C;rYY!e_npzN;)Y{rv8k^eDgeBcul98&Q)e0%7r3SS#=Dq z6J1etS9YL7iUJ9W5S9rhOfwYMp^&%&a>_7ws0Iwj_8FeWQ3X56> zp8#)xQeOCB%%_!*&v@vvij^#SKeEE{c+|*7=tEjfud-kFFTpz~emKI3{Cgz4IX%>m5 zNd{@j7UoG7MyZyj7FJOEf2l4Dgyuv@ZmsaLLtAIJ_y5V7vq?Ar_sAHcSM81pPA>Blb@gK29`4V#wY~R1vYEj%dAffoXkp${~>_kBUl(rJnXw4 X(aU6Th)Eoz96t#E-O#O4>V6vlePC@B diff --git a/packages/client/batcher/midnight-level-db/289387.ldb b/packages/client/batcher/midnight-level-db/289387.ldb deleted file mode 100644 index be1761b041751f42a42b56f89b52ea6db1e912ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmZQ*WlT^kD9S8LEJ@WZE&-BOW{IX|sU}I5Mv19u#%U&oCdrnmW)^0~CT1yy$p*<5 zifKk^iAF}|h8E@)Nv4*@DanZj7Aa{4sfkJEsmY0k29~KN#^z>emKI3{Cgz4IX%>m5 zNd{@j7UoG7MyZyj7FJOEfM81pPA>Blb@gK29`4V#wY~R1vYEj%dAffoXkp${~>_kBUl(rJnXw4 X(aU6Th)Eoz96t#E-O#O4>V6vl0ZVPm diff --git a/packages/client/batcher/midnight-level-db/289396.ldb b/packages/client/batcher/midnight-level-db/289396.ldb deleted file mode 100644 index 511f01efe812600e5b21e7d6caed570b67cc546e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmaKly-UMD7{)IagyQDn+~Ovd%jJ^06dY1*B9)4*^mFoZx$9SKLQRTha&Z#a=8oOfeb+cCUXhwvvWcudr%o*CJ-P0h6ZP*`DZatF{+6V!J=2U0%nh zAvJJROXg}Ylu38u_mUmlcLr#9rpK)|W6gl?2Vu%kQoH@ScK~?FsLN@PU(^ct0Avdk zb8_TIVpj6ST=F+aG_CkSaBOEI^scO?SJ|(d=kXiJ=ZebrufS^-XZCdc*u7eU ScS~#O`v2;Do?ouV&chETRBieI diff --git a/packages/client/batcher/midnight-level-db/CURRENT b/packages/client/batcher/midnight-level-db/CURRENT deleted file mode 100644 index 0304970..0000000 --- a/packages/client/batcher/midnight-level-db/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-289402 diff --git a/packages/client/batcher/midnight-level-db/LOCK b/packages/client/batcher/midnight-level-db/LOCK deleted file mode 100644 index e69de29..0000000 diff --git a/packages/client/batcher/midnight-level-db/LOG b/packages/client/batcher/midnight-level-db/LOG deleted file mode 100644 index 64f1d1e..0000000 --- a/packages/client/batcher/midnight-level-db/LOG +++ /dev/null @@ -1,3 +0,0 @@ -2026/03/20-14:52:15.938666 1f732b840 Recovering log #289401 -2026/03/20-14:52:15.939160 1f732b840 Delete type=0 #289401 -2026/03/20-14:52:15.939190 1f732b840 Delete type=3 #289400 diff --git a/packages/client/batcher/midnight-level-db/LOG.old b/packages/client/batcher/midnight-level-db/LOG.old deleted file mode 100644 index 84ed90a..0000000 --- a/packages/client/batcher/midnight-level-db/LOG.old +++ /dev/null @@ -1,3 +0,0 @@ -2026/03/20-14:52:15.936717 1f732b840 Recovering log #289399 -2026/03/20-14:52:15.937567 1f732b840 Delete type=0 #289399 -2026/03/20-14:52:15.937595 1f732b840 Delete type=3 #289398 diff --git a/packages/client/batcher/midnight-level-db/MANIFEST-289402 b/packages/client/batcher/midnight-level-db/MANIFEST-289402 deleted file mode 100644 index a49ce266843a2d8ad07c46272b77a4240fde72bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18517 zcmd6vYp7t?Rmbn1Nj!=+aXwW9#X%Gcw(j>M;1`1+eo-n`KM2Eqri0BSPKJgcq6ShQ zNmJ9bRcvcb5p5BPsivi9i(_a>V`#OFwbYulLDW)}6r@;#Y4>|Mg$%#y&xYvolxaL1*7`}y1YjknxxHEN7g#17-DhBw3=mX zL-t%kuUW~|tqFdlSfmZAjhJ1LqsCC8?#aa)o`2qt&5sk`d-`DhH68rh6KC!{{k8T+ zA9{0@xG^1k>1qgyx;ldLyc^TO(^o@Klf<9J8r#n{+0{pZtjv_2V4K%B<{a_=J!v1=M|4Va=q_9_1yLT|NJ+T zc<>{eAix4G)iuDUjvyt!k-ZuRTDfN2h^`tR;tV}4=Fv^)vbsJ3h?Z51@+crZeCY8@ zhkz4tV-o-J__m;=UYxQqmoBaC(N|+tGPd^ST(>&-)^o4b%G!<3U1>L@tTlQ)TG#q$ zQ2uFiP)@>)Nqpv!O+j&82AQNQLpE39aITf!NQYX2(cUIGhEY_hFo=?t#uPK8sy#T_ zedAh&_y-%Yg zXHSfPtjWJFZi!#BM#BMhg&6mjXfUf|$_%ueFHzj$Wpba7BQxJ@v@ z_70O#sz};{Qe&1wC2KQY7Bze^aGx4!3?RxyRm6`h#RtW6USH|-PG%?Z#rLg5x|dwt z@Bt|$4Mh`ERI4etQClljDrJ36u?I8wIS>_GO-1(rL5Wglq1Ge4li5kU^Wkfkk>)Mu zU{o+MrqXPxIu#dv$ZD38bheH?g1MvF?m%@;6$(JIr?h;M%Zr}r^CiBCPb z%xUF~^w8((ydrsXF51KvJRNv#W(JR1o3A-0`V7#8Bq!9qtH}EZx`-cLO^?0Ik{*v< zzl?OKgZ9W6RSV5P)g8vGd!rQ63pE1??sW_g7WzDFa=})5fmWcwv;;%^_#V@=|Kap2{sX-Cgv`G)=jvG%rEg)FWiZ4GW>K&B~#TEhF;>;s;kFy_eZZ zeDP%+n}LeLE{*5qCzYFqLySn0>p8mz#ROu+X>_nUgMG@3WL1r&cyZ$)t+)cG z_cA+)r@#Eoi}cWjHxDz05dw4G+d<{?a|tZDkp;9xE(A3)9gtSB4$ux!rWnppWr^Z# ztLd?q*-8Ay3gqb+MkRmY@*ds3BGjjvjcZ=h`q!kYuUeZ*D^PL>9k)YvNurPh!xnVrNR-Miv606k1^8p_mSa;dAJTcu)?Hp#l210n*kZ9Um6L!xPhF3-}OwzS&f ze^w)Hb}|d~edCJL#pz-o@IiHFsUV8ySwt|Kncw)@DSc+<8exDIB+#%%^J|pGFvwN$ zm360gG7B|*Y6*E}!0$k?%(dffC!e{Fl1hz$0_#v#+f+(J*s{*h+m6tSZ}VU)7E6T@ zuUUn>z088sD^7#CymO7S4i&{0=o|>=WpA}x+EA3+qB#wU_3ULubPEV6PFJ{LH9`E; zs?&R!oy6TM7ttbm;RF}VEAWD%XGsN1!J{5J1&9<9ct)4l9d83Bg7O1`m~PsrMSREV zMcm8mBtHD!WqLr6i-dwE#BL9Az~3su|4c=dbsmNZp(eW>xYt7&wlUMB(mM)gO16ks ztw(w%vy=Ffw=7v6Z+aLFK>L9VlWDF>G|A)wIZ6#i)Owek0F9qECh5x0pddPg(qMyD z{QJ7oJDHutf4pOf9=2HRee)(j>Z?RYFL*sGD2VFJgAJh3sVO#U2hG;bBE?%XcL9{3 zXhm92kDbg;;tOwHI_>5`wS{KPdF(wY(zId<(&(48BH49-c4XQ|YiSBO3rdIV9r!ck zi51taI=z?KNxb8+B{g-jRHGBIMR_fS@{A-+JuI-%W>DyJ4QNyioExfZm1N8yNdVMo zaK{igt>W}vX6amh;q-zqQBZ2m#Fi{*7Vsk(OhiDZNcP6{OrB1RLqL`2uzU?v9oPuP zCy>)o+_jq1dzqcYQ}-?#TR2b5%E*eSWeQ;|(RKy{j(Gqe4aER3iuUM#W>!o&SR;|L z$(8P9MDf77(>s}+L|Dc-WZ0)2+`tCir^&52hQy0jpqsV?7cvJ7G92vQ(>-=ILm`xj z*Wr`+`WJ9{AG4GA!?&(nLkK^kC`XwvjX@p1FY&p_8*OZWfKYWP;g*SuDJ?qAQ7c24 zK%TIn(uG>ThC7*M6nodwX|E&uFtL)K+%!aEW#nmCjx2yE}xuH?2o{C$p3I9 zVpi)qx{L<6jq%h&cnR8FF+d)dlh@(B`0n-e*vTxA_h&1SW&l1cEflqR#6eAA`i;m4 zX<{lAHCqgU^q@yzXu-lOB(EVwR_LH&-f6_0>yh5c>?A(=OG~FC9YE?R>77oi!cP$n zXtxqm3uafIO^r%y`=~m^#JC`4OVkk;N(rAlQO;=X&9Wt=J!fQPK)r+{7*-8A@8&)Ds9bvvdNDZWY zhAo6wS;YTf^b8#H$k?6x^URas*|PC$CskQ6895ijCX*G_!ndYpGMJBeq%x-2cp7(^L_ z7wR0Hcb%h+HsfSP*BC}p-(%zw7FLVcJ10xXo6J$?eZF3!c+YB__cA+)H~sojr(v$v zq}7sE96BnIG7>#GtQ0krtta>>N*J4`bT9nFC@!l~m7Di6Ge^qe`gNywGCPUieR_$~ zFeKV)BzxTwBMSwmW*o#83&cV)C8A^GCv&dPh(U^XBy2=U8iQPAs^aI@o!-gpB(8n< zls~$D@S>YmhaMel{A-hV_x;P4%RG!(Uqx7(H=md;%&QEFVY?YcN_5#;SR-J?VP1?w zDo1aoEJxNOHQ1%3_{Y_2w3pdQ{N^*)Ebjii(2!jBMuYMwK$2}%)Ime7;Pyc3Ez3Jx zo0yRH=RHr&`~sYVp=}r*|^TeE9QAE(}=F7-bb`GwCuktT9F7I}=PM!el2M zYGic-UW_aY6!2zHWHL%V6BU!hg>|QQGCPTXUXk$GuFi&Q5(`TYOw0_(WE%unM;LI0 z@F>j+9fvXqjMRyu&}|V0@~m5oD$2UkJDHut2Ue;XZ<;B!^qHBq358#*>e!yyDPj6l zE1M4xc9u72miUr~H7+z!ISPsltDopBA78!udzqcYZ?B~XA`8M4*h(W5A%oD~jyyQC zf>}~-XrX9C42wHE18fttOu1$Osk2AWlohXCkMvGv*{S{SQ;Unpl2B^kEQ+$7H7#na zg6x2=^NO6FWpb3EUTLHl5Y8JNNT2K*K;Ka6oE1N}>hxY_C-I)AmZhOEe1aYzt|d4Q z+e8!w$K_*kO>qe5Lrpb_f=KOv^=RlZ11VIwu7Sd=#O7Blm%?&<8V6RlMMpYtTXC&712g{adwOmSWffV1h?(|M(C-Ka0EvYaq z1p`%jHL6^mMOm9bxw#HPACa-3Z-X@r^Z>r2K=$&8i!681P|LYAdhzPjPH8W*@b`Bu zn~q9q&YKz)WQLD1_0{t#9D4&g1A*)>A`s5sc|@(${3%gLR2q&G8B#H|7Z0vSdMC4d z&+^G-dH@%68r~weu+zXs3{gi<878B%H8LF>p>=8;js9rfS<9LW%F+yLjd6H!!>ZGJ znFTx^T7n0ag`s7JVX(l3;Kt%5VgNdQuk<4WSun#>UtUTt|i-w;5k2{!b?_V*wWy1 z;xM7FTx;FSEd2eJrO#ZyN@|3`;Ec4Uekhn5UwLV^bmrqf6}gdEZ3O|NBm7V( zh*T73S94?RWR}_RGs}b?ks*SG-p!PnO&8jq$eOrx7O+E_OLVE|Zwz^;*C0wjR-Ogi z&}U%-y!+X8r*|?tiMQRk6b?uBhVC=+wUcI5#|O4@=~5HT0B8Zv;7dBhLYN7)l91CS zG+qmz3?WF<6pFjoo!-eTi${-~`ON9tkF-iJ{?*EHzf~0PK1}+r)pIBDtB-9t_A$Rk z6o0qz*gHLU5`XaL8;*V4?+wNM+m5~0b4l{Q+HmaSeo-i{JNzi)r6&1a&z;2IKfdAE z$Ngqdd~w^`-s`yxa6Yl+*vI@TP+YU^ZO5ISJBd3lpZWBupLmH#{-UbbJ%?}er0lX| z{OE?ab=0ZyB#Fsy^;n>Ihu1~yw+p+h0?j(NY@`hs{_sc!;#%;&m>$#Ko yohP;&`;4z-G7E12 diff --git a/packages/client/batcher/src/adapter-effectstreaml2.ts b/packages/client/batcher/src/adapter-effectstreaml2.ts index e4eed64..0902043 100644 --- a/packages/client/batcher/src/adapter-effectstreaml2.ts +++ b/packages/client/batcher/src/adapter-effectstreaml2.ts @@ -1,18 +1,49 @@ import { PaimaL2DefaultAdapter } from "@paimaexample/batcher"; import * as chains from "viem/chains"; +import { contractAddressesEvmMain } from "@go-fish/evm-contracts" +import { getEnv } from "@paimaexample/utils"; + + +const isMainnet = getEnv("EFFECTSTREAM_ENV") === "mainnet"; +const isPreprod = getEnv("EFFECTSTREAM_ENV") === "preprod"; +const isUndeployed = getEnv("EFFECTSTREAM_ENV") === "dev"; + +let chainNameId: keyof typeof contractAddressesEvmMain; +if (isMainnet) { + chainNameId = "chain42161" as keyof typeof contractAddressesEvmMain; +} else if (isPreprod) { + chainNameId = "chain421614" as keyof typeof contractAddressesEvmMain; +} else if (isUndeployed) { + chainNameId = "chain31337" as keyof typeof contractAddressesEvmMain; +} else { + throw new Error("Invalid effectstream environment"); +} + + +// PaimaL2Contract address from deployment +const paimaL2Address = contractAddressesEvmMain()[chainNameId]["effectstreaml2Module#effectstreaml2"] as `0x${string}`; +if (!paimaL2Address) { + throw new Error("EffectstreamL2 address not found"); +} +// Hardhat account #0 — the batcher's own funded key for submitting batched transactions +const batcherPrivateKey = (Deno.env.get("BATCHER_PRIVATE_KEY") || + (isUndeployed && "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")) as `0x${string}`; +if (!batcherPrivateKey) { + throw new Error("BATCHER_PRIVATE_KEY not set"); +} -// For NTP-only development, we use a minimal adapter configuration -// In production with EVM, this would connect to a real Effectstream L2 contract -const mockAddress = "0x0000000000000000000000000000000000000000" as `0x${string}`; -const mockPrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000001" as `0x${string}`; const paimaL2Fee = 0n; -const paimaSyncProtocolName = "mainNtp"; +const paimaSyncProtocolName = "mainEvmRPC"; + +const evmRpcUrl = Deno.env.get("EVM_RPC_URL") || (isUndeployed && "http://localhost:8545"); +if (!evmRpcUrl) { + throw new Error("EVM_RPC_URL is unset"); +} -// Using hardhat chain as placeholder for NTP mode export const effectstreaml2Adapter = new PaimaL2DefaultAdapter( - mockAddress, - mockPrivateKey, + paimaL2Address, + batcherPrivateKey, paimaL2Fee, paimaSyncProtocolName, - chains.hardhat, + { ...chains.hardhat, rpcUrls: { default: { http: [evmRpcUrl] } } } as typeof chains.hardhat, ); diff --git a/packages/client/batcher/src/adapter-midnight-balancing.ts b/packages/client/batcher/src/adapter-midnight-balancing.ts index c228c5c..cc673a6 100644 --- a/packages/client/batcher/src/adapter-midnight-balancing.ts +++ b/packages/client/batcher/src/adapter-midnight-balancing.ts @@ -1,318 +1,65 @@ -/** - * Go Fish Midnight Balancing Adapter - * - * Handles pre-proven unbound transactions that originate from the browser's WASM - * prover. The browser calls findDeployedContract + callTx to evaluate the circuit - * and generate a ZK proof locally, then intercepts at balanceTx and POSTs the - * proven UnboundTransaction here with txStage="unbound". - * - * This adapter only adds dust fees (balanceUnboundTransaction) and submits — - * it never calls the proof server. - * - * We deserialize using ledger-v8 — the same version used by WalletFacade internally - * (via @paimaexample/midnight-contracts@0.10.7 → wallet-sdk-facade@3.0.0 → ledger-v8). - * The browser WASM prover serializes in ledger-v8 format. - */ - -import type { - BlockchainAdapter, - BatchBuildingOptions, - BatchBuildingResult, - DefaultBatcherInput, -} from "@paimaexample/batcher"; import { - buildWalletFacade, - syncAndWaitForFunds, - type NetworkUrls as MidnightNetworkUrls, -} from "@paimaexample/midnight-contracts/wallet-info"; -import type { WalletResult } from "@paimaexample/midnight-contracts/types"; -import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; -import { Transaction } from "@midnight-ntwrk/ledger-v8"; -import { hexStringToUint8Array } from "@paimaexample/utils"; - -type BlockchainHash = string; -interface BlockchainTransactionReceipt { - hash: BlockchainHash; - blockNumber: bigint; - status: number; - [key: string]: unknown; -} - -const TTL_DURATION_MS = 5 * 60 * 1000; -const createTtl = (): Date => new Date(Date.now() + TTL_DURATION_MS); - -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - -const isTestnet = Deno.env.get("EFFECTSTREAM_ENV") === "testnet"; -const networkID = (isTestnet ? "testnet" : "undeployed") as "testnet" | "undeployed"; - -const indexer = - Deno.env.get("INDEXER_HTTP_URL") || "http://localhost:8088/api/v3/graphql"; -const indexerWS = - Deno.env.get("INDEXER_WS_URL") || "ws://localhost:8088/api/v3/graphql/ws"; -const node = Deno.env.get("NODE_URL") || "http://localhost:9944"; -// The balancing adapter never calls the proof server (it only balances -// already-proven unbound transactions), but the wallet facade's -// createWalletConfiguration requires a valid URL. Default to the standard -// local proof server address; override via PROOF_SERVER_URL if needed. -const proofServer = - Deno.env.get("PROOF_SERVER_URL") || "http://localhost:6300"; -const backendApiUrl = - Deno.env.get("BACKEND_API_URL") ?? "http://localhost:9996"; - -// --------------------------------------------------------------------------- -// Batch payload type -// --------------------------------------------------------------------------- - -interface BalancingBatchPayload { - tx: string; // hex-encoded serialized UnboundTransaction - txStage: string; // "unbound" - circuitId: string; - lobbyId?: string; // optional: lobby ID for setup notifications - playerId?: number; // optional: player ID for setup notifications + type BatcherConfig, + FileStorage, + MidnightAdapter, +} from "@paimaexample/batcher"; +import { readMidnightContract } from "@paimaexample/midnight-contracts/read-contract"; +import { midnightNetworkConfig } from "@paimaexample/midnight-contracts/midnight-env"; +import * as path from "@std/path"; +// import { MidnightBalancingAdapter } from "./adapters/midnight-balancing-adapter.ts"; +import { MidnightBalancingAdapter } from "@paimaexample/batcher"; +import process from "node:process"; +const batchIntervalMs = 1000; +const port = Number(Deno.env.get("BATCHER_PORT") ?? "3334"); +// Try to load contract data (needed for the standard midnight adapter). +// May fail if the contract hasn't been deployed yet (no address JSON file). +let midnightContractData: ReturnType | null = null; +try { + midnightContractData = readMidnightContract( + "go-fish-contract", + { + baseDir: path.resolve(import.meta.dirname!, "..", "..", "..", "shared", "contracts", "midnight"), + networkId: midnightNetworkConfig.id, + }, + ); +} catch (e) { + console.warn( + `⚠️ Could not load contract address file: ${(e as Error).message}`, + ); + console.warn( + " The standard midnight adapter will be disabled. " + + "The midnight_balancing adapter (for delegated tx) will still work.", + ); + throw e; } -// --------------------------------------------------------------------------- -// Adapter -// --------------------------------------------------------------------------- - -export class GoFishBalancingAdapter - implements BlockchainAdapter { - readonly maxBatchSize = 1; - - private walletResult: WalletResult | null = null; - private isInitialized = false; - private initializationPromise: Promise | null = null; - private walletAddress: string | null = null; - private readonly walletSeed: string; - - constructor() { - this.walletSeed = - Deno.env.get("MIDNIGHT_WALLET_SEED") || - "0000000000000000000000000000000000000000000000000000000000000001"; - this.initializationPromise = this.initialize(); - } - - private async initialize(): Promise { - // Retry loop: Midnight indexer/node may not be ready when the batcher starts. - // Keep retrying every 5s until the wallet is ready. - while (!this.isInitialized) { - try { - setNetworkId(networkID as any); - const networkUrls: MidnightNetworkUrls = { - indexer, - indexerWS, - node, - proofServer, - }; - this.walletResult = await buildWalletFacade(networkUrls, this.walletSeed, networkID); - this.walletAddress = this.walletResult.dustAddress; - console.log(`⚡ [balancing] wallet ready at ${this.walletAddress}`); - this.isInitialized = true; - } catch (err) { - console.error("❌ [balancing] wallet init failed, retrying in 5s:", err); - await new Promise(resolve => setTimeout(resolve, 5000)); - } - } - } - - private async ensureFunds(): Promise { - if (!this.walletResult) return; - // Always use syncAndWaitForFunds so the wallet syncs its UTXOs after each - // transaction. Without this, the wallet may attempt to spend already-spent - // DUST notes from the previous transaction, causing "insufficient funds" when - // two players submit back-to-back (e.g. applyMask for player 1 then player 2). - try { - const balances = await syncAndWaitForFunds(this.walletResult.wallet, { timeoutMs: 120_000 }); - const hasDust = balances.dustBalance > 0n; - console.log(`⚡ [balancing] dust balance after sync: ${balances.dustBalance}`); - if (!hasDust) { - console.warn("⚠️ [balancing] no DUST after 120s sync — proceeding anyway"); - } - // hasFunds field removed — syncAndWaitForFunds is always called - } catch (err) { - console.warn("⚠️ [balancing] ensureFunds failed:", err); - } - } - - // Parse `{ tx, txStage, circuitId }` from the batcher input - buildBatchData( - inputs: DefaultBatcherInput[], - _options?: BatchBuildingOptions, - ): BatchBuildingResult | null { - if (inputs.length === 0) return null; - - const input = inputs[0]; - try { - const parsed = JSON.parse(input.input); - if (!parsed.tx || !parsed.txStage) { - console.warn("[balancing] buildBatchData: missing tx/txStage in input"); - return { selectedInputs: [input], data: null }; - } - return { - selectedInputs: [input], - data: { - tx: parsed.tx, - txStage: parsed.txStage, - circuitId: parsed.circuitId ?? "unknown", - lobbyId: parsed.lobbyId, - playerId: parsed.playerId, - }, - }; - } catch (err) { - console.error("[balancing] buildBatchData parse error:", err); - return { selectedInputs: [input], data: null }; - } - } - - async submitBatch(data: BalancingBatchPayload | null, _fee?: string | bigint): Promise { - if (this.initializationPromise) { - await this.initializationPromise; - } - - if (!this.isInitialized || !this.walletResult) { - throw new Error("[balancing] adapter not initialized"); - } - - if (!data || !data.tx) { - throw new Error("[balancing] submitBatch called with empty data"); - } - - await this.ensureFunds(); - - const { wallet, walletZswapSecretKeys, walletDustSecretKey, unshieldedKeystore } = - this.walletResult; - - console.log(`⚡ [balancing] balancing circuit=${data.circuitId} txStage=${data.txStage}`); - - // Deserialize using ledger-v8 (same WASM instance as WalletFacade) to avoid assertClass mismatch. - // The browser WASM prover serializes in v8-format; ledger-v8 Transaction.deserialize consumes it directly. - const txBytes = hexStringToUint8Array(data.tx); - const unboundTx = Transaction.deserialize( - "signature", - "proof", - "pre-binding", - txBytes, - ); - - // Balance: add dust fee input/output. - // Do NOT restrict tokenKindsToBalance — let the wallet facade balance all token kinds - // correctly. Restricting to ["dust"] was causing the node to silently reject the tx - // because the unbound transaction's fee segment was not being properly computed. - const recipe = await wallet.balanceUnboundTransaction(unboundTx as any, { - shieldedSecretKeys: walletZswapSecretKeys, - dustSecretKey: walletDustSecretKey, - }, { ttl: createTtl() } as any); - console.log(`⚡ [balancing] recipe type=${(recipe as any).type} hasBalancingTx=${!!(recipe as any).balancingTransaction}`); - - // Use signRecipe which handles all recipe types (UNBOUND_TRANSACTION, UNPROVEN_TRANSACTION, - // FINALIZED_TRANSACTION) correctly. Previously we manually extracted balancingTransaction - // and called signUnprovenTransaction, but signRecipe is the canonical approach used by - // the official MidnightBalancingAdapter and handles edge cases we were missing. - const signSegment = (payload: Uint8Array) => unshieldedKeystore.signData(payload); - const signedRecipe = await wallet.signRecipe(recipe as any, signSegment); - console.log(`⚡ [balancing] signRecipe done, recipe type=${(signedRecipe as any).type}`); - - const finalizedTx = await wallet.finalizeRecipe(signedRecipe as any); - console.log(`⚡ [balancing] finalizeRecipe done`); - - // Log the actual transaction hash (for indexer lookup) vs the submission ID - let derivedTxHash: string | null = null; - try { - derivedTxHash = (finalizedTx as any).transactionHash?.()?.toString?.() ?? null; - if (derivedTxHash) console.log(`⚡ [balancing] derived txHash=${derivedTxHash}`); - } catch { /* ignore */ } - - // submitTransaction can hang indefinitely if the Midnight node rejects the tx - // (e.g. duplicate nonce, TTL conflict from back-to-back transactions). Apply a - // 90s timeout so the batcher doesn't get stuck and can retry on the next poll. - const submitTimeoutMs = 90_000; - let submitTimerId: ReturnType | undefined; - const submittedTxId = await Promise.race([ - wallet.submitTransaction(finalizedTx), - new Promise((_, reject) => { - submitTimerId = setTimeout( - () => reject(new Error(`[balancing] submitTransaction timed out after ${submitTimeoutMs}ms`)), - submitTimeoutMs, - ); - }), - ]).finally(() => clearTimeout(submitTimerId)); - const txHash = derivedTxHash ?? submittedTxId?.toString(); - console.log(`✅ [balancing] circuit=${data.circuitId} submittedId=${submittedTxId} txHash=${txHash}`); - - // Notify backend of setup completion so the other player can proceed immediately - // (don't wait for on-chain confirmation — submission is sufficient for coordination) - if (data.lobbyId && data.playerId && (data.circuitId === "applyMask" || data.circuitId === "dealCards")) { - const action = data.circuitId === "applyMask" ? "mask_applied" : "dealt_complete"; - fetch(`${backendApiUrl}/api/midnight/notify_setup`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ lobby_id: data.lobbyId, player_id: data.playerId, action }), - }).then(r => { - if (r.ok) console.log(`✅ [balancing] notify_setup: ${action} lobby=${data.lobbyId} player=${data.playerId}`); - else console.warn(`⚠️ [balancing] notify_setup failed: ${r.status}`); - }).catch(err => console.warn(`⚠️ [balancing] notify_setup error:`, err)); - } - - return txHash; - } - - async waitForTransactionReceipt( - hash: BlockchainHash, - _timeout: number = 300_000, - ): Promise { - // Midnight circuit proving/confirmation takes 60–300s, well beyond the batcher's - // internal 60s processBatchForTarget timeout. Returning immediately here removes the - // input from the batcher queue as soon as submitTransaction succeeds, preventing - // duplicate submission on the next polling cycle (Custom error: 193). - // - // The transaction was already submitted to the network — it will confirm independently. - // We use the current block height as a best-effort blockNumber (0 is also acceptable). - const currentBlock = await this.getBlockNumber().catch(() => 0n); - console.log(`⚡ [balancing] waitForTransactionReceipt: tx=${hash.slice(0, 16)}… accepted (no-wait), block≈${currentBlock}`); - return { hash, blockNumber: currentBlock, status: 1 }; - } - - getAccountAddress(): string { - return this.walletAddress ?? "balancing-adapter-not-ready"; - } - - getChainName(): string { - return `Midnight (${networkID})`; - } - - getSyncProtocolName(): string { - return "parallelMidnight"; - } - - isReady(): boolean { - return this.isInitialized && this.walletResult !== null; - } - - estimateBatchFee(_data: BalancingBatchPayload | null): bigint { - return 0n; - } - - async getBlockNumber(): Promise { - try { - const res = await fetch(indexer, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ query: `query { block { height } }` }), - }); - const json = await res.json(); - return BigInt(json?.data?.block?.height ?? 0); - } catch { - return 0n; - } - } - - verifySignature(_input: DefaultBatcherInput): boolean { - // Midnight transactions are self-authenticating (ZK proofs); no signature needed - return true; +const zkConfigPath = midnightContractData?.zkConfigPath ?? + path.resolve( + import.meta.dirname!, + "..", "..", "..", "shared", "contracts", "midnight", "go-fish-contract", "src", "managed" + ); + +// The balancing adapter handles delegated transactions from BatcherClient. +let seeds = process.env.MIDNIGHT_WALLET_SEEDS?.split(','); +if (midnightNetworkConfig.id === 'undeployed') { + seeds = [midnightNetworkConfig.walletSeed!]; +} else { + if (!seeds || seeds.length === 0) { + throw new Error('MIDNIGHT_WALLET_SEEDS is not set'); } } -export const goFishBalancingAdapter = new GoFishBalancingAdapter(); +export const midnightBalancingAdapter = new MidnightBalancingAdapter( + seeds, + { + syncProtocolName: 'parallelMidnight', + indexer: midnightNetworkConfig.indexer, + indexerWS: midnightNetworkConfig.indexerWS, + node: midnightNetworkConfig.node, + proofServer: midnightNetworkConfig.proofServer, + walletNetworkId: midnightNetworkConfig.id, + walletFundingTimeoutSeconds: 60 * 20, + addShieldedPadding: false, // true, + }, + ); + diff --git a/packages/client/batcher/src/adapter-midnight.ts b/packages/client/batcher/src/adapter-midnight.ts deleted file mode 100644 index 6122bba..0000000 --- a/packages/client/batcher/src/adapter-midnight.ts +++ /dev/null @@ -1,911 +0,0 @@ -/** - * Midnight Adapter for Go Fish Batcher - * - * This adapter handles Midnight blockchain transactions for the Go Fish game. - * It uses the MidnightAdapter from @paimaexample/batcher to connect to the - * Midnight network and submit transactions. - * - * IMPORTANT: This adapter supports client-side secrets for proper mental poker. - * When a circuit call includes playerSecret and shuffleSeed fields, these are - * extracted and set as dynamic secrets before proof generation. This allows - * the batcher to generate proofs using the client's actual secrets without - * permanently storing them. - */ - -import { type BatchBuildingOptions, type BatchBuildingResult, type DefaultBatcherInput, MidnightAdapter, type MidnightBatchPayload } from "@paimaexample/batcher"; -import { readMidnightContract } from "@paimaexample/midnight-contracts/read-contract"; -import * as goFishContractInfo from "@go-fish/midnight-contract"; -import * as goFishContract from "@go-fish/midnight-contract/contract"; -import { setPlayerSecrets, clearPlayerSecrets } from "@go-fish/midnight-contract/witnesses"; -import { CryptoManager } from "@paimaexample/crypto"; -import { storage } from "./config.ts"; -import { createUnprovenCallTxFromInitialStates, getPublicStates } from "@midnight-ntwrk/midnight-js-contracts"; -import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; -import { parseCoinPublicKeyToHex } from "@midnight-ntwrk/midnight-js-utils"; -import { getNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; -import { CompiledContract } from "@midnight-ntwrk/compact-js"; - -// Network configuration - use environment variables or defaults -const isTestnet = Deno.env.get("EFFECTSTREAM_ENV") === "testnet"; - -// Read contract info from the deployed contract file -const { - contractInfo: contractInfo0, - contractAddress: contractAddress0, - zkConfigPath: zkConfigPath0, -} = readMidnightContract("go-fish-contract", "go-fish-contract.undeployed.json"); - -// Genesis mint wallet seed for the batcher -const GENESIS_MINT_WALLET_SEED = - "0000000000000000000000000000000000000000000000000000000000000001"; - -// Midnight infrastructure URLs -const indexer = Deno.env.get("INDEXER_HTTP_URL") || "http://localhost:8088/api/v3/graphql"; -const indexerWS = Deno.env.get("INDEXER_WS_URL") || "ws://localhost:8088/api/v3/graphql/ws"; -const node = Deno.env.get("NODE_URL") || "http://localhost:9944"; -const proofServer = Deno.env.get("PROOF_SERVER_URL") || "http://localhost:6300"; -const networkID = isTestnet ? "testnet" : "undeployed"; -const syncProtocolName = "parallelMidnight"; - -// Midnight adapter configuration -// IMPORTANT: privateStateId and privateStateStoreName MUST match what was used during deployment -// See packages/shared/contracts/midnight/deploy.ts for the deployment config -const midnightAdapterConfig0 = { - indexer, - indexerWS, - node, - proofServer, - zkConfigPath: zkConfigPath0, - privateStateStoreName: "private-state", // Must match deploy.ts - privateStateId: "privateState", // Must match deploy.ts - walletNetworkId: networkID, - contractJoinTimeoutSeconds: 300, - walletFundingTimeoutSeconds: 300, -}; - -// Skip signature verification in development mode -const SKIP_SIGNATURE_VERIFICATION = Deno.env.get("SKIP_SIGNATURE_VERIFICATION") === "true" || - Deno.env.get("NODE_ENV") !== "production"; - -/** - * Circuit call with optional client-side secrets - */ -interface CircuitCallWithSecrets { - circuit: string; - args: unknown[]; - playerSecret?: string; // Hex-encoded bigint - shuffleSeed?: string; // Hex-encoded 32 bytes - opponentSecret?: string; // Hex-encoded bigint (needed for goFish) - opponentShuffleSeed?: string; // Hex-encoded 32 bytes (needed for goFish) -} - -/** - * Convert hex string to Uint8Array - */ -function hexToBytes(hex: string): Uint8Array { - const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex; - const bytes = new Uint8Array(cleanHex.length / 2); - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16); - } - return bytes; -} - -/** - * Extended MidnightAdapter with EVM signature verification and client-side secrets - * - * This allows transactions to be signed by EVM wallets and verified - * before being submitted to the Midnight network. - * - * Additionally, it supports client-side secrets for mental poker: - * - Frontend includes playerSecret and shuffleSeed in the circuit call - * - Adapter extracts these and sets them as dynamic witnesses - * - Proofs are generated using the client's actual secrets - * - Secrets are cleared after proof generation - * - * In development mode (SKIP_SIGNATURE_VERIFICATION=true or NODE_ENV != production), - * signature verification is skipped for easier testing. - */ -export class GoFishMidnightAdapter extends MidnightAdapter { - /** - * Stash the raw batcher input from the most recent buildBatchData call so - * submitBatch can read playerSecret/shuffleSeed/opponentSecret from it. - * MidnightBatchBuilderLogic strips these custom fields from the payload — they - * only survive in the original DefaultBatcherInput.input JSON string. - */ - private _pendingRawInput: CircuitCallWithSecrets | null = null; - - /** - * Fetch a stored player secret from the backend node. - * Called when the circuit payload doesn't include the opponent's secret - * (which happens for game-phase circuits like askForCard when the asking player's - * browser doesn't have the opponent's secret). - * - * Returns null if the backend doesn't have the secret (e.g. node just restarted). - */ - private async fetchSecretFromBackend( - lobbyId: string, - playerId: 1 | 2, - ): Promise<{ secret: string; shuffleSeed: string | null } | null> { - const backendUrl = Deno.env.get("BACKEND_URL") || "http://localhost:9996"; - const url = `${backendUrl}/api/midnight/player_secret?lobby_id=${encodeURIComponent(lobbyId)}&player_id=${playerId}`; - - // Retry up to 3 times with a short delay: the backend may still be processing - // notify_setup (which persists secrets) at the time this is called. - const MAX_FETCH_ATTEMPTS = 3; - const FETCH_RETRY_MS = 2000; - - for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) { - try { - const response = await fetch(url); - if (response.ok) { - const data = await response.json() as { secret: string; shuffleSeed: string | null }; - console.log(`[GoFishMidnightAdapter] Fetched opponent secret from backend for ${lobbyId} player ${playerId}${attempt > 1 ? ` (attempt ${attempt})` : ""}`); - return data; - } - console.warn(`[GoFishMidnightAdapter] Backend returned ${response.status} for player_secret lookup (lobby=${lobbyId} player=${playerId}, attempt ${attempt}/${MAX_FETCH_ATTEMPTS})`); - if (response.status !== 404) { - // Non-404 error (e.g. 500) — don't retry - return null; - } - if (attempt < MAX_FETCH_ATTEMPTS) { - console.log(`[GoFishMidnightAdapter] Retrying player_secret lookup in ${FETCH_RETRY_MS}ms...`); - await new Promise(resolve => setTimeout(resolve, FETCH_RETRY_MS)); - } - } catch (err) { - console.warn(`[GoFishMidnightAdapter] Could not fetch opponent secret from backend (attempt ${attempt}/${MAX_FETCH_ATTEMPTS}):`, err); - if (attempt < MAX_FETCH_ATTEMPTS) { - await new Promise(resolve => setTimeout(resolve, FETCH_RETRY_MS)); - } - } - } - console.error(`[GoFishMidnightAdapter] Failed to fetch player ${playerId} secret for lobby ${lobbyId} after ${MAX_FETCH_ATTEMPTS} attempts — circuit will use fallback key and likely fail`); - return null; - } - - /** - * Override submitBatch to extract and set client-side secrets before circuit execution. - * The Midnight SDK executes the circuit locally (in WASM) to build the unproven transaction. - * Circuits like applyMask and dealCards require the player's secret key and shuffle seed - * as witness values. Without setting these before execution, the WASM runtime crashes - * with "RuntimeError: unreachable" because the default random keys don't match - * the keys used during mask application. - */ - override async submitBatch( - data: MidnightBatchPayload | null, - fee?: string | bigint, - ): Promise { - const circuit = data?.payloads?.[0]?.circuit ?? "unknown"; - let secretInfo: { gameId: string; playerId: number } | null = null; - - // Extract and set client-side secrets before circuit execution. - // IMPORTANT: MidnightBatchBuilderLogic strips custom fields (playerSecret, - // shuffleSeed, etc.) from MidnightBatchPayload.payloads — read them from - // _pendingRawInput which was stashed in buildBatchData from the raw input JSON. - const rawInput = this._pendingRawInput; - this._pendingRawInput = null; // consume it - if (rawInput?.playerSecret && rawInput?.shuffleSeed) { - const args = data?.payloads?.[0]?.args; - if (Array.isArray(args) && args.length >= 2) { - const gameId = args[0] as string; - const playerId = Number(args[1]) as 1 | 2; - const secret = BigInt("0x" + rawInput.playerSecret); - const shuffleSeed = hexToBytes(rawInput.shuffleSeed); - - setPlayerSecrets(gameId, playerId, secret, shuffleSeed); - secretInfo = { gameId, playerId }; - console.log(`[GoFishMidnightAdapter] Set client secrets for ${circuit}: gameIdHex=${gameId} player=${playerId} secret=${secret}`); - - const opponentId = (playerId === 1 ? 2 : 1) as 1 | 2; - - if (rawInput.opponentSecret && rawInput.opponentShuffleSeed) { - // Opponent secret provided directly in the payload — use it. - const opponentSecret = BigInt("0x" + rawInput.opponentSecret); - const opponentShuffleSeed = hexToBytes(rawInput.opponentShuffleSeed); - setPlayerSecrets(gameId, opponentId, opponentSecret, opponentShuffleSeed); - console.log(`[GoFishMidnightAdapter] Set opponent secrets for ${circuit}: game ${gameId}, player ${opponentId}`); - } else { - // Opponent secret not in payload (frontend doesn't have it — it's theirs to keep). - // Fall back to the backend node which stores both players' secrets after setup replay. - // This is needed for game-phase circuits (askForCard, respondToAsk, goFish, - // afterGoFish) that call player_secret_key for BOTH players unconditionally. - const lobbyId = this.hexToLobbyId(gameId); - console.log(`[GoFishMidnightAdapter] ${circuit}: no opponent secret in payload, fetching from backend for player ${opponentId} (lobby=${lobbyId})`); - const backendOpponent = await this.fetchSecretFromBackend(lobbyId, opponentId); - if (backendOpponent) { - const opponentSecret = BigInt("0x" + backendOpponent.secret); - setPlayerSecrets(gameId, opponentId, opponentSecret, - backendOpponent.shuffleSeed ? hexToBytes(backendOpponent.shuffleSeed) : new Uint8Array(32)); - console.log(`[GoFishMidnightAdapter] Set backend-fetched opponent secret for ${circuit}: game ${gameId}, player ${opponentId}`); - // Cache on rawInput so retries re-use it without another backend round-trip - rawInput.opponentSecret = backendOpponent.secret; - if (backendOpponent.shuffleSeed) rawInput.opponentShuffleSeed = backendOpponent.shuffleSeed; - } else { - console.warn(`[GoFishMidnightAdapter] No opponent secret available for ${circuit} — proof may fail`); - } - } - } - } - - // Circuits that carry a `now` timestamp arg and their arg position. - // blockTimeLt(now + 6) requires now to be within 6s of the block time. - // The batcher stores the original payload, so `now` can be stale by the - // time we execute — refresh it before every attempt (including retries). - // askForCard: args[3] → [gameId, playerId, rank, now] - // respondToAsk: args[2] → [gameId, playerId, now] - // goFish: args[2] → [gameId, playerId, now] - // afterGoFish: args[3] → [gameId, playerId, drewRequestedCard, now] - const NOW_ARG_INDEX: Record = { - askForCard: 3, - respondToAsk: 2, - goFish: 2, - afterGoFish: 3, - }; - - const refreshNowArg = () => { - const nowArgIdx = NOW_ARG_INDEX[circuit]; - if (nowArgIdx !== undefined && data?.payloads?.[0] && Array.isArray(data.payloads[0].args)) { - const freshNow = Math.floor(Date.now() / 1000); - data.payloads[0].args[nowArgIdx] = freshNow; - console.log(`[GoFishMidnightAdapter] Refreshed now=${freshNow} at args[${nowArgIdx}] for ${circuit}`); - } - }; - - refreshNowArg(); - - // Fix for MalformedFeeCalculation (Midnight node Custom error 168): - // - // Root cause: after the previous circuit (e.g. respondToAsk) spends the dust UTXO, - // the wallet's RxJS BehaviorSubject caches the pre-spend balance. waitForDustFunds() - // and syncAndWaitForFunds() both return the stale non-zero value immediately because - // the observable's isCompleteWithin(0n) filter stays true — the wallet doesn't flip - // itself to "syncing" just because a coin was spent. balanceFinalizedTransaction() - // then tries to use the already-spent UTXO → node rejects with error 168. - // - // Fix: wait for the indexer to advance by at least one block from the block that - // contained the previous circuit's transaction. Once the block is indexed, the - // wallet re-syncs and the spend is reflected in the observable state. Then force - // ensureFunds() to run the full cold path so it stores the fresh dust balance. - try { - const blockBefore = await this.getBlockNumber(); - console.log(`[GoFishMidnightAdapter] ${circuit}: pre-dust-fix block=${blockBefore}, waiting for indexer to advance...`); - // Poll until block advances or 90s timeout - const dustFixStart = Date.now(); - let advanced = false; - while (Date.now() - dustFixStart < 90_000) { - await new Promise(resolve => setTimeout(resolve, 3_000)); - try { - const blockNow = await this.getBlockNumber(); - if (blockNow > blockBefore) { - console.log(`[GoFishMidnightAdapter] ${circuit}: indexer advanced to block ${blockNow}, proceeding`); - advanced = true; - break; - } - } catch { /* ignore polling errors */ } - } - if (!advanced) { - console.warn(`[GoFishMidnightAdapter] ${circuit}: block did not advance within 90s — proceeding anyway`); - } - } catch (err) { - // getBlockNumber not available or failed — fall back to a fixed delay of one block (~6s) - console.warn(`[GoFishMidnightAdapter] ${circuit}: block polling unavailable, sleeping 8s for dust UTXO refresh`); - await new Promise(resolve => setTimeout(resolve, 8_000)); - } - // Force ensureFunds() inside super.submitBatch() to run the full cold path - // (syncAndWaitForFunds) so it reads the now-fresh dust balance from the wallet. - (this as any).hasFunds = false; - - // Retry logic for WASM "unreachable" errors, which typically mean the - // Midnight indexer hasn't synced the latest state yet (e.g., the circuit - // asserts both masks are applied but the indexer is a few blocks behind). - // The batcher's callTx fetches state from the indexer, so stale state - // causes Compact asserts to fail as WASM unreachable traps. - const MAX_RETRIES = 5; - const RETRY_DELAY_MS = 15000; // 15 seconds between retries - - const startTime = Date.now(); - try { - // Log indexer block at start for diagnostics - try { - const startBlock = await this.getBlockNumber(); - console.log(`[GoFishMidnightAdapter] ${circuit} starting, indexer at block ${startBlock}`); - } catch { /* ignore */ } - - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - try { - if (secretInfo) { - console.log(`[GoFishMidnightAdapter] ${circuit} attempt ${attempt}: calling super.submitBatch with secretInfo game=${secretInfo.gameId} player=${secretInfo.playerId}`); - } - const result = await super.submitBatch(data, fee); - if (attempt > 1) { - const elapsed = Math.round((Date.now() - startTime) / 1000); - try { - const endBlock = await this.getBlockNumber(); - console.log(`[GoFishMidnightAdapter] ${circuit} succeeded on attempt ${attempt}/${MAX_RETRIES} (${elapsed}s total, indexer at block ${endBlock})`); - } catch { - console.log(`[GoFishMidnightAdapter] ${circuit} succeeded on attempt ${attempt}/${MAX_RETRIES} (${elapsed}s total)`); - } - } - - // Notify the backend about successful setup circuit calls. - // This is more reliable than relying solely on the frontend notification, - // since the batcher is the one that actually confirmed the transaction. - await this.notifyBackendSetup(circuit, data, rawInput); - - return result; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - - // FailFallible = transaction was submitted, proved, and mined on-chain, but the - // contract's runtime assertion failed (e.g. wrong phase, card not in hand). - // This is a PERMANENT failure — retrying with the same payload will always fail. - // Purge the entire pending queue so the stale input doesn't re-fire on future polls, - // which would advance the game phase unexpectedly and break subsequent moves. - const isFailFallible = (error as any)?.name === "CallTxFailedError" || - (error as any)?.finalizedTxData?.status === "FailFallible" || - errMsg.includes("FailFallible"); - if (isFailFallible) { - console.error(`[GoFishMidnightAdapter] ${circuit} FailFallible — purging batcher queue to prevent stale replay`); - try { - await storage.clearAllInputs(); - console.warn(`[GoFishMidnightAdapter] Batcher queue cleared after FailFallible`); - } catch (clearErr) { - console.error(`[GoFishMidnightAdapter] Failed to clear queue:`, clearErr); - } - throw error; // Don't retry - } - - const isUnreachable = error instanceof Error && - (errMsg.includes("unreachable") || errMsg.includes("RuntimeError")); - // Proof server 500 means the circuit's on-chain assertions failed during /check, - // which happens when the indexer hasn't yet confirmed the preceding transaction - // (e.g. both applyMask transactions must be on-chain before dealCards /check passes). - const isProofServer500 = errMsg.includes("code=\"500\"") || errMsg.includes("code=500"); - // Custom error 168 = transaction TTL expired on the Midnight Substrate node. - // The `now` timestamp in the circuit args (blockTimeLt(now + 6)) was stale by the - // time the signed tx reached the node. refreshNowArg() already updates `now` before - // each retry, so re-submitting with a fresh timestamp will succeed. - const isTtlExpired = errMsg.includes("Custom error: 168") || - errMsg.includes("SubmissionError") || - errMsg.includes("Transaction submission error") || - errMsg.includes("Transaction submission failed"); - // "failed assert" from createUnprovenCallTxFromInitialStates means the local WASM simulation - // read stale indexer state. For setup-dependent circuits (dealCards, askForCard), the - // preceding transaction (e.g. opponent's dealCards) may not yet be indexed. - // - // HOWEVER: some assertions are permanent — the circuit has already been run on-chain - // for this player/game, so retrying will always fail. These must be treated like - // FailFallible: purge the queue immediately so the duplicate doesn't block subsequent - // circuits (e.g. a stale second applyMask blocking dealCards for 75+ seconds). - const isPermanentAssert = errMsg.includes("already applied") || - errMsg.includes("already dealt") || - errMsg.includes("already initialized") || - errMsg.includes("already started") || - // Duplicate game-phase circuit: a previous transaction already advanced the phase. - // Retrying will never succeed — purge the queue immediately. - errMsg.includes("Not waiting for a response") || - errMsg.includes("Not your turn") || - errMsg.includes("Can only ask for cards at turn start") || - errMsg.includes("Cannot ask for a rank you don't have"); - const isStaleAssert = errMsg.includes("failed assert") && !isPermanentAssert; - const isRetryable = isUnreachable || isProofServer500 || isStaleAssert || isTtlExpired; - - if (isPermanentAssert) { - console.error(`[GoFishMidnightAdapter] ${circuit} permanent assert ("${errMsg.slice(0, 120)}") — purging batcher queue to prevent stale replay`); - try { - await storage.clearAllInputs(); - console.warn(`[GoFishMidnightAdapter] Batcher queue cleared after permanent assert`); - } catch (clearErr) { - console.error(`[GoFishMidnightAdapter] Failed to clear queue:`, clearErr); - } - throw error; // Don't retry - } - - if (isRetryable && attempt < MAX_RETRIES) { - const elapsed = Math.round((Date.now() - startTime) / 1000); - const reason = isTtlExpired ? "TTL expired (Custom error 168 — now timestamp stale, refreshing)" : - isProofServer500 ? "proof server 500 (on-chain state not yet confirmed)" : - isStaleAssert ? "failed assert (indexer state likely stale — opponent tx not yet indexed)" : - "WASM unreachable"; - try { - const blockNum = await this.getBlockNumber(); - console.warn( - `[GoFishMidnightAdapter] ${circuit} failed with ${reason} (attempt ${attempt}/${MAX_RETRIES}, ${elapsed}s elapsed, indexer at block ${blockNum}), ` + - `retrying in ${RETRY_DELAY_MS / 1000}s...` - ); - } catch { - console.warn( - `[GoFishMidnightAdapter] ${circuit} failed with ${reason} (attempt ${attempt}/${MAX_RETRIES}, ${elapsed}s elapsed), ` + - `retrying in ${RETRY_DELAY_MS / 1000}s (indexer may be behind)...` - ); - } - // Re-set secrets before retry since they were consumed by the failed attempt. - // Read from rawInput (stashed in buildBatchData) — payload fields were stripped. - if (secretInfo && rawInput?.playerSecret && rawInput?.shuffleSeed) { - const secret = BigInt("0x" + rawInput.playerSecret); - const shuffleSeed = hexToBytes(rawInput.shuffleSeed); - setPlayerSecrets(secretInfo.gameId, secretInfo.playerId as 1 | 2, secret, shuffleSeed); - // Re-set opponent secrets too - if (rawInput.opponentSecret && rawInput.opponentShuffleSeed) { - const opponentId = (secretInfo.playerId === 1 ? 2 : 1) as 1 | 2; - const opponentSecret = BigInt("0x" + rawInput.opponentSecret); - const opponentShuffleSeed = hexToBytes(rawInput.opponentShuffleSeed); - setPlayerSecrets(secretInfo.gameId, opponentId, opponentSecret, opponentShuffleSeed); - } - } - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); - // After the 15s sleep, the block containing the previous attempt has been indexed. - // Force ensureFunds() to run the full cold path so it reads fresh dust balance. - (this as any).hasFunds = false; - // Refresh `now` after the sleep so it's fresh for the next attempt - refreshNowArg(); - continue; - } - - throw error; - } - } - // Should not reach here - throw new Error("Unexpected: retry loop exited without returning or throwing"); - } finally { - // Always clear secrets after all attempts - if (secretInfo) { - this.clearSecrets(secretInfo.gameId, secretInfo.playerId); - // Also clear opponent secrets if they were set (read from rawInput, not stripped payload) - if (rawInput?.opponentSecret) { - const opponentId = (secretInfo.playerId === 1 ? 2 : 1) as 1 | 2; - this.clearSecrets(secretInfo.gameId, opponentId); - } - } - } - } - - /** - * Override buildBatchData to always select exactly ONE input per batch, - * and stash the raw parsed input so submitBatch can read custom fields - * (playerSecret, shuffleSeed, opponentSecret, opponentShuffleSeed) that - * MidnightBatchBuilderLogic strips from MidnightBatchPayload.payloads. - */ - override buildBatchData( - inputs: DefaultBatcherInput[], - options?: BatchBuildingOptions, - ): BatchBuildingResult | null { - if (inputs.length === 0) return null; - // Stash the raw parsed CircuitCall from the first input so submitBatch - // can read playerSecret/shuffleSeed even after the builder strips them. - try { - this._pendingRawInput = JSON.parse(inputs[0].input) as CircuitCallWithSecrets; - } catch { - this._pendingRawInput = null; - } - // Delegate to base implementation with only the first input - return super.buildBatchData([inputs[0]], options); - } - - override async verifySignature(input: DefaultBatcherInput): Promise { - // Skip signature verification in development mode - if (SKIP_SIGNATURE_VERIFICATION) { - console.log("[GoFishMidnightAdapter] Skipping signature verification (dev mode)"); - return true; - } - - const { target, address, addressType, timestamp, signature } = input; - const cryptoManager = CryptoManager.getCryptoManager(addressType); - const signerAddress = input.address; - const message = `${target}:${address}:${addressType}:${timestamp}`; - const isValid = await cryptoManager.verifySignature( - signerAddress, - message, - signature! - ); - return isValid && super.verifySignature(input); - } - - /** - * Notify the backend about successful setup circuit calls (applyMask, dealCards). - * This updates the backend's in-memory setup state map so both players can - * coordinate. More reliable than frontend-only notifications since the batcher - * is the one that actually confirmed the transaction on-chain. - */ - private async notifyBackendSetup( - circuit: string, - data: MidnightBatchPayload | null, - rawInput: CircuitCallWithSecrets | null, - ): Promise { - const actionMap: Record = { - "applyMask": "mask_applied", - "dealCards": "dealt_complete", - }; - const action = actionMap[circuit]; - if (!action || !data?.payloads?.[0]) return; - - const payload = data.payloads[0]; - const args = payload.args; - if (!Array.isArray(args) || args.length < 2) return; - - // Convert hex gameId back to lobbyId string - const gameIdHex = args[0] as string; - const playerId = Number(args[1]); - const lobbyId = this.hexToLobbyId(gameIdHex); - - // Include player secrets so the node can replay the circuit locally on its own - // in-memory contract instance, keeping the local actionContext in sync with - // what was just written to the real Midnight chain. - // Also forward opponent secrets — dealCards needs both players' secrets to reproduce - // the same cardOwnership ledger as the real on-chain transaction. - // Read from rawInput — payload fields were stripped by MidnightBatchBuilderLogic. - const body: Record = { lobby_id: lobbyId, player_id: playerId, action }; - if (rawInput?.playerSecret) body.player_secret = rawInput.playerSecret; - if (rawInput?.shuffleSeed) body.shuffle_seed = rawInput.shuffleSeed; - if (rawInput?.opponentSecret) body.opponent_secret = rawInput.opponentSecret; - if (rawInput?.opponentShuffleSeed) body.opponent_shuffle_seed = rawInput.opponentShuffleSeed; - - const backendUrl = Deno.env.get("BACKEND_URL") || "http://localhost:9996"; - try { - const response = await fetch(`${backendUrl}/api/midnight/notify_setup`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - if (response.ok) { - const secretSnippet = rawInput?.playerSecret ? `secret=0x${rawInput.playerSecret.slice(0,16)}...` : 'no secret'; - console.log(`[GoFishMidnightAdapter] Backend notified: ${action} for ${lobbyId} player ${playerId} (${secretSnippet})`); - } else { - console.warn(`[GoFishMidnightAdapter] Failed to notify backend: ${response.status}`); - } - } catch (error) { - console.warn(`[GoFishMidnightAdapter] Could not notify backend:`, error); - } - } - - /** - * Convert hex-encoded gameId back to lobby ID string - */ - private hexToLobbyId(hexStr: string): string { - const clean = hexStr.startsWith("0x") ? hexStr.slice(2) : hexStr; - const bytes = new Uint8Array(clean.length / 2); - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(clean.substr(i * 2, 2), 16); - } - // Remove null padding bytes - let end = bytes.length; - while (end > 0 && bytes[end - 1] === 0) end--; - return new TextDecoder().decode(bytes.slice(0, end)); - } - - /** - * Query public game state from the real Midnight indexer. - * - * Runs the public impure circuits (getGamePhase, getCurrentTurn, getScores, etc.) - * against the indexer's current ledger state using createUnprovenCallTxFromInitialStates. - * No secrets or proofs are needed — these are all public reads. - * - * This is the authoritative source of truth for game phase. The backend must call this - * instead of maintaining an optimistic local simulation. - * - * @param lobbyId The game lobby ID (UTF-8 string, padded to 32 bytes internally) - * @returns Full game state from the real chain, or null if the game doesn't exist yet - */ - async queryGameState(lobbyId: string): Promise<{ - phase: number; - currentTurn: number; - scores: [number, number]; - handSizes: [number, number]; - deckCount: number; - isGameOver: boolean; - lastAskedRank: number | null; - lastAskingPlayer: number | null; - } | null> { - // Ensure the adapter is initialized (wallet, indexer connection) - if ((this as any).initializationPromise) { - await (this as any).initializationPromise; - } - if (!(this as any).isInitialized) { - throw new Error("[GoFishMidnightAdapter.queryGameState] Adapter not initialized"); - } - - const publicDataProvider = (this as any).publicDataProvider; - const walletProvider = (this as any).walletProvider; - const config = (this as any).config as typeof midnightAdapterConfig0; - - if (!publicDataProvider || !walletProvider) { - throw new Error("[GoFishMidnightAdapter.queryGameState] Required providers not initialized"); - } - - // Use dummy witnesses — no secrets needed for public circuit reads. - // The only witnesses that could be invoked are player_secret_key and shuffle_seed - // (for circuits that branch on private state), but all circuits called here are - // pure public reads that never invoke those witnesses. - const dummyWitnesses = { - getFieldInverse: () => { throw new Error("getFieldInverse should not be called in queryGameState"); }, - player_secret_key: () => { throw new Error("player_secret_key should not be called in queryGameState"); }, - shuffle_seed: () => { throw new Error("shuffle_seed should not be called in queryGameState"); }, - get_sorted_deck_witness: () => { throw new Error("get_sorted_deck_witness should not be called in queryGameState"); }, - }; - - const compiledContract = CompiledContract.make( - "go-fish-contract", - (this as any).contractClass, - ).pipe( - CompiledContract.withWitnesses(dummyWitnesses as never), - CompiledContract.withCompiledFileAssets(config.zkConfigPath), - ); - - // Build gameId as Uint8Array (32 bytes, UTF-8 lobby string zero-padded) - const encoder = new TextEncoder(); - const lobbyBytes = encoder.encode(lobbyId); - const gameIdBytes = new Uint8Array(32); - gameIdBytes.set(lobbyBytes.slice(0, 32)); - - const zkConfigProvider = new NodeZkConfigProvider(config.zkConfigPath); - - // Fetch contract public state once, then reuse for all circuit calls. - const { contractState, zswapChainState } = await (getPublicStates as any)( - publicDataProvider, - contractAddress0, - ); - - const coinPublicKey = parseCoinPublicKeyToHex(walletProvider.getCoinPublicKey(), getNetworkId()); - const walletEncKey = walletProvider.getEncryptionPublicKey(); - - /** - * Run a single public impure circuit against the cached indexer state. - * Returns the raw circuit result, or null on error. - */ - const runCircuit = async (circuitId: string, extraArgs: unknown[] = []): Promise => { - try { - const callTxData = await (createUnprovenCallTxFromInitialStates as any)( - zkConfigProvider, - { - compiledContract, - circuitId, - contractAddress: contractAddress0, - coinPublicKey, - initialContractState: contractState, - initialZswapChainState: zswapChainState, - args: [gameIdBytes, ...extraArgs], - }, - walletEncKey, - ); - return callTxData?.private?.result ?? null; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - // "Game does not exist" / null cell are expected before the game is set up - if (msg.includes("Game does not exist") || msg.includes("expected a cell, received null")) { - return null; - } - console.warn(`[GoFishMidnightAdapter.queryGameState] ${circuitId} error: ${msg}`); - return null; - } - }; - - // Run all public query circuits in parallel against the same indexer snapshot - const [ - phase, - currentTurn, - scores, - handSizes, - deckSize, - topCardIndex, - isGameOver, - lastAskedRankRaw, - lastAskingPlayerRaw, - maskP1Raw, - maskP2Raw, - dealtP1Raw, - dealtP2Raw, - ] = await Promise.all([ - runCircuit("getGamePhase"), - runCircuit("getCurrentTurn"), - runCircuit("getScores"), - runCircuit("getHandSizes"), - runCircuit("get_deck_size"), - runCircuit("get_top_card_index"), - runCircuit("isGameOver"), - runCircuit("getLastAskedRank"), - runCircuit("getLastAskingPlayer"), - runCircuit("hasMaskApplied", [1n]), - runCircuit("hasMaskApplied", [2n]), - runCircuit("hasDealt", [1n]), - runCircuit("hasDealt", [2n]), - ]); - - // If phase is null the game doesn't exist on-chain yet - if (phase === null) return null; - - const [score1, score2] = (scores as [bigint, bigint] | null) ?? [0n, 0n]; - const [hand1, hand2] = (handSizes as [bigint, bigint] | null) ?? [7n, 7n]; - const deckRemaining = Number(deckSize as bigint ?? 38n) - Number(topCardIndex as bigint ?? 0n); - - // lastAskedRank = 255 means "none pending" - const lastAskedRankNum = Number(lastAskedRankRaw as bigint ?? 255n); - const lastAskingPlayerNum = Number(lastAskingPlayerRaw as bigint ?? 0n); - - console.log(`[GoFishMidnightAdapter.queryGameState] lobby=${lobbyId} maskP1Raw=${JSON.stringify(maskP1Raw)} maskP2Raw=${JSON.stringify(maskP2Raw)} dealtP1Raw=${JSON.stringify(dealtP1Raw)} dealtP2Raw=${JSON.stringify(dealtP2Raw)}`); - - return { - phase: Number(phase as number), - currentTurn: Number(currentTurn as bigint ?? 1n), - scores: [Number(score1), Number(score2)], - handSizes: [Number(hand1), Number(hand2)], - deckCount: Math.max(0, deckRemaining), - isGameOver: Boolean(isGameOver), - lastAskedRank: lastAskedRankNum < 255 ? lastAskedRankNum : null, - lastAskingPlayer: lastAskingPlayerNum > 0 ? lastAskingPlayerNum : null, - maskApplied: [Boolean(maskP1Raw), Boolean(maskP2Raw)] as [boolean, boolean], - hasDealt: [Boolean(dealtP1Raw), Boolean(dealtP2Raw)] as [boolean, boolean], - }; - } - - /** - * Clear secrets after circuit execution - */ - clearSecrets(gameId: string, playerId: number): void { - clearPlayerSecrets(gameId, playerId as 1 | 2); - console.log(`[GoFishMidnightAdapter] Cleared client secrets for game ${gameId}, player ${playerId}`); - } - - /** - * Query the player's current hand from the on-chain indexer state. - * - * Uses createUnprovenCallTx to run doesPlayerHaveSpecificCard locally against - * the indexer's current ledger state (no proof generated, no tx submitted). - * This reflects the REAL on-chain hand — including cards received/lost via - * respondToAsk/goFish — unlike the backend's local simulation which only - * knows the post-deal state. - * - * @param lobbyId The game lobby ID (UTF-8 string, padded to 32 bytes) - * @param playerId 1 or 2 - * @param playerSecretHex Hex-encoded player secret (no "0x" prefix) - * @param shuffleSeedHex Hex-encoded 32-byte shuffle seed (no "0x" prefix) - * @param opponentSecretHex Optional opponent secret hex (no "0x" prefix) - * @param opponentShuffleSeedHex Optional opponent shuffle seed hex - * @returns Array of {rank, suit} for cards the player currently holds - */ - async queryPlayerHand( - lobbyId: string, - playerId: 1 | 2, - playerSecretHex: string, - shuffleSeedHex: string, - opponentSecretHex?: string, - opponentShuffleSeedHex?: string, - ): Promise> { - // Ensure the adapter is initialized (wallet, indexer connection) - if ((this as any).initializationPromise) { - await (this as any).initializationPromise; - } - if (!(this as any).isInitialized) { - throw new Error("[GoFishMidnightAdapter.queryPlayerHand] Adapter not initialized"); - } - - // Access private fields from the base class at runtime (TS private is compile-time only). - // The JSR adapter stores compiledContract locally inside initialize() and doesn't expose it - // as a field — build a fresh one from the stored contractClass/witnesses/zkConfigPath. - const publicDataProvider = (this as any).publicDataProvider; - const walletProvider = (this as any).walletProvider; - const config = (this as any).config as typeof midnightAdapterConfig0; - - if (!publicDataProvider || !walletProvider) { - throw new Error("[GoFishMidnightAdapter.queryPlayerHand] Required providers not initialized"); - } - - const compiledContract = CompiledContract.make( - "go-fish-contract", - (this as any).contractClass, - ).pipe( - CompiledContract.withWitnesses((this as any).witnesses as never), - CompiledContract.withCompiledFileAssets(config.zkConfigPath), - ); - - // Build gameId as Uint8Array (32 bytes, UTF-8 lobby string zero-padded) - // AND as "0x"-prefixed hex (needed for setPlayerSecrets witness key format) - const encoder = new TextEncoder(); - const lobbyBytes = encoder.encode(lobbyId); - const gameIdBytes = new Uint8Array(32); - gameIdBytes.set(lobbyBytes.slice(0, 32)); - const gameIdHex = "0x" + Array.from(gameIdBytes).map(b => b.toString(16).padStart(2, "0")).join(""); - - // Set player secrets so the witness function resolves correctly during simulation - const secret = BigInt("0x" + playerSecretHex); - const shuffleSeed = hexToBytes(shuffleSeedHex); - setPlayerSecrets(gameIdHex, playerId, secret, shuffleSeed); - - const opponentId = (playerId === 1 ? 2 : 1) as 1 | 2; - let opponentSet = false; - if (opponentSecretHex && opponentShuffleSeedHex) { - setPlayerSecrets(gameIdHex, opponentId, BigInt("0x" + opponentSecretHex), hexToBytes(opponentShuffleSeedHex)); - opponentSet = true; - } else { - // doesPlayerHaveSpecificCard calls deck_getSecretFromPlayerId for both players - // unconditionally. Fetch the opponent's secret from the backend so the witness - // resolves correctly instead of falling back to the static random key. - const backendOpponent = await this.fetchSecretFromBackend(lobbyId, opponentId); - if (backendOpponent) { - setPlayerSecrets( - gameIdHex, opponentId, - BigInt("0x" + backendOpponent.secret), - backendOpponent.shuffleSeed ? hexToBytes(backendOpponent.shuffleSeed) : new Uint8Array(32), - ); - opponentSet = true; - } - } - - const zkConfigProvider = new NodeZkConfigProvider(config.zkConfigPath); - - // Fetch contract public state ONCE — reused for all 21 card checks to avoid - // 21 separate indexer round-trips. createUnprovenCallTxFromInitialStates - // takes the state directly instead of fetching it on each call. - const { contractState, zswapChainState } = await (getPublicStates as any)( - publicDataProvider, - contractAddress0, - ); - // parseCoinPublicKeyToHex converts the wallet's CoinPublicKey to the hex string - // format that createUnprovenCallTxFromInitialStates expects. - const coinPublicKey = parseCoinPublicKeyToHex(walletProvider.getCoinPublicKey(), getNetworkId()); - const walletEncKey = walletProvider.getEncryptionPublicKey(); - - const hand: Array<{ rank: number; suit: number }> = []; - - try { - // Check all 21 card indices (7 ranks × 3 suits, index = rank + suit*7) - // Args to doesPlayerHaveSpecificCard: (gameId: Uint8Array, playerId: bigint, cardIndex: bigint) - for (let cardIndex = 0; cardIndex < 21; cardIndex++) { - try { - const callTxData = await (createUnprovenCallTxFromInitialStates as any)( - zkConfigProvider, - { - compiledContract, - circuitId: "doesPlayerHaveSpecificCard", - contractAddress: contractAddress0, - coinPublicKey, - initialContractState: contractState, - initialZswapChainState: zswapChainState, - args: [gameIdBytes, BigInt(playerId), BigInt(cardIndex)], - }, - walletEncKey, - ); - const hasCard = callTxData?.private?.result as boolean | undefined; - if (hasCard === true) { - const rank = cardIndex % 7; - const suit = Math.floor(cardIndex / 7); - hand.push({ rank, suit }); - } - } catch (err) { - // Log individual card check errors but continue — a WASM assert for one - // card index (e.g. because of a stale assert) shouldn't abort the whole query - console.warn(`[GoFishMidnightAdapter.queryPlayerHand] cardIndex=${cardIndex} error:`, err instanceof Error ? err.message : String(err)); - } - } - } finally { - clearPlayerSecrets(gameIdHex, playerId); - if (opponentSet) { - clearPlayerSecrets(gameIdHex, opponentId); - } - } - - console.log(`[GoFishMidnightAdapter.queryPlayerHand] player=${playerId} hand=${JSON.stringify(hand)}`); - return hand; - } -} - -// Create the Go Fish Midnight adapter instance -// NOTE: We pass the Contract CLASS (not an instance) so the vendor adapter can build a -// CompiledContract using its own compact-js module (avoids Symbol mismatch across module instances). -export const midnightAdapter_go_fish = new GoFishMidnightAdapter( - contractAddress0, - GENESIS_MINT_WALLET_SEED, - { ...midnightAdapterConfig0, contractTag: "go-fish-contract" }, - goFishContract.Contract, // Pass the CLASS, not an instance - goFishContractInfo.witnesses, - contractInfo0, - syncProtocolName, // syncProtocolName - // maxBatchSize in bytes — must be large enough for any single circuit call payload. - // The actual "one circuit per batch" limit is enforced by maxInputs: 1 in buildBatchData(). - // applyMask/dealCards payloads with secrets can be ~500-1000 bytes. - 10000, -); - -// Export all Midnight adapters for the batcher -export const midnightAdapters: Record = { - "go-fish": midnightAdapter_go_fish, -}; diff --git a/packages/client/batcher/src/config.ts b/packages/client/batcher/src/config.ts index 6fd0e33..df19672 100644 --- a/packages/client/batcher/src/config.ts +++ b/packages/client/batcher/src/config.ts @@ -19,7 +19,7 @@ export const config: BatcherConfig = { // timeout" errors that spam the log and don't affect correctness. confirmationLevel: { effectstreaml2: "wait-receipt", - "go-fish": "no-wait", + // "go-fish": "no-wait", midnight_balancing: "no-wait", }, enableEventSystem: false, diff --git a/packages/client/batcher/src/main.ts b/packages/client/batcher/src/main.ts index c180c1b..9dd473b 100644 --- a/packages/client/batcher/src/main.ts +++ b/packages/client/batcher/src/main.ts @@ -8,9 +8,11 @@ import { main, suspend } from "effection"; import { createNewBatcher, MidnightAdapter } from "@paimaexample/batcher"; import { config, storage, BATCHER_DATA_DIR } from "./config.ts"; -import { effectstreaml2Adapter } from "./adapter-effectstreaml2.ts"; import Fastify from "npm:fastify@^5.4.0"; +import { midnightBalancingAdapter } from "./adapter-midnight-balancing.ts"; +import { effectstreaml2Adapter } from "./adapter-effectstreaml2.ts"; + // Clear stale batcher data on startup to prevent processing old transactions // This is important because old transactions may reference games that no longer exist // and would cause "Game does not exist" errors @@ -33,73 +35,77 @@ batcher criteriaType: "time", timeWindowMs: batchIntervalMs, }) + .addBlockchainAdapter("midnight_balancing", midnightBalancingAdapter, { + criteriaType: "time", + timeWindowMs: batchIntervalMs, + }) .setDefaultTarget("effectstreaml2"); -// Add Midnight adapters with time-based batching with very short window -// This ensures transactions are processed quickly and sequentially -// The MidnightAdapter handles the actual circuit invocation -// Skip when using TypeScript contract (no Midnight infrastructure needed) -// Use dynamic import to avoid eagerly connecting to Midnight infrastructure -const skipMidnightInfra = Deno.env.get("SKIP_MIDNIGHT_INFRA") === "true"; -const useTypescriptContract = Deno.env.get("USE_TYPESCRIPT_CONTRACT") === "true"; -let goFishMidnightAdapter: import("./adapter-midnight.ts").GoFishMidnightAdapter | null = null; - -if (!useTypescriptContract && !skipMidnightInfra) { - const midnightAdapters = await import("./adapter-midnight.ts"); - for (const [contract, adapter] of Object.entries(midnightAdapters.midnightAdapters)) { - if (adapter instanceof MidnightAdapter) { - batcher.addBlockchainAdapter(contract, adapter, { - criteriaType: "size", - maxBatchSize: 1, // Each Midnight circuit call is its own batch — the JSR adapter - // only executes the first invocation per submitBatch() call, so - // grouping multiple inputs drops all but the first. - }); - } - } - goFishMidnightAdapter = midnightAdapters.midnightAdapter_go_fish; -} else if (!useTypescriptContract && skipMidnightInfra) { - // SKIP_MIDNIGHT_INFRA=true means the local node/indexer/proof-server were started - // externally (or on testnet). We still need the query adapter so /query-game-state - // can read on-chain state. We do NOT register it as a batcher target — the - // balancing adapter handles actual tx submission in this mode. - try { - const midnightAdapters = await import("./adapter-midnight.ts"); - goFishMidnightAdapter = midnightAdapters.midnightAdapter_go_fish; - console.log("🔍 Midnight query adapter initialized (SKIP_MIDNIGHT_INFRA mode)"); - } catch (err) { - console.warn("⚠️ Could not initialize Midnight query adapter:", err); - } -} else { - console.log("📝 Skipping legacy Midnight circuit adapter"); -} - -// midnight_balancing: handles pre-proven unbound transactions from the browser WASM prover. -// Registered whenever batcher mode is active — SKIP_MIDNIGHT_INFRA only skips launching -// the local Midnight node/indexer/proof-server, not the balancing adapter itself, which -// connects to an already-running (possibly external) Midnight network. -if (!useTypescriptContract) { - try { - const { goFishBalancingAdapter } = await import("./adapter-midnight-balancing.ts"); - batcher.addBlockchainAdapter("midnight_balancing", goFishBalancingAdapter, { - criteriaType: "time", - timeWindowMs: 500, - }); - console.log("⚡ midnight_balancing adapter registered (browser WASM proving)"); - } catch (err) { - console.error("❌ Failed to register midnight_balancing adapter:", err); - } -} +// // Add Midnight adapters with time-based batching with very short window +// // This ensures transactions are processed quickly and sequentially +// // The MidnightAdapter handles the actual circuit invocation +// // Skip when using TypeScript contract (no Midnight infrastructure needed) +// // Use dynamic import to avoid eagerly connecting to Midnight infrastructure +// const skipMidnightInfra = Deno.env.get("SKIP_MIDNIGHT_INFRA") === "true"; +// const useTypescriptContract = Deno.env.get("USE_TYPESCRIPT_CONTRACT") === "true"; +// let goFishMidnightAdapter: import("./adapter-midnight.ts").GoFishMidnightAdapter | null = null; + +// if (!useTypescriptContract && !skipMidnightInfra) { +// const midnightAdapters = await import("./adapter-midnight.ts"); +// for (const [contract, adapter] of Object.entries(midnightAdapters.midnightAdapters)) { +// if (adapter instanceof MidnightAdapter) { +// batcher.addBlockchainAdapter(contract, adapter, { +// criteriaType: "size", +// maxBatchSize: 1, // Each Midnight circuit call is its own batch — the JSR adapter +// // only executes the first invocation per submitBatch() call, so +// // grouping multiple inputs drops all but the first. +// }); +// } +// } +// goFishMidnightAdapter = midnightAdapters.midnightAdapter_go_fish; +// } else if (!useTypescriptContract && skipMidnightInfra) { +// // SKIP_MIDNIGHT_INFRA=true means the local node/indexer/proof-server were started +// // externally (or on testnet). We still need the query adapter so /query-game-state +// // can read on-chain state. We do NOT register it as a batcher target — the +// // balancing adapter handles actual tx submission in this mode. +// try { +// const midnightAdapters = await import("./adapter-midnight.ts"); +// goFishMidnightAdapter = midnightAdapters.midnightAdapter_go_fish; +// console.log("🔍 Midnight query adapter initialized (SKIP_MIDNIGHT_INFRA mode)"); +// } catch (err) { +// console.warn("⚠️ Could not initialize Midnight query adapter:", err); +// } +// } else { +// console.log("📝 Skipping legacy Midnight circuit adapter"); +// } + +// // midnight_balancing: handles pre-proven unbound transactions from the browser WASM prover. +// // Registered whenever batcher mode is active — SKIP_MIDNIGHT_INFRA only skips launching +// // the local Midnight node/indexer/proof-server, not the balancing adapter itself, which +// // connects to an already-running (possibly external) Midnight network. +// if (!useTypescriptContract) { +// try { +// const { goFishBalancingAdapter } = await import("./adapter-midnight-balancing.ts"); +// batcher.addBlockchainAdapter("midnight_balancing", goFishBalancingAdapter, { +// criteriaType: "time", +// timeWindowMs: 500, +// }); +// console.log("⚡ midnight_balancing adapter registered (browser WASM proving)"); +// } catch (err) { +// console.error("❌ Failed to register midnight_balancing adapter:", err); +// } +// } // Secondary HTTP server exposing hand-query endpoint (port 9997). // The primary batcher HTTP server (port config.port) is managed internally by // @paimaexample/batcher and does not allow custom routes, so we run a separate // Fastify instance for this read-only query endpoint. -const QUERY_PORT = Number(Deno.env.get("BATCHER_QUERY_PORT") || "9997"); -const queryServer = Fastify({ logger: false }); +// const QUERY_PORT = Number(Deno.env.get("BATCHER_QUERY_PORT") || "9997"); +// const queryServer = Fastify({ logger: false }); -queryServer.get("/health", async (_req, reply) => { - reply.send({ ok: true }); -}); +// queryServer.get("/health", async (_req, reply) => { +// reply.send({ ok: true }); +// }); /** * POST /query-hand @@ -120,70 +126,70 @@ queryServer.get("/health", async (_req, reply) => { * This is the authoritative source of truth for game phase; do NOT use the backend's * optimistic gameStateMap for phase tracking. */ -queryServer.post("/query-game-state", async (req, reply) => { - const body = req.body as { lobbyId: string }; - - if (!body || !body.lobbyId) { - return reply.status(400).send({ error: "Missing required field: lobbyId" }); - } - - if (!goFishMidnightAdapter) { - return reply.status(503).send({ error: "Midnight adapter not available (USE_TYPESCRIPT_CONTRACT=true or not initialized)" }); - } - - try { - const state = await goFishMidnightAdapter.queryGameState(body.lobbyId); - if (state === null) { - return reply.send({ exists: false }); - } - return reply.send({ exists: true, ...state }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.error("[query-game-state] Error:", msg); - return reply.status(500).send({ error: msg }); - } -}); - -queryServer.post("/query-hand", async (req, reply) => { - const body = req.body as { - lobbyId: string; - playerId: 1 | 2; - playerSecretHex: string; - shuffleSeedHex: string; - opponentSecretHex?: string; - opponentShuffleSeedHex?: string; - }; - - if (!body || !body.lobbyId || !body.playerId || !body.playerSecretHex || !body.shuffleSeedHex) { - return reply.status(400).send({ error: "Missing required fields: lobbyId, playerId, playerSecretHex, shuffleSeedHex" }); - } - - if (!goFishMidnightAdapter) { - return reply.status(503).send({ error: "Midnight adapter not available (USE_TYPESCRIPT_CONTRACT=true or not initialized)" }); - } - - try { - const hand = await goFishMidnightAdapter.queryPlayerHand( - body.lobbyId, - body.playerId, - body.playerSecretHex, - body.shuffleSeedHex, - body.opponentSecretHex, - body.opponentShuffleSeedHex, - ); - return reply.send({ hand }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.error("[query-hand] Error:", msg); - return reply.status(500).send({ error: msg }); - } -}); - -queryServer.listen({ port: QUERY_PORT, host: "0.0.0.0" }).then(() => { - console.log(`🔍 Query server listening on port ${QUERY_PORT} (POST /query-hand)`); -}).catch(err => { - console.error("❌ Failed to start query server:", err); -}); +// queryServer.post("/query-game-state", async (req, reply) => { +// const body = req.body as { lobbyId: string }; + +// if (!body || !body.lobbyId) { +// return reply.status(400).send({ error: "Missing required field: lobbyId" }); +// } + +// if (!goFishMidnightAdapter) { +// return reply.status(503).send({ error: "Midnight adapter not available (USE_TYPESCRIPT_CONTRACT=true or not initialized)" }); +// } + +// try { +// const state = await goFishMidnightAdapter.queryGameState(body.lobbyId); +// if (state === null) { +// return reply.send({ exists: false }); +// } +// return reply.send({ exists: true, ...state }); +// } catch (err) { +// const msg = err instanceof Error ? err.message : String(err); +// console.error("[query-game-state] Error:", msg); +// return reply.status(500).send({ error: msg }); +// } +// }); + +// queryServer.post("/query-hand", async (req, reply) => { +// const body = req.body as { +// lobbyId: string; +// playerId: 1 | 2; +// playerSecretHex: string; +// shuffleSeedHex: string; +// opponentSecretHex?: string; +// opponentShuffleSeedHex?: string; +// }; + +// if (!body || !body.lobbyId || !body.playerId || !body.playerSecretHex || !body.shuffleSeedHex) { +// return reply.status(400).send({ error: "Missing required fields: lobbyId, playerId, playerSecretHex, shuffleSeedHex" }); +// } + +// if (!goFishMidnightAdapter) { +// return reply.status(503).send({ error: "Midnight adapter not available (USE_TYPESCRIPT_CONTRACT=true or not initialized)" }); +// } + +// try { +// const hand = await goFishMidnightAdapter.queryPlayerHand( +// body.lobbyId, +// body.playerId, +// body.playerSecretHex, +// body.shuffleSeedHex, +// body.opponentSecretHex, +// body.opponentShuffleSeedHex, +// ); +// return reply.send({ hand }); +// } catch (err) { +// const msg = err instanceof Error ? err.message : String(err); +// console.error("[query-hand] Error:", msg); +// return reply.status(500).send({ error: msg }); +// } +// }); + +// queryServer.listen({ port: QUERY_PORT, host: "0.0.0.0" }).then(() => { +// console.log(`🔍 Query server listening on port ${QUERY_PORT} (POST /query-hand)`); +// }).catch(err => { +// console.error("❌ Failed to start query server:", err); +// }); // Startup banner via state transition batcher diff --git a/packages/client/batcher/vendor/batcher/adapters/adapter.ts b/packages/client/batcher/vendor/batcher/adapters/adapter.ts deleted file mode 100644 index 094d752..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/adapter.ts +++ /dev/null @@ -1,156 +0,0 @@ -// Implements a adapter interface for the batcher responsible for handling blockchain interactions - -import type { DefaultBatcherInput } from "../core/types.ts"; - -/** - * Generic blockchain transaction hash type - * Can represent transaction hashes from any blockchain - */ -export type BlockchainHash = string; - -/** - * Result of input validation operations - */ -export type ValidationResult = { - /** Whether the input is valid */ - valid: boolean; - /** Error message if validation failed */ - error?: string; -}; - -/** - * Generic blockchain transaction receipt type - * Contains common fields that most blockchains have - */ -export interface BlockchainTransactionReceipt { - /** Transaction hash */ - hash: BlockchainHash; - /** Block number where transaction was included */ - blockNumber: bigint; - /** Transaction status (1 = success, 0 = failure) */ - status: number; - /** Additional blockchain-specific fields can be added via extension */ - [key: string]: any; -} - -/** - * Options for batch building - */ -export interface BatchBuildingOptions { - /** Maximum size of the batch in bytes */ - maxSize?: number; -} - -/** - * Result of batch building operation - */ -export interface BatchBuildingResult { - /** Inputs that were selected for this batch */ - selectedInputs: DefaultBatcherInput[]; - /** Serialized batch data. The type (TOutput) is defined by the adapter implementation. */ - data: TOutput; -} - -/** - * Base interface for blockchain adapters that handle chain-specific operations - * Provides a unified interface for different blockchain interactions - */ -export interface BlockchainAdapter { - /** - * Submit a batch transaction to the blockchain. - * @param data - The type-safe batch data, as constructed by buildBatchData. - * @param fee - The fee to pay for the transaction. - * @returns Promise resolving to transaction hash - */ - submitBatch(data: TOutput, fee: string | bigint): Promise; - - /** - * Estimate the fee for submitting a batch. - * @param data - The type-safe batch data payload to estimate for. - * @returns Estimated fee - */ - estimateBatchFee(data: TOutput): Promise | string | bigint; - - /** - * Build batch data from a collection of inputs. - * This method is now part of the adapter itself. - * @param inputs - Array of inputs to batch - * @param options - Options for batch building - * @returns Batch building result or null if no inputs could be batched - */ - buildBatchData( - inputs: DefaultBatcherInput[], - options?: BatchBuildingOptions, - ): BatchBuildingResult | null; - - /** - * Wait for a transaction to be confirmed on the blockchain - * @param hash - The transaction hash to wait for - * @param timeout - Optional timeout in milliseconds - * @returns Promise resolving to transaction receipt - */ - waitForTransactionReceipt( - hash: BlockchainHash, - timeout?: number, - ): Promise; - - /** - * Get the current account/address for this adapter - * @returns The account address as a string - */ - getAccountAddress(): string; - - /** - * Get the current chain name or identifier - * @returns The chain name/identifier - */ - getChainName(): string; - - /** - * Check if the adapter is ready to submit transactions - * @returns True if the adapter is operational - */ - isReady(): boolean; - - /** - * Get the block number of the latest confirmed block - * @returns Promise resolving to current block number - */ - getBlockNumber(): Promise; - - /** - * Optional sync protocol name used to filter EffectStream Sync events - * If not provided, the batcher will fall back to the adapter's chain name - */ - getSyncProtocolName?(): string; - - /** - * (Optional) Verifies the input signature. - * If not implemented, the batcher will use its default EVM verification logic. - * Adapters for chains without signatures (like Midnight) should override this - * to return `true`. - * @param input - The input containing the signature. - * @returns A promise resolving to true if the signature is valid. - */ - verifySignature?(input: DefaultBatcherInput): boolean | Promise; - - /** - * (Optional) Validate an input _before_ it is added to the storage queue. - * This is used for adapter-specific semantic validation, like checking - * circuit arguments or payload formats. - * @param input - The input to validate. - * @returns A promise resolving to a ValidationResult. - */ - validateInput?( - input: DefaultBatcherInput, - ): ValidationResult | Promise; - - /** - * (Optional) Recover adapter state after batcher initialization. - * This is called after storage.init() but before processing starts, - * allowing adapters to rebuild internal state from persisted inputs. - * Useful for stateful adapters (e.g., Bitcoin tracking reserved funds). - * @param pendingInputs - All pending inputs for this adapter from storage. - */ - recoverState?(pendingInputs: DefaultBatcherInput[]): Promise | void; -} diff --git a/packages/client/batcher/vendor/batcher/adapters/bitcoin-adapter.ts b/packages/client/batcher/vendor/batcher/adapters/bitcoin-adapter.ts deleted file mode 100644 index f9a6108..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/bitcoin-adapter.ts +++ /dev/null @@ -1,482 +0,0 @@ -import * as bitcoin from "npm:bitcoinjs-lib@^6.1.5"; -import * as bitcoinMessage from "npm:bitcoinjs-message@^2.2.0"; -import * as ecpair from "npm:ecpair@^2.1.0"; -import * as tinysecp from "npm:tiny-secp256k1@^2.2.1"; -import { createHash } from "node:crypto"; -import type { - BlockchainAdapter, - BlockchainHash, - BlockchainTransactionReceipt, - ValidationResult, - BatchBuildingOptions, - BatchBuildingResult, -} from "./adapter.ts"; -import type { DefaultBatcherInput } from "../core/types.ts"; - -const ECPair = ecpair.ECPairFactory(tinysecp); - -// Interface for the input payload signed by the user -interface BitcoinRequest { - toAddress: string; - amountSats: number; -} - -/* -The shape of data passed from Builder to Submitter -*/ -export interface BitcoinBatchPayload { - recipients: { address: string; value: number }[]; - totalAmountSats: number; -} - -export interface BitcoinAdapterConfig { - rpcUrl: string; - rpcUser: string; - rpcPass: string; - batcherWif?: string; // Wallet Import Format private key (optional if seed provided) - seed?: string; // Seed string to generate private key - network?: bitcoin.Network; // Defaults to regtest - maxBatchSize?: number; - syncProtocolName?: string; -} - -export function buildBitcoinSignatureMessage(payload: BitcoinRequest, timestamp: string) { - return `send ${payload.amountSats} sats to ${payload.toAddress} at ${timestamp}` -} - -export class BitcoinAdapter implements BlockchainAdapter { - private readonly rpcUrl: string; - private readonly rpcAuth: string; // "user:password" - private readonly keyPair: any; - private readonly network: bitcoin.Network; - public readonly maxBatchSize: number; - private readonly batcherAddress: string; - private reservedSatFunds: number = 0; - private addressChecked = false; - private readonly syncProtocolName: string; - - constructor(config: BitcoinAdapterConfig) { - this.rpcUrl = config.rpcUrl; - this.rpcAuth = btoa(`${config.rpcUser}:${config.rpcPass}`); - this.network = config.network ?? bitcoin.networks.regtest; - this.syncProtocolName = config.syncProtocolName ?? "parallelBitcoin"; - if (config.seed) { - const privateKeyBuffer = createHash("sha256") - .update(config.seed) - .digest(); - this.keyPair = ECPair.fromPrivateKey(privateKeyBuffer, { - network: this.network, - }); - } else if (config.batcherWif) { - this.keyPair = ECPair.fromWIF(config.batcherWif, this.network); - } else { - throw new Error("BitcoinAdapter: Must provide either seed or batcherWif"); - } - - this.maxBatchSize = config.maxBatchSize ?? 50; - - const { address } = bitcoin.payments.p2wpkh({ - pubkey: this.keyPair.publicKey, - network: this.network - }); - console.log("BitcoinAdapter: Batcher address:", address); - this.batcherAddress = address!; - } - getSyncProtocolName(): string { - return this.syncProtocolName; - } - getChainName(): string { - return "Bitcoin Regtest"; - } - - getAccountAddress(): string { - return this.batcherAddress; - } - - isReady(): boolean { - return !!this.keyPair; - } - - async getBlockNumber(): Promise { - const count = await this.rpcCall("getblockcount", []); - return BigInt(count); - } - - async verifySignature(input: DefaultBatcherInput): Promise { - try { - // 1. Parse the intent - const payload: BitcoinRequest = JSON.parse(input.input); - - // 2. Reconstruct the message the user should have signed - // Format: "I authorize sending to at " - // This format must match exactly what your frontend generates - const message = buildBitcoinSignatureMessage(payload, input.timestamp); - - // 3. Verify signature using bitcoinjs-message - // Note: input.address is the User's Bitcoin Address - return bitcoinMessage.verify( - message, - input.address, - input.signature!, - undefined, - true // checkSegwitAlways - ); - } catch (e) { - console.error("Sig verification failed:", e); - return false; - } - } - - async recoverState(pendingInputs: DefaultBatcherInput[]): Promise { - // Rebuild reserved funds from pending inputs in storage - this.reservedSatFunds = 0; - - for (const input of pendingInputs) { - try { - const payload: BitcoinRequest = JSON.parse(input.input); - this.reservedSatFunds += payload.amountSats; - } catch (e) { - console.warn(`BitcoinAdapter: Failed to parse input during state recovery:`, e); - } - } - - console.log(`BitcoinAdapter: Recovered state - ${this.reservedSatFunds} sats reserved across ${pendingInputs.length} pending inputs`); - } - - async validateInput(input: DefaultBatcherInput): Promise { - try { - const payload: BitcoinRequest = JSON.parse(input.input); - - // Check Dust Limit (approx 546 sats) - if (payload.amountSats < 546) { - return { valid: false, error: "Amount below dust limit (546 sats)" }; - } - - // Basic address validation - try { - bitcoin.address.toOutputScript(payload.toAddress, this.network); - } catch { - return { valid: false, error: "Invalid Regtest address" }; - } - - // Check if batcher has sufficient funds - const balance = await this.getBatcherBalance(); - const availableFunds = balance - this.reservedSatFunds; - const estimatedFee = await this.estimateSingleTransactionFee(payload.amountSats); - - if (availableFunds < payload.amountSats + Number(estimatedFee)) { - return { - valid: false, - error: `Insufficient batcher funds. - Available funds: ${availableFunds} sats: - - wallet balance: ${balance} sats, - - reserved funds: ${this.reservedSatFunds} sats, - Required funds: ${payload.amountSats + Number(estimatedFee)} sats` - }; - } - // Reserve funds for the transaction - this.reservedSatFunds += payload.amountSats; - return { valid: true }; - } catch (e) { - return { valid: false, error: "Malformed JSON input" }; - } - } - - buildBatchData( - inputs: DefaultBatcherInput[], - options?: BatchBuildingOptions - ): BatchBuildingResult | null { - if (inputs.length === 0) return null; - - const maxSize = options?.maxSize ?? this.maxBatchSize; - const selectedInputs: DefaultBatcherInput[] = []; - const recipients: { address: string; value: number }[] = []; - let totalAmountSats = 0; - - for (const input of inputs) { - if (selectedInputs.length >= maxSize) break; - - const payload: BitcoinRequest = JSON.parse(input.input); - - recipients.push({ - address: payload.toAddress, - value: payload.amountSats, - }); - - totalAmountSats += payload.amountSats; - selectedInputs.push(input); - } - - return { - selectedInputs, - data: { recipients, totalAmountSats }, - }; - } - - async estimateBatchFee(data: BitcoinBatchPayload): Promise { - // Estimate VBytes: - // Overhead (10) + Input (148 * 1 assumption) + Outputs (34 * N) - // We assume 1 UTXO input is enough (optimistic), actual submit might use more - const estVBytes = 10 + 148 + (data.recipients.length + 1) * 34; // +1 for change - - // Get fee rate from node (conservative estimate, 6 blocks) - const feeRateResult = await this.rpcCall("estimatesmartfee", [6]); - - // Fallback fee rate (0.00001 BTC/kB = 1 sat/vbyte) if regtest has no history - const feeRateBtcPerKvB = feeRateResult.feerate || 0.00001; - const feeRateSatsPerByte = (feeRateBtcPerKvB * 100_000_000) / 1000; - - // Round up - const feeSats = Math.ceil(estVBytes * feeRateSatsPerByte); - return BigInt(feeSats); - } - - async submitBatch(data: BitcoinBatchPayload, fee: string | bigint): Promise { - await this.ensureAddressWatched(); - - const feeSats = Number(fee); - const totalRequired = data.totalAmountSats + feeSats; - - // 1. Select UTXOs (Coin Selection) - const unspent = await this.fetchBatcherUtxos(1); - - let inputSum = 0; - const selectedUtxos: any[] = []; - - // Simple Accumulation Strategy - for (const utxo of unspent) { - const amountSats = Math.round(utxo.amount * 100_000_000); - selectedUtxos.push(utxo); - inputSum += amountSats; - if (inputSum >= totalRequired) break; - } - - if (inputSum < totalRequired) { - throw new Error(`Insufficient Batcher funds. Need ${totalRequired}, have ${inputSum} in address ${this.batcherAddress}`); - } - - // 2. Build Transaction - const psbt = new bitcoin.Psbt({ network: this.network }); - - // Add Inputs - for (const utxo of selectedUtxos) { - // Fetch raw hex for input signing (required for non-segwit or mixed) - // Since we use p2wpkh (Segwit), passing the value is critical - const amountSats = Math.round(utxo.amount * 100_000_000); - psbt.addInput({ - hash: utxo.txid, - index: utxo.vout, - witnessUtxo: { - script: bitcoin.payments.p2wpkh({ - pubkey: this.keyPair.publicKey, - network: this.network - }).output!, - value: amountSats, - }, - }); - } - let liberatedSatFunds = 0; - // Add Recipient Outputs - for (const recipient of data.recipients) { - psbt.addOutput({ - address: recipient.address, - value: recipient.value, - }); - liberatedSatFunds += recipient.value; - } - - // Add Change Output - const change = inputSum - totalRequired; - // Dust protection for change - if (change > 546) { - psbt.addOutput({ - address: this.batcherAddress, - value: change, - }); - } else { - // If change is dust, add it to fee (miners take it) - console.log(`Dust change (${change} sats) added to fee`); - } - - // 3. Sign - psbt.signAllInputs(this.keyPair); - psbt.finalizeAllInputs(); - - // 4. Broadcast - const tx = psbt.extractTransaction(); - const txHex = tx.toHex(); - const txId = await this.rpcCall("sendrawtransaction", [txHex]); - this.reservedSatFunds -= liberatedSatFunds; - console.log(`🚀 Submitted Bitcoin Batch: ${txId} - - liberated funds: ${liberatedSatFunds} sats - - reserved funds: ${this.reservedSatFunds} sats`); - return txId; - } - - // ---------------------------------------------------------------- - // 5. Confirmation - // ---------------------------------------------------------------- - - async waitForTransactionReceipt( - hash: BlockchainHash, - timeout: number = 60000 - ): Promise { - const start = Date.now(); - - while (Date.now() - start < timeout) { - try { - // Get TX status - // verbose=true to see confirmations - const tx = await this.rpcCall("getrawtransaction", [hash, true]); - - if (tx && tx.confirmations && tx.confirmations > 0) { - return { - hash: hash, - blockNumber: BigInt(0), // RPC might not give easy block height in this call - status: 1, - confirmations: tx.confirmations - }; - } - } catch (e) { - // TX might not be in mempool/block yet - } - - await new Promise(r => setTimeout(r, 2000)); - } - - throw new Error("Transaction confirmation timed out"); - } - - // ---------------------------------------------------------------- - // Utils - // ---------------------------------------------------------------- - - private async ensureAddressWatched(): Promise { - if (this.addressChecked) return; - - try { - const info = await this.rpcCall("getaddressinfo", [this.batcherAddress]); - - // If address is not mine and not watchonly, we need to import it - if (!info.ismine && !info.iswatchonly) { - console.log(`BitcoinAdapter: Address ${this.batcherAddress} not tracked. Importing...`); - - try { - // Try legacy importaddress first - await this.rpcCall("importaddress", [this.batcherAddress, "batcher", true]); - } catch (e: any) { - const errorString = e.toString(); - console.warn(`BitcoinAdapter: importaddress failed (${errorString}). Attempting importdescriptors fallback...`); - - // Fallback for descriptor wallets (default in newer Bitcoin Core) - // 1. Get public key hex - const pubKeyHex = this.keyPair.publicKey.toString("hex"); - - // 2. Construct descriptor (wpkh for P2WPKH) - const descBase = `wpkh(${pubKeyHex})`; - - // 3. Get descriptor with checksum - const descInfo = await this.rpcCall("getdescriptorinfo", [descBase]); - const descriptor = descInfo.descriptor; - - // 4. Import descriptor (timestamp: 0 to rescan from genesis) - await this.rpcCall("importdescriptors", [[{ - desc: descriptor, - timestamp: 0, - active: true, - label: "batcher" - }]]); - } - console.log("BitcoinAdapter: Address imported and rescanned."); - } - this.addressChecked = true; - } catch (e) { - console.error("BitcoinAdapter: Error ensuring address is watched:", e); - // Don't set addressChecked to true if it failed, so we retry next time - // But we shouldn't block everything forever if it's a different issue - } - } - - private async fetchBatcherUtxos(minConf = 1): Promise> { - try { - const utxos = await this.rpcCall("listunspent", [minConf, 9999999, [this.batcherAddress]]); - if (utxos.length > 0) { - return utxos; - } - console.warn("BitcoinAdapter: listunspent returned no UTXOs for batcher address, attempting scantxoutset fallback."); - } catch (error) { - console.warn("BitcoinAdapter: listunspent failed, attempting scantxoutset fallback.", error); - } - - const scanResult = await this.rpcCall("scantxoutset", ["start", [`addr(${this.batcherAddress})`]]); - const unspents = scanResult?.unspents; - if (!scanResult?.success || !Array.isArray(unspents) || unspents.length === 0) { - console.warn("BitcoinAdapter: scantxoutset did not return any spendable outputs for the batcher address."); - return []; - } - - console.log(`BitcoinAdapter: scantxoutset found ${unspents.length} outputs totaling ${scanResult.total_amount ?? 0} BTC for the batcher.`); - - return unspents.map((entry: any) => ({ - txid: entry.txid, - vout: entry.vout, - amount: entry.amount, - })); - } - - private async getBatcherBalance(): Promise { - await this.ensureAddressWatched(); - const utxos = await this.fetchBatcherUtxos(1); - return Math.round(utxos.reduce((sum, utxo) => sum + (utxo.amount * 100_000_000), 0)); - } - - private async estimateSingleTransactionFee(amountSats: number): Promise { - // Estimate fee for a single-output transaction - // Conservative estimate: overhead (10) + 1 input (148) + 2 outputs (34*2) = ~256 vbytes - const estVBytes = 10 + 148 + (2 * 34); // 1 recipient + 1 change output - - try { - const feeRateResult = await this.rpcCall("estimatesmartfee", [6]); - - // Fallback fee rate (0.00001 BTC/kB = 1 sat/vbyte) if regtest has no history - const feeRateBtcPerKvB = feeRateResult.feerate || 0.00001; - const feeRateSatsPerByte = (feeRateBtcPerKvB * 100_000_000) / 1000; - - // Round up and add some buffer for safety - const feeSats = Math.ceil(estVBytes * feeRateSatsPerByte * 1.2); // 20% buffer - return BigInt(Math.max(feeSats, 1000)); // Minimum 1000 sats fee - } catch (error) { - console.warn("BitcoinAdapter: Fee estimation failed, using conservative default", error); - // Conservative default: 1000 sats for regtest - return BigInt(1000); - } - } - - private async rpcCall(method: string, params: any[]): Promise { - const response = await fetch(this.rpcUrl, { - method: "POST", - headers: { - "Authorization": `Basic ${this.rpcAuth}`, - "Content-Type": "application/json" - }, - body: JSON.stringify({ - jsonrpc: "1.0", - id: "batcher", - method, - params - }) - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Bitcoin RPC HTTP Error: ${response.status} ${response.statusText} - ${text}`); - } - - const json = await response.json(); - if (json.error) { - throw new Error(`Bitcoin RPC Error: ${JSON.stringify(json.error)}`); - } - - return json.result; - } -} diff --git a/packages/client/batcher/vendor/batcher/adapters/evm-contract-adapter.ts b/packages/client/batcher/vendor/batcher/adapters/evm-contract-adapter.ts deleted file mode 100644 index 44224c4..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/evm-contract-adapter.ts +++ /dev/null @@ -1,314 +0,0 @@ -import type { - Abi, - AbiFunction, - Account, - Chain, - Hash, - PublicClient, - WalletClient, -} from "npm:viem@^2.21.3"; -import { - createPublicClient, - createWalletClient, - encodeFunctionData, - http, -} from "npm:viem@^2.21.3"; -import { privateKeyToAccount } from "npm:viem@^2.21.3/accounts"; -import * as chains from "npm:/viem@^2.21.3/chains"; - -import type { - BatchBuildingOptions, - BatchBuildingResult, - BlockchainAdapter, - BlockchainHash, - BlockchainTransactionReceipt, - ValidationResult, -} from "./adapter.ts"; -import type { DefaultBatcherInput } from "../core/types.ts"; -import { - EvmBatchBuilderLogic, - type EvmBatchPayload, - parseEvmBatcherInput, -} from "../batch-data-builder/evm-builder-logic.ts"; - -export interface HardhatArtifact { - contractName: string; - abi: Abi; - bytecode?: `0x${string}`; - deployedBytecode?: `0x${string}`; -} - -export interface EvmContractAdapterConfig { - contractAddress: `0x${string}`; - privateKey: `0x${string}`; - syncProtocolName: string; - artifact: HardhatArtifact; - chain?: Chain; - rpcUrl?: string; - maxBatchSize?: number; -} - -type NormalizedPayload = { - method: string; - args: unknown[]; - value?: bigint; -}; - -function isViewFunction(fn: AbiFunction): boolean { - return fn.stateMutability === "view" || fn.stateMutability === "pure"; -} - -export class EvmContractAdapter - implements BlockchainAdapter { - private readonly contractAddress: `0x${string}`; - private readonly syncProtocolName: string; - private readonly abi: Abi; - private readonly account: Account; - private readonly publicClient: PublicClient; - private readonly walletClient: WalletClient; - private readonly maxBatchSize: number; - - private readonly builder = new EvmBatchBuilderLogic(); - - constructor(config: EvmContractAdapterConfig) { - this.contractAddress = config.contractAddress; - this.syncProtocolName = config.syncProtocolName; - this.abi = config.artifact.abi; - this.maxBatchSize = config.maxBatchSize ?? 10000; - - this.account = privateKeyToAccount(config.privateKey); - - const transport = http(config.rpcUrl); - const chain = config.chain ?? chains.hardhat; - - this.publicClient = createPublicClient({ - chain, - transport, - }); - - this.walletClient = createWalletClient({ - account: this.account, - chain, - transport, - }); - } - - getSyncProtocolName(): string { - return this.syncProtocolName; - } - - getAccountAddress(): string { - return this.account.address; - } - - getChainName(): string { - return this.publicClient.chain?.name ?? "EVM Chain"; - } - - isReady(): boolean { - return Boolean(this.publicClient && this.walletClient); - } - - async getBlockNumber(): Promise { - return await this.publicClient.getBlockNumber(); - } - - buildBatchData( - inputs: DefaultBatcherInput[], - _options?: BatchBuildingOptions, - ): BatchBuildingResult | null { - const options = { maxSize: this.maxBatchSize }; - return this.builder.buildBatchData(inputs, options); - } - - async estimateBatchFee( - data: EvmBatchPayload | null, - ): Promise { - if (!data || data.payloads.length === 0) { - return 0n; - } - - const payload = this.normalizePayload(data.payloads[0]); - const fn = this.findMatchingFunction(payload.method, payload.args); - - if (isViewFunction(fn)) return 0n; - - const gas = await this.publicClient.estimateContractGas({ - account: this.account, - address: this.contractAddress, - abi: this.abi, - functionName: payload.method, - args: payload.args as any[], - value: payload.value, - }); - - const fees = await this.publicClient.estimateFeesPerGas(); - const gasPrice = fees.maxFeePerGas ?? fees.gasPrice ?? 0n; - return gas * gasPrice; - } - - async submitBatch( - data: EvmBatchPayload | null, - _fee?: string | bigint, - ): Promise { - if (!data || data.payloads.length === 0) { - throw new Error("EVM batch payload contained no invocations"); - } - - if (data.payloads.length > 1) { - console.warn( - `EvmContractAdapter received ${data.payloads.length} invocations. Only the first will be processed.`, - ); - } - - const payload = this.normalizePayload(data.payloads[0]); - console.log( - `[EvmAdapter] Submitting payload for ${this.contractAddress} ::`, - payload, - ); - const fn = this.findMatchingFunction(payload.method, payload.args); - - if (isViewFunction(fn)) { - const result = await this.publicClient.readContract({ - address: this.contractAddress, - abi: this.abi, - functionName: payload.method, - args: payload.args as any[], - }); - console.log( - `[EvmAdapter] Pure/view call result for ${payload.method}:`, - result, - ); - return `query:${payload.method}:${JSON.stringify(result)}`; - } - - console.log( - "[EvmAdapter] Calling impure method", - payload.method, - "with args", - payload.args, - "value", - payload.value, - ); - const hash = await this.walletClient.writeContract({ - account: this.account, - address: this.contractAddress, - abi: this.abi, - functionName: payload.method, - args: payload.args as any[], - value: payload.value, - chain: this.walletClient.chain, - }); - console.log("[EvmAdapter] Submitted tx hash:", hash); - - return hash; - } - - async waitForTransactionReceipt( - hash: BlockchainHash, - timeout?: number, - ): Promise { - console.log(`[EvmAdapter] Waiting for receipt: ${hash}`); - const receipt = await this.publicClient.waitForTransactionReceipt({ - hash: hash as Hash, - timeout, - }); - console.log( - `[EvmAdapter] Receipt confirmed for ${hash}:`, - receipt.status, - "block", - receipt.blockNumber, - ); - - return { - hash, - blockNumber: receipt.blockNumber, - status: receipt.status === "success" ? 1 : 0, - _viemReceipt: receipt, - }; - } - - async validateInput( - input: DefaultBatcherInput, - ): Promise { - try { - const payload = parseEvmBatcherInput(input); - const normalized: NormalizedPayload = { - method: payload.method, - args: payload.args, - value: payload.value !== undefined ? BigInt(payload.value) : undefined, - }; - - const fn = this.findMatchingFunction( - normalized.method, - normalized.args, - ); - - if (normalized.value !== undefined && fn.stateMutability === "nonpayable") { - return { - valid: false, - error: `Function "${normalized.method}" is nonpayable but value was provided`, - }; - } - - // Attempt encoding to verify argument shapes - encodeFunctionData({ - abi: this.abi, - functionName: normalized.method, - args: normalized.args as any[], - }); - - return { valid: true }; - } catch (error) { - return { - valid: false, - error: error instanceof Error ? error.message : String(error), - }; - } - } - - private normalizePayload( - raw: EvmBatchPayload["payloads"][number], - ): NormalizedPayload { - return { - method: raw.method, - args: raw.args, - value: raw.value !== undefined ? BigInt(raw.value) : undefined, - }; - } - - private findMatchingFunction( - method: string, - args: unknown[], - ): AbiFunction { - const candidates = (this.abi ?? []).filter( - (item): item is AbiFunction => - item.type === "function" && item.name === method, - ); - - if (candidates.length === 0) { - throw new Error(`Function "${method}" not found in ABI`); - } - - for (const candidate of candidates) { - if (candidate.inputs.length !== args.length) { - continue; - } - - try { - encodeFunctionData({ - abi: [candidate], - functionName: method, - args: args as any[], - }); - return candidate; - } catch { - // Try next overload - } - } - - throw new Error( - `No ABI overload for "${method}" matches ${args.length} argument(s)`, - ); - } -} diff --git a/packages/client/batcher/vendor/batcher/adapters/midnight-adapter.ts b/packages/client/batcher/vendor/batcher/adapters/midnight-adapter.ts deleted file mode 100644 index 9fb58eb..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/midnight-adapter.ts +++ /dev/null @@ -1,1042 +0,0 @@ -// Midnight blockchain adapter for the EffectStream batcher -// Handles transaction submission to Midnight contracts via circuit invocation - -import type { - BlockchainAdapter, - BlockchainHash, - BlockchainTransactionReceipt, - ValidationResult, - BatchBuildingOptions, - BatchBuildingResult, -} from "./adapter.ts"; -import type { ContractInfo } from "./midnight-arg-parser.ts"; -import { parseCircuitArgs } from "./midnight-arg-parser.ts"; -import type { DefaultBatcherInput } from "../core/types.ts"; -import { MidnightBatchBuilderLogic, type MidnightBatchPayload } from "../batch-data-builder/midnight-builder-logic.ts"; -import { hexStringToUint8Array } from "jsr:@paimaexample/utils@^0.8.4"; -import type { - MidnightProvider, - UnboundTransaction, - WalletProvider, -} from "npm:@midnight-ntwrk/midnight-js-types@4.0.2"; -import type { - CoinPublicKey, - DustSecretKey, - EncPublicKey, - FinalizedTransaction, - TransactionId, - ZswapSecretKeys, -} from "npm:@midnight-ntwrk/ledger-v8@8.0.3"; -import { - type DeployedContract, - findDeployedContract, - type FoundContract, -} from "npm:@midnight-ntwrk/midnight-js-contracts@4.0.2"; -import { CompiledContract, ContractExecutable } from "npm:@midnight-ntwrk/compact-js@2.5.0"; -import { indexerPublicDataProvider } from "npm:@midnight-ntwrk/midnight-js-indexer-public-data-provider@4.0.2"; -import { httpClientProofProvider } from "npm:@midnight-ntwrk/midnight-js-http-client-proof-provider@4.0.2"; -import { NodeZkConfigProvider } from "npm:@midnight-ntwrk/midnight-js-node-zk-config-provider@4.0.2"; -import { levelPrivateStateProvider } from "npm:@midnight-ntwrk/midnight-js-level-private-state-provider@4.0.2"; -import { setNetworkId } from "npm:@midnight-ntwrk/midnight-js-network-id@4.0.2"; -import { - buildWalletFacade, - getInitialShieldedState, - syncAndWaitForFunds, - waitForDustFunds, - type NetworkUrls as MidnightNetworkUrls, -} from "jsr:@paimaexample/midnight-contracts@^0.8.4/wallet-info"; -import type { WalletResult } from "jsr:@paimaexample/midnight-contracts@^0.8.4/types"; -import type { NetworkId as WalletNetworkId } from "npm:@midnight-ntwrk/wallet-sdk-abstractions@1.0.0"; - -export interface MidnightAdapterConfig { - indexer: string; - indexerWS: string; - node: string; - proofServer: string; - zkConfigPath: string; - privateStateStoreName: string; // LevelDB store name (local) - privateStateId?: string; // Contract private state ID (on-chain), defaults to privateStateStoreName if not provided - contractJoinTimeoutSeconds?: number; // Defaults to 120 seconds - walletFundingTimeoutSeconds?: number; // Defaults to 180 seconds - walletNetworkId?: WalletNetworkId.NetworkId; // Optional override for modular wallet network id - contractTag?: string; // Tag for CompiledContract (e.g. "go-fish-contract") -} - -const TTL_DURATION_MS = 60 * 60 * 1000; -const createTtl = (): Date => new Date(Date.now() + TTL_DURATION_MS); - -/** - * Midnight blockchain adapter implementing BlockchainAdapter interface - * Enables batcher to submit transactions by invoking Compact contract circuits - */ -export class MidnightAdapter implements BlockchainAdapter { - private readonly contractAddress: string; - private readonly config: MidnightAdapterConfig; - private readonly contractInfo: ContractInfo; - private readonly syncProtocolName: string; - public readonly maxBatchSize?: number; - - // Private helper for building batch data - private readonly batchBuilderLogic = new MidnightBatchBuilderLogic(); - - private walletResult: WalletResult | null = null; - private walletProvider: (WalletProvider & MidnightProvider) | null = null; - private deployedContract: any = null; - private publicDataProvider: any | null = null; - private hasFunds = false; - private lastFundingBalances: - | { shieldedBalance: bigint; unshieldedBalance: bigint; dustBalance: bigint } - | null = null; - private isInitialized = false; - private initializationPromise: Promise | null = null; - private walletAddress: string | null = null; - private contractJoined = false; - private contractJoiningPromise: Promise | null = null; - private contractInstance: any = null; - private compiledContract: any = null; - private witnesses: any = null; - private contractTag: string = ""; - private readonly contractJoinTimeoutMs: number; - private readonly walletFundingTimeoutMs: number; - private readonly walletNetworkId: WalletNetworkId.NetworkId; - - constructor( - contractAddress: string, - walletSeed: string, - config: MidnightAdapterConfig, - contractInstance: any, - witnesses: any, - contractInfo: ContractInfo, - syncProtocolName: string, - maxBatchSize: number = 10000, - compiledContract?: any, - ) { - this.contractAddress = contractAddress; - this.config = config; - this.contractInfo = contractInfo; - this.syncProtocolName = syncProtocolName; - this.maxBatchSize = maxBatchSize; - this.contractJoinTimeoutMs = (config.contractJoinTimeoutSeconds ?? 120) * 1000; - this.walletFundingTimeoutMs = (config.walletFundingTimeoutSeconds ?? 180) * 1000; - this.walletNetworkId = config.walletNetworkId ?? "undeployed" as WalletNetworkId.NetworkId; - - // Store contract info for lazy joining - this.contractInstance = contractInstance; - this.witnesses = witnesses; - - // Build CompiledContract using compact-js from the SAME module that findDeployedContract uses. - // This is critical: Symbol-keyed internal properties must come from the same module instance. - if (compiledContract) { - this.compiledContract = compiledContract; - } else { - const tag = config.contractTag ?? "contract"; - this.contractTag = tag; - try { - // contractInstance may be a class (constructor) or an instance. - // CompiledContract.make() expects a constructor. - const ctor = typeof contractInstance === "function" - ? contractInstance - : contractInstance.constructor; - - // deno-lint-ignore no-explicit-any - let cc: any = CompiledContract.make(tag, ctor); - // deno-lint-ignore no-explicit-any - cc = (CompiledContract as any).withWitnesses(cc, witnesses); - if (config.zkConfigPath) { - // deno-lint-ignore no-explicit-any - cc = (CompiledContract as any).withCompiledFileAssets(cc, config.zkConfigPath); - } - - // Verify the built CompiledContract works - const exec = ContractExecutable.make(cc); - const circuitIds = exec.getImpureCircuitIds(); - console.log(`✅ Built CompiledContract for tag "${tag}" with ${circuitIds.length} impure circuits`); - - this.compiledContract = cc; - } catch (err) { - console.warn("⚠️ Failed to build CompiledContract in vendor adapter:", err); - this.compiledContract = null; - } - } - - // Start async initialization but don't await - this.initializationPromise = this.initialize(walletSeed); - } - - /** - * Initialize wallet and providers, wait for funds, and join contract - * This ensures the adapter is fully ready before accepting transactions - */ - private async initialize(walletSeed: string): Promise { - try { - // Use lowercase network ID to match the wallet SDK expectations - // This is consistent with the working e2e tests and manual scripts - setNetworkId(this.walletNetworkId as any); - - console.log("🔗 Building Midnight wallet (modular SDK)..."); - - const networkUrls: MidnightNetworkUrls = { - indexer: this.config.indexer, - indexerWS: this.config.indexerWS, - node: this.config.node, - proofServer: this.config.proofServer, - }; - - this.walletResult = await buildWalletFacade( - networkUrls, - walletSeed, - this.walletNetworkId, - ); - - const initialState = await getInitialShieldedState( - this.walletResult.wallet.shielded, - ); - this.walletAddress = initialState.address.coinPublicKeyString(); - console.log("✅ Wallet built and sync started"); - console.log(`📍 Batcher wallet address: ${this.walletAddress}`); - console.log(`📍 Batcher dust address: ${this.walletResult.dustAddress}`); - console.log(`🔑 Coin public key: ${initialState.address.coinPublicKeyString()}`); - console.log( - `🛡️ Encryption public key: ${initialState.address.encryptionPublicKeyString()}`, - ); - - this.walletProvider = this.createWalletAndMidnightProvider( - this.walletResult, - ); - - this.publicDataProvider = indexerPublicDataProvider( - this.config.indexer, - this.config.indexerWS, - ); - - // Wait for wallet to be funded and synced before starting batcher - console.log( - "💰 Waiting for wallet to be funded and synced before starting batcher...", - ); - await this.ensureFunds(); - - // NOTE: We skip joining the contract during initialization to avoid long startup times - // The contract will be joined lazily when the first transaction is submitted - console.log("⚠️ Contract join deferred until first transaction (lazy join)"); - - console.log("✅ Midnight adapter fully initialized and ready!"); - - this.isInitialized = true; - } catch (error) { - console.error("❌ Failed to initialize Midnight adapter:", error); - throw error; - } - } - - /** - * Join the contract lazily (after wallet is synced and ready) - * This mirrors what the interact script does - */ - private async ensureContractJoined(): Promise { - // If already joined, return immediately - if (this.contractJoined) { - return; - } - - // If already joining, wait for existing join to complete - if (this.contractJoiningPromise) { - console.log("⏳ Contract join already in progress, waiting..."); - await this.contractJoiningPromise; - return; - } - - // Guard against concurrent join attempts - if (!this.walletResult || !this.walletProvider) { - throw new Error("Cannot join contract: wallet not initialized"); - } - - // Start the join process - this.contractJoiningPromise = (async () => { - try { - console.log("⚙️ Configuring providers for contract join..."); - - const walletAndMidnightProvider = this.walletProvider!; - - // For the batcher, we use minimal private state config. - // We provide privateStateStoreName but omit midnightDbName to use in-memory storage. - // This avoids persisting/syncing historical private state which can take minutes and timeout. - // The batcher only needs to submit transactions, not read historical private state. - const zkConfigProvider = new NodeZkConfigProvider(this.config.zkConfigPath); - const providers = { - privateStateProvider: levelPrivateStateProvider({ - privateStateStoreName: this.config.privateStateStoreName, - walletProvider: walletAndMidnightProvider, - } as any), - publicDataProvider: this.publicDataProvider, - zkConfigProvider, - proofProvider: httpClientProofProvider(this.config.proofServer, zkConfigProvider), - walletProvider: walletAndMidnightProvider, - midnightProvider: walletAndMidnightProvider, - }; - - console.log("🔗 Joining contract at address:", this.contractAddress); - - // Check if indexer is responding before attempting to join - try { - console.log("🔍 Checking indexer health..."); - const blockQuery = `query { block { height } }`; - const healthResponse = await fetch(this.config.indexer, { - method: "POST", - body: JSON.stringify({ query: blockQuery }), - headers: { "Content-Type": "application/json" }, - }); - if (!healthResponse.ok) { - throw new Error(`Indexer returned ${healthResponse.status}`); - } - const healthData = await healthResponse.json(); - console.log(`✅ Indexer is responding. Current block: ${healthData.data?.block?.height || "unknown"}`); - } catch (error) { - console.error("❌ Indexer health check failed:", error); - throw new Error(`Cannot join contract: Midnight indexer is not responding at ${this.config.indexer}`); - } - - // Use privateStateId if provided, otherwise fall back to privateStateStoreName - const privateStateId = this.config.privateStateId ?? - this.config.privateStateStoreName; - console.log(`🔑 Using privateStateId: ${privateStateId}`); - - // With minimal private state config, joining should be fast (no historical sync needed) - // But we still keep a timeout as a safety measure - const contractJoinTimeoutSeconds = Math.round(this.contractJoinTimeoutMs / 1000); - console.log(`⏱️ Contract join timeout: ${contractJoinTimeoutSeconds}s`); - console.log("🔍 Starting findDeployedContract..."); - console.log(`🔍 compiledContract available: ${!!this.compiledContract}, tag: ${this.contractTag}`); - - const joinStartTime = Date.now(); - this.deployedContract = await Promise.race([ - (async () => { - // SDK v3 uses compiledContract; fall back to legacy contract for v2 - const contractOption = this.compiledContract - ? { compiledContract: this.compiledContract } - : { contract: this.contractInstance }; - const result = await findDeployedContract(providers, { - contractAddress: this.contractAddress, - ...contractOption, - privateStateId: privateStateId, - initialPrivateState: {}, - }); - const joinDuration = Math.round((Date.now() - joinStartTime) / 1000); - console.log(`✅ findDeployedContract completed in ${joinDuration}s`); - return result; - })(), - new Promise((_, reject) => - setTimeout(() => { - const elapsed = Math.round((Date.now() - joinStartTime) / 1000); - reject(new Error( - `Timeout: Contract join operation did not complete within ${contractJoinTimeoutSeconds} seconds ` + - `(elapsed: ${elapsed}s). This indicates the Midnight indexer/node is not responding properly ` + - "or there's an issue with private state synchronization even with minimal config." - )); - }, this.contractJoinTimeoutMs) - ) - ]); - - console.log("✅ Contract joined successfully"); - - // With empty private state config, no sync is needed - ready immediately - this.contractJoined = true; - } catch (error) { - console.error("❌ Failed to join contract:", error); - this.contractJoiningPromise = null; // Reset so it can be retried - throw error; - } - })(); - - await this.contractJoiningPromise; - } - - /** - * Create wallet and midnight provider wrapper - */ - private createWalletAndMidnightProvider( - walletResult: WalletResult, - ): WalletProvider & MidnightProvider { - const { - wallet, - zswapSecretKeys, - walletZswapSecretKeys, - dustSecretKey, - walletDustSecretKey, - unshieldedKeystore, - } = walletResult; - - return { - getCoinPublicKey(): CoinPublicKey { - return zswapSecretKeys.coinPublicKey; - }, - getEncryptionPublicKey(): EncPublicKey { - return zswapSecretKeys.encryptionPublicKey; - }, - async balanceTx( - tx: UnboundTransaction, - ttl?: Date, - ): Promise { - // Use balanceUnboundTransaction for proven transactions (output of proveTx). - // balanceFinalizedTransaction + signRecipe fails because signRecipe internally - // clones intents with hardcoded 'pre-proof' markers, but proven transactions - // have 'proof' markers — causing "expected header tag 'pre-proof', got 'proof'". - // Fix: balance the unbound tx, then sign ONLY the balancing tx (which is still - // unproven), leaving the base proven transaction untouched. - const recipe = await wallet.balanceUnboundTransaction(tx, { - shieldedSecretKeys: walletZswapSecretKeys, - dustSecretKey: walletDustSecretKey, - }, { ttl: ttl ?? createTtl() }); - - if (recipe.balancingTransaction) { - const signed = await wallet.signUnprovenTransaction( - recipe.balancingTransaction, - (payload: Uint8Array) => unshieldedKeystore.signData(payload), - ); - return wallet.finalizeRecipe({ ...recipe, balancingTransaction: signed }); - } - return wallet.finalizeRecipe(recipe); - }, - submitTx(tx: FinalizedTransaction): Promise { - return wallet.submitTransaction(tx); - }, - }; - } - - /** - * Wait for wallet to be synced and have funds (called lazily on first transaction) - */ - private async ensureFunds(): Promise { - if (this.hasFunds || !this.walletResult) { - // Even if we previously had funds, make sure dust is still available - if (this.walletResult && (!this.lastFundingBalances || this.lastFundingBalances.dustBalance === 0n)) { - try { - const dust = await waitForDustFunds( - this.walletResult.wallet, - this.walletFundingTimeoutMs, - ); - if (this.lastFundingBalances) { - this.lastFundingBalances.dustBalance = dust; - } else { - this.lastFundingBalances = { - shieldedBalance: 0n, - unshieldedBalance: 0n, - dustBalance: dust, - }; - } - if (dust > 0n) { - this.hasFunds = true; - } - } catch (_err) { - // If dust still not available, keep existing state; callTx will log balances - } - } - return; - } - - console.log("💰 Checking wallet sync and balance with modular SDK..."); - - try { - const balances = await syncAndWaitForFunds(this.walletResult.wallet, { - timeoutMs: this.walletFundingTimeoutMs, - }); - // If dust is missing but we have unshielded funds, try to sync dust explicitly - if (balances.dustBalance === 0n && balances.unshieldedBalance > 0n) { - try { - const dust = await waitForDustFunds( - this.walletResult.wallet, - this.walletFundingTimeoutMs, - ); - balances.dustBalance = dust; - } catch (_err) { - // keep dustBalance as-is; will be logged on failure - } - } - this.lastFundingBalances = { - shieldedBalance: balances.shieldedBalance, - unshieldedBalance: balances.unshieldedBalance, - // fallback to 0n if older syncAndWaitForFunds doesn't return dustBalance - dustBalance: balances.dustBalance ?? 0n, - }; - console.log("✅ Wallet fully synced and funded"); - this.hasFunds = true; - } catch (error) { - throw new Error( - `Failed to ensure wallet funds: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - - public buildBatchData( - inputs: DefaultBatcherInput[], - _options?: BatchBuildingOptions, - ): BatchBuildingResult | null { - const options = { - maxSize: this.maxBatchSize, - // Midnight can only process one circuit call per transaction, so limit to 1 input per batch. - // Without this, multiple inputs get selected but only the first is executed, - // while all get reported as successful (resolving their callbacks with the same receipt). - maxInputs: 1, - }; - // Cast is safe because we know our helper returns this type - return this.batchBuilderLogic.buildBatchData(inputs, options) as BatchBuildingResult | null; - } - - /** - * Cleanup method to properly release resources - * Should be called when the adapter is being destroyed/shutdown - */ - public async cleanup(): Promise { - console.log("🧹 Cleaning up Midnight adapter resources..."); - - // Close wallet - if (this.walletResult?.wallet) { - try { - await this.walletResult.wallet.stop(); - console.log("✅ Wallet stopped"); - } catch (error) { - console.warn("⚠️ Error stopping wallet:", error); - } - } - - // Note: The deployedContract and privateStateProvider don't have explicit close methods - // The LevelDB connections are managed per-operation by levelPrivateStateProvider - // However, we should allow a small delay for any pending async operations - console.log("⏳ Waiting for pending operations to complete..."); - await new Promise(resolve => setTimeout(resolve, 1000)); - console.log("✅ Midnight adapter cleanup complete"); - } - - /** - * Submit a batch transaction to the Midnight contract - */ - async submitBatch( - data: MidnightBatchPayload | null, - fee?: string | bigint, - ): Promise { - if (this.initializationPromise) { - await this.initializationPromise; - this.initializationPromise = null; - } - - if (!this.isInitialized || !this.walletResult) { - throw new Error("Midnight adapter not initialized"); - } - - // Ensure wallet has funds (lazy check) - await this.ensureFunds(); - - // Join contract AFTER wallet is ready (lazy join) - await this.ensureContractJoined(); - - if (!this.deployedContract) { - throw new Error("Failed to join contract"); - } - - try { - - if (!data || !data.payloads || data.payloads.length === 0) { - throw new Error("Batch payload contained no invocations"); - } - - if (data.payloads.length > 1) { - console.warn( - `⚠️ Midnight adapter received ${data.payloads.length} invocations in a single batch. ` + - "Currently only the first invocation will be processed.", - ); - } - - const { circuit, args } = data.payloads[0]; - - // Check if circuit is pure (read-only query) or impure (state-changing transaction) - const circuitDef = this.contractInfo.circuits.find((c) => - c.name === circuit - ); - if (!circuitDef) { - throw new Error( - `Circuit "${circuit}" not found in contract. Available circuits: ${ - this.contractInfo.circuits.map((c) => c.name).join(", ") - }`, - ); - } - - console.log( - `🔄 Invoking circuit "${circuit}" with ${args.length} arguments`, - ); - - const parsedArgs = parseCircuitArgs( - circuit, - args, - this.contractInfo, - ); - - console.log( - `🔍 Circuit "${circuit}" is ${ - circuitDef.pure ? "PURE (query)" : "IMPURE (transaction)" - }`, - ); - console.log("🔄 Parsed arguments:", parsedArgs); - - let result; - - if (circuitDef.pure) { - // Pure circuit - use call (local query, no transaction) - console.log("📖 Calling pure circuit (read-only query)..."); - try { - const queryResult = await this.deployedContract.call[circuit]( - ...parsedArgs, - ); - console.log("✅ Pure circuit query succeeded! Result:", queryResult); - - // For pure circuits, we return a fake transaction ID with the result encoded - // Since the batcher expects a hash, we'll return a special format - return `query:${circuit}:${JSON.stringify(queryResult)}`; - } catch (callError) { - console.error("❌ Pure circuit call threw an error:"); - console.error( - " Error message:", - callError instanceof Error ? callError.message : String(callError), - ); - throw callError; - } - } else { - // Impure circuit - use callTx (submit transaction) - console.log("📝 Calling impure circuit (transaction)..."); - console.log("🔄 deployedContract type:", typeof this.deployedContract); - console.log("🔄 callTx available?:", !!this.deployedContract?.callTx); - console.log( - "🔄 circuit method available?:", - !!this.deployedContract?.callTx?.[circuit], - ); - - try { - result = await this.deployedContract.callTx[circuit]( - ...parsedArgs, - ); - } catch (callTxError) { - console.error("❌ callTx threw an error:"); - console.error(" Error type:", typeof callTxError); - console.error( - " Error message:", - callTxError instanceof Error - ? callTxError.message - : String(callTxError), - ); - console.error( - " Error stack:", - callTxError instanceof Error ? callTxError.stack : "N/A", - ); - if (this.lastFundingBalances) { - console.error( - ` Last synced balances -> shielded: ${this.lastFundingBalances.shieldedBalance.toString()}, unshielded: ${this.lastFundingBalances.unshieldedBalance.toString()}, dust: ${this.lastFundingBalances.dustBalance.toString()}`, - ); - } else { - console.error(" Last synced balances: unavailable (ensureFunds did not complete)"); - } - throw callTxError; // Re-throw to be caught by outer catch - } - - // Check if result has public.txHash (FinalizedTxData) or needs balancing - if (result && result.public && result.public.txHash) { - const txHash = result.public.txHash; - console.log( - `🚀 Circuit invoked successfully! Transaction Hash: ${txHash}`, - ); - return txHash; - } else { - // Maybe it's an UnbalancedTransaction that needs balancing - console.log( - "🔄 Result doesn't have public.txHash, might need balancing:", - result, - ); - throw new Error( - "Transaction result format unexpected - may need balancing", - ); - } - } - } catch (error) { - console.error("❌ Failed to submit batch:", error); - throw new Error( - `Failed to submit batch: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - - /** - * Wait for a transaction to be confirmed on the blockchain - */ - async waitForTransactionReceipt( - hash: BlockchainHash, - timeout: number = 300000, - ): Promise { - if (!this.publicDataProvider) { - throw new Error("Public data provider not initialized"); - } - - console.log(`⏳ Waiting for transaction confirmation: ${hash}`); - - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - try { - const txInfo = await this.queryTransactionStatus(hash); - - if (txInfo && txInfo.confirmed) { - console.log( - `✅ Transaction confirmed! Block: ${txInfo.blockNumber}, Hash: ${hash}`, - ); - - return { - hash, - blockNumber: txInfo.blockNumber, - status: 1, // Success - _midnightTxInfo: txInfo, - }; - } - } catch (error) { - console.warn(`Failed to query transaction status: ${error}`); - } - - // Wait before next poll - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - throw new Error(`Transaction confirmation timeout after ${timeout}ms`); - } - - /** - * Query transaction status from indexer using GraphQL API - * - * Note: The Midnight indexer v3 schema does not have an `applyStage` field. - * Instead, we check if the transaction is included in a block, which indicates - * successful execution. Transactions that fail validation are not included in blocks. - */ - private async queryTransactionStatus( - txId: string, - ): Promise<{ confirmed: boolean; blockNumber: bigint } | null> { - if (!this.publicDataProvider) { - throw new Error("Public data provider not initialized"); - } - - try { - // Normalize hash format - ensure it's lowercase and proper length - let normalizedHash = txId.toLowerCase().replace(/^0x/, ""); - - // Midnight TransactionId is 72 hex chars (288 bits), but GraphQL expects 64 (256 bits) - // The actual transaction hash appears to be in the last 64 characters - if (normalizedHash.length > 64) { - normalizedHash = normalizedHash.slice(-64); - } else if (normalizedHash.length < 64) { - normalizedHash = normalizedHash.padStart(64, "0"); - } - - console.log( - `Querying transaction: original=${txId}, normalized=${normalizedHash}`, - ); - - // Query the indexer for transaction details by hash - // The v3 indexer schema uses `transactions(offset: { hash })` and returns - // transaction with block info. If a transaction is included in a block, - // it's considered confirmed (failed transactions are rejected before inclusion). - const query = `query ($hash: String!) { - transactions(offset: { hash: $hash }) { - hash - block { - height - } - } - }`; - - const response = await this.gqlQuery(query, { hash: normalizedHash }); - - if ( - !response || !response.transactions || - response.transactions.length === 0 - ) { - // Transaction not found yet - return null; - } - - const tx = response.transactions[0]; - - if (!tx.block) { - // Transaction exists but not yet included in a block - return null; - } - - // Transaction is confirmed if it's included in a block - // In Midnight, transactions that fail validation are rejected before inclusion, - // so presence in a block indicates successful execution - return { - confirmed: true, - blockNumber: BigInt(tx.block.height), - }; - } catch (error) { - console.warn(`Failed to query transaction ${txId}:`, error); - return null; - } - } - - /** - * Execute a GraphQL query against the Midnight indexer - */ - private async gqlQuery( - query: string, - variables?: Record, - ): Promise { - const response = await fetch(this.config.indexer, { - method: "POST", - body: JSON.stringify({ query, variables }), - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error( - `GraphQL query failed: ${response.status} ${response.statusText}`, - ); - } - - const body = await response.json(); - - if (body.errors) { - throw new Error( - `GraphQL query returned errors: ${JSON.stringify(body.errors)}`, - ); - } - - if (!body.data) { - throw new Error("GraphQL query returned no data"); - } - - return body.data; - } - - /** - * Get the current account/address for this adapter - */ - getAccountAddress(): string { - if (!this.walletResult || !this.walletAddress) { - throw new Error("Wallet not initialized"); - } - return this.walletAddress; - } - - /** - * Get the current chain name or identifier - */ - getChainName(): string { - return `Midnight (${this.walletNetworkId})`; - } - - /** - * Estimate the fee for submitting a batch - */ - estimateBatchFee(data: MidnightBatchPayload | null): bigint { - // Midnight uses native token for fees - // For now, return 0 as fees are handled by the wallet - return 0n; - } - - /** - * Check if the adapter is ready to submit transactions - */ - isReady(): boolean { - return this.isInitialized && this.walletResult !== null; - } - - /** - * Get the block number of the latest confirmed block - */ - async getBlockNumber(): Promise { - if (!this.publicDataProvider) { - throw new Error("Public data provider not initialized"); - } - - try { - // Query latest block from indexer using GraphQL - const query = `query { - block { - height - } - }`; - - const response = await this.gqlQuery(query); - - if (!response || !response.block || response.block.height === undefined) { - throw new Error("Failed to get block from indexer"); - } - - return BigInt(response.block.height); - } catch (error) { - throw new Error( - `Failed to get block number: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - - /** - * Get the sync protocol name for this adapter - */ - getSyncProtocolName(): string { - return this.syncProtocolName; - } - - private parseBatchPayload( - data: string, - ): Array<{ circuit: string; args: any[] }> { - const decoded = this.decodeHexString(data); - let payload: unknown; - try { - payload = JSON.parse(decoded); - } catch (error) { - throw new Error( - `Failed to parse Midnight batch payload JSON: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - - if (!payload || typeof payload !== "object" || Array.isArray(payload)) { - throw new Error("Invalid Midnight batch payload structure"); - } - - const { prefix, payloads } = payload as { - prefix: unknown; - payloads: Array<{ - circuit: unknown; - args: unknown; - addressType?: unknown; - address?: unknown; - signature?: unknown; - timestamp?: unknown; - }>; - }; - - if (prefix !== "&B") { - throw new Error(`Invalid batch prefix: expected "&B", got "${prefix}"`); - } - - if (!Array.isArray(payloads)) { - throw new Error( - "Invalid Midnight batch payload structure: missing payloads array", - ); - } - - const sanitized = payloads.map((entry, index) => { - if (!entry || typeof entry.circuit !== "string") { - throw new Error(`Invalid circuit name at index ${index}`); - } - - if (!Array.isArray(entry.args)) { - throw new Error(`Invalid circuit args at index ${index}`); - } - - return { circuit: entry.circuit, args: entry.args }; - }); - - return sanitized; - } - - private decodeHexString(hex: string): string { - const normalized = hex.startsWith("0x") ? hex.slice(2) : hex; - try { - return new TextDecoder().decode(hexStringToUint8Array(normalized)); - } catch (error) { - throw new Error( - `Failed to decode Midnight batch payload hex: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - - public verifySignature(input: DefaultBatcherInput): boolean { - // Midnight inputs are not signed in a way the core batcher understands. - // The adapter is responsible for this logic (e.g., inside the circuit). - // We return true to bypass this check, matching the previous hardcoded behavior. - return true; - } - - public validateInput( - input: DefaultBatcherInput, - ): ValidationResult { - try { - // 1. Decode the raw input - const decodedInput = this.decodeHexIfNeeded(input.input); - - // 2. Shallow Parse (from MidnightBatchDataBuilder) - let parsed: any; - try { - parsed = JSON.parse(decodedInput); - } catch (error) { - return { - valid: false, - error: "Input is not valid JSON", - }; - } - - if ( - !parsed || typeof parsed !== "object" || - typeof parsed.circuit !== "string" || !Array.isArray(parsed.args) - ) { - return { - valid: false, - error: - "Invalid input structure. Expected { circuit: string, args: [] }", - }; - } - - // 3. Deep Parse (from submitBatch) - const circuitDef = this.contractInfo.circuits.find((c) => - c.name === parsed.circuit - ); - if (!circuitDef) { - return { - valid: false, - error: `Circuit "${parsed.circuit}" not found. Available: ${ - this.contractInfo.circuits.map((c) => c.name).join(", ") - }`, - }; - } - - // 4. Validate arguments - // parseCircuitArgs will throw if validation fails - parseCircuitArgs( - parsed.circuit, - parsed.args, - this.contractInfo, - ); - - return { valid: true }; - } catch (error) { - return { - valid: false, - error: error instanceof Error - ? error.message - : "Unknown validation error", - }; - } - } - - private decodeHexIfNeeded(value: string): string { - if (/^0x[0-9a-fA-F]+$/.test(value)) { - return new TextDecoder().decode(hexStringToUint8Array(value.slice(2))); - } - // Also handle hex without 0x prefix, if needed - if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) { - try { - return new TextDecoder().decode(hexStringToUint8Array(value)); - } catch { - // Fallback to original value if not valid hex - } - } - return value; - } -} diff --git a/packages/client/batcher/vendor/batcher/adapters/midnight-arg-parser.ts b/packages/client/batcher/vendor/batcher/adapters/midnight-arg-parser.ts deleted file mode 100644 index ac18c8c..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/midnight-arg-parser.ts +++ /dev/null @@ -1,315 +0,0 @@ -// Type parser for converting JSON representations to Midnight-compatible types -// based on contract-info.json circuit definitions - -export interface ContractInfo { - circuits: CircuitDefinition[]; - witnesses: any[]; - contracts: any[]; -} - -export interface CircuitDefinition { - name: string; - pure: boolean; - arguments: ArgumentDefinition[]; - "result-type": TypeDefinition; -} - -export interface ArgumentDefinition { - name: string; - type: TypeDefinition; -} - -export interface TypeDefinition { - "type-name": string; - maxval?: string; - length?: number; - tsType?: string; - name?: string; - elements?: ArgumentDefinition[]; - types?: TypeDefinition[]; -} - -/** - * Parse circuit arguments from JSON format to Midnight-compatible types - * @param circuitName - Name of the circuit to invoke - * @param argsJson - Array of JSON arguments - * @param contractInfo - Contract metadata from contract-info.json - * @returns Parsed arguments ready for circuit invocation - */ -export function parseCircuitArgs( - circuitName: string, - argsJson: any[], - contractInfo: ContractInfo, -): any[] { - // Find circuit definition - const circuitDef = contractInfo.circuits.find((c) => c.name === circuitName); - if (!circuitDef) { - throw new Error( - `Circuit "${circuitName}" not found in contract. Available circuits: ${ - contractInfo.circuits.map((c) => c.name).join(", ") - }`, - ); - } - - // Validate argument count - if (argsJson.length !== circuitDef.arguments.length) { - throw new Error( - `Circuit "${circuitName}" expects ${circuitDef.arguments.length} arguments, but got ${argsJson.length}`, - ); - } - - // Parse each argument according to its type definition - return circuitDef.arguments.map((argDef, index) => { - try { - return parseValue(argsJson[index], argDef.type); - } catch (error) { - throw new Error( - `Failed to parse argument "${argDef.name}" (index ${index}) for circuit "${circuitName}": ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - }); -} - -/** - * Parse a value according to its type definition - */ -function parseValue(value: any, typeSpec: TypeDefinition): any { - const typeName = typeSpec["type-name"]; - - switch (typeName) { - case "Uint": - return parseUint(value, typeSpec); - case "Bytes": - return parseBytes(value, typeSpec.length!); - case "Boolean": - return parseBoolean(value); - case "Opaque": - return parseString(value); - case "Struct": - return parseStruct(value, typeSpec); - case "Tuple": - return parseTuple(value, typeSpec); - default: - throw new Error(`Unsupported type: ${typeName}`); - } -} - -/** - * Parse Uint type to bigint - * Validates non-negative values - */ -export function parseUint( - value: string | number, - typeSpec: TypeDefinition, -): bigint { - if (typeof value === "string") { - // Handle string representation of bigint - try { - const parsed = BigInt(value); - if (parsed < 0n) { - throw new Error("Uint values must be non-negative"); - } - return parsed; - } catch (error) { - throw new Error( - `Invalid Uint value "${value}": ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } else if (typeof value === "number") { - if (value < 0) { - throw new Error("Uint values must be non-negative"); - } - if (!Number.isInteger(value)) { - throw new Error("Uint values must be integers"); - } - return BigInt(value); - } else { - throw new Error( - `Uint value must be a string or number, got ${typeof value}`, - ); - } -} - -/** - * Parse Bytes type from hex string to Uint8Array - * Validates hex format and length - */ -export function parseBytes(value: string, length: number): Uint8Array { - if (typeof value !== "string") { - throw new Error(`Bytes value must be a hex string, got ${typeof value}`); - } - - // Strip 0x prefix if present - let hex = value.toLowerCase(); - if (hex.startsWith("0x")) { - hex = hex.slice(2); - } - - // Validate hex format - if (!/^[0-9a-f]*$/.test(hex)) { - throw new Error(`Invalid hex string: "${value}"`); - } - - // Ensure even length - if (hex.length % 2 !== 0) { - hex = "0" + hex; - } - - // Convert to Uint8Array - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); - } - - // Validate length if specified - if (length !== undefined && bytes.length !== length) { - throw new Error( - `Bytes length mismatch: expected ${length} bytes, got ${bytes.length}`, - ); - } - - return bytes; -} - -/** - * Parse Boolean type - */ -export function parseBoolean(value: any): boolean { - if (typeof value !== "boolean") { - throw new Error(`Boolean value must be a boolean, got ${typeof value}`); - } - return value; -} - -/** - * Parse String type (Opaque) - */ -export function parseString(value: any): string { - if (typeof value !== "string") { - throw new Error(`String value must be a string, got ${typeof value}`); - } - return value; -} - -/** - * Parse Either struct type - * Validates required fields: is_left, left, right - */ -export function parseEither(value: any, typeSpec: TypeDefinition): any { - if (typeof value !== "object" || value === null) { - throw new Error(`Either value must be an object, got ${typeof value}`); - } - - // Validate required fields - if (!("is_left" in value)) { - throw new Error('Either type must have "is_left" field'); - } - if (!("left" in value)) { - throw new Error('Either type must have "left" field'); - } - if (!("right" in value)) { - throw new Error('Either type must have "right" field'); - } - - // Parse as struct - return parseStruct(value, typeSpec); -} - -/** - * Parse Struct type - * Recursively parses nested structures - */ -export function parseStruct(value: any, typeSpec: TypeDefinition): any { - if (typeof value !== "object" || value === null) { - throw new Error(`Struct value must be an object, got ${typeof value}`); - } - - if (!typeSpec.elements) { - throw new Error("Struct type definition missing elements"); - } - - const result: any = {}; - - // Parse each field according to its type definition - for (const element of typeSpec.elements) { - const fieldName = element.name; - - if (!(fieldName in value)) { - throw new Error(`Missing required field "${fieldName}" in struct`); - } - - try { - result[fieldName] = parseValue(value[fieldName], element.type); - } catch (error) { - throw new Error( - `Failed to parse field "${fieldName}": ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } - - return result; -} - -/** - * Parse Tuple type - * Handles empty tuples and array types - */ -export function parseTuple(value: any, typeSpec: TypeDefinition): any[] { - // Handle empty tuples - if (!typeSpec.types || typeSpec.types.length === 0) { - return []; - } - - if (!Array.isArray(value)) { - throw new Error(`Tuple value must be an array, got ${typeof value}`); - } - - if (value.length !== typeSpec.types.length) { - throw new Error( - `Tuple length mismatch: expected ${typeSpec.types.length} elements, got ${value.length}`, - ); - } - - return typeSpec.types.map((elementType, index) => { - try { - return parseValue(value[index], elementType); - } catch (error) { - throw new Error( - `Failed to parse tuple element at index ${index}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - }); -} - -/** - * Parse Array type - */ -export function parseArray(value: any, typeSpec: TypeDefinition): any[] { - if (!Array.isArray(value)) { - throw new Error(`Array value must be an array, got ${typeof value}`); - } - - // For arrays, we'd need the element type from the type spec - // This would be similar to Tuple but with variable length - // Implementation depends on how arrays are defined in contract-info.json - return value.map((item, index) => { - try { - // This is a placeholder - actual implementation depends on type spec structure - return item; - } catch (error) { - throw new Error( - `Failed to parse array element at index ${index}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - }); -} diff --git a/packages/client/batcher/vendor/batcher/adapters/mod.ts b/packages/client/batcher/vendor/batcher/adapters/mod.ts deleted file mode 100644 index e37ad9e..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/mod.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Blockchain Adapters Module - * - * Clean exports for all blockchain adapters and their interfaces. - * This module centralizes adapter-related imports for the batcher system. - */ - -// Base blockchain adapter interface and types -export type { - BlockchainAdapter, - BatchBuildingOptions, - BatchBuildingResult, -} from "./adapter.ts"; - -// PaimaL2 adapter implementation -export { PaimaL2DefaultAdapter } from "./paimal2-adapter.ts"; - -// Midnight adapter implementation -export { MidnightAdapter } from "./midnight-adapter.ts"; -export type { MidnightAdapterConfig } from "./midnight-adapter.ts"; - -// Midnight helper utilities -export { parseCircuitArgs } from "./midnight-arg-parser.ts"; - -// Generic EVM adapter implementation -export { - EvmContractAdapter, - type EvmContractAdapterConfig, - type HardhatArtifact, -} from "./evm-contract-adapter.ts"; - -// Bitcoin adapter implementation -export { BitcoinAdapter, buildBitcoinSignatureMessage } from "./bitcoin-adapter.ts"; -export type { BitcoinAdapterConfig } from "./bitcoin-adapter.ts"; diff --git a/packages/client/batcher/vendor/batcher/adapters/paimal2-adapter.ts b/packages/client/batcher/vendor/batcher/adapters/paimal2-adapter.ts deleted file mode 100644 index 2127c78..0000000 --- a/packages/client/batcher/vendor/batcher/adapters/paimal2-adapter.ts +++ /dev/null @@ -1,221 +0,0 @@ -import type { - Account, - Chain, - Hash, - PublicClient, - TransactionReceipt as ViemTransactionReceipt, - WalletClient, -} from "npm:viem@^2.21.3"; -import type { - BlockchainAdapter, - BlockchainHash, - BlockchainTransactionReceipt, - BatchBuildingOptions, - BatchBuildingResult, -} from "./adapter.ts"; -import { DefaultBatchBuilderLogic } from "../batch-data-builder/default-builder-logic.ts"; -import type { DefaultBatcherInput } from "../core/types.ts"; -import { createPublicClient, createWalletClient, http } from "npm:viem@^2.21.3"; -import * as chains from "npm:/viem@^2.21.3/chains"; -import { privateKeyToAccount } from "npm:viem@^2.21.3/accounts"; -import type { EvmAddress, EvmPrivateKey } from "jsr:@paimaexample/utils@^0.8.4"; - -// Type conversion utilities -function viemReceiptToGenericReceipt( - receipt: ViemTransactionReceipt, -): BlockchainTransactionReceipt { - return { - hash: receipt.transactionHash, - blockNumber: receipt.blockNumber, - status: receipt.status === "success" ? 1 : 0, - // Include original receipt for EVM-specific access if needed - _viemReceipt: receipt, - }; -} - -function encodeHexFromString(value: string): `0x${string}` { - const bytes = new TextEncoder().encode(value); - const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")) - .join(""); - return `0x${hex}`; -} - -/** - * EVM-specific implementation of the blockchain adapter interface - * Handles all EVM blockchain interactions including transaction submission and confirmation - */ -export class PaimaL2DefaultAdapter implements BlockchainAdapter { - private readonly walletClient: WalletClient; - private readonly publicClient: PublicClient; - private readonly account: Account; - private readonly paimaL2Address: EvmAddress; - private readonly paimaL2Fee: bigint; - private readonly effectstreamSyncProtocolName: string; - public readonly maxBatchSize: number; - - // Private helper for building batch data - private readonly batchBuilderLogic = new DefaultBatchBuilderLogic(); - - // TODO: Import this from the actual ABI package when available - private readonly paimaL2Abi = [ - { - inputs: [{ name: "data", type: "bytes" }], - name: "paimaSubmitGameInput", - outputs: [], - stateMutability: "payable", - type: "function", - }, - ] as const; - - constructor( - paimaL2Address: EvmAddress, - batcherPrivateKey: EvmPrivateKey, - paimaL2Fee: bigint, - effectstreamSyncProtocolName: string, - chain: Chain = chains.hardhat, - maxBatchSize: number = 10000, - ) { - this.paimaL2Address = paimaL2Address; - this.paimaL2Fee = paimaL2Fee; - this.effectstreamSyncProtocolName = effectstreamSyncProtocolName; - this.maxBatchSize = maxBatchSize; - - // Initialize viem clients - this.account = privateKeyToAccount(batcherPrivateKey); - - this.walletClient = createWalletClient({ - chain, - transport: http(), - }); - - this.publicClient = createPublicClient({ - chain, - transport: http(), - }); - } - - /** - * Return the EffectStream Sync protocol name used for event filtering - */ - getSyncProtocolName(): string { - return this.effectstreamSyncProtocolName; - } - - /** - * Build batch data from a collection of inputs - */ - public buildBatchData( - inputs: DefaultBatcherInput[], - _options?: BatchBuildingOptions, - ): BatchBuildingResult | null { - const options = { - maxSize: this.maxBatchSize, - }; - // Cast is safe because we know our helper returns a string - return this.batchBuilderLogic.buildBatchData(inputs, options) as BatchBuildingResult | null; - } - - /** - * Submit a batch transaction to the PaimaL2 contract - */ - async submitBatch( - data: string, - fee?: string | bigint, - ): Promise { - let actualFee = this.paimaL2Fee; - if (fee) { - actualFee = typeof fee === "string" ? BigInt(fee) : fee; - } - const hexData = encodeHexFromString(data); - const hash = await this.walletClient.writeContract({ - account: this.account, - chain: this.walletClient.chain, - address: this.paimaL2Address, - abi: this.paimaL2Abi, - functionName: "paimaSubmitGameInput", - args: [hexData], - value: actualFee, - }); - - console.log(`🚀 Submitted batch transaction: ${hash}`); - return hash; - } - - /** - * Wait for a transaction to be confirmed on the blockchain - */ - async waitForTransactionReceipt( - hash: BlockchainHash, - timeout?: number, - ): Promise { - const receipt = await this.publicClient.waitForTransactionReceipt({ - hash: hash as Hash, - timeout, - }); - - console.log( - `✅ Transaction confirmed! Block: ${receipt.blockNumber}, Hash: ${hash}, Status: ${receipt.status}`, - ); - - return viemReceiptToGenericReceipt(receipt); - } - - /** - * Get the current account/address for this adapter - */ - getAccountAddress(): string { - return this.account.address; - } - - /** - * Get the current chain name or identifier - */ - getChainName(): string { - return this.walletClient.chain?.name || "Unknown EVM Chain"; - } - - /** - * Estimate the fee for submitting a batch (returns the configured PaimaL2 fee) - * This matches the approach used in the old batcher implementation which - * simply used the pre-configured fee rather than performing complex estimation. - */ - estimateBatchFee(data: string): bigint { - // Note: Fee estimation doesn't need hex encoding since it just returns the configured fee - return this.paimaL2Fee; - } - - /** - * Check if the adapter is ready to submit transactions - */ - isReady(): boolean { - return this.walletClient !== undefined && this.publicClient !== undefined; - } - - /** - * Get the block number of the latest confirmed block - */ - async getBlockNumber(): Promise { - return await this.publicClient.getBlockNumber(); - } - - /** - * Get the underlying wallet client for advanced operations - */ - getWalletClient(): WalletClient { - return this.walletClient; - } - - /** - * Get the underlying public client for advanced operations - */ - getPublicClient(): PublicClient { - return this.publicClient; - } - - /** - * Get the PaimaL2 contract address - */ - getContractAddress(): EvmAddress { - return this.paimaL2Address; - } -} diff --git a/packages/client/batcher/vendor/batcher/batch-data-builder/default-builder-logic.ts b/packages/client/batcher/vendor/batcher/batch-data-builder/default-builder-logic.ts deleted file mode 100644 index 17e4892..0000000 --- a/packages/client/batcher/vendor/batcher/batch-data-builder/default-builder-logic.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Default batch builder logic implementation - * - * This implementation follows the same logic as the original @paimaexample/concise buildBatchData - * but works with the generic DefaultBatcherInput type and supports target-specific configurations. - */ - -import type { DefaultBatcherInput } from "../core/types.ts"; - -const BATCHER_GRAMMAR_PREFIX = "&B"; - -export class DefaultBatchBuilderLogic { - /** - * Build batch data using the standard EffectStream batching algorithm - * - * @param inputs - Array of inputs to batch - * @param options - Options for batch building - * @returns Batch building result or null if no inputs could be batched - */ - buildBatchData( - inputs: T[], - options?: { - /** Maximum size of the batch in bytes */ - maxSize?: number; - }, - ): { selectedInputs: T[]; data: string } | null { - return this.buildDefaultBatchData(inputs, options); - } - - /** - * Internal implementation of the default batch building logic - */ - private buildDefaultBatchData( - inputs: T[], - options?: { - /** Maximum size of the batch in bytes */ - maxSize?: number; - }, - ): { selectedInputs: T[]; data: string } | null { - if (inputs.length === 0) return null; - - const maxSize = options?.maxSize ?? 10000; - const selectedInputs: T[] = []; - const batchedTransaction: string[] = []; - - // Calculate initial remaining space - let remainingSpace = maxSize - `["${BATCHER_GRAMMAR_PREFIX}", []`.length; - - for (const input of inputs) { - const packed = this.generateStmInput(input); - - if (packed.length + 1 > remainingSpace) break; - - const packedString = JSON.stringify(packed); - batchedTransaction.push(packedString); - - // Update remaining space calculation - remainingSpace -= JSON.stringify(packed).length - '[""]'.length - - ",".length; - selectedInputs.push(input); - } - - if (batchedTransaction.length === 0) { - return null; - } - - const batchedData = this.generateBatchStmInput(batchedTransaction); - return { - selectedInputs, - data: JSON.stringify(batchedData), - }; - } - - /** - * Generate STM input for individual batched subunit - * This replicates the logic from generateStmInput in @paimaexample/sdk/concise - * - * @param input - The batcher input data - * @returns Array representation of the STM input - */ - private generateStmInput(input: DefaultBatcherInput): any[] { - // e.g. [addressType, userAddress, userSignature, conciseInput, millisecondTimestamp] - return [ - `${input.addressType}`, - input.address, - input.signature, - input.input, - input.timestamp, - ]; - } - - /** - * Generate the outer STM input for the batch - * This replicates the logic for creating the batch wrapper - * - * @param batchedTransaction - Array of serialized individual inputs - * @returns Array representation of the batched STM input - */ - private generateBatchStmInput(batchedTransaction: string[]): any[] { - // e.g. ["&B", [input1, input2, ...]] - return [ - BATCHER_GRAMMAR_PREFIX, - batchedTransaction, - ]; - } -} diff --git a/packages/client/batcher/vendor/batcher/batch-data-builder/evm-builder-logic.ts b/packages/client/batcher/vendor/batcher/batch-data-builder/evm-builder-logic.ts deleted file mode 100644 index 329c9e7..0000000 --- a/packages/client/batcher/vendor/batcher/batch-data-builder/evm-builder-logic.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { hexStringToUint8Array } from "jsr:@paimaexample/utils@^0.8.4"; -import type { DefaultBatcherInput } from "../core/types.ts"; - -const BATCH_PREFIX = "&B"; - -export interface EvmBatchPayload { - prefix: string; - payloads: Array<{ - method: string; - args: unknown[]; - value?: string; - addressType: number; - address: string; - signature: string; - timestamp: string; - }>; -} - -type RawEvmInput = { - method: unknown; - args?: unknown; - value?: unknown; -}; - -export interface ParsedEvmInput { - method: string; - args: unknown[]; - value?: string; -} - -function normalizeValue(value: unknown): string | undefined { - if (value === undefined || value === null) return undefined; - if (typeof value === "string") return value; - if (typeof value === "number") return value.toString(); - if (typeof value === "bigint") return value.toString(); - throw new Error("EVM input value must be string, number, bigint, or omitted"); -} - -export function decodeHexIfNeeded(value: string): string { - if (typeof value !== "string") { - throw new Error("EVM batch builder expects string input payloads"); - } - - if (/^0x[0-9a-fA-F]+$/.test(value)) { - const normalized = value.slice(2); - return new TextDecoder().decode(hexStringToUint8Array(normalized)); - } - - if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) { - return new TextDecoder().decode(hexStringToUint8Array(value)); - } - - return value; -} - -export function parseEvmBatcherInput(input: DefaultBatcherInput): ParsedEvmInput { - const decoded = decodeHexIfNeeded(input.input); - let parsed: RawEvmInput; - try { - parsed = JSON.parse(decoded); - } catch (error) { - throw new Error( - `Invalid EVM input JSON: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - - if (!parsed || typeof parsed !== "object") { - throw new Error("EVM input must be a JSON object"); - } - - if (typeof parsed.method !== "string" || parsed.method.length === 0) { - throw new Error("EVM input must include a method name"); - } - - if (parsed.args !== undefined && !Array.isArray(parsed.args)) { - throw new Error("EVM input args must be an array"); - } - - const value = normalizeValue(parsed.value); - - return { - method: parsed.method, - args: Array.isArray(parsed.args) ? parsed.args : [], - value, - }; -} - -export class EvmBatchBuilderLogic { - buildBatchData( - inputs: T[], - options?: { - /** Maximum size of the batch in bytes */ - maxSize?: number; - }, - ): { selectedInputs: T[]; data: EvmBatchPayload | null } | null { - if (inputs.length === 0) return null; - - const maxSize = options?.maxSize ?? 10000; - const encoder = new TextEncoder(); - const emptyBatch = JSON.stringify({ prefix: BATCH_PREFIX, payloads: [] }); - let currentSize = encoder.encode(emptyBatch).length; - - const selectedInputs: T[] = []; - const payloads: EvmBatchPayload["payloads"] = []; - - for (const input of inputs) { - let payloadEntry; - try { - const parsed = parseEvmBatcherInput(input); - payloadEntry = { - method: parsed.method, - args: parsed.args, - value: parsed.value, - addressType: input.addressType, - address: input.address, - signature: input.signature ?? "", - timestamp: input.timestamp, - }; - } catch (error) { - console.error( - "[EvmBatchBuilder] Skipping invalid input for address", - input.address, - "-", - error instanceof Error ? error.message : String(error), - ); - continue; - } - - const entrySize = encoder.encode(JSON.stringify(payloadEntry)).length; - if (currentSize + entrySize > maxSize) { - console.error("[EvmBatchBuilder] Batch size exceeded maxSize", currentSize + entrySize, maxSize); - break; - } - - selectedInputs.push(input); - payloads.push(payloadEntry); - currentSize += entrySize; - } - - if (payloads.length === 0) { - return { selectedInputs: [], data: null }; - } - - return { - selectedInputs, - data: { - prefix: BATCH_PREFIX, - payloads, - }, - }; - } -} diff --git a/packages/client/batcher/vendor/batcher/batch-data-builder/midnight-builder-logic.ts b/packages/client/batcher/vendor/batcher/batch-data-builder/midnight-builder-logic.ts deleted file mode 100644 index 16429c4..0000000 --- a/packages/client/batcher/vendor/batcher/batch-data-builder/midnight-builder-logic.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { hexStringToUint8Array } from "jsr:@paimaexample/utils@^0.8.4"; -import type { DefaultBatcherInput } from "../core/types.ts"; - -const BATCH_PREFIX = "&B"; - -export interface MidnightBatchPayload { - prefix: string; - payloads: Array<{ - circuit: string; - args: unknown[]; - addressType: number; - address: string; - signature: string; - timestamp: string; - playerSecret?: string; - shuffleSeed?: string; - opponentSecret?: string; - opponentShuffleSeed?: string; - }>; -} - -function decodeHexIfNeeded(value: string): string { - if (typeof value !== "string") { - throw new Error("Midnight batch builder expects string input payloads"); - } - - if (/^0x[0-9a-fA-F]+$/.test(value)) { - const normalized = value.slice(2); - return new TextDecoder().decode(hexStringToUint8Array(normalized)); - } - - if (/^[0-9a-fA-F]+$/.test(value)) { - return new TextDecoder().decode(hexStringToUint8Array(value)); - } - - return value; -} - -export class MidnightBatchBuilderLogic { - buildBatchData( - inputs: T[], - options?: { - /** Maximum size of the batch in bytes */ - maxSize?: number; - /** Maximum number of inputs per batch (default: unlimited) */ - maxInputs?: number; - }, - ): { selectedInputs: T[]; data: MidnightBatchPayload | null } | null { - if (inputs.length === 0) return null; - - const maxSize = options?.maxSize ?? 10000; - const maxInputs = options?.maxInputs ?? Infinity; - const selectedInputs: T[] = []; - const payloads: Array<{ - circuit: string; - args: unknown[]; - addressType: number; - address: string; - signature: string; - timestamp: string; - }> = []; - - const encoder = new TextEncoder(); - const emptyBatch = JSON.stringify({ prefix: BATCH_PREFIX, payloads: [] }); - let currentSize = encoder.encode(emptyBatch).length; - - for (const input of inputs) { - if (selectedInputs.length >= maxInputs) { - break; - } - - // Inputs are now pre-validated, so we can trust the structure - const parsed = JSON.parse(decodeHexIfNeeded(input.input)); - - const payloadEntry: { - circuit: string; - args: unknown[]; - addressType: number; - address: string; - signature: string; - timestamp: string; - playerSecret?: string; - shuffleSeed?: string; - opponentSecret?: string; - opponentShuffleSeed?: string; - } = { - circuit: parsed.circuit, - args: parsed.args, - addressType: input.addressType, - address: input.address, - signature: input.signature ?? "", - timestamp: input.timestamp, - }; - - // Preserve client-side secrets if present (for mental poker / ZK proofs) - if (parsed.playerSecret) payloadEntry.playerSecret = parsed.playerSecret; - if (parsed.shuffleSeed) payloadEntry.shuffleSeed = parsed.shuffleSeed; - if (parsed.opponentSecret) payloadEntry.opponentSecret = parsed.opponentSecret; - if (parsed.opponentShuffleSeed) payloadEntry.opponentShuffleSeed = parsed.opponentShuffleSeed; - - const entrySize = encoder.encode(JSON.stringify(payloadEntry)).length; - - if (currentSize + entrySize > maxSize) { - break; - } - - selectedInputs.push(input); - payloads.push(payloadEntry); - currentSize += entrySize; - } - - if (payloads.length === 0) { - return { selectedInputs: [], data: null }; - } - - return { - selectedInputs, - data: { - prefix: BATCH_PREFIX, - payloads, - }, - }; - } -} diff --git a/packages/client/batcher/vendor/batcher/core/batch-processor.ts b/packages/client/batcher/vendor/batcher/core/batch-processor.ts deleted file mode 100644 index 08dfc18..0000000 --- a/packages/client/batcher/vendor/batcher/core/batch-processor.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { - BlockchainAdapter, - BlockchainTransactionReceipt, -} from "../adapters/adapter.ts"; -import type { DefaultBatcherInput } from "./types.ts"; - - -/** - * Handles the complex batch processing logic for a specific target. - * Separated from the main Batcher class to improve maintainability. - */ -export class BatchProcessor { - constructor( - private batcher: { - emitStateTransition: (prefix: string, payload: any) => Promise; - storage: { removeProcessedInputs: (inputs: T[], target: string) => Promise }; - submissionCallbacks: Map< - string, - { - resolve: (result: any) => void; - reject: (error: Error) => void; - timeoutId: number; - } - >; - waitForEffectStreamProcessed: ( - target: string, - receipt: BlockchainTransactionReceipt, - timeout: number, - ) => Promise<{ latestBlock: number; rollup: number } | null>; - getCallbackKey: (input: T) => string; - }, - ) {} - - async processBatchForTarget( - adapter: BlockchainAdapter, - target: string, - inputs: T[], - timeout: number = 60000, - ): Promise { - console.log(`🔗 Processing ${inputs.length} inputs for target: ${target}`); - - // Build batch data directly from adapter - const batchResult = adapter.buildBatchData(inputs as DefaultBatcherInput[]); - - if (!batchResult || !batchResult.data) { - console.log(`📭 No valid inputs for target ${target}, skipping...`); - return; - } - - const { selectedInputs, data } = batchResult; // data is 'unknown' - - try { - await this.submitAndConfirmTransaction( - adapter, - target, - data, - selectedInputs as T[], - timeout, - ); - } catch (error) { - // Reject all callbacks for the selected inputs so callers get an error - // instead of hanging until timeout - this.rejectInputCallbacks( - selectedInputs as T[], - error instanceof Error ? error : new Error(String(error)), - ); - throw error; - } - } - - private async submitAndConfirmTransaction( - adapter: BlockchainAdapter, - target: string, - data: unknown, // CHANGED from hexData: string - selectedInputs: T[], - timeout: number, - ): Promise { - const estimatedFee = await adapter.estimateBatchFee(data); - - this.batcher.emitStateTransition("batch:fee-estimate", { - target, - estimatedFee, - time: Date.now(), - }); - - const hash = await adapter.submitBatch(data, estimatedFee); - console.log(`✅ Submitted batch for ${target}: ${hash}`); - - this.batcher.emitStateTransition("batch:submit", { - target, - estimatedFee, - txHash: hash, - time: Date.now(), - }); - - // Wait for confirmation and EffectStream processing - await this.handleTransactionConfirmation( - adapter, - target, - hash, - selectedInputs, - timeout, - ); - } - - private async handleTransactionConfirmation( - adapter: BlockchainAdapter, - target: string, - hash: string, - selectedInputs: T[], - timeout: number, - ): Promise { - const receipt = await adapter.waitForTransactionReceipt(hash); - this.batcher.emitStateTransition("batch:receipt", { - target, - blockNumber: receipt.blockNumber, - time: Date.now(), - }); - - // Remove processed inputs from storage after successful receipt - await this.batcher.storage.removeProcessedInputs(selectedInputs, target); - - // Resolve all callbacks with the receipt - // Individual callers will decide if they want to continue waiting for EffectStream - this.resolveInputCallbacks(selectedInputs, receipt); - - // Optional: Still trigger EffectStream processing check for event emission - this.waitForEffectStreamProcessing( - receipt, - adapter, - target, - timeout, - ).catch((error) => { - console.error( - `⚠️ Error waiting for EffectStream processing for target ${target}:`, - error, - ); - }); - } - - private async waitForEffectStreamProcessing( - receipt: BlockchainTransactionReceipt, - adapter: BlockchainAdapter, - target: string, - timeout: number, - ): Promise { - try { - const processingResult = await this.batcher.waitForEffectStreamProcessed( - target, - receipt, - timeout, - ); - - if (processingResult) { - this.batcher.emitStateTransition("batch:effectstream-processed", { - target, - latestBlock: processingResult.latestBlock, - rollup: processingResult.rollup, - time: Date.now(), - }); - } else { - console.error( - `❌ EffectStream processing validation failed for target ${target}`, - ); - this.batcher.emitStateTransition("error", { - phase: "effectstream", - target, - error: new Error("EffectStream processing validation failed"), - time: Date.now(), - }); - } - } catch (error) { - console.error( - `❌ Error waiting for EffectStream processing for target ${target}:`, - error, - ); - this.batcher.emitStateTransition("error", { - phase: "effectstream", - target, - error, - time: Date.now(), - }); - } - } - - private resolveInputCallbacks( - selectedInputs: T[], - receipt: BlockchainTransactionReceipt, - ): void { - for (const input of selectedInputs) { - const callbackKey = this.batcher.getCallbackKey(input); - const callbacks = this.batcher.submissionCallbacks.get(callbackKey); - if (callbacks) { - callbacks.resolve(receipt); - clearTimeout(callbacks.timeoutId); - this.batcher.submissionCallbacks.delete(callbackKey); - } - } - } - - private rejectInputCallbacks( - selectedInputs: T[], - error: Error, - ): void { - for (const input of selectedInputs) { - const callbackKey = this.batcher.getCallbackKey(input); - const callbacks = this.batcher.submissionCallbacks.get(callbackKey); - if (callbacks) { - callbacks.reject(error); - clearTimeout(callbacks.timeoutId); - this.batcher.submissionCallbacks.delete(callbackKey); - } - } - } -} diff --git a/packages/client/batcher/vendor/batcher/core/batcher-events.ts b/packages/client/batcher/vendor/batcher/core/batcher-events.ts deleted file mode 100644 index a045b11..0000000 --- a/packages/client/batcher/vendor/batcher/core/batcher-events.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { DefaultBatcherInput } from "./types.ts"; -import type { Batcher } from "./batcher.ts"; - -/** - * Default grammar for batcher lifecycle events. - * Consumers can treat prefixes as string keys and payload as unknown - * if they don't need compile-time safety. - */ -export type BatcherGrammar = Record & { - startup: { - publicConfig: ReturnType["getPublicConfig"]>; - time: number; - }; - "http:start": { port: number; time: number }; - "http:stop": { time: number }; - "poll:targets-ready": { targets: string[]; time: number }; - "batch:process:start": { target: string; inputCount: number; time: number }; - "batch:fee-estimate": { target: string; estimatedFee: bigint; time: number }; - "batch:submit": { - target: string; - estimatedFee: bigint; - txHash: string; - time: number; - }; - "batch:receipt": { - target: string; - blockNumber: number | bigint; - time: number; - }; - "batch:effectstream-processed": { - target: string; - latestBlock: number; - rollup: number; - time: number; - }; - "batch:process:end": { - target: string; - processedCount: number; - success: boolean; - time: number; - }; - error: { phase: string; target?: string; error: unknown; time: number }; -}; - -export type BatcherListener< - Grammar extends Record, - Prefix extends keyof Grammar & string, -> = (payload: Grammar[Prefix]) => void | Promise; - -/** - * Helper to attach console logs for common events. This does NOT auto-attach. - * Call from the entrypoint if you want the default banner and lifecycle logs. - */ -export function attachDefaultConsoleListeners< - T extends DefaultBatcherInput = DefaultBatcherInput, ->( - batcher: Batcher, -): void { - try { - batcher.addStateTransition("startup", ({ publicConfig }) => { - const banner = - `🎯 Batcher started - polling every ${publicConfig.pollingIntervalMs} ms\n` + - ` | 📍 Default Target: ${publicConfig.defaultTarget}\n` + - ` | ⛓️ Adapter Targets: ${ - publicConfig.adapterTargets.join(", ") - }\n` + - ` | 📦 Batching Criteria: ${ - Object.entries(publicConfig.criteriaTypes).map(([target, type]) => - `${target}=${type}` - ).join(", ") - }\n` + - `${ - publicConfig.enableHttpServer - ? ` | 🌐 HTTP Server: http://localhost:${publicConfig.port}\n` - : "" - }` + - ` | 📋 Press Ctrl+C to stop gracefully`; - console.log(banner); - }); - } catch { - // ignore duplicate registration - } -} diff --git a/packages/client/batcher/vendor/batcher/core/batcher.ts b/packages/client/batcher/vendor/batcher/core/batcher.ts deleted file mode 100644 index eae5fed..0000000 --- a/packages/client/batcher/vendor/batcher/core/batcher.ts +++ /dev/null @@ -1,1443 +0,0 @@ -import { CryptoManager } from "jsr:@paimaexample/crypto@^0.8.4"; -import { call, lift, resource, sleep, spawn, suspend } from "npm:effection@^3.5.0"; -import type { Operation } from "npm:effection@^3.5.0"; -import type { BatcherStorage } from "./storage.ts"; -import type { DefaultBatcherInput } from "./types.ts"; -import type { - BlockchainAdapter, - BlockchainTransactionReceipt, -} from "../adapters/adapter.ts"; -import type { BatchingCriteriaConfig, BatcherConfig } from "./config.ts"; -import { - applyBatcherConfigDefaults, - DEFAULT_BATCHING_CRITERIA, - validateBatcherConfig, - validateBatchingCriteria, - validatePreInit, -} from "./config.ts"; -import { startBatcherHttpServer } from "../server/batcher-server.ts"; -import { BatcherFileStorage } from "./mod.ts"; -import { BatchProcessor } from "./batch-processor.ts"; -import { - type BatcherShutdownState, - type ShutdownHooks, - ShutdownManager, -} from "./shutdown-manager.ts"; -import type { BatcherGrammar, BatcherListener } from "./batcher-events.ts"; -import { BuiltinEvents, PaimaEventManager as EffectStreamEventManager } from "jsr:@paimaexample/event-client@^0.8.4"; - -/** - * Custom error class for input validation failures - * Provides structured error information with appropriate HTTP status codes - */ -export class InputValidationError extends Error { - constructor(message: string, public statusCode: number = 400) { - super(message); - this.name = "InputValidationError"; - } -} - -/** - * EffectStream Batcher - A type-safe, simplified blockchain batching system - * - * ARCHITECTURE: - * - Storage is the SINGLE SOURCE OF TRUTH for all data - * - Batching criteria is configurable via BatchingCriteriaConfig - * - No in-memory pool - eliminates consistency issues entirely - * - All operations are atomic and crash-safe - * - Composed of specialized components for better maintainability - * - * COMPONENTS: - * - BatchProcessor: Handles complex batch processing and transaction lifecycle - * - ShutdownManager: Coordinates graceful shutdown procedures - * - Storage: Single source of truth for all batch data - * - * BATCHING CRITERIA: - * - "time": Process based on time windows (e.g., every 5 minutes) - * - "size": Process based on batch size (e.g., when 100 inputs accumulated) - * - "value": Process based on accumulated value (e.g., when total value reaches threshold) - * - "hybrid": Process when either time OR size criteria is met - * - "custom": Process based on user-defined function - */ - -export class Batcher { - /** Namespace used for signature verification messages */ - namespace: string = "effectstream_batcher"; - /** Timer ID for periodic batch processing */ - private pollingIntervalID?: number; - /** Available blockchain adapters keyed by target name */ - private adapters: Record>; - /** Default target to use when input.target is not specified */ - public defaultTarget?: string; - /** Per-adapter batching criteria configuration */ - private readonly batchingCriteria: Map>; - /** Track when the last batch was processed for time-based criteria (per adapter) */ - private lastProcessTime: Map; - /** Track if the batcher is initialized */ - public isInitialized: boolean = false; - /** HTTP server instance */ - private httpServer?: any; - /** HTTP server port */ - private readonly port: number; - /** Whether to enable HTTP server */ - private readonly enableHttpServer: boolean; - /** Whether to enable event system */ - private readonly enableEventSystem: boolean; - /** Shutdown state tracking */ - public readonly shutdownState: BatcherShutdownState = { - isShuttingDown: false, - shutdownInitiatedAt: null, - shutdownTimeoutMs: 30000, - processingAdapters: new Set(), - }; - /** Callbacks to return the transaction receipt after the transaction is confirmed */ - private submissionCallbacks: Map< - string, - { - resolve: (result: BlockchainTransactionReceipt) => void; - reject: (error: Error) => void; - timeoutId: number; - } - > = new Map(); - /** Batch processor for handling complex batch operations */ - private readonly batchProcessor: BatchProcessor; - /** Shutdown manager for handling graceful shutdowns */ - private readonly shutdownManager: ShutdownManager; - /** State transition listeners keyed by prefix */ - private stateTransitionListeners: Map< - string, - (payload: any) => void | Promise - > = new Map(); - - /** - * Create a new Batcher with type-safe configuration - * - * @param config - Type-safe configuration with unified batching criteria - * @param storage - The storage system for persisting inputs (default: file storage) - * - * Runtime validation ensures: - * - At least one adapter is provided - * - If defaultTarget is specified, it exists in adapters - * - Default target falls back to first available adapter if not specified - */ - public readonly config: BatcherConfig< - T, - Record> - >; - - constructor( - config: BatcherConfig< - T, - Record> - >, - private readonly storage: BatcherStorage = new BatcherFileStorage( - "./batcher-data", - ), - ) { - const cfg = applyBatcherConfigDefaults(config); - this.config = cfg; - this.adapters = cfg.adapters || {}; - this.validateConfig(); - - // Resolve defaultTarget: if adapters exist in config, auto-set to first adapter if not specified - // If no adapters in config, defer until first adapter is added via addBlockchainAdapter() - if (Object.keys(this.adapters).length > 0) { - // Auto-set to first adapter if defaultTarget not explicitly provided - this.defaultTarget = cfg.defaultTarget || - Object.keys(this.adapters)[0]; - if (!cfg.defaultTarget) { - console.log( - `🎯 Auto-set default target to '${this.defaultTarget}' (first adapter from config)`, - ); - } - } else { - // No adapters in config - will be set when first adapter is added via addBlockchainAdapter() - this.defaultTarget = cfg.defaultTarget; - } - - // Initialize per-adapter batching criteria - this.batchingCriteria = new Map(); - for (const target of Object.keys(this.adapters)) { - const criteria = cfg.batchingCriteria - ?.[target as keyof typeof cfg.batchingCriteria] ?? - DEFAULT_BATCHING_CRITERIA; - this.batchingCriteria.set(target, criteria); - } - - // Initialize last process times map (will be populated in init()/runBatcher()) - this.lastProcessTime = new Map(); - - this.batchProcessor = new BatchProcessor({ - emitStateTransition: async (prefix: string, payload: any) => { - // For async contexts, we need to handle this differently - // Since we're in an async method but need to call an Effection operation, - // we'll create a simple non-blocking implementation - if (this.enableEventSystem) { - const listener = this.stateTransitionListeners.get(prefix); - if (listener) { - try { - // Execute the listener asynchronously without blocking - await listener(payload); - } catch (error) { - const hasErrorListener = this.stateTransitionListeners.has( - "error", - ); - if (prefix !== "error" && hasErrorListener) { - try { - await this.stateTransitionListeners.get("error")!({ - phase: `event-listener:${prefix}`, - error, - time: Date.now(), - }); - } catch { - // swallow - } - } - } - } - } - }, - storage: this.storage, - submissionCallbacks: this.submissionCallbacks, - getCallbackKey: (input: T) => this.getInputCallbackKey(input), - waitForEffectStreamProcessed: ( - target: string, - receipt: BlockchainTransactionReceipt, - timeout: number, - ) => this.waitForEffectStreamProcessed(target, receipt, timeout), - }); - this.shutdownManager = new ShutdownManager( - { - shutdownState: this.shutdownState, - stopPolling: () => this.stopPolling(), - stopHttpServer: () => this.stopHttpServer(), - cleanupResources: () => this.cleanupResources(), - }, - this, - ); - this.port = this.config.port!; - this.enableHttpServer = this.config.enableHttpServer!; - this.enableEventSystem = this.config.enableEventSystem!; - this.namespace = this.config.namespace ?? this.namespace; - } - - /** - * Register a state transition listener for a given prefix. - * Throws if a listener already exists for the prefix. - */ - addStateTransition( - prefix: Prefix, - listener: BatcherListener, - ): Batcher { - if (this.stateTransitionListeners.has(prefix)) { - throw new Error( - `Disallowed: duplicate listener for prefix ${prefix}. Duplicate prefixes can cause determinism issues`, - ); - } - this.stateTransitionListeners.set(prefix, listener); - return this; - } - - /** Remove a previously registered state transition listener. */ - removeStateTransition(prefix: string): void { - this.stateTransitionListeners.delete(prefix); - } - - /** - * Emit a state transition event. - * This runs the listener in a separate, supervised fiber using `spawn`, - * ensuring that a slow or failing listener does not block the main batcher process. - */ - *emitStateTransition(prefix: string, payload: any): Operation { - if (!this.enableEventSystem) return; - const listener = this.stateTransitionListeners.get(prefix); - if (!listener) return; - - // `spawn` starts the listener in the background. - // The `emitStateTransition` operation can return immediately. - yield* spawn((function* (this: Batcher) { - try { - // We still use `call` here to handle the listener being async. - yield* lift(listener)(payload); - } catch (error) { - // Error handling now happens inside the spawned fiber, - // preventing a listener crash from taking down the whole batcher. - const hasErrorListener = this.stateTransitionListeners.has("error"); - if (prefix !== "error" && hasErrorListener) { - // Re-emit the error, again in a supervised manner. - yield* lift(this.stateTransitionListeners.get("error")!)({ - phase: `event-listener:${prefix}`, - error, - time: Date.now(), - }); - } - } - }).bind(this)); - } - - /** - * Validate the batcher configuration. Can be overridden by subclasses for custom validation. - * By default, uses the standard validation from batcher-config.ts - */ - protected validateConfig(): void { - validateBatcherConfig(this.config); - } - - /** - * Add a blockchain adapter dynamically before batcher startup. - * Must be called before runBatcher() or init(). - * - * @param name - Unique name for the adapter (e.g., "ethereum", "midnight") - * @param adapter - The blockchain adapter instance - * @param batchingCriteria - Optional batching criteria for this adapter. If not provided, uses DEFAULT_BATCHING_CRITERIA - * @throws If batcher is already initialized - * @throws If adapter name already exists - * @throws If batching criteria is invalid - */ - addBlockchainAdapter( - name: string, - adapter: BlockchainAdapter, - batchingCriteria?: BatchingCriteriaConfig, - ): Batcher { - if (this.isInitialized) { - throw new Error( - "Cannot add adapters after batcher has been initialized. " + - "Call addBlockchainAdapter() before init() or runBatcher().", - ); - } - - if (name in this.adapters) { - throw new Error( - `Adapter with name '${name}' already exists. Available adapters: ${ - Object.keys(this.adapters).join(", ") - }`, - ); - } - - this.adapters[name] = adapter; - - // Resolve batching criteria (provided > default > global default) - const criteria = batchingCriteria ?? DEFAULT_BATCHING_CRITERIA; - validateBatchingCriteria(criteria); - this.batchingCriteria.set(name, criteria); - - if (!this.defaultTarget) { - this.defaultTarget = name; - console.log(`🎯 Auto-set default target to '${name}' (first adapter)`); - } - - if (!this.config.batchingCriteria) { - this.config.batchingCriteria = {}; - } - (this.config.batchingCriteria as any)[name] = criteria; - return this; - } - - /** - * Update batching criteria for an adapter before startup. - * Must be called before runBatcher() or init(). - * - * @param adapterName - Name of the adapter to update - * @param criteria - New batching criteria configuration - * @throws If batcher is already initialized - * @throws If adapter doesn't exist - * @throws If batching criteria is invalid - */ - setBatchingCriteria( - adapterName: string, - criteria: BatchingCriteriaConfig, - ): Batcher { - if (this.isInitialized) { - throw new Error( - "Cannot modify batching criteria after batcher has been initialized.", - ); - } - - if (!(adapterName in this.adapters)) { - throw new Error( - `Adapter '${adapterName}' not found. Available adapters: ${ - Object.keys(this.adapters).join(", ") - }`, - ); - } - - validateBatchingCriteria(criteria); - this.batchingCriteria.set(adapterName, criteria); - - if (!this.config.batchingCriteria) { - this.config.batchingCriteria = {}; - } - (this.config.batchingCriteria as any)[adapterName] = criteria; - - return this; - } - - /** - * Set the default target adapter before startup. - * Must be called before runBatcher() or init(). - * - * @param adapterName - Name of the adapter to set as default target - * @throws If batcher is already initialized - * @throws If adapter doesn't exist - */ - setDefaultTarget(adapterName: string): Batcher { - if (this.isInitialized) { - throw new Error( - "Cannot modify default target after batcher has been initialized.", - ); - } - - if (!(adapterName in this.adapters)) { - throw new Error( - `Adapter '${adapterName}' not found. Available adapters: ${ - Object.keys(this.adapters).join(", ") - }`, - ); - } - - this.defaultTarget = adapterName; - return this; - } - - async init(): Promise { - if (this.isInitialized) return; - - validatePreInit(this.adapters, this.defaultTarget); - - const now = Date.now(); - for (const target of Object.keys(this.adapters)) { - this.lastProcessTime.set(target, now); - } - - await this.storage.init(); - - for (const [target, adapter] of Object.entries(this.adapters)) { - if (typeof adapter.recoverState === "function") { - const pendingInputs = await this.storage.getInputsByTarget( - target, - this.defaultTarget!, // defaultTarget is guaranteed to be set at this point - ); - await adapter.recoverState(pendingInputs); - } - } - - this.pollingIntervalID = setInterval( - async () => { - await this.pollBatcher(); - }, - this.config.pollingIntervalMs, - ); - - // Start HTTP server if enabled - if (this.enableHttpServer) { - await this.startHttpServer(); - } - - this.isInitialized = true; - await this.emitStateTransition("startup", { - publicConfig: this.getPublicConfig(), - time: Date.now(), - }); - } - /** - * Add a user input to the batch queue after validating the signature - * @param input - The input to add to the batch queue - * @param confirmationLevel - The level of confirmation to wait for - * @param timeoutMs - Timeout in milliseconds for confirmation (default: 60000) - * @returns Promise resolving to transaction receipt or null based on confirmation level - */ - async batchInput( - input: T, - confirmationLevel: "no-wait" | "wait-receipt" | "wait-effectstream-processed" = - "wait-receipt", - timeoutMs: number = 300000, - ): Promise { - if (this.shutdownState.isShuttingDown) { - // 503 Service Unavailable - throw new InputValidationError( - "Batcher is shutting down, not accepting new inputs", - 503, - ); - } - - if (!this.defaultTarget && !input.target) { - throw new InputValidationError( - "No default target configured and input.target not specified. " + - "Add adapters using addBlockchainAdapter() before initialization.", - 400, - ); - } - - const target = input.target || this.defaultTarget!; - const adapter = this.adapters[target]; - if (!adapter) { - throw new InputValidationError(`Adapter for target ${target} not found. Available targets: ${Object.keys(this.adapters).join(", ")}`, 404); - } - - // 1. Signature Validation (Pre-Queue, Adapter-Driven) - let verifiedSignature: boolean; - - if (adapter && typeof adapter.verifySignature === "function") { - verifiedSignature = await adapter.verifySignature(input); - } else if (input.signature) { - // Fall back to the batcher's default EVM verification when a signature is provided - verifiedSignature = await this._defaultVerifyInputSignature(input); - } else { - throw new InputValidationError( - `Adapter for target ${target} requires either a signature or a custom verifySignature implementation`, - ); - } - - if (!verifiedSignature) { - throw new InputValidationError("Invalid signature", 401); - } - - // 2. Adapter-Specific Input Validation (Pre-Queue) - if (adapter && typeof adapter.validateInput === "function") { - const validationResult = await adapter.validateInput(input); - if (!validationResult.valid) { - throw new InputValidationError( - validationResult.error || "Invalid input for target adapter", - ); - } - } - - // 3. Add to Storage (Only if all validation passes) - await this.addInput(input); - const { count, size } = await this.storage.getInputCountAndSize(); - console.log( - `✅ Added input from ${input.address} to batch queue. Queue size: ${count} inputs, ${size} bytes`, - ); - - if (confirmationLevel === "no-wait") { - return null; - } - - // Create promise for callback with timeout - const receiptPromise = new Promise( - (resolve, reject) => { - const callbackKey = this.getInputCallbackKey(input); - const timeoutId = setTimeout(() => { - this.submissionCallbacks.delete(callbackKey); - reject(new Error("Receipt confirmation timeout")); - }, timeoutMs); - this.submissionCallbacks.set(callbackKey, { - resolve: (result) => { - clearTimeout(timeoutId); - resolve(result); - }, - reject: (error) => { - clearTimeout(timeoutId); - reject(error); - }, - timeoutId, - }); - }, - ); - - // Wait for transaction receipt - const receipt = await receiptPromise; - - // If only waiting for receipt, return now - if (confirmationLevel === "wait-receipt") { - return receipt; - } - - // If waiting for EffectStream processing, continue waiting - if (confirmationLevel === "wait-effectstream-processed") { - const target = input.target || this.defaultTarget; - if (!target) { - throw new Error( - "Cannot wait for EffectStream processing: no target specified and no default target configured.", - ); - } - try { - const processingResult = await this.waitForEffectStreamProcessed( - target, - receipt, - timeoutMs, - ); - if (processingResult) { - return { - ...receipt, - rollup: processingResult.rollup, - }; - } else { - throw new Error("EffectStream processing validation failed"); - } - } catch (error) { - throw new Error( - `Failed to wait for EffectStream processing: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ); - } - } - - return receipt; - } - - /** - * Wait for a transaction to be processed by EffectStream - * @param receipt - The transaction receipt to wait for - * @param timeout - Timeout in milliseconds - * @returns Promise with latest block and rollup number, or null on failure - */ - private async waitForEffectStreamProcessed( - target: string, - receipt: BlockchainTransactionReceipt, - timeout: number = 120000, - ): Promise<{ latestBlock: number; rollup: number } | null> { - // We need to get the chain name from the receipt - // Since receipt doesn't have chain info, we need to track which adapter submitted it - const adapter = this.adapters[target]; - const chainName = adapter.getSyncProtocolName?.() ?? - adapter.getChainName(); - - let subscriptionReference: symbol | undefined = undefined; - let latestBlock = 0; - let timer: number | undefined = undefined; - - try { - const result = await Promise.race([ - new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error("Timeout")), timeout); - }), - new Promise<{ latestBlock: number; rollup: number }>( - (resolve, reject) => { - EffectStreamEventManager.Instance.subscribe( - { - topic: BuiltinEvents.SyncChains, - filter: { chain: chainName, block: undefined }, - }, - (event) => { - latestBlock = Math.max(event.block, latestBlock); - if (latestBlock > Number(receipt.blockNumber)) { - resolve({ latestBlock, rollup: event.rollup }); - } - }, - ) - .then((subscription) => subscriptionReference = subscription) - .catch(reject); - }, - ), - ]); - return result || null; - } catch (error) { - console.error("Error waiting for EffectStream processing:", error); - return null; - } finally { - if (subscriptionReference) { - EffectStreamEventManager.Instance.unsubscribe(subscriptionReference); - } - if (timer) { - clearTimeout(timer); - } - } - } - - /** - * Add input to storage - * Storage is the single source of truth - no pool needed - */ - async addInput(input: T): Promise { - const target = input.target ?? this.defaultTarget; - if (!target) { - throw new Error( - "Cannot add input: no target specified and no default target configured.", - ); - } - await this.storage.addInput(input, target); - } - - private async _defaultVerifyInputSignature( - input: T, - ): Promise { - // This is the default EVM verification logic - // Create the signature message using EVM-specific logic - if (!input.signature) { - throw new Error( - "Default signature verification requires a signature to be provided", - ); - } - - let walletAddress; - - const cryptoManager = CryptoManager.getCryptoManager(input.addressType); - walletAddress = cryptoManager.decodeAddress(input.address); - - const message = ( - this.namespace + - (input.target ?? "") + - input.timestamp + - walletAddress + - input.input - ) - .replace(/[^a-zA-Z0-9]/g, "-") - .toLocaleLowerCase(); - return await cryptoManager.verifySignature(input.address, message, input.signature); - } - - private getInputCallbackKey(input: T): string { - const target = input.target || this.defaultTarget; - if (!target) { - throw new Error( - "Cannot generate callback key: no target specified and no default target configured.", - ); - } - return [ - input.addressType, - target, - input.address, - input.timestamp, - input.signature ?? "", - input.input, - ].join("|"); - } - - async pollBatcher(): Promise { - if (this.shutdownState.isShuttingDown) return; - - // Check each adapter target independently for batching readiness - const targetsToProcess: string[] = []; - for (const target of Object.keys(this.adapters)) { - if (await this.isTargetReadyForBatching(target)) { - targetsToProcess.push(target); - } - } - - if (targetsToProcess.length === 0) return; - await this.emitStateTransition("poll:targets-ready", { - targets: targetsToProcess, - time: Date.now(), - }); - - // Process batches for ready targets - await this.processBatchesForTargets(targetsToProcess); - - // Update last process times for processed targets - const now = Date.now(); - for (const target of targetsToProcess) { - this.lastProcessTime.set(target, now); - } - } - - /** - * Check if a specific target is ready for batching based on its configured criteria - */ - private async isTargetReadyForBatching(target: string): Promise { - if (!this.defaultTarget) { - // This shouldn't happen after init(), but handle gracefully - return false; - } - const targetInputs = await this.storage.getInputsByTarget( - target, - this.defaultTarget, - ); - - // If no inputs for this target, nothing is ready - if (!targetInputs.length) return false; - - const criteria = this.batchingCriteria.get(target)!; - const { criteriaType } = criteria; - - switch (criteriaType) { - case "time": - return this.checkTimeCriteriaForTarget(target); - case "size": - return this.checkSizeCriteriaForTarget(targetInputs, criteria); - case "value": - return this.checkValueCriteriaForTarget(targetInputs, criteria); - case "hybrid": - return this.checkHybridCriteriaForTarget( - target, - targetInputs, - criteria, - ); - case "custom": - return this.checkCustomCriteriaForTarget( - target, - targetInputs, - criteria, - ); - default: - console.warn( - `Unknown criteria type for target ${target}: ${criteriaType}`, - ); - return false; - } - } - - /** - * Check if time-based criteria is met for a specific target - */ - private checkTimeCriteriaForTarget(target: string): boolean { - const criteria = this.batchingCriteria.get(target)!; - const timeSinceLastProcess = Date.now() - this.lastProcessTime.get(target)!; - return timeSinceLastProcess >= criteria.timeWindowMs!; - } - - /** - * Check if size-based criteria is met for a specific target - */ - private checkSizeCriteriaForTarget( - targetInputs: T[], - criteria: BatchingCriteriaConfig, - ): boolean { - return targetInputs.length >= criteria.maxBatchSize!; - } - - /** - * Check if value-based criteria is met - */ - private checkValueCriteriaForTarget( - targetInputs: T[], - criteria: BatchingCriteriaConfig, - ): boolean { - if (!criteria.valueAccumulatorFn || !criteria.targetValue) { - return false; - } - - const totalValue = targetInputs.reduce((sum, input) => { - return sum + criteria.valueAccumulatorFn!(input as T); - }, 0); - return totalValue >= criteria.targetValue; - } - - /** - * Check if hybrid (time + size) criteria is met for a specific target - */ - private checkHybridCriteriaForTarget( - target: string, - targetInputs: T[], - criteria: BatchingCriteriaConfig, - ): boolean { - const timeReady = this.checkTimeCriteriaForTarget(target); - const sizeReady = this.checkSizeCriteriaForTarget(targetInputs, criteria); - return timeReady || sizeReady; - } - - /** - * Check if custom criteria is met for a specific target - */ - private async checkCustomCriteriaForTarget( - target: string, - targetInputs: T[], - criteria: BatchingCriteriaConfig, - ): Promise { - if (!criteria.isBatchReadyFn) { - return false; - } - try { - return await criteria.isBatchReadyFn( - targetInputs as T[], - this.lastProcessTime.get(target)!, - ); - } catch (error) { - console.error( - `❌ Error in custom batch criteria function for target ${target}:`, - error, - ); - return false; - } - } - - /** - * Force process current batch (useful for testing or manual triggers) - */ - async forceProcessBatches(): Promise { - if (this.shutdownState.isShuttingDown) { - throw new Error("Cannot force process batches during shutdown"); - } - - console.log("🔧 Force processing batches for all targets..."); - const allTargets = Object.keys(this.adapters); - await this.processBatchesForTargets(allTargets); - - // Update last process times for all targets - const now = Date.now(); - for (const target of allTargets) { - this.lastProcessTime.set(target, now); - } - } - - /** - * Clear all pending inputs (useful for testing) - */ - async clearPendingInputs(): Promise { - if (this.shutdownState.isShuttingDown) { - throw new Error("Cannot clear pending inputs during shutdown"); - } - - await this.storage.clearAllInputs(); - } - - /** - * Start the HTTP server for the batcher - * This provides REST API endpoints for interacting with the batcher - */ - async startHttpServer(): Promise { - if (this.httpServer) { - console.log("⚠️ HTTP server already running"); - return; - } - - try { - this.httpServer = await startBatcherHttpServer(this, this.port); - await this.emitStateTransition("http:start", { - port: this.port, - time: Date.now(), - }); - } catch (error) { - console.error("❌ Failed to start HTTP server:", error); - throw error; - } - } - - /** - * Stop the HTTP server - */ - async stopHttpServer(): Promise { - if (this.httpServer) { - await this.httpServer.close(); - this.httpServer = undefined; - await this.emitStateTransition("http:stop", { time: Date.now() }); - } - } - - /** - * Get current batching status and statistics - */ - async getBatchingStatus(): Promise<{ - targets: Array<{ - target: string; - isReady: boolean; - pendingInputs: number; - criteriaType: string; - timeSinceLastProcess: number; - }>; - totalPendingInputs: number; - adapterTargets: string[]; - }> { - const adapterTargets = Object.keys(this.adapters); - const targets: Array<{ - target: string; - isReady: boolean; - pendingInputs: number; - criteriaType: string; - timeSinceLastProcess: number; - }> = []; - - let totalPendingInputs = 0; - - if (!this.defaultTarget) { - return { - targets: [], - totalPendingInputs: 0, - adapterTargets: [], - }; - } - - for (const target of adapterTargets) { - const targetInputs = await this.storage.getInputsByTarget( - target, - this.defaultTarget, - ); - const isReady = await this.isTargetReadyForBatching(target); - const timeSinceLastProcess = Date.now() - - this.lastProcessTime.get(target)!; - const criteria = this.batchingCriteria.get(target)!; - - targets.push({ - target, - isReady, - pendingInputs: targetInputs.length, - criteriaType: criteria.criteriaType, - timeSinceLastProcess, - }); - - totalPendingInputs += targetInputs.length; - } - - return { - targets, - totalPendingInputs, - adapterTargets, - }; - } - - /** - * Get shutdown status information - */ - getShutdownStatus() { - return this.shutdownManager.getShutdownStatus(); - } - - /** - * Get public configuration information (safe for external exposure) - */ - getPublicConfig(): { - pollingIntervalMs: number; - defaultTarget: string | undefined; - enableHttpServer: boolean; - enableEventSystem: boolean; - confirmationLevel: string | Partial>; - port: number; - adapterTargets: string[]; - /** Per-adapter batching criteria types */ - criteriaTypes: Record; - } { - const criteriaTypes: Record = {}; - for (const [target, criteria] of this.batchingCriteria) { - criteriaTypes[target] = criteria.criteriaType; - } - - return { - pollingIntervalMs: this.config.pollingIntervalMs, - defaultTarget: this.defaultTarget, - enableHttpServer: this.enableHttpServer, - enableEventSystem: this.enableEventSystem, - confirmationLevel: this.config.confirmationLevel || "undefined", - port: this.port, - adapterTargets: Object.keys(this.adapters), - criteriaTypes, - }; - } - - /** - * Graceful shutdown - stop accepting new batches and wait for current processing to finish - * Effection-compatible version that can be used with yield* - */ - *gracefulShutdownOp( - hooks?: ShutdownHooks, - options?: { timeoutMs?: number; force?: boolean }, - ): Operation { - yield* this.shutdownManager.gracefulShutdownOp(hooks, options); - } - - /** - * Graceful shutdown - stop accepting new batches and wait for current processing to finish - * Legacy async version for backward compatibility - */ - gracefulShutdown( - hooks?: ShutdownHooks, - options?: { timeoutMs?: number; force?: boolean }, - ): Promise { - return this.shutdownManager.gracefulShutdown(hooks, options); - } - - /** - * Stop the polling interval - */ - private stopPolling(): void { - if (this.pollingIntervalID) { - clearInterval(this.pollingIntervalID); - this.pollingIntervalID = undefined; - } - } - - /** - * Cleanup additional resources (can be overridden by subclasses) - */ - protected async cleanupResources(): Promise { - // Default implementation - can be extended by subclasses - } - - /** - * Process and submit batches using the appropriate blockchain adapters - * This method handles the core batch processing logic including: - * - Grouping inputs by target/adapter - * - Building optimized batch data - * - Submitting to appropriate blockchain via adapters - * - Handling confirmations and callbacks - */ - async processBatches(): Promise { - if (this.shutdownState.isShuttingDown) return; - - const pendingInputs = await this.storage.getAllInputs(); - - if (pendingInputs.length === 0) { - console.log("📭 No pending inputs to process"); - return; - } - - console.log(`🚀 Processing ${pendingInputs.length} pending inputs...`); - - // Group inputs by target (adapter) - const inputsByTarget = new Map(); - - for (const input of pendingInputs) { - const target = input.target || this.defaultTarget; - if (!target) { - console.error( - `❌ Skipping input: no target specified and no default target configured.`, - ); - continue; - } - if (!inputsByTarget.has(target)) { - inputsByTarget.set(target, []); - } - inputsByTarget.get(target)!.push(input); - } - - for (const [target, inputs] of inputsByTarget) { - const adapter = this.adapters[target]; - if (!adapter) { - console.error(`❌ No adapter available for target: ${target}`); - continue; - } - - // Mark target as processing when it enters - this.shutdownState.processingAdapters.add(target); - try { - await this.batchProcessor.processBatchForTarget( - adapter, - target, - inputs, - ); - } catch (error) { - console.error( - `❌ Error processing batch for target ${target}:`, - error, - ); - // Continue processing other targets even if one fails - } finally { - // Remove target from processing when it finishes - this.shutdownState.processingAdapters.delete(target); - } - } - } - - /** - * Process batches for specific targets - * @param targetsToProcess - Array of target names to process batches for - */ - async processBatchesForTargets(targetsToProcess: string[]): Promise { - if (this.shutdownState.isShuttingDown) return; - - if (targetsToProcess.length === 0) { - return; - } - - for (const target of targetsToProcess) { - const adapter = this.adapters[target]; - if (!adapter) { - console.error(`❌ No adapter available for target: ${target}`); - continue; - } - - // Get inputs for this specific target - if (!this.defaultTarget) { - console.error( - `❌ Cannot process batches: no default target configured.`, - ); - continue; - } - const targetInputs = await this.storage.getInputsByTarget( - target, - this.defaultTarget, - ); - - if (targetInputs.length === 0) { - continue; - } - - // Mark target as processing when it enters - this.shutdownState.processingAdapters.add(target); - try { - await this.emitStateTransition("batch:process:start", { - target, - inputCount: targetInputs.length, - time: Date.now(), - }); - await this.batchProcessor.processBatchForTarget( - adapter, - target, - targetInputs, - ); - } catch (error) { - console.error( - `❌ Error processing batch for target ${target}:`, - error, - ); - await this.emitStateTransition("error", { - phase: "batch", - target, - error, - time: Date.now(), - }); - // Continue processing other targets even if one fails - } finally { - // Remove target from processing when it finishes - this.shutdownState.processingAdapters.delete(target); - } - } - } - - /** - * Validate the input and return a boolean indicating if the input is valid. - * Default is a placeholder to be overridden by the user extending the Batcher class. - * @param input - The input to validate. - * @returns A boolean or Promise in the case is implemented as async indicating if the input is valid. - */ - validateInput(input: T): boolean | Promise { - return !!input.address; - } - - /** - * It starts the server and holds it until the operation is halted, - * at which point it automatically stops the server. - */ - *runHttpServer(): Operation { - if (!this.enableHttpServer) { - return; - } - - yield* resource( - (function* (this: Batcher, provide: (value: any) => void) { - const server = yield* call(() => this.startHttpServer()); - provide(server); - yield* suspend(); // Keep the server alive until cancelled - }).bind(this), - ); - } - - /** - * An Effection operation that runs the polling loop for a specific adapter target. - * Each adapter gets its own independent polling loop, eliminating cross-adapter blocking. - * - * @param target - The adapter target name to poll for - */ - *runAdapterPollingLoop(target: string): Operation { - while (true) { - yield* sleep(this.config.pollingIntervalMs); - - if (this.shutdownState.isShuttingDown) return; - - const isReady = yield* call(() => this.isTargetReadyForBatching(target)); - if (!isReady) continue; - - this.shutdownState.processingAdapters.add(target); - - try { - const targetInputs = yield* call(() => - this.storage.getInputsByTarget(target, this.defaultTarget!) - ); - - if (targetInputs.length > 0) { - yield* this.emitStateTransition("batch:process:start", { - target, - inputCount: targetInputs.length, - time: Date.now(), - }); - - const adapter = this.adapters[target]; - yield* call(() => - this.batchProcessor.processBatchForTarget( - adapter, - target, - targetInputs - ) - ); - } - - this.lastProcessTime.set(target, Date.now()); - } catch (error) { - console.error(`❌ Error processing batch for target ${target}:`, error); - yield* this.emitStateTransition("error", { - phase: "batch", - target, - error, - time: Date.now(), - }); - } finally { - this.shutdownState.processingAdapters.delete(target); - } - } - } - - /** - * An Effection operation that runs independent polling loops for each adapter. - * This operation spawns a separate polling loop for each adapter target, - * ensuring that slow adapters don't block fast ones. - */ - *runPollingLoop(): Operation { - for (const target of Object.keys(this.adapters)) { - yield* spawn(() => this.runAdapterPollingLoop(target)); - } - yield* suspend(); // Keep alive while spawned tasks run - } - - /** - * Run the batcher using Effection structured concurrency. - * This operation initializes the batcher and then runs the HTTP server - * and polling loop as concurrent, managed background tasks. - * - * @returns An Effection operation that runs the batcher. - */ - *runBatcher(): Operation { - // 1. Validate adapters before initialization - validatePreInit(this.adapters, this.defaultTarget); - - // 2. Initialize last process times for all adapters at startup - const now = Date.now(); - for (const target of Object.keys(this.adapters)) { - this.lastProcessTime.set(target, now); - } - - // 3. Perform sequential setup tasks - yield* call(() => this.storage.init()); - - // 4. Recover adapter state from storage (e.g., Bitcoin reserved funds) - if (this.defaultTarget) { - for (const [target, adapter] of Object.entries(this.adapters)) { - if (typeof adapter.recoverState === "function") { - const pendingInputs = yield* call(() => - this.storage.getInputsByTarget(target, this.defaultTarget!) - ); - yield* call(async () => await adapter.recoverState!(pendingInputs)); - } - } - } - - this.isInitialized = true; - yield* this.emitStateTransition("startup", { - publicConfig: this.getPublicConfig(), - time: Date.now(), - }); - - // 5. Run the main background tasks concurrently - // Spawn ensures that if one task fails or stops, the other is also stopped. - // This is the essence of structured concurrency. - yield* spawn(() => this.runHttpServer()); - yield* spawn(() => this.runPollingLoop()); - } -} - -/** - * Signal handler for graceful shutdown - */ -class SignalHandler { - private listeners: (() => void)[] = []; - - /** - * Setup signal listeners for graceful shutdown - */ - setup( - shutdownFn: () => Promise, - config: { - signals?: string[]; - customShutdownHandler?: (signal: string) => Promise | void; - exitCode?: number; - } = {}, - ): void { - const signals = config.signals || ["SIGINT", "SIGTERM"]; - - for (const signal of signals) { - const listener = async () => { - console.log(`🛑 Received ${signal}, initiating graceful shutdown...`); - - try { - if (config.customShutdownHandler) { - await config.customShutdownHandler(signal); - } else { - await shutdownFn(); - } - } catch (error) { - console.error(`❌ Error during shutdown on ${signal}:`, error); - } finally { - Deno.exit(config.exitCode || 0); - } - }; - - Deno.addSignalListener(signal as Deno.Signal, listener); - this.listeners.push(listener); - } - } - - /** - * Cleanup signal listeners - */ - cleanup(): void { - // Deno doesn't provide removeSignalListener, so we rely on process exit - this.listeners.length = 0; - } -} - -/** - * Factory function to create a new Batcher instance. - * Provides a cleaner API than using the constructor directly. - * - * @param config - Batcher configuration (adapters can be empty for dynamic registration) - * @param storage - Optional storage instance (defaults to BatcherFileStorage) - * @returns A new Batcher instance - * - * @example - * ```typescript - * const batcher = createNewBatcher({ - * pollingIntervalMs: 1000, - * adapters: {}, - * }); - * - * batcher.addBlockchainAdapter('ethereum', evmAdapter); - * await batcher.init(); - * ``` - */ -export function createNewBatcher( - config: BatcherConfig>>, - storage?: BatcherStorage, -): Batcher { - return new Batcher(config, storage); -} - -/** - * Create and launch a new Batcher with optional signal handling - */ -export async function createAndLaunchBatcher( - storage: BatcherStorage, - config: BatcherConfig, -): Promise { - const batcher = createNewBatcher(config, storage); - await batcher.init(); - - // Setup signal handling if configured - let signalHandler: SignalHandler | undefined; - if (config.shutdown?.signalHandling) { - signalHandler = new SignalHandler(); - signalHandler.setup( - () => - batcher.gracefulShutdown( - config.shutdown!.hooks, - { - timeoutMs: config.shutdown!.timeoutMs, - }, - ), - config.shutdown.signalHandling, - ); - } - - // Log startup information - const publicConfig = batcher.getPublicConfig(); - console.log( - `🎯 Batcher started - polling every ${publicConfig.pollingIntervalMs} milliseconds`, - ); - console.log(`📍 Default Target: ${publicConfig.defaultTarget}`); - console.log( - `⛓️ Adapter Targets: ${publicConfig.adapterTargets.join(", ")}`, - ); - console.log( - `📦 Batching Criteria: ${ - Object.entries(publicConfig.criteriaTypes).map(([target, type]) => - `${target}=${type}` - ).join(", ") - }`, - ); - if (publicConfig.enableHttpServer) { - console.log(`🌐 HTTP Server: http://localhost:${publicConfig.port}`); - } - console.log("📋 Press Ctrl+C to stop gracefully"); - - // Keep process alive (batcher runs via polling) - // The process will exit when signals are received - await new Promise(() => {}); // Never resolves, waits for signals -} diff --git a/packages/client/batcher/vendor/batcher/core/config.ts b/packages/client/batcher/vendor/batcher/core/config.ts deleted file mode 100644 index e6d1956..0000000 --- a/packages/client/batcher/vendor/batcher/core/config.ts +++ /dev/null @@ -1,460 +0,0 @@ -/** - * Type-safe configuration system for EffectStream Batcher with compile-time adapter validation - * and runtime safety checks. - */ - -import type { DefaultBatcherInput } from "./types.ts"; -import type { BlockchainAdapter } from "../adapters/adapter.ts"; -import type { ShutdownHooks } from "./shutdown-manager.ts"; -import { type Static, Type } from "npm:@sinclair/typebox@0.34.41"; -import { Value } from "npm:/@sinclair/typebox@0.34.41/value"; - -/** - * Type-safe batcher configuration with compile-time adapter validation. - * - * Ensures type safety for adapter keys and provides runtime validation. - * Supports per-adapter batching strategies and flexible confirmation levels. - */ -export type ValidAdapterKey = T extends Record ? K : never; - -/** - * Configuration for when and how batches should be processed. - * Supports time-based, size-based, value-based, hybrid, and custom criteria. - */ -export interface BatchingCriteriaConfig< - T extends DefaultBatcherInput = DefaultBatcherInput, -> { - criteriaType: "time" | "size" | "value" | "hybrid" | "custom"; - - // Time-based criteria - timeWindowMs?: number; - - // Size-based criteria - maxBatchSize?: number; - - // Value-based criteria - valueAccumulatorFn?: (input: T) => number; - targetValue?: number; - - // Custom criteria - isBatchReadyFn?: ( - pendingInputs: T[], - lastProcessTime?: number, - ) => boolean | Promise; -} - -/** - * Maps adapter targets to their specific batching criteria. - * Adapters without criteria use DEFAULT_BATCHING_CRITERIA. - */ -export type PerAdapterBatchingCriteria< - TInput extends DefaultBatcherInput = DefaultBatcherInput, - TAdapters extends Record> = Record< - string, - BlockchainAdapter - >, -> = Partial< - Record, BatchingCriteriaConfig> ->; - -/** - * Default criteria when none specified for an adapter. - * Processes inputs immediately for maximum responsiveness. - */ -export const DEFAULT_BATCHING_CRITERIA: BatchingCriteriaConfig = { - criteriaType: "size", - maxBatchSize: 1, -}; - -/** - * TypeBox schema for BatchingCriteriaConfig validation. - * Function fields use T.Any since they're runtime-validated separately. - */ -const TimeCriteriaSchema = Type.Object({ - criteriaType: Type.Literal("time"), - timeWindowMs: Type.Number({ minimum: 1 }), -}, { additionalProperties: false }); - -const SizeCriteriaSchema = Type.Object({ - criteriaType: Type.Literal("size"), - maxBatchSize: Type.Number({ minimum: 1, default: 1 }), -}, { additionalProperties: false }); - -const ValueCriteriaSchema = Type.Object({ - criteriaType: Type.Literal("value"), - valueAccumulatorFn: Type.Any(), - targetValue: Type.Number({ minimum: 0 }), -}, { additionalProperties: false }); - -const HybridCriteriaSchema = Type.Object({ - criteriaType: Type.Literal("hybrid"), - timeWindowMs: Type.Number({ minimum: 1 }), - maxBatchSize: Type.Number({ minimum: 1 }), -}, { additionalProperties: false }); - -const CustomCriteriaSchema = Type.Object({ - criteriaType: Type.Literal("custom"), - isBatchReadyFn: Type.Any(), -}, { additionalProperties: false }); - -export const BatchingCriteriaConfigSchema = Type.Union([ - TimeCriteriaSchema, - SizeCriteriaSchema, - ValueCriteriaSchema, - HybridCriteriaSchema, - CustomCriteriaSchema, -]); - -export type BatchingCriteriaConfigFromSchema = Static< - typeof BatchingCriteriaConfigSchema ->; - -/** Per-adapter criteria as a record keyed by adapter target */ -export const PerAdapterBatchingCriteriaSchema = Type.Optional( - Type.Record(Type.String(), BatchingCriteriaConfigSchema), -); - -export type ConfirmationLevel = - | "no-wait" - | "wait-receipt" - | "wait-effectstream-processed"; - -export interface BatcherConfig< - TInput extends DefaultBatcherInput = DefaultBatcherInput, - TAdapters extends Record> = Record< - string, - BlockchainAdapter - >, -> { - // Core configuration - pollingIntervalMs: number; - adapters?: TAdapters; - defaultTarget?: ValidAdapterKey; - - // Signature and networking - namespace?: string; - port?: number; - enableHttpServer?: boolean; - enableEventSystem?: boolean; - - // Batching behavior - batchingCriteria?: PerAdapterBatchingCriteria; - - // Transaction handling - confirmationLevel?: - | ConfirmationLevel - | Partial, ConfirmationLevel>>; - maxRetries?: number; - retryDelayMs?: number; - - // Shutdown configuration - shutdown?: { - hooks?: ShutdownHooks; - signalHandling?: { - signals?: string[]; - customShutdownHandler?: (signal: string) => Promise | void; - exitCode?: number; - }; - timeoutMs?: number; - }; -} - -/** Default configuration values for optional fields */ -export const DEFAULT_CONFIG_VALUES = { - namespace: "effectstream_batcher", - pollingIntervalMs: 1000, - confirmationLevel: "wait-receipt" as const, - port: 3000, - enableHttpServer: true, - enableEventSystem: false, - maxRetries: 3, - retryDelayMs: 1000, - shutdown: { - timeoutMs: 30000, - signalHandling: { - signals: ["SIGINT", "SIGTERM"], - exitCode: 0, - }, - }, -}; - -/** - * TypeBox schema for BatcherConfig validation. - * Uses T.Any for adapter and builder instances. - */ -export const BatcherConfigSchema = Type.Object({ - pollingIntervalMs: Type.Optional( - Type.Number({ - minimum: 1, - default: DEFAULT_CONFIG_VALUES.pollingIntervalMs, - }), - ), - adapters: Type.Optional( - Type.Record(Type.String(), Type.Any(), { default: {} }), - ), - defaultTarget: Type.Optional(Type.String()), - namespace: Type.Optional( - Type.String({ default: DEFAULT_CONFIG_VALUES.namespace }), - ), - - batchingCriteria: PerAdapterBatchingCriteriaSchema, - - port: Type.Optional( - Type.Number({ - minimum: 1, - maximum: 65535, - default: DEFAULT_CONFIG_VALUES.port, - }), - ), - confirmationLevel: Type.Optional( - Type.Union([ - Type.Union([ - Type.Literal("no-wait"), - Type.Literal("wait-receipt"), - Type.Literal("wait-effectstream-processed"), - ]), - Type.Record( - Type.String(), - Type.Union([ - Type.Literal("no-wait"), - Type.Literal("wait-receipt"), - Type.Literal("wait-effectstream-processed"), - ]), - ), - ], { default: DEFAULT_CONFIG_VALUES.confirmationLevel }), - ), - maxRetries: Type.Optional( - Type.Number({ minimum: 0, default: DEFAULT_CONFIG_VALUES.maxRetries }), - ), - retryDelayMs: Type.Optional( - Type.Number({ minimum: 0, default: DEFAULT_CONFIG_VALUES.retryDelayMs }), - ), - enableHttpServer: Type.Optional( - Type.Boolean({ default: DEFAULT_CONFIG_VALUES.enableHttpServer }), - ), - enableEventSystem: Type.Optional( - Type.Boolean({ default: DEFAULT_CONFIG_VALUES.enableEventSystem }), - ), - - shutdown: Type.Optional(Type.Object({ - hooks: Type.Optional(Type.Object({ - preShutdown: Type.Optional(Type.Any()), - stopAcceptingInputs: Type.Optional(Type.Any()), - waitForProcessing: Type.Optional(Type.Any()), - cleanup: Type.Optional(Type.Any()), - postShutdown: Type.Optional(Type.Any()), - }, { additionalProperties: false })), - signalHandling: Type.Optional(Type.Object({ - signals: Type.Optional( - Type.Array(Type.String(), { - default: DEFAULT_CONFIG_VALUES.shutdown.signalHandling.signals, - }), - ), - customShutdownHandler: Type.Optional(Type.Any()), - exitCode: Type.Optional( - Type.Number({ - default: DEFAULT_CONFIG_VALUES.shutdown.signalHandling.exitCode, - }), - ), - }, { additionalProperties: false })), - timeoutMs: Type.Optional( - Type.Number({ - minimum: 0, - default: DEFAULT_CONFIG_VALUES.shutdown.timeoutMs, - }), - ), - }, { additionalProperties: false })), -}, { additionalProperties: false }); - -export type BatcherConfigFromSchema = Static< - typeof BatcherConfigSchema ->; - -/** - * Applies TypeBox defaults to configuration object. - * Does not replace domain validation - use validateBatcherConfig() separately. - */ -export function applyBatcherConfigDefaults< - T extends DefaultBatcherInput, - TAdapters extends Record>, ->( - config: BatcherConfig, -): BatcherConfig { - // Cast applies defaults while preserving provided values - const casted = Value.Cast(BatcherConfigSchema as any, config as any); - return casted as BatcherConfig; -} - -/** - * Validates batcher configuration for consistency and required fields. - * Allows empty adapters for dynamic registration before initialization. - * Throws error if configuration is invalid. - */ -export function validateBatcherConfig< - T extends DefaultBatcherInput, - TAdapters extends Record>, ->(config: BatcherConfig): void { - // Allow empty adapters during initial config validation - // Full validation happens before initialization via validatePreInit() - const adapters = config.adapters || {}; - - // TypeScript already ensures defaultTarget is a valid key if specified, - // but we can add runtime validation for additional safety - if (config.defaultTarget && !(config.defaultTarget in adapters)) { - throw new Error( - `Default target '${config.defaultTarget}' is not present in adapters. Available adapters: ${ - Object.keys(adapters).join(", ") - }`, - ); - } - - // Validate batching criteria configuration for each adapter - if (config.batchingCriteria) { - for ( - const [target, criteria] of Object.entries(config.batchingCriteria) as [ - string, - BatchingCriteriaConfig, - ][] - ) { - if (!(target in adapters)) { - throw new Error( - `Batching criteria specified for unknown adapter '${target}'. Available adapters: ${ - Object.keys(adapters).join(", ") - }`, - ); - } - validateBatchingCriteria(criteria); - } - } - - if (Object.keys(adapters).length > 0) { - console.log( - `🔧✅ Configuration validated. Available adapters: ${ - Object.keys(adapters) - }`, - ); - if (config.defaultTarget) { - console.log(`🎯 Default target: ${config.defaultTarget}`); - } else { - console.log( - `🎯 Using first available adapter as default: ${ - Object.keys(adapters)[0] - }`, - ); - } - - // Log batching criteria per adapter - const adapterTargets = Object.keys(adapters); - for (const target of adapterTargets) { - const criteria = (config.batchingCriteria - ?.[target as keyof typeof config.batchingCriteria] as - | BatchingCriteriaConfig - | undefined) ?? DEFAULT_BATCHING_CRITERIA; - console.log(`📏 ${target}: ${criteria.criteriaType} criteria`); - } - } else { - console.log( - `🔧✅ Configuration validated. No adapters configured yet. Use addBlockchainAdapter() to add adapters before initialization.`, - ); - } -} - -/** - * Validates that batcher is ready for initialization. - * Requires at least one adapter to be configured and a valid defaultTarget. - * Call this before init() or runBatcher(). - */ -export function validatePreInit< - T extends DefaultBatcherInput, - TAdapters extends Record>, ->( - adapters: Record>, - defaultTarget?: string, -): void { - if (Object.keys(adapters).length === 0) { - throw new Error( - "At least one blockchain adapter must be added before initialization. " + - "Use addBlockchainAdapter() to add adapters before calling init() or runBatcher().", - ); - } - - // Ensure defaultTarget is set - if (!defaultTarget) { - throw new Error( - "Default target must be configured before initialization. " + - "If using addBlockchainAdapter(), the first adapter will automatically become the default target. " + - "Otherwise, specify defaultTarget in the configuration.", - ); - } - - // Validate defaultTarget exists in adapters - if (!(defaultTarget in adapters)) { - throw new Error( - `Default target '${defaultTarget}' is not present in adapters. Available adapters: ${ - Object.keys(adapters).join(", ") - }`, - ); - } -} - -/** - * Validates batching criteria configuration based on criteria type. - * Throws error if required fields are missing. - */ -export function validateBatchingCriteria( - criteria: BatchingCriteriaConfig, -): void { - // Check required fields for each criteria type - switch (criteria.criteriaType) { - case "time": - if (!criteria.timeWindowMs) { - throw new Error("timeWindowMs is required for 'time' criteria type"); - } - break; - - case "size": - if (!criteria.maxBatchSize) { - throw new Error("maxBatchSize is required for 'size' criteria type"); - } - break; - - case "hybrid": - if (!criteria.timeWindowMs) { - throw new Error( - "timeWindowMs is required for 'hybrid' criteria type", - ); - } - if (!criteria.maxBatchSize) { - throw new Error( - "maxBatchSize is required for 'hybrid' criteria type", - ); - } - break; - - case "value": - if (!criteria.valueAccumulatorFn) { - throw new Error( - "valueAccumulatorFn is required for 'value' criteria type", - ); - } - if (!criteria.targetValue) { - throw new Error("targetValue is required for 'value' criteria type"); - } - break; - - case "custom": - if (!criteria.isBatchReadyFn) { - throw new Error( - "isBatchReadyFn is required for 'custom' criteria type", - ); - } - break; - - default: - throw new Error( - `Unknown criteria type: ${(criteria as any).criteriaType}`, - ); - } - - console.log(`✅ Batching criteria validated: ${criteria.criteriaType}`); -} diff --git a/packages/client/batcher/vendor/batcher/core/mod.ts b/packages/client/batcher/vendor/batcher/core/mod.ts deleted file mode 100644 index 7d64081..0000000 --- a/packages/client/batcher/vendor/batcher/core/mod.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { Batcher } from "./batcher.ts"; -export type { - BatchingCriteriaConfig, - BatcherConfig, - ValidAdapterKey, -} from "./config.ts"; -export { validateBatcherConfig, validateBatchingCriteria } from "./config.ts"; -export type { BatcherStorage } from "./storage.ts"; -export { - DatabaseStorage, - FileStorage as BatcherFileStorage, -} from "./storage.ts"; -export type { DefaultBatcherInput } from "./types.ts"; diff --git a/packages/client/batcher/vendor/batcher/core/shutdown-manager.ts b/packages/client/batcher/vendor/batcher/core/shutdown-manager.ts deleted file mode 100644 index 10293da..0000000 --- a/packages/client/batcher/vendor/batcher/core/shutdown-manager.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { lift } from "npm:effection@^3.5.0"; -import type { Operation } from "npm:effection@^3.5.0"; -import type { DefaultBatcherInput } from "./types.ts"; -import type { Batcher } from "./batcher.ts"; - -export interface BatcherShutdownState { - isShuttingDown: boolean; - shutdownInitiatedAt: number | null; - shutdownTimeoutMs: number; - /** Set of adapter targets currently processing batches */ - processingAdapters: Set; -} - -export interface ShutdownHooks< - T extends DefaultBatcherInput, -> { - preShutdown?: (batcher: Batcher) => Promise | void; - stopAcceptingInputs?: (batcher: Batcher) => Promise | void; - waitForProcessing?: (batcher: Batcher) => Promise | void; - cleanup?: (batcher: Batcher) => Promise | void; - postShutdown?: (batcher: Batcher) => Promise | void; -} - -/** - * Manages the graceful shutdown process for the batcher. - * Separated from the main Batcher class to improve maintainability. - */ -export class ShutdownManager { - constructor( - private batcherInterface: { - shutdownState: BatcherShutdownState; - stopPolling(): void; - stopHttpServer(): Promise; - cleanupResources(): Promise; - }, - private batcherInstance: any, // For hooks that need the full batcher - ) {} - - /** - * Graceful shutdown - stop accepting new batches and wait for current processing to finish - * Effection-compatible version that can be used with yield* - */ - *gracefulShutdownOp( - hooks?: ShutdownHooks, - options?: { timeoutMs?: number; force?: boolean }, - ): Operation { - if (this.batcherInterface.shutdownState.isShuttingDown) return; - - this.batcherInterface.shutdownState.isShuttingDown = true; - this.batcherInterface.shutdownState.shutdownInitiatedAt = Date.now(); - this.batcherInterface.shutdownState.shutdownTimeoutMs = - options?.timeoutMs ?? - this.batcherInterface.shutdownState.shutdownTimeoutMs; - - console.log("Stopping batcher gracefully..."); - - try { - // Phase 1: Pre-shutdown (custom hook) - if (hooks?.preShutdown) { - yield* lift(hooks.preShutdown!)(this.batcherInstance); - } - - // Phase 2: Stop accepting new inputs - this.batcherInterface.stopPolling(); - yield* lift(this.batcherInstance.stopHttpServer)(); - if (hooks?.stopAcceptingInputs) { - yield* lift(hooks.stopAcceptingInputs!)(this.batcherInstance); - } - - // Phase 3: Wait for ongoing processing - yield* lift(this.waitForOngoingProcessing)(options?.timeoutMs); - if (hooks?.waitForProcessing) { - yield* lift(hooks.waitForProcessing!)(this.batcherInstance); - } - - // Phase 4: Cleanup resources - yield* lift(this.batcherInstance.cleanupResources)(); - if (hooks?.cleanup) { - yield* lift(hooks.cleanup!)(this.batcherInstance); - } - - // Phase 5: Post-shutdown (custom hook) - if (hooks?.postShutdown) { - yield* lift(hooks.postShutdown!)(this.batcherInstance); - } - - console.log("✅ Batcher shutdown complete"); - } catch (error) { - console.error("❌ Error during graceful shutdown:", error); - if (options?.force) { - console.log("🔧 Force shutdown due to error"); - } else { - throw error; - } - } - } - - /** - * Graceful shutdown - stop accepting new batches and wait for current processing to finish - * Legacy async version for backward compatibility - */ - async gracefulShutdown( - hooks?: ShutdownHooks, - options?: { timeoutMs?: number; force?: boolean }, - ): Promise { - if (this.batcherInterface.shutdownState.isShuttingDown) return; - - this.batcherInterface.shutdownState.isShuttingDown = true; - this.batcherInterface.shutdownState.shutdownInitiatedAt = Date.now(); - this.batcherInterface.shutdownState.shutdownTimeoutMs = - options?.timeoutMs ?? - this.batcherInterface.shutdownState.shutdownTimeoutMs; - - console.log("Stopping batcher gracefully..."); - - try { - // Phase 1: Pre-shutdown (custom hook) - await hooks?.preShutdown?.(this.batcherInstance); - - // Phase 2: Stop accepting new inputs - this.batcherInterface.stopPolling(); - await this.batcherInstance.stopHttpServer(); - await hooks?.stopAcceptingInputs?.(this.batcherInstance); - - // Phase 3: Wait for ongoing processing - await this.waitForOngoingProcessing(options?.timeoutMs); - await hooks?.waitForProcessing?.(this.batcherInstance); - - // Phase 4: Cleanup resources - await this.batcherInterface.cleanupResources(); - await hooks?.cleanup?.(this.batcherInstance); - - // Phase 5: Post-shutdown (custom hook) - await hooks?.postShutdown?.(this.batcherInstance); - - console.log("✅ Batcher shutdown complete"); - } catch (error) { - console.error("❌ Error during graceful shutdown:", error); - if (options?.force) { - console.log("🔧 Force shutdown due to error"); - } else { - throw error; - } - } - } - - /** - * Wait for any ongoing batch processing to complete - */ - private async waitForOngoingProcessing(timeoutMs?: number): Promise { - const timeout = timeoutMs ?? - this.batcherInterface.shutdownState.shutdownTimeoutMs; - const startTime = Date.now(); - - const processingAdapters = this.batcherInterface.shutdownState.processingAdapters; - - if (processingAdapters.size === 0) { - return; - } - - console.log(`⏳ Waiting for ${processingAdapters.size} adapters to complete: ${[...processingAdapters].join(', ')}`); - - while (processingAdapters.size > 0) { - if (Date.now() - startTime > timeout) { - throw new Error( - `Shutdown timeout: adapters still processing: ${[...processingAdapters].join(', ')}`, - ); - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - - /** - * Get shutdown status information - * Returns backward-compatible isProcessingBatch derived from processingAdapters - */ - getShutdownStatus(): BatcherShutdownState & { isProcessingBatch: boolean } { - return { - isShuttingDown: this.batcherInterface.shutdownState.isShuttingDown, - shutdownInitiatedAt: - this.batcherInterface.shutdownState.shutdownInitiatedAt, - shutdownTimeoutMs: this.batcherInterface.shutdownState.shutdownTimeoutMs, - processingAdapters: this.batcherInterface.shutdownState.processingAdapters, - isProcessingBatch: this.batcherInterface.shutdownState.processingAdapters.size > 0, - }; - } -} diff --git a/packages/client/batcher/vendor/batcher/core/storage.ts b/packages/client/batcher/vendor/batcher/core/storage.ts deleted file mode 100644 index 96b6b85..0000000 --- a/packages/client/batcher/vendor/batcher/core/storage.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { DefaultBatcherInput } from "./types.ts"; - -/** - * Interface for batcher storage operations - */ -export interface BatcherStorage< - T extends DefaultBatcherInput = DefaultBatcherInput, -> { - /** - * Initialize the storage (create directories, tables, etc.) - */ - init(): Promise; - - /** - * Add a new input to storage - */ - addInput(input: T, target: string): Promise; - - /** - * Get all pending inputs - */ - getAllInputs(): Promise; - - /** - * Remove specific processed inputs from storage (after successful processing) - * This ensures we remove exactly the inputs that were processed, not just the first N - */ - removeProcessedInputs(processedInputs: T[], target: string): Promise; - - /** - * Get the count of pending inputs - */ - getInputCountAndSize(): Promise<{ count: number; size: number }>; - - /** - * Get all pending inputs for a specific target (efficient filtering) - * @param target - The target adapter name - * @param defaultTarget - The default target to use when input.target is not specified - */ - getInputsByTarget(target: string, defaultTarget: string): Promise; - - /** - * Clear all inputs (useful for testing) - */ - clearAllInputs(): Promise; -} - -/** - * File-based storage implementation using JSONL format - */ -export class FileStorage - implements BatcherStorage { - private readonly filePath: string; - private readonly dataDirectory: string; - - constructor(dataDirectory: string = "./batcher-data") { - Deno.mkdirSync(dataDirectory, { recursive: true }); - this.dataDirectory = dataDirectory; - this.filePath = `${dataDirectory}/pending-inputs.jsonl`; - } - - async init(): Promise { - try { - await Deno.mkdir(this.dataDirectory, { recursive: true }); - } catch (error) { - console.error("Error creating data directory:", error); - throw new Error(`Failed to initialize storage: ${error}`); - } - } - - async addInput(input: T): Promise { - try { - await Deno.writeFile( - this.filePath, - new TextEncoder().encode(JSON.stringify(input) + "\n"), - { append: true }, - ); - } catch (error) { - console.error("Error adding input to storage:", error); - throw new Error(`Failed to add input: ${error}`); - } - } - - async getAllInputs(): Promise { - try { - const content = new TextDecoder().decode( - await Deno.readFile(this.filePath), - ); - const lines = content.trim().split("\n").filter((line) => line.trim()); - return lines.map((line) => JSON.parse(line)); - } catch (error) { - if ((error as any).name === "NotFound") { - // File doesn't exist yet, return empty array - return []; - } - console.error("Error reading inputs from storage:", error); - throw new Error(`Failed to read inputs: ${error}`); - } - } - - async removeProcessedInputs( - processedInputs: T[], - target: string, - ): Promise { - try { - // Create a set of keys for the processed inputs for fast lookup - const processedKeys = new Set(processedInputs.map((input) => this.createInputKey(input, target))); - - // Read all current inputs - const allInputs = await this.getAllInputs(); - - // Filter out the processed inputs - const remainingInputs = allInputs.filter((input) => - !processedKeys.has(this.createInputKey(input, target)) - ); - - // Write the remaining inputs back to the file - const content = remainingInputs.map((input) => JSON.stringify(input)) - .join("\n"); - await Deno.writeFile( - this.filePath, - new TextEncoder().encode( - content + (remainingInputs.length > 0 ? "\n" : ""), - ), - ); - - const removedCount = allInputs.length - remainingInputs.length; - if (removedCount !== processedInputs.length) { - console.warn( - `⚠️ Expected to remove ${processedInputs.length} inputs, but removed ${removedCount}. Some inputs may have been processed already.`, - ); - } - } catch (error) { - console.error("Error removing processed inputs:", error); - throw new Error(`Failed to remove processed inputs: ${error}`); - } - } - - /** - * Create a unique key for a DefaultBatcherInput for comparison - */ - private createInputKey(input: T, target: string): string { - return `${input.addressType}-${target}-${input.address}-${input.input}-${input.timestamp}-${input.signature ?? ""}`; - } - - async getInputCountAndSize(): Promise<{ count: number; size: number }> { - try { - const inputs = await this.getAllInputs(); - const size = inputs.reduce( - (acc, input) => acc + JSON.stringify(input).length, - 0, - ); - return { count: inputs.length, size }; - } catch (error) { - console.error("Error getting input count:", error); - throw new Error(`Failed to get input count: ${error}`); - } - } - - async getInputsByTarget(target: string, defaultTarget: string): Promise { - try { - const allInputs = await this.getAllInputs(); - return allInputs.filter((input) => - (input.target || defaultTarget) === target - ); - } catch (error) { - console.error("Error getting inputs by target:", error); - throw new Error(`Failed to get inputs by target: ${error}`); - } - } - - async clearAllInputs(): Promise { - try { - await Deno.remove(this.filePath); - } catch (error) { - if ((error as any).name !== "NotFound") { - console.error("Error clearing inputs:", error); - throw new Error(`Failed to clear inputs: ${error}`); - } - // File doesn't exist, which means it's already cleared - } - } -} - -/** - * TODO: database storage implementation. - * This could be implemented with PostgreSQL, - * Perhaps passing the connection string as an argument. - */ -export class DatabaseStorage< - T extends DefaultBatcherInput = DefaultBatcherInput, -> implements BatcherStorage { - constructor(private connectionString: string) {} - - // TODO: Implement database storage - init(): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } - addInput(input: T): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } - getAllInputs(): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } - removeProcessedInputs( - processedInputs: T[], - ): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } - getInputCountAndSize(): Promise<{ count: number; size: number }> { - throw new Error("DatabaseStorage not implemented yet"); - } - getInputsByTarget(target: string, defaultTarget: string): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } - clearAllInputs(): Promise { - throw new Error("DatabaseStorage not implemented yet"); - } -} diff --git a/packages/client/batcher/vendor/batcher/core/types.ts b/packages/client/batcher/vendor/batcher/core/types.ts deleted file mode 100644 index 5df6009..0000000 --- a/packages/client/batcher/vendor/batcher/core/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AddressType } from "jsr:@paimaexample/utils@^0.8.4"; - -export interface DefaultBatcherInput { - addressType: AddressType; - input: string; - signature?: string; - address: string; - timestamp: string; - target?: string; // Optional since by default we will target the PaimaL2 contract -} diff --git a/packages/client/batcher/vendor/batcher/mod.ts b/packages/client/batcher/vendor/batcher/mod.ts deleted file mode 100644 index c7a05cb..0000000 --- a/packages/client/batcher/vendor/batcher/mod.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Batcher - Main Module Exports - * - * This module provides a clean interface to the batcher system, - * including the core batcher class, configuration types, storage interfaces, - * and chain adapters. - */ - -// Core batcher functionality -export { Batcher, createNewBatcher } from "./core/batcher.ts"; - -// Configuration types and validation -export type { - BatchingCriteriaConfig, - BatcherConfig, - ValidAdapterKey, -} from "./core/config.ts"; -export { - applyBatcherConfigDefaults, - BatchingCriteriaConfigSchema, - DEFAULT_CONFIG_VALUES, - BatcherConfigSchema, - PerAdapterBatchingCriteriaSchema, - validateBatcherConfig, - validateBatchingCriteria, -} from "./core/config.ts"; - -// Storage interfaces and implementations -export type { BatcherStorage } from "./core/storage.ts"; -export { DatabaseStorage, FileStorage } from "./core/storage.ts"; - -// Chain adapter interface and implementations -export type { BlockchainAdapter, BatchBuildingOptions, BatchBuildingResult } from "./adapters/adapter.ts"; -export { PaimaL2DefaultAdapter } from "./adapters/paimal2-adapter.ts"; -export { MidnightAdapter } from "./adapters/midnight-adapter.ts"; -export { BitcoinAdapter, buildBitcoinSignatureMessage } from "./adapters/bitcoin-adapter.ts"; -export { parseCircuitArgs } from "./adapters/mod.ts"; -export { - EvmContractAdapter, - type EvmContractAdapterConfig, - type HardhatArtifact, -} from "./adapters/evm-contract-adapter.ts"; - -export type { BitcoinAdapterConfig } from "./adapters/bitcoin-adapter.ts"; -export type { MidnightAdapterConfig } from "./adapters/midnight-adapter.ts"; - -// HTTP server -export { startBatcherHttpServer } from "./server/batcher-server.ts"; - -// Utility types -export type { DefaultBatcherInput } from "./core/types.ts"; - -// Event/listener helpers -export type { BatcherGrammar, BatcherListener } from "./core/batcher-events.ts"; -export { attachDefaultConsoleListeners } from "./core/batcher-events.ts"; - -export { DefaultBatchBuilderLogic } from "./batch-data-builder/default-builder-logic.ts"; -export { - EvmBatchBuilderLogic, - type EvmBatchPayload, -} from "./batch-data-builder/evm-builder-logic.ts"; -export { - MidnightBatchBuilderLogic, - type MidnightBatchPayload, -} from "./batch-data-builder/midnight-builder-logic.ts"; diff --git a/packages/client/batcher/vendor/batcher/server/batcher-server.ts b/packages/client/batcher/vendor/batcher/server/batcher-server.ts deleted file mode 100644 index 206669e..0000000 --- a/packages/client/batcher/vendor/batcher/server/batcher-server.ts +++ /dev/null @@ -1,421 +0,0 @@ -import fastify, { type FastifyInstance, type FastifyRequest } from "npm:fastify@^5.4.0"; -import cors from "npm:@fastify/cors@^11.0.1"; -import { type Static, Type } from "npm:@sinclair/typebox@0.34.41"; -import type { Batcher, DefaultBatcherInput } from "../core/mod.ts"; -import { InputValidationError } from "../core/batcher.ts"; -import fastifySwagger, { - type FastifyDynamicSwaggerOptions, -} from "npm:@fastify/swagger@^9.5.1"; -import fastifySwaggerUi, { - type FastifySwaggerUiOptions, -} from "npm:@fastify/swagger-ui@^5.2.3"; - -// TypeBox schema for DefaultBatcherInput (adapted for new batcher input format) -const BatcherInputSchema = Type.Object({ - address: Type.String(), - addressType: Type.Number(), - input: Type.String(), - signature: Type.Optional(Type.String()), - timestamp: Type.String(), - target: Type.Optional(Type.String()), -}); - -const BatcherInputWrapper = Type.Object({ - data: BatcherInputSchema, - confirmationLevel: Type.Union([ - Type.Literal("no-wait"), - Type.Literal("wait-receipt"), - Type.Literal("wait-effectstream-processed"), - ], { - default: "wait-receipt", - }), -}); - -type BatcherInputWrapper = Static; - -/** - * Register the OpenAPI documentation for the batcher server. - * Create the OpenAPI specification and the UI. - * UI is attached at /documentation - * @param server - The Fastify instance. - * @param port - The port to listen on. - */ -async function registerOpenApiDocumentation( - server: FastifyInstance, - port: number, -) { - // Generate OpenAPI documentation - // Documentation is available at /documentation /documentation/json /documentation/yaml - const openApiOptions: FastifyDynamicSwaggerOptions = { - openapi: { - info: { - title: "Batcher", - description: - "Batcher API - Simplified architecture with configuration-driven batching", - version: "2.0.0", - }, - tags: [ - { - name: "batcher", - description: "User Batcher related end-points", - }, - { - name: "developer", - description: "Devops/Status related end-points", - }, - ], - servers: [ - { - url: `http://localhost:${port}`, - description: "Local server", - }, - ], - }, - hideUntagged: true, - }; - - const uiOptions: FastifySwaggerUiOptions = { - routePrefix: "/documentation", - uiConfig: { - docExpansion: "list", - deepLinking: false, - }, - uiHooks: { - onRequest: function (request, reply, next) { - next(); - }, - preHandler: function (request, reply, next) { - next(); - }, - }, - staticCSP: true, - transformStaticCSP: (header) => { - return header.replace(/ frame-ancestors 'self';/, ""); - }, - transformSpecification: (swaggerObject, request, reply) => { - return swaggerObject; - }, - transformSpecificationClone: true, - theme: { - css: [ - { - filename: "custom.css", - content: ` - .swagger-ui .topbar { - display: none; - } - `, - }, - ], - }, - }; - - await server.register(fastifySwagger as any, openApiOptions); - await server.register(fastifySwaggerUi as any, uiOptions); - - // Register error-catching handler - server.setErrorHandler((error, request, reply) => { - console.error("[HTTP SERVER] Error: ", error, request.url); - - // Handle validation errors (return 400 instead of 500) - if ((error as any).validation) { - return reply.status(400).send({ - success: false, - error: "Validation failed", - message: "Invalid request data", - details: (error as any).validation - }); - } - - // Handle InputValidationError (return appropriate status code) - if (error instanceof InputValidationError) { - return reply.status(error.statusCode).send({ - success: false, - error: "Validation failed", - message: error.message, - }); - } - - reply.status(500).send({ ok: false, error: (error as any).message ?? "Unknown error" }); - }); -} - -/** - * Start the batcher HTTP server. - * @param batcher - Batcher instance. - * @param port - The port to listen on. - */ -export async function startBatcherHttpServer( - batcher: Batcher, - port: number, -): Promise { - const server = fastify(); - - await registerOpenApiDocumentation(server, port); - - await server.register(cors as any, { origin: "*" }); - - server.get("/health", { - schema: { - tags: ["developer"], - response: { - 200: Type.Object({ - status: Type.String(), - isInitialized: Type.Boolean(), - isRunning: Type.Boolean(), - }), - }, - }, - }, () => { - return { - status: "ok", - isInitialized: batcher.isInitialized || false, - isRunning: true, - }; - }); - - server.get("/status", { - schema: { - tags: ["developer"], - response: { - 200: Type.Object({ - batcher: Type.Object({ - isInitialized: Type.Boolean(), - pendingInputs: Type.Number(), - criteriaType: Type.String(), - timeSinceLastProcess: Type.Number(), - adapterTargets: Type.Array(Type.String()), - }), - config: Type.Object({ - pollingIntervalMs: Type.Number(), - defaultTarget: Type.String(), - enableHttpServer: Type.Boolean(), - enableEventSystem: Type.Boolean(), - confirmationLevel: Type.Union([ - Type.String(), - Type.Record(Type.String(), Type.String()), - ]), - }), - timestamp: Type.String(), - }), - }, - }, - }, async () => { - const status = await batcher.getBatchingStatus(); - const config = batcher.getPublicConfig(); - return { - batcher: { - isInitialized: batcher.isInitialized || false, - totalPendingInputs: status.totalPendingInputs, - targets: status.targets, - adapterTargets: status.adapterTargets, - }, - config, - timestamp: new Date().toISOString(), - }; - }); - - server.get("/queue-stats", { - schema: { - tags: ["developer"], - response: { - 200: Type.Object({ - totalPendingInputs: Type.Number(), - targets: Type.Array(Type.Object({ - target: Type.String(), - pendingInputs: Type.Number(), - isReady: Type.Boolean(), - criteriaType: Type.String(), - timeSinceLastProcess: Type.Number(), - })), - }), - }, - }, - }, async () => { - const status = await batcher.getBatchingStatus(); - return { - totalPendingInputs: status.totalPendingInputs, - targets: status.targets, - }; - }); - - // Add user input to batcher - server.post("/send-input", { - schema: { - tags: ["batcher"], - body: BatcherInputWrapper, - response: { - 200: Type.Object({ - success: Type.Boolean(), - message: Type.String(), - inputsProcessed: Type.Number(), - transactionHash: Type.Optional(Type.String()), - rollup: Type.Optional(Type.Number()), - }), - }, - }, - }, async ( - request: FastifyRequest, - reply, - ) => { - try { - const body = request.body as any; - const batcherInput = body.data; - let confirmationLevel = body.confirmationLevel as any; - if (!confirmationLevel) { - const cfg = batcher.config?.confirmationLevel; - if (typeof cfg === "string") { - confirmationLevel = cfg; - } else if (cfg && typeof batcher === "object") { - const target = (body.data?.target as string) || - (batcher.getPublicConfig().defaultTarget) || "undefined"; - confirmationLevel = cfg[target] ?? "wait-receipt"; - } else { - confirmationLevel = "wait-receipt"; - } - } - - // Adapt the input format for the new batcher - const adaptedInput = { - address: batcherInput.address, - addressType: batcherInput.addressType, - input: batcherInput.input, - signature: batcherInput.signature, - timestamp: batcherInput.timestamp, - target: batcherInput.target, - }; - - // Add input to batcher with confirmation level - const result = await batcher.batchInput( - adaptedInput as any, - confirmationLevel, - ); - - // Return appropriate response based on confirmation level - switch (confirmationLevel) { - case "no-wait": - return { - success: true, - message: "Input queued for batching", - inputsProcessed: 1, - }; - case "wait-receipt": - return { - success: true, - message: "Input processed successfully", - transactionHash: result?.hash, - inputsProcessed: 1, - }; - case "wait-effectstream-processed": - return { - success: true, - message: "Input processed and validated by EffectStream", - transactionHash: result?.hash, - rollup: result?.rollup, - inputsProcessed: 1, - }; - default: - return { - success: true, - message: "Input processed successfully", - inputsProcessed: 1, - }; - } - } catch (error) { - console.error("Error adding input to batcher:", error); - - if (error instanceof InputValidationError) { - return reply.status(error.statusCode).send({ - success: false, - error: "Validation failed", - message: error.message, - }); - } - - return reply.status(500).send({ - success: false, - error: "Internal server error", - message: error instanceof Error ? error.message : "Unknown error", - }); - } - }); - - // Force process current batch (useful for testing/debugging) - server.post("/force-batch", { - schema: { - tags: ["developer"], - response: { - 200: Type.Object({ - success: Type.Boolean(), - message: Type.String(), - remainingInputs: Type.Number(), - }), - 500: Type.Object({ - success: Type.Boolean(), - error: Type.String(), - message: Type.String(), - }), - }, - }, - }, async (_, reply) => { - try { - await batcher.forceProcessBatches(); - const status = await batcher.getBatchingStatus(); - return { - success: true, - message: "Batch processing forced", - remainingInputs: status.totalPendingInputs, - }; - } catch (error) { - console.error("Error forcing batch:", error); - return reply.status(500).send({ - success: false, - error: "Failed to force batch processing", - message: error instanceof Error ? error.message : "Unknown error", - }); - } - }); - - // Clear all pending inputs (administrative endpoint) - server.delete("/clear-inputs", { - schema: { - tags: ["developer"], - response: { - 200: Type.Object({ - success: Type.Boolean(), - message: Type.String(), - }), - }, - }, - }, async () => { - try { - await batcher.clearPendingInputs(); - return { - success: true, - message: "All pending inputs cleared", - }; - } catch (error) { - console.error("Error clearing inputs:", error); - return { - success: false, - error: "Failed to clear inputs", - message: error instanceof Error ? error.message : "Unknown error", - }; - } - }); - - // Start the server - server.listen( - { port, host: "0.0.0.0" }, - (err: Error | null, address: string) => { - if (err) { - console.error("Batcher HTTP server error:", err); - throw err; - } - console.log(`🎯 Batcher HTTP server running on ${address}`); - console.log( - `📖 OpenAPI documentation available at ${address}/documentation`, - ); - }, - ); -} diff --git a/packages/client/database/deno.json b/packages/client/database/deno.json index 8502ece..6c55866 100644 --- a/packages/client/database/deno.json +++ b/packages/client/database/deno.json @@ -6,10 +6,10 @@ "pgtyped:update": "npx pgtyped -c pgtyped.config.json" }, "imports": { - "@paimaexample/config": "jsr:@paimaexample/config@0.9.0", - "@paimaexample/runtime": "jsr:@paimaexample/runtime@0.9.0", - "@paimaexample/sm": "jsr:@paimaexample/sm@0.9.0", - "@paimaexample/db": "jsr:@paimaexample/db@0.9.0", + "@paimaexample/config": "jsr:@paimaexample/config@0.10.24", + "@paimaexample/runtime": "jsr:@paimaexample/runtime@0.10.24", + "@paimaexample/sm": "jsr:@paimaexample/sm@0.10.24", + "@paimaexample/db": "jsr:@paimaexample/db@0.10.24", "@pgtyped/runtime": "npm:@pgtyped/runtime@2.4.2", "pg": "npm:pg@^8.14.0", "effection": "npm:effection@3.5.0" diff --git a/packages/client/node/data/game-state.json b/packages/client/node/data/game-state.json deleted file mode 100644 index 91f8f9c..0000000 --- a/packages/client/node/data/game-state.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "lobby_8_1773412316021": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 1, - "lastAskingPlayer": 1 - }, - "lobby_17_1773416326876": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 0, - "lastAskingPlayer": 1 - }, - "lobby_6_1773672635718": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 3, - "lastAskingPlayer": 1 - }, - "lobby_8_1773677045816": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 6, - "lastAskingPlayer": 1 - }, - "lobby_3_1773685165806": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 0, - "lastAskingPlayer": 1 - }, - "lobby_53_1773688341647": { - "phase": "wait_draw", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 6, - "lastAskingPlayer": 1 - }, - "lobby_22_1773757915223": { - "phase": "wait_response", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 4, - "lastAskingPlayer": 1 - }, - "lobby_10_1773856012585": { - "phase": "turn_start", - "currentTurn": 2, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 5, - "lastAskingPlayer": 1 - }, - "lobby_3_1773857286675": { - "phase": "turn_start", - "currentTurn": 2, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 6, - "lastAskingPlayer": 1 - }, - "lobby_13_1773862412269": { - "phase": "wait_draw", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 0, - "lastAskingPlayer": 1 - }, - "lobby_14_1773864280525": { - "phase": "turn_start", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 1, - "lastAskingPlayer": 1 - }, - "lobby_9_1773923699366": { - "phase": "turn_start", - "currentTurn": 1, - "scores": [ - 0, - 0 - ], - "handSizes": [ - 7, - 7 - ], - "deckCount": 38, - "isGameOver": false, - "lastAskedRank": 4, - "lastAskingPlayer": 1 - } -} \ No newline at end of file diff --git a/packages/client/node/data/player-secrets.json b/packages/client/node/data/player-secrets.json deleted file mode 100644 index 3fe851c..0000000 --- a/packages/client/node/data/player-secrets.json +++ /dev/null @@ -1,378 +0,0 @@ -{ - "secrets": { - "6c6f6262795f31375f3137373334313633323638373600000000000000000000-1": "01fd8c2b80cac4774a2641a91fcb4d418f61c1c92d538123e24a3ba1d39c4a7e", - "6c6f6262795f31375f3137373334313633323638373600000000000000000000-2": "0816a0d854b252374dcda82e2240ac0c69e3c5a950a96a6deb0a5ece2dbeb6a2", - "6c6f6262795f33345f3137373334323333383233343600000000000000000000-2": "09e8bdb28ca08e02556d21638d1f16b3a1a248708f53c6a8256150e94d71bf16", - "6c6f6262795f33345f3137373334323333383233343600000000000000000000-1": "040c73efc55c61c3a9bcd0b7305117bfded623518e907f3a52b4adac7c9d7035", - "6c6f6262795f31395f3137373334323833333632393200000000000000000000-2": "0ce647f28b2c99fb2f8c4f71741789ac5ff58caa6822d3f59b2e356fbc2f4974", - "6c6f6262795f31395f3137373334323833333632393200000000000000000000-1": "017bc9a55fd1b66138e0faf369487016dc64ac9379718cf4af4e579d1798014c", - "6c6f6262795f365f313737333637323633353731380000000000000000000000-1": "0aa788aca1f9644d5d434591850b953e211c403da7cd81b51e18242a9bf1939f", - "6c6f6262795f365f313737333637323633353731380000000000000000000000-2": "0d02a9e2a42be13b0fe2897fad4ba7eca100f9d3c95d6abfcd649ef6697eb602", - "6c6f6262795f31325f3137373336373539313337303900000000000000000000-2": "01f45ebf532ec200081065ed9d14e477a3c713892e387c90365a1249707ccb63", - "6c6f6262795f31325f3137373336373539313337303900000000000000000000-1": "0bb187a3db3b1be356519004bfd33f6e8d1532d913be4fc337327324a402c71e", - "6c6f6262795f385f313737333637373034353831360000000000000000000000-2": "02e3b6099dc3c80c04b185b0a5e3b3ad4ba0651cb67833481a4b092ea43c72ef", - "6c6f6262795f385f313737333637373034353831360000000000000000000000-1": "0a23a318be9266c2b1b8e9218a29b6c97d363761943108c08b5818235ac49258", - "6c6f6262795f32345f3137373336383239313738393500000000000000000000-2": "02923e8c6c1b56e00f539f1e55ef02d108e64f99a3352d4f82203e9dd14e03b1", - "6c6f6262795f32345f3137373336383239313738393500000000000000000000-1": "06fe2683c8266758b3df7b8e71d278333cf81b7fffb43b304f43a6cf22d1acfa", - "6c6f6262795f375f313737333638333837393239320000000000000000000000-2": "0b3831ff8cd16ad047d285741217b9dcc60b5105d04d74f8d4023ca24041030e", - "6c6f6262795f375f313737333638333837393239320000000000000000000000-1": "088733798fd30acb3631cc29c18721c80e5f159b784526cf908e5c1902325484", - "6c6f6262795f335f313737333638353136353830360000000000000000000000-2": "0519987ea026cec230d2e22e58e168a589efd2cad436ca6f0c277b872e44f946", - "6c6f6262795f335f313737333638353136353830360000000000000000000000-1": "0c48f511bb5f417d80c8b438e13eb59b542ddb3510fe9c8a5419f844d226f895", - "6c6f6262795f31385f3137373336383632313833383600000000000000000000-2": "03764456c13af11ec761cb0bd9385c2edb4bf84ecf6d77f0f7a82e390c9e5ed1", - "6c6f6262795f31385f3137373336383632313833383600000000000000000000-1": "06e38342430477586df5c4e3bbc086b1508cf5dc455adbeda9815e6eea6d1aca", - "6c6f6262795f35335f3137373336383833343136343700000000000000000000-2": "011242a2dd5c88d561999d2c12c49ceaac6c6dbe2f1562ded751656ae6bf1f71", - "6c6f6262795f35335f3137373336383833343136343700000000000000000000-1": "046ddf28ca212c609f7e6de6e13f45ce759d0c831fa80758473816f1cf700f37", - "6c6f6262795f355f313737333639303432363130390000000000000000000000-1": "0926ab14866c2153f0ec5dfcae228e8e41210ed90fc88b9226ec04c328b82616", - "6c6f6262795f355f313737333639303432363130390000000000000000000000-2": "0071d79e8cf50b6deb757ed6a49fada62c7861a466ae752d4d458d089afebc7d", - "6c6f6262795f32325f3137373337353739313532323300000000000000000000-1": "09e9cff9dc86dc100a30d36d18cbf427134d2675638b166eb2e3816c9aecfac9", - "6c6f6262795f32325f3137373337353739313532323300000000000000000000-2": "02cf5a9ed9bac162350bc505c129d819c1a6446e0cdc74163ad698c5a4a4ff26", - "6c6f6262795f31335f3137373337373733383939383800000000000000000000-1": "0445f00effbd544b447c2c52d0621e702b6b05c4b31148303912673e071f4d7a", - "6c6f6262795f31335f3137373337373733383939383800000000000000000000-2": "0d28328ee402564469521358a3602631278e2ae9e6e36fc26600eb7ad00eb4d9", - "6c6f6262795f32315f3137373337373932333731373000000000000000000000-2": "0c22e89056650f934981641cc4cc0e37a4aac23805c51d9784550ffaee2f5376", - "6c6f6262795f32315f3137373337373932333731373000000000000000000000-1": "09beddb0734ae8028827e67d3e520e8df972951211b1a8241b4eeda310c95bd2", - "6c6f6262795f313936375f313737333738313138313536310000000000000000-1": "07099b95940d40358e254ce6f6a806841d654163d388b77b73c30e3e5131289a", - "6c6f6262795f313936375f313737333738313138313536310000000000000000-2": "09514a414189eca0f6e1cc86562f051bcc28f8cac606286c0fb48e64954f4dc9", - "6c6f6262795f31315f3137373338343739323438373200000000000000000000-1": "0528f89efd72941a7567dbe8c05ce9cbe613188a949dd61c74cd9f0951e4262c", - "6c6f6262795f31315f3137373338343739323438373200000000000000000000-2": "06fb17f3ca1f111eff81c5a9096a357a2a4deb38a1a3a88c939d824aa3de3873", - "6c6f6262795f32305f3137373338353030323331343600000000000000000000-1": "0569e33eee10d16810b481fbff9b64e4b14d4bddebc2e34b98c52f966a758436", - "6c6f6262795f32305f3137373338353030323331343600000000000000000000-2": "0176fbb2b0f96b83dafd5c670405319251eb6434d15ae0a844350aba438b3ea8", - "6c6f6262795f31345f3137373338353233363735373100000000000000000000-1": "09d810eda7f6a87d56a9862814f988e491c6ae0c7e79091d3aaee62dde793912", - "6c6f6262795f31345f3137373338353233363735373100000000000000000000-2": "02cdf18159e85958b5221f2702510966ffa49ba36551b9cce52d67c02b106bdd", - "6c6f6262795f355f313737333835333432383732390000000000000000000000-2": "092b7dfb842b13cd8b840bf381d7bb80c064b4cba87f435b36f92a9eb5be13ed", - "6c6f6262795f355f313737333835333432383732390000000000000000000000-1": "03158e093bf6c397d9796c8b32259d426327c7bde12a93f01961c120def31593", - "6c6f6262795f31305f3137373338353630313235383500000000000000000000-1": "0e74827caed1d83691993a2b7d2fc402ceb28d23119e48adf384c5ef1fd37819", - "6c6f6262795f31305f3137373338353630313235383500000000000000000000-2": "030ec24feddf041353f7b8127142be4c53f0ecd51e6a28cad774d968d6e81eb6", - "6c6f6262795f335f313737333835373238363637350000000000000000000000-1": "08e68ff7ea60bf16f690ad2167588f2b3fdac8dd57585df68770265be0a2fe44", - "6c6f6262795f335f313737333835373238363637350000000000000000000000-2": "09ceaf5f6153bb93e98f0f3949f03cb23f6500cc4d7e48cca2c9976b1b9af5ba", - "6c6f6262795f31335f3137373338363234313232363900000000000000000000-2": "0c4dc0ac02d3b8a2439bdffece5b8da5f366123213715835d42019e46236e97b", - "6c6f6262795f31335f3137373338363234313232363900000000000000000000-1": "0a3c7cf290bfaa8d55a2f86d41f027a14037a485df766fad8c64459d3cbe2fb4", - "6c6f6262795f31345f3137373338363432383035323500000000000000000000-1": "01fd51455a3d2d796641e5ee153429eb4862b3b53b788f03ad799c033798e4f3", - "6c6f6262795f31345f3137373338363432383035323500000000000000000000-2": "0a0d52f1f001a946435d5f6c048007efb21a6141fdb5c7d4b6fd6780a741001d", - "6c6f6262795f395f313737333932333639393336360000000000000000000000-2": "010eabb0f3c9038e088cb26c968e193740035b5e4f1d2836cbd85d629ca85d76", - "6c6f6262795f395f313737333932333639393336360000000000000000000000-1": "091ffb85c8d48e8e935cf9887d7d1be6bee9a39828182c773a93fa547a6794a6", - "6c6f6262795f365f313737333932363138323433370000000000000000000000-2": "067d0d69f4fcca2280e71a1005350f341fef8329b5e9a50f6bcd35801a9e265f", - "6c6f6262795f365f313737333932363138323433370000000000000000000000-1": "0659cae2f15b2680dba8be4fbcfdb316736d78ef28277140132f7720116afdfd", - "6c6f6262795f345f313737333933333033353837300000000000000000000000-1": "09df73a2e7d1de69cb4830c026dc85309332cb72b14eeccd48748a3da0e41e8c", - "6c6f6262795f345f313737333933333033353837300000000000000000000000-2": "07a73719f568537da0b7b9ea08b2b1e1c447b136f9c0c997da2e19104119bc22", - "6c6f6262795f33305f3137373339353038333937353800000000000000000000-1": "0462927c1ab8df4acafc11c4be2c0bf86a11a05ee2e01ec5a04611934dee000b", - "6c6f6262795f33305f3137373339353038333937353800000000000000000000-2": "05d8d57507e649d9b09793a9986354d9103735d40e8f5b11d4d4a9b150d90133", - "6c6f6262795f345f313737333935313936363334360000000000000000000000-2": "0e7a87fdaa256bb434ffeb0abd827a175590a872f59acda4e5ba3788e157dd91", - "6c6f6262795f345f313737333935313936363334360000000000000000000000-1": "0d14979994aa73d2e26366805612ad391cb99371b3913244f0c021081a80d7b2", - "6c6f6262795f375f313737343031383534313139380000000000000000000000-1": "0a2d1e9b42dd18f1db9db92195d81e24e4f7e63195ce070b06e5ae302ac41ca5", - "6c6f6262795f375f313737343031383534313139380000000000000000000000-2": "03ff07c4428017c6f3ecbdbaf01ce3c90496b65fe2d9bbe1926d7a78b9db87ef", - "6c6f6262795f365f313737343032313833313231330000000000000000000000-1": "0ca275ce7bcf12444ababbb94efda0463a007a0539010e2abf9ef7917fb61e8d", - "6c6f6262795f365f313737343032313833313231330000000000000000000000-2": "0a9aa2b0c419386591ac16559dd766125bbfb82d7f297e9145fd25d0e169e395", - "6c6f6262795f33315f3137373430323433363233393500000000000000000000-1": "02c79b6479fad29d7ed51c6f9ce9db65c4c53e9f267d2e4d7a6a0e02aa1dd060", - "6c6f6262795f33315f3137373430323433363233393500000000000000000000-2": "0e3a23c3a14da4d2239417bdbf48206f2e33ad5fc6f930ec9907363f33f82c55", - "6c6f6262795f35375f3137373430333139363637313000000000000000000000-2": "0444d724bf91ed8791690cfdc9f18ca6de00fcf4d2f4cdf7ef9bb2d65d91be4e", - "6c6f6262795f35375f3137373430333139363637313000000000000000000000-1": "04f2156128cd6a97b52b57a36a1f50cf607b1b85a0f8bb975213ec854b9488cf", - "6c6f6262795f31305f3137373430333631363033363400000000000000000000-1": "09e6f2fda0222aef7c3e92698f74f55677c738844595cd6892e54b27b345fbbd", - "6c6f6262795f31305f3137373430333631363033363400000000000000000000-2": "05a1ea1ecff64b0d2ae692a34d0ec5adfc3ebc086838603060ea04a272154ab3", - "6c6f6262795f395f313737343033363532343239310000000000000000000000-2": "0264f50c635c34b154e47d62b409b3123e045c28604b877dbfc57436d8c40be1", - "6c6f6262795f395f313737343033363532343239310000000000000000000000-1": "0dd7f0d06689c408d903e58e3bc3d99b3d2d81172bc8e6fa809143a264c14796", - "6c6f6262795f375f313737343033373337343839390000000000000000000000-2": "0ccea5f838488fe31238011d27a4fe09bbefe0bdea2af089713ab24e44f4847d", - "6c6f6262795f375f313737343033373337343839390000000000000000000000-1": "09c29bcbb38cf0ef46dcaeb734b2fad937c8dea814690cbe6b8e460810ee9dbc", - "6c6f6262795f3136365f31373734303338303238363031000000000000000000-2": "0e1688d8fa9315579231b58fdd946a464b392fc5db97929df18a49f31c5d26af", - "6c6f6262795f3136365f31373734303338303238363031000000000000000000-1": "07129af5cf4c0fd010a455217f1e4b2c179d71775aaa9443fcbc42732078e7d7", - "6c6f6262795f375f313737343034303434393832310000000000000000000000-2": "0bf71197309d6711560f25583e26f24584a94c595890088593db88f34784be1d", - "6c6f6262795f375f313737343034303434393832310000000000000000000000-1": "0aa40100808a2069ebd42bffa4f6a5e70ce569051f946d211a65bed4938226b8", - "6c6f6262795f335f313737343034323737313437300000000000000000000000-2": "02167b9ba6c8cca7e03221587d83a90079e47f33645c4cf4d717e1bb4906a838", - "6c6f6262795f335f313737343034323737313437300000000000000000000000-1": "00e5fb7f07fe1c397c150bc86446d651867de6dfab476d7446ff18523ced2d03", - "6c6f6262795f355f313737343034343735313838390000000000000000000000-2": "046a82750fbce0cc83bc575fe65aaf80304cea9aee8495a202a5ca9935741e88", - "6c6f6262795f355f313737343034343735313838390000000000000000000000-1": "051fffa2425bdf011f8125f844880e4568a52b10e17d0aff3a0e7cfe9660585a", - "6c6f6262795f3337325f31373734323733313338373538000000000000000000-2": "0a63ebf96fdb509fd6c156d2fc0d801c2f4ec901bc6b9b2d6824d6b2801902d5", - "6c6f6262795f3337325f31373734323733313338373538000000000000000000-1": "090cf39100643fd6f9b0e0a2a0416ff64ca20f72a2fd1e45e4c8a76f8525e87c", - "6c6f6262795f33315f3137373432373532373335313800000000000000000000-2": "05f669fe282c083bb2ae0e93fc25b7c8180f7f1d60b0499778bca44b7acd5e2d", - "6c6f6262795f33315f3137373432373532373335313800000000000000000000-1": "097305eb1ce3e50e5c2f019486612f046fe1afe700e56480e84c796caa30adf5", - "6c6f6262795f375f313737343237383032353734370000000000000000000000-1": "06b5f1e6cc32673e7e13d0648c08c8711e95d95e3ecf728c1988840aff6980cd", - "6c6f6262795f375f313737343237383032353734370000000000000000000000-2": "0a122bd01b10a7860df157a677d5d02a43709255e1147ba7efb8d64a31a488c1", - "6c6f6262795f365f313737343238333932313334380000000000000000000000-1": "024afdd296c27c635bdc67b011acdd7bc0664872c79ad884ad210198ca51201d", - "6c6f6262795f365f313737343238333932313334380000000000000000000000-2": "004b747a4192ba46384c1b677daf55a1db8ae58c86fa2250718451d2caed2ede", - "6c6f6262795f345f313737343238353435373035380000000000000000000000-2": "0aec77504f724afc9a677e5dbd54f2118276afc2e618c667c2a6d37d6af17e65", - "6c6f6262795f345f313737343238353435373035380000000000000000000000-1": "009ed6afbcd1782a2c2a5ae7cfa8cfb4f771bc2d47e2a7f2550e1a63d07e5d57", - "6c6f6262795f395f313737343238363539313432320000000000000000000000-2": "03bef91da4f9cfad35867f6017dc8154b7a7321e2e05ab14725cf5fb982ec2b8", - "6c6f6262795f395f313737343238363539313432320000000000000000000000-1": "0c41d0df10d533d251eadd01cbc947b2be0d29dc42d347f91ba8d453b2ba6d95", - "6c6f6262795f333035365f313737343238393633353936350000000000000000-2": "0928c1f552041fd0557b4be48f3c692e8394206a39b47210fdb87d8efc7ba542", - "6c6f6262795f333035365f313737343238393633353936350000000000000000-1": "07080c746435dcfff26a780eb4e5c1a7d996f340e860c1cfd2201a0ac2b1878a", - "6c6f6262795f365f313737343239313233323338390000000000000000000000-1": "06fc0e7dfaae09a06c98e8e10f85080ecb439a1208268a219fab916c1ecb22b3", - "6c6f6262795f365f313737343239313233323338390000000000000000000000-2": "0c58aaf2124467ff80f222bcd18b65ca70c4f1f5d552e1144c913c5cf8fed4c9", - "6c6f6262795f355f313737343239323238353230340000000000000000000000-2": "031de7c8f6fc94a8bad3f77d45c7944fc3df436fb775a2927281ee8e9e27578e", - "6c6f6262795f355f313737343239323238353230340000000000000000000000-1": "0dd276804aa37c045c1047f2aea66928cea0951d611b90e9cec64d4bea06f3d3", - "6c6f6262795f365f313737343239333839313132320000000000000000000000-2": "0681a0b2ac59e2a2142593b899676206f11f6efa63c9700447552803d14a4589", - "6c6f6262795f365f313737343239333839313132320000000000000000000000-1": "031d21f557b3c1c4cae19ac00815f0988bce83b1090824700c9fb70305e78c54", - "6c6f6262795f365f313737343239363537303036380000000000000000000000-1": "0e44cf4859dbddf283aef0bf8438ccf9983a2ad12c08d77c71f37c1028279f61", - "6c6f6262795f365f313737343239363537303036380000000000000000000000-2": "0de69fee7341499aebd77a4e3045f30db9d401c219acda4a494906ee97c6711b", - "6c6f6262795f31325f3137373432393831303636313400000000000000000000-2": "0dbc47700802a6b12b12ac841b15681856081f226c67753d4dccc1cc18a88f2c", - "6c6f6262795f31325f3137373432393831303636313400000000000000000000-1": "03252c29d7b67d1325e31db947504bc3feb78bbe7336b2d67e459cb4d113908e", - "6c6f6262795f3139385f31373734323938323932363738000000000000000000-2": "0a94e8f7103293ff8e36dc5ee9b53061c35bf9ed542d4c169668f4444f45e16c", - "6c6f6262795f3139385f31373734323938323932363738000000000000000000-1": "07c3bbeee4092c21237109e7df3803addc5e8d5c3f6415d468ece419dba65832", - "6c6f6262795f31315f3137373433303131303736313900000000000000000000-1": "0c2631058716fb4556cf4c0f0eb5179b8259c340d859f372cb9ce02cd8e2ee34", - "6c6f6262795f31315f3137373433303131303736313900000000000000000000-2": "0671cc7744f6a721eebebebd5360337319cd1dd7bdc0f007341203b084f00d8d", - "6c6f6262795f3636375f31373734333034363239393833000000000000000000-1": "03a19a7868e057b5e05b1122ffdd67f5e7c5dbcea27f943da0a60a3bcd7b7926", - "6c6f6262795f3636375f31373734333034363239393833000000000000000000-2": "0d2a8c8635fb9bcfec05f212b5949b26e6ea592beeba256d9b8e2d3cfdb437b8", - "6c6f6262795f31315f3137373433353838343439323800000000000000000000-2": "082d60a7c5e8be8a1fb0a620ece4b91d11995e45ef10957a5075623f64e2a06c", - "6c6f6262795f31315f3137373433353838343439323800000000000000000000-1": "04f6801b6f1829374456aad89259d25f392e14a3bacce131372d67862b047f93", - "6c6f6262795f365f313737343337363638303730350000000000000000000000-2": "0d34370a196b45e0d5aa86be06dc3d1022851d467a5ac5707204acd8fcf2eba6", - "6c6f6262795f365f313737343337363638303730350000000000000000000000-1": "09a05d51a07c27e8a34e144ad27bd795b3b3fbb3b187e72ca89fbac5526ec734", - "6c6f6262795f36325f3137373433383538323535303300000000000000000000-2": "06d0a357d0075d9e19fccd27444fa484c16bb516bbf013595b026c18ed00f540", - "6c6f6262795f36325f3137373433383538323535303300000000000000000000-1": "0e4b53db7f2f8c37731fc907e3aea6adcd3158bd91a1af21528c8afa3bd3230f", - "6c6f6262795f335f313737343338373830353535380000000000000000000000-2": "0e526c6b088080da7c22691196b5f3b436ad10381f5bced2e4437e80ae799d30", - "6c6f6262795f335f313737343338373830353535380000000000000000000000-1": "0a55ddd9c44e4cbb5e0d75cd19e03f794ae5e39a053e60592e3a50cfc5cd88bc", - "6c6f6262795f355f313737343338383635313239300000000000000000000000-1": "08028f2f0c3f6461a4f158e6db12fd9eeeff3a3587561f20a5b5ecc7cb54300d", - "6c6f6262795f355f313737343338383635313239300000000000000000000000-2": "05fd95c2359944124841d94575db0aef1aaa40df16054c2aa8d6f37138bda0f8", - "6c6f6262795f375f313737343434353131393432340000000000000000000000-1": "0258ce2ca4864997f0dcf51315b7aac487746fcaa4a890e970e03628199a0d4b", - "6c6f6262795f375f313737343434353131393432340000000000000000000000-2": "06758488b9d96b2b1b11127700e8fae232d2e8444b2d80d113c8744e531d32b0", - "6c6f6262795f345f313737343434353737363831350000000000000000000000-1": "0e0773dd9e7988fc316a90bddc0f4d8b15133a244fcec12773b517d93a35ef61", - "6c6f6262795f345f313737343434353737363831350000000000000000000000-2": "0076eed7b59de776baacf3089e19761e29b468ffbf0c0727385937947c396934", - "6c6f6262795f31305f3137373434343730383135333600000000000000000000-2": "005b27f5c1358130e9d02e221396badc4cd23bdd16374df7b88602933d6e45c6", - "6c6f6262795f31305f3137373434343730383135333600000000000000000000-1": "0623de8c8e379cd9741120cc401e284cf912a5d3f9c1c369130d82ffcff3e49a", - "6c6f6262795f31305f3137373434353139303633353800000000000000000000-2": "02061a7dfdbf97720013b61a2615c750b8232e6560dd3f8da66be1320dba7288", - "6c6f6262795f31305f3137373434353139303633353800000000000000000000-1": "02adf5f68cf94892b949f3f5fbbc7935e87bf247ea442c188f8fd32107e10b6e", - "6c6f6262795f32355f3137373434353434333733373400000000000000000000-2": "0bf0434d3aa9af532c9a0a8fb3afac3154fadceacab98089de4dd10d4a42a0b1", - "6c6f6262795f32355f3137373434353434333733373400000000000000000000-1": "01623551a2d8a2649933929400a65d5e66147ada346118c8cc5ec99b41a76df1", - "6c6f6262795f32335f3137373434353632303131313900000000000000000000-1": "068eb7c384010f113d77f5efa0221864e45bdf64b3e81baff2899b8d251827cb", - "6c6f6262795f32335f3137373434353632303131313900000000000000000000-2": "0616d93b5d53f462c50d708b85a598b3ac4d7003e00aeb1d7dc8dbd2971d4dc5", - "6c6f6262795f3235365f31373734343537313937393639000000000000000000-1": "08e6ae228c5c0aad7b40deb7dec40f1f21359780e2fd295d6552902ee0aafea0", - "6c6f6262795f3235365f31373734343537313937393639000000000000000000-2": "073882be01d9235eeb3368168a66a84eca7739ba379daa3c2ee7f587b0e8d9a3", - "6c6f6262795f335f313737343436363733343732330000000000000000000000-2": "0b9d340f6932ec1849f47e30629800c60c40bc53c8703fbf7affe9ab9a5f2248", - "6c6f6262795f335f313737343436363733343732330000000000000000000000-1": "0abe8ee5c785f5d7ccebf8331c1e2d668b0892ccddfd83431f345c81e1fc1045", - "6c6f6262795f355f313737343533353136333530380000000000000000000000-2": "0d4661db3c7f882982489d34d78c790ca7631009bd59161a960e61a9a8db482e", - "6c6f6262795f355f313737343533353136333530380000000000000000000000-1": "01746a688c5ed4a2e35be4037f87ae2d6be7eabf568076c1bb332222be5a8e4e", - "6c6f6262795f365f313737343533353838333037330000000000000000000000-1": "0a9cd448243dc855a92350e73d62f459760f8c2e63db4975a512ab5b550e3937", - "6c6f6262795f365f313737343533353838333037330000000000000000000000-2": "0098b9aebb9e9d3beb43bede220efaa3aa68df00e0499106a34240418a7662db", - "6c6f6262795f31365f3137373435333633383937383400000000000000000000-1": "043e466b8eccc779ee9ecc30828a9f0498816c78ba8972c31410c945fc408b8f", - "6c6f6262795f31365f3137373435333633383937383400000000000000000000-2": "0a4d8390150f3b5891ea695bfb36d7af32d3c40f970f5e8e1968084cfacf0f65", - "6c6f6262795f35335f3137373435333730353830363800000000000000000000-2": "01ab02b7115e307ca4a8e038da9582aa5ef9aca8b65f9c293a00b8aca6b75a7e", - "6c6f6262795f35335f3137373435333730353830363800000000000000000000-1": "00d35cdc5084c3c06f6185c9806f653e1f3154b4effda2f728991334881abb60", - "6c6f6262795f395f313737343533393239393434370000000000000000000000-1": "0783fb53febed17022edc500daad92612c8682c56611a9f2c5631225cd9fcf36", - "6c6f6262795f395f313737343533393239393434370000000000000000000000-2": "072b01b63f692f8c53f1d70a9184e380a82ee2388efcb7d2e9403748b7bc9b66", - "6c6f6262795f31305f3137373435343038363237323400000000000000000000-2": "0dd67271af7868b7a23de4923fb17d038eda880abfa96ed93f1cd2672b8bb261", - "6c6f6262795f31305f3137373435343038363237323400000000000000000000-1": "02cf482fbcd943de09c7f762df152c931ed8ff1bf29fa60baf9e2a0b49280cd5", - "6c6f6262795f3139335f31373734353431303435373932000000000000000000-2": "048b174d43764839858a1840dd1fa47b528b1af01238b4bd80f714b8d3ca2d44", - "6c6f6262795f3139335f31373734353431303435373932000000000000000000-1": "0c9182e9af101377681c8aaf17097c5831872c1167b49e807608e26f551da676", - "6c6f6262795f34305f3137373435343937343333313600000000000000000000-2": "0bbdbff72cc5f981fde0a9400fbbf740d41fad632c43f6701133992261992399", - "6c6f6262795f34305f3137373435343937343333313600000000000000000000-1": "023ad911f8c44b413276f7ee03ffd75100b73e0df3d3c4f9f84de79ebe60aa1f", - "6c6f6262795f365f313737343535303631373431330000000000000000000000-1": "0bcec5c6dbc8c7cd68643f8b55097ed073d80e4fc840e2ecfd44fc6056f99508", - "6c6f6262795f365f313737343535303631373431330000000000000000000000-2": "076e20ce806f0ac651fdc02cbb6764b30be9f968fc52206a4c431e533827e3a4", - "6c6f6262795f31305f3137373435353138363639383600000000000000000000-1": "0accd41955ee80167b38af98e8f57fac0d0e21cf59d7240d98eff3e0ad374352", - "6c6f6262795f31305f3137373435353138363639383600000000000000000000-2": "0b7a218f6d55410b69c583e15922a70244ce64679574faf9dd3ac26437a6f0f0", - "6c6f6262795f33315f3137373435353631333733383800000000000000000000-2": "05a738fe4fc93c5dedabfd57742c8b61de45f103adabfa2380efa5b0ac73a2df", - "6c6f6262795f33315f3137373435353631333733383800000000000000000000-1": "04ac4888e01dc26740438b7d42908dd9ad25beb96eff0bbfa36281633ea1d14c", - "6c6f6262795f325f313737343535393030353130380000000000000000000000-2": "0c64d8a95aab1938caa1baf5ef6bbddc36e74a9b1e7aa485b5b020f71da911a9", - "6c6f6262795f325f313737343535393030353130380000000000000000000000-1": "09dab9a16a1ded42987dce1d92cf1cecbe39a79639d3823eabf8a6cc75472205", - "6c6f6262795f31365f3137373436313934323235303100000000000000000000-2": "0515d48971f61794b80f7467c88fff5b5947a85ef92c24b02ad963850d3cb1fa", - "6c6f6262795f31365f3137373436313934323235303100000000000000000000-1": "08fca99b372e2ad980081ce9a91b2bbdc0fead46ae9b00b0cb4bccdad74179a7", - "6c6f6262795f345f313737343632333830333138380000000000000000000000-2": "0b7077d57a864ed3a9220ca6a66a401b5928101b3635ee99901fe4e82331f730", - "6c6f6262795f345f313737343632333830333138380000000000000000000000-1": "026383bace93823f78580dcb62cab8aa2744bbc5373319a8b916e3edeb8379b6", - "6c6f6262795f31355f3137373436323535363738353700000000000000000000-2": "0396105d5b2f261d42975b094f689849c38baefe097bc0541bcf709f220f509d", - "6c6f6262795f31355f3137373436323535363738353700000000000000000000-1": "089760df0121ca5e1ecfdef2edb0882445b9851059cd7c0dbe44f54f5fa98e71", - "6c6f6262795f355f313737343632363236303334310000000000000000000000-2": "0154c2e7fbaa4b4ec65de3415076beeb00ddb98f42f0eb6f983de2e6fbdd5085", - "6c6f6262795f355f313737343632363236303334310000000000000000000000-1": "0522deea4efe7ef299e4eaa94f44413b788f81d28f174ac74dc0b196b6e5af9a", - "6c6f6262795f3139395f31373734363238353734353236000000000000000000-2": "0cfa48a43309f00a8a0d1b8fc1668f283137968427a1433ef77d37d5abbf71c2", - "6c6f6262795f3139395f31373734363238353734353236000000000000000000-1": "0291eeeeddfdd3b2155a67337d835e98fa34487827a601b177844e833ec4abb1", - "6c6f6262795f365f313737343838303537313332350000000000000000000000-2": "005cddc0d35c0e9bcb1c9e56fdd368708ad1dcec18a459973108ecba1bdb9a9c", - "6c6f6262795f365f313737343838303537313332350000000000000000000000-1": "09c3fb17391025efb335110cd94bbf56d8e7f51e171a94fd2ce7a4cf6eaae83a", - "6c6f6262795f385f313737343838313134353736300000000000000000000000-1": "00cc1ea313cb8efafe1918ee73aae68696d23e1c5266a1900516f3ded3ce39eb", - "6c6f6262795f385f313737343838313134353736300000000000000000000000-2": "028bfc7efa3a1fbcab87c0c7013102c0dcaa65f5579cbadfb9d4b932094038a1", - "6c6f6262795f385f313737343838313838383635350000000000000000000000-1": "0bb9d47270f37432995394f724ce5ad7f68e660813084061a6e61a80aa41427e", - "6c6f6262795f385f313737343838313838383635350000000000000000000000-2": "00be02429ced25139de535bec6f4d3627a5168059730748e98601fd1f6328027", - "6c6f6262795f32325f3137373438383637333232333100000000000000000000-2": "006e398239922118fed0bc0dcca99db7ad0ce595ba0c3ebbca9312bb74c7680c", - "6c6f6262795f32325f3137373438383637333232333100000000000000000000-1": "0c48d01720f229b083e56e53431e145bfc50e1dbce23bae9f0fdcb55c9e63d01", - "6c6f6262795f355f313737343838373437353738360000000000000000000000-1": "093af4dcdefdaea5b32d594ffeca5e30bf504ec465311f9020c7d12b90147da4", - "6c6f6262795f355f313737343838373437353738360000000000000000000000-2": "022e5d729ab250ac4d9a043c360e323736576dbd482c05fa726e45ced6402ea1", - "6c6f6262795f32385f3137373438383835373932383000000000000000000000-1": "0e77485109829b45cfab5c7c9b883ff2e7369beb0a882a3768d3fc34a006f58b", - "6c6f6262795f32385f3137373438383835373932383000000000000000000000-2": "0bdb6069e77c0be5310f334a3c912ba7a6588227227659c54e80f1057cff227a", - "6c6f6262795f31355f3137373438393030353237383800000000000000000000-1": "0971a29c24b515c9c8e952eee35dd1b21766693d6849f4e58b068dd7db22b860", - "6c6f6262795f31355f3137373438393030353237383800000000000000000000-2": "07f60fe52281b323cbad87536bc236663820325fc3865634cc5da926ae33b89f", - "6c6f6262795f385f313737343839303432383536330000000000000000000000-2": "0d666315a34bfffe453a8439cbcccb86ec84e1470ca1b864eb8fd1ace3701eb7", - "6c6f6262795f385f313737343839303432383536330000000000000000000000-1": "08b6931b05e12e49a29f95d90581c06e3da1e67110e79e26ee8f2eb77921d1e4" - }, - "seeds": { - "6c6f6262795f31375f3137373334313633323638373600000000000000000000-1": "3cf0d47948a1169bb6b38f273692f3a425acddcc189e1a8b23c5aea9c1805b80", - "6c6f6262795f31375f3137373334313633323638373600000000000000000000-2": "796c6edbee1be87735bedd927de3b478554f3808b1bf3623f3ef56628087ce17", - "6c6f6262795f33345f3137373334323333383233343600000000000000000000-2": "4643023715804ef59c4f095e453b3b30e321b2d585af3104a9e6681e79b4c24b", - "6c6f6262795f33345f3137373334323333383233343600000000000000000000-1": "f8280d72571034ed5fa71d3763a8fd1a44ff9d73ab2eec04a3b050ef788a95bb", - "6c6f6262795f31395f3137373334323833333632393200000000000000000000-2": "c60668470b8b4743f16bd7f28dd4dd0ffabebddfdef2613a83fbbc3db74873ab", - "6c6f6262795f31395f3137373334323833333632393200000000000000000000-1": "9647a0b4e0b244cc61d02a1ed1498095e1da75f210100e0eefad30d30e055b9d", - "6c6f6262795f365f313737333637323633353731380000000000000000000000-1": "2d085af80aef9a039740f6cda89e13fd9a677344556819902a9b200a558f2cd8", - "6c6f6262795f365f313737333637323633353731380000000000000000000000-2": "ec4857e4767d4c2011623247d0153479ba7250b777b4357b15ae511d26a3b34b", - "6c6f6262795f31325f3137373336373539313337303900000000000000000000-2": "1eab47eb23285ae73b0b7f85832f9d3510b69aa6e3c6b087de1dc70751076e3b", - "6c6f6262795f31325f3137373336373539313337303900000000000000000000-1": "1a77c2af04d320244615c3c9a69a018d2839b7eee7c61943a6d21b1dd3656074", - "6c6f6262795f385f313737333637373034353831360000000000000000000000-2": "e5452296a204ba0ea2dc11b79436934c0a6d6b379bea7eed2521fcfad21b3b47", - "6c6f6262795f385f313737333637373034353831360000000000000000000000-1": "5d5de9229f6f69a819add62b206e4cccbca2067dc67ca1cf72581e54fc83729b", - "6c6f6262795f32345f3137373336383239313738393500000000000000000000-2": "9343a113d8cd2f060313040655ec4be18cea7ddb262c8f7ae67a228a86760686", - "6c6f6262795f32345f3137373336383239313738393500000000000000000000-1": "e6ce5517ffd956dfa80db6909613215102506353b99f9656f7c8abf0605305d8", - "6c6f6262795f375f313737333638333837393239320000000000000000000000-2": "fc1f6285f3659643ac5babf0b1be1fa86445d7c8ecb4c122a616854092ce5823", - "6c6f6262795f375f313737333638333837393239320000000000000000000000-1": "c1bdda753f8d406f08757ff88b1ef9e29101d6ae33a2043da432299f7f26eafd", - "6c6f6262795f335f313737333638353136353830360000000000000000000000-2": "baef6ab763528fc1829b2eab9df1081b6c18c01c177afd4de74fe6c71837dd21", - "6c6f6262795f335f313737333638353136353830360000000000000000000000-1": "af1347649580daba73b9c29ce506e8d11387e5e059e1e54b2c595fd6f0455224", - "6c6f6262795f31385f3137373336383632313833383600000000000000000000-2": "679908711ee861a91434c2b9aaf8acd0edef9fd7a5006eed2cd5bcbe13394bde", - "6c6f6262795f31385f3137373336383632313833383600000000000000000000-1": "eca38c8a8bc1ea6928ddc8182bba37092bef687494f98f094aaa47665c8141fa", - "6c6f6262795f35335f3137373336383833343136343700000000000000000000-2": "c2952413cd9eae9fb72269871a43b65d63412ccaad4f964b56d4561da0ca419d", - "6c6f6262795f35335f3137373336383833343136343700000000000000000000-1": "686da9350f895328d4736427c1c94faca53390c8a2ef13f465019f8b306d4bfd", - "6c6f6262795f355f313737333639303432363130390000000000000000000000-1": "0db74c233e9a80365ea3403137ba013864bb1345d1a5702b5fad1efa3c574379", - "6c6f6262795f355f313737333639303432363130390000000000000000000000-2": "bbcb7752b296eb63f5e444a7b40f7aedf1f622d35f49975659483d83185c6585", - "6c6f6262795f32325f3137373337353739313532323300000000000000000000-1": "5deac0ff56dc131ae445ba5e0dbbf366f936afcbba8afad9601730442ba6634b", - "6c6f6262795f32325f3137373337353739313532323300000000000000000000-2": "1889fdf22e789daf0b816a894f45be5912a0046c13117e0afd9234bbfa144827", - "6c6f6262795f31335f3137373337373733383939383800000000000000000000-1": "5a44cd16efb802f3770b4869235ea9a863d364427e5c572b2904a9eb19c25b90", - "6c6f6262795f31335f3137373337373733383939383800000000000000000000-2": "d01484b54cdb3ed15552d137d9c3214fe03cf4c5b788a2ba2b94b95e0aee130c", - "6c6f6262795f32315f3137373337373932333731373000000000000000000000-2": "34e4094bb2aeea31a5cdf0ac8a1a90d1e5afb768bef3ab1e23d4dc62c39cc279", - "6c6f6262795f32315f3137373337373932333731373000000000000000000000-1": "18bfda0eed51aef6eafbf913202787d8427f7e668fd73cb36adeee723406a8c1", - "6c6f6262795f313936375f313737333738313138313536310000000000000000-1": "b7b4a2e5a90fe091590296d23a97db0224478610c24010b59e687cdada6d4cd2", - "6c6f6262795f313936375f313737333738313138313536310000000000000000-2": "b6e7d91713514b04cdaaf83aa2dc40723c040f746e79de13f4c51a14d9f9f1c5", - "6c6f6262795f31315f3137373338343739323438373200000000000000000000-1": "35d03da460494d57f4e7865377b3430c26fd2583dc4781a386fa3bab048d47cc", - "6c6f6262795f31315f3137373338343739323438373200000000000000000000-2": "d3d885bf14a9e8df4f7cd0e0e68fc4ddba650b4cab21b4ffb72621192085976e", - "6c6f6262795f32305f3137373338353030323331343600000000000000000000-1": "2f5c0fe4b7d97bec3a853f4f8d64a4aa73ea17c47a23cc26cd2af961be2e1516", - "6c6f6262795f32305f3137373338353030323331343600000000000000000000-2": "fb5e87931331a258d6e14c892d43ceabd15e163190c5151a9c23e65e184a2a65", - "6c6f6262795f31345f3137373338353233363735373100000000000000000000-1": "cea1b7bfc5309ac22a422b08a23291bc235280f668d9768e83867d6378edacf2", - "6c6f6262795f31345f3137373338353233363735373100000000000000000000-2": "c0a56795568d8baf29f1804a252c8bd8fc0cec1c43495e50fab200dfcfeb332d", - "6c6f6262795f355f313737333835333432383732390000000000000000000000-2": "b34def6c655f5e8a3ce8221e67a389bcee7cd97ceb26a437fbcec1954c928274", - "6c6f6262795f355f313737333835333432383732390000000000000000000000-1": "2341c0249b3a39079016afeb1d9108355600496c5b1232e8507234ca342a9439", - "6c6f6262795f31305f3137373338353630313235383500000000000000000000-1": "43c8a553f9b38720eefe46b67b99f1192956bb0af1cbc873e3773ef888c26303", - "6c6f6262795f31305f3137373338353630313235383500000000000000000000-2": "df405c80308ce4444e605d7480c4501c30d680937e4539add660657ec867b044", - "6c6f6262795f335f313737333835373238363637350000000000000000000000-1": "3ccc858755b3420e304a85a10069c1449961cd050e4a94ce05ef4099a49cd080", - "6c6f6262795f335f313737333835373238363637350000000000000000000000-2": "c093d909f311442afdb2e64daa9e90ce9d5e9e3cefd70c38f5de143789274031", - "6c6f6262795f31335f3137373338363234313232363900000000000000000000-2": "62cc3057432bbcdbde26e714433677d3202289146650d8961a9ac22d7d12737f", - "6c6f6262795f31335f3137373338363234313232363900000000000000000000-1": "ab64559983f1b44c3ebf9b1edebb82d9ed13ed0c0abecfe93804df13412f3b56", - "6c6f6262795f31345f3137373338363432383035323500000000000000000000-1": "1347c751554614340baf91f95ee4d7130ec1652b48575b9e1e8d7bfbffb7042d", - "6c6f6262795f31345f3137373338363432383035323500000000000000000000-2": "cfb253110fae6b891f534b200dcf113e250536bdbb517edd410fddbb0fe9d185", - "6c6f6262795f395f313737333932333639393336360000000000000000000000-2": "9cadae76cae1632a0ea811125012ff82a231987171e7490e9ba59b5490ac03e4", - "6c6f6262795f395f313737333932333639393336360000000000000000000000-1": "7b05dc4ea22cf29d503584fca79dda586ac1c2a394b11ea734941184f61303ef", - "6c6f6262795f365f313737333932363138323433370000000000000000000000-2": "393b4efee9af456326d2861117f46e3254a118e4444de0fa095b3a4cf14b6358", - "6c6f6262795f365f313737333932363138323433370000000000000000000000-1": "c9843fc265a8e2d2eddf1c49d7742b114db2e7eff0b55164622f114097e17b72", - "6c6f6262795f345f313737333933333033353837300000000000000000000000-1": "ffe19bbdf17f77d8c7c903edf3f11b2d37a04ebeb03eea21b28be729b76df4e7", - "6c6f6262795f345f313737333933333033353837300000000000000000000000-2": "fc51ea325e379c36376b6e634fbb956be42b0b76197df4e077c0de06698cea50", - "6c6f6262795f33305f3137373339353038333937353800000000000000000000-1": "f745c23af2a5bf0915ca7aa118fd2b9771c361b1c72aef21777ece6dbf894676", - "6c6f6262795f33305f3137373339353038333937353800000000000000000000-2": "bb45a4aeffc1aff499299745bb0351c1d54cebbc67432d8ba2438c6702a9e070", - "6c6f6262795f345f313737333935313936363334360000000000000000000000-2": "a9b489b550dc3f59a9c15fb5796610debadcf32c4dcba1550f5a76078a00a070", - "6c6f6262795f345f313737333935313936363334360000000000000000000000-1": "d3713365f538516f49da97402c7df100cd617f874ba24b95210e70c0029ee299", - "6c6f6262795f375f313737343031383534313139380000000000000000000000-1": "2a3d21f67185c9984d665506c80d8cf371cef74418561fab19565d2118122518", - "6c6f6262795f375f313737343031383534313139380000000000000000000000-2": "06ae64b935af06ce2f6ae9a3f39c892222f03f4e86304b787766efab3552d593", - "6c6f6262795f365f313737343032313833313231330000000000000000000000-1": "0ad01b80de2bff345f508faee225e728462c4a417357fd79c9d3b4750c33b03e", - "6c6f6262795f365f313737343032313833313231330000000000000000000000-2": "93f8cf5adb0312483ce5b7d9d9b9e1a8a6acfe40b52047bfe529bf158a967e45", - "6c6f6262795f33315f3137373430323433363233393500000000000000000000-1": "a611b07e1f659a7c74a0cd2c115424f65f047f9c5b36319a01d5ec02255a3a76", - "6c6f6262795f33315f3137373430323433363233393500000000000000000000-2": "8a8aac4071bef8f8c74f66603743a8906ad4b4868d7ad69cf99b24d620f64181", - "6c6f6262795f35375f3137373430333139363637313000000000000000000000-2": "252377655c69dcb2f0501973cdc57c56f5ab9a844950cc6c45773b7bc8a790cc", - "6c6f6262795f35375f3137373430333139363637313000000000000000000000-1": "b8506e7948bd4f0e155be6b976c31270061bdfa7643a1aa7d4fd93266077e61b", - "6c6f6262795f31305f3137373430333631363033363400000000000000000000-1": "d588d7c3a2a54fc803627f5216624e3fa48d7b7f118d5867e5cd332837d389b8", - "6c6f6262795f31305f3137373430333631363033363400000000000000000000-2": "c7737fe667cb9b824a57e49d0c7f8831109076d899036e56d8cb6cf371e5a93a", - "6c6f6262795f395f313737343033363532343239310000000000000000000000-2": "644f8b503b4e3bec773f8de70efc19b20f65b004b9ab5cce58612d9d64fb17d5", - "6c6f6262795f395f313737343033363532343239310000000000000000000000-1": "8a8571540b8d1ddfc60daf058fcef15f2e8c5229f49201a89cfaf4d0a98d5092", - "6c6f6262795f375f313737343033373337343839390000000000000000000000-2": "e06cf77861520a1a104378ec5099ce6dd0bee6633149b94c5c316c9ceac2ee39", - "6c6f6262795f375f313737343033373337343839390000000000000000000000-1": "67b71e03b7b64b70bbf4adb47c498685c88d6308237901a3707b4acffbcacf07", - "6c6f6262795f3136365f31373734303338303238363031000000000000000000-2": "dd56d294917fb202151db37f90f534c12398d65596d5476662fd81d783e84e5b", - "6c6f6262795f3136365f31373734303338303238363031000000000000000000-1": "cc797a31abe947a705be8a93a84f9a2c38ec1e3b64dfd37b358488465f2c591a", - "6c6f6262795f375f313737343034303434393832310000000000000000000000-2": "28b2f84ae177a89877d9c32bc669b090d10b8aed2c69bef254f12d06acff05b7", - "6c6f6262795f375f313737343034303434393832310000000000000000000000-1": "eded32299653a07fbcadc65f95afbb07215f795d9a927995f112fafe15556c85", - "6c6f6262795f335f313737343034323737313437300000000000000000000000-2": "ba372c421dde3fbded7fe48e56003abfd6678f120b37c3e93c343cdb27266bc9", - "6c6f6262795f335f313737343034323737313437300000000000000000000000-1": "c82947c0785678129632cb1e2197726dda9831819b26cb5401b7286eb54face6", - "6c6f6262795f355f313737343034343735313838390000000000000000000000-2": "45b5e24b3e0aac44373da86c7ffdb3bfb91d44a3f945b70a0d821b456c7748fe", - "6c6f6262795f355f313737343034343735313838390000000000000000000000-1": "0d3b4db63716d71aefc6ec0a7cdb86d2d070981ca6a03f0049ae37887bb6e605", - "6c6f6262795f3337325f31373734323733313338373538000000000000000000-2": "6b56da2510206fdde89a09588cb79a3aabd1d6be6cbebfaa3d6816ecd185386e", - "6c6f6262795f3337325f31373734323733313338373538000000000000000000-1": "a12e80168f294fa783b0134c6733a9b1fd2819a54c6c98da2e07943dee68ed44", - "6c6f6262795f33315f3137373432373532373335313800000000000000000000-2": "fec753aabfbeb921a15ecad2500d78e39905fab1c0f592e92163d22eb42d0bf2", - "6c6f6262795f33315f3137373432373532373335313800000000000000000000-1": "788d60a78a6d162f28a3f87009cab7c2abf5e0e5e28e7cfdaeb206664d215e7f", - "6c6f6262795f375f313737343237383032353734370000000000000000000000-1": "ba5e82e92d5bbe0f2e23ad9b4ffeb7a01eee6f88b5f45e0f447fabf219e3a1bc", - "6c6f6262795f375f313737343237383032353734370000000000000000000000-2": "bc25d7a342ce0e6245ba6a5eb02c224fa6e31cbcb21194f8d294db70dbc08224", - "6c6f6262795f365f313737343238333932313334380000000000000000000000-1": "064720352123002cadc430e53de491b7018fd33bfcf56f489cde99c37be588e1", - "6c6f6262795f365f313737343238333932313334380000000000000000000000-2": "ec724fbb8507c33da0af1499bc824b7370c274b6ba603780db278c06f5674dad", - "6c6f6262795f345f313737343238353435373035380000000000000000000000-2": "54fe2101ae449e709f5021e27a260ff34df1cb86238e5727c15630308d3cdbca", - "6c6f6262795f345f313737343238353435373035380000000000000000000000-1": "baedd6e3615615d4d88ab9a4b176e9a49b6fd3ea73b5d0b770e09f459626ac2c", - "6c6f6262795f395f313737343238363539313432320000000000000000000000-2": "d7980fb237fb84426209a18d054b23241c146c5c4c569ec47322c302144da2e8", - "6c6f6262795f395f313737343238363539313432320000000000000000000000-1": "39ca78d10b02dc8c4fe79749d18bfe30a2ff04d3c4ec099901bcdfa3bfe0b8f9", - "6c6f6262795f333035365f313737343238393633353936350000000000000000-2": "8184ec862edf49ecb04eca43ab3dd763ba97fe0fafa675c6a5218d12e45152ba", - "6c6f6262795f333035365f313737343238393633353936350000000000000000-1": "6f9568cfcdb663e1e9101b104bd88525ec959fd0e1bbd7f4ada18722949d2edb", - "6c6f6262795f365f313737343239313233323338390000000000000000000000-1": "5b5ed325400273710daf48a94776a8fcd7179fd6e72c157782af153ecad2af26", - "6c6f6262795f365f313737343239313233323338390000000000000000000000-2": "2f553eaf6e5d6148a04b65c6e53ee42cf6b08487f5ce03166bcb8bf45d9bacdb", - "6c6f6262795f355f313737343239323238353230340000000000000000000000-2": "3d5a51742e83f143e7706c3716c33b154348dabeb2c9277367d2fdf8024228bd", - "6c6f6262795f355f313737343239323238353230340000000000000000000000-1": "1bade49f2bf010abb1d41f478a2fe18e178844fcb0a57df8ac10fc5fc56229d5", - "6c6f6262795f365f313737343239333839313132320000000000000000000000-2": "ad674297cc72b10777d2030ce39fe9e04401984b08d2bd254d91ef9eab520f8e", - "6c6f6262795f365f313737343239333839313132320000000000000000000000-1": "83e4bc9c172a18c064e106e84a6a9028c561162c2b06fab02526cde866aa64a6", - "6c6f6262795f365f313737343239363537303036380000000000000000000000-1": "40c4b27310c04ff060dd8b1910c0688400976c18f58093d710c04d1dddcfcd4c", - "6c6f6262795f365f313737343239363537303036380000000000000000000000-2": "27590f3c1f5d7598d68cd6fdd8e23298ef3657e0cd87da87d0e1140f4fc5be8e", - "6c6f6262795f31325f3137373432393831303636313400000000000000000000-2": "b93a92cdea9f46f21812e58bb6e217f95d6a7a3af5ac79a0a6b0d0dce899e7a0", - "6c6f6262795f31325f3137373432393831303636313400000000000000000000-1": "b9a6ff800e36fce1e932510c046704b8acc5a358d58afeef6b372206136b075f", - "6c6f6262795f3139385f31373734323938323932363738000000000000000000-2": "f7e91a2fa62ebdba0c8ca5d642d68ba4e93b276ec1447aba9d269e2eea02257d", - "6c6f6262795f3139385f31373734323938323932363738000000000000000000-1": "363f52b09f9720f16a2b20f985ac07ee39e56caab9a7964aab36aa065c5523d9", - "6c6f6262795f31315f3137373433303131303736313900000000000000000000-1": "d41a7a00c3bee766ce8305fcb743c1f385f9469a5d31c480654d8333cc1c0b80", - "6c6f6262795f31315f3137373433303131303736313900000000000000000000-2": "502c43c9328bcc3943eb41239d1a9b7c3f206bc352037c3447c2868e9de381f6", - "6c6f6262795f3636375f31373734333034363239393833000000000000000000-1": "b7efd1e7b3f9679818a89ee3a3055279010dcc968ce62229de6acf368a3f2fcb", - "6c6f6262795f3636375f31373734333034363239393833000000000000000000-2": "0391c70779dff68f464b5028327b8e91be384b8134d838a3fa1f7a0a820565b9", - "6c6f6262795f31315f3137373433353838343439323800000000000000000000-2": "8456060610d72eaac99127df6255ac8f256623e396c89835e7b1dc00d629016f", - "6c6f6262795f31315f3137373433353838343439323800000000000000000000-1": "733e99095ea3190d408f86180c94d8ba4688eb3557925da6eebc2236149179d6", - "6c6f6262795f365f313737343337363638303730350000000000000000000000-2": "09ee0f53b1075a0b85d15fdfd4925a27fb974ce6dbf04339e9077b75d736d494", - "6c6f6262795f365f313737343337363638303730350000000000000000000000-1": "485ffb880273bba5f9d157fdb9e343860b3521903f77de55d5d5e4a917c4bfdb", - "6c6f6262795f36325f3137373433383538323535303300000000000000000000-2": "517e8d84c2a3ce9ca52270e3880b56a60042726c41ea04e6991e7f2f75579299", - "6c6f6262795f36325f3137373433383538323535303300000000000000000000-1": "ed7278fae7202cbe92d2d8f789c7104c721a9c6831f9aa2e26b4954b3ca323ec", - "6c6f6262795f335f313737343338373830353535380000000000000000000000-2": "e4f416c61a493be832603c9ae233b63e53155469e76edf1896dfd62e21c816c7", - "6c6f6262795f335f313737343338373830353535380000000000000000000000-1": "ce29f1fa605b7cae4f5cc53f4ffb67229d24b3048124a72d533c05bb8d14d9b5", - "6c6f6262795f355f313737343338383635313239300000000000000000000000-1": "b88b70de242d621b676705eeb376a849dacdd3d4b2551495933c3951479f4a6e", - "6c6f6262795f355f313737343338383635313239300000000000000000000000-2": "b1412e8c1cd84316dc1c80f7a2b84dd34895db19ce5c72d72feb3e53b6cd526e", - "6c6f6262795f375f313737343434353131393432340000000000000000000000-1": "1454b7ea16473469bf14f9d5f9ca7612b55720097ede1434994d1d17ff489b2a", - "6c6f6262795f375f313737343434353131393432340000000000000000000000-2": "eb447fd1786b56e416c88f02e9e55d72d0b45a4a3b88d3146e76b4d284b09a48", - "6c6f6262795f345f313737343434353737363831350000000000000000000000-1": "2544069bc91e7fb9a86790b671f2729e52e7310d32a67f82ade8fe9474585dee", - "6c6f6262795f345f313737343434353737363831350000000000000000000000-2": "f8258416061cb7caf40c6667b7f7fa01605b2d366f79acab9bee18691d8e8260", - "6c6f6262795f31305f3137373434343730383135333600000000000000000000-2": "978729d65c9924c9ba8e1f534cbbb022c346984696a0df6c48373f6f1bbbeb6d", - "6c6f6262795f31305f3137373434343730383135333600000000000000000000-1": "e5f970d66a06312940e727e28f12dd057e85fc887b9920d63a3d6f63f9f18c8b", - "6c6f6262795f31305f3137373434353139303633353800000000000000000000-2": "a236c42762498e86730d218c9465ef3960f09b86e6abaf375bca077171584d08", - "6c6f6262795f31305f3137373434353139303633353800000000000000000000-1": "5aae3a39a89007cbe0b05a99ac8a17739c8bae99be33be451472f44119df3310", - "6c6f6262795f32355f3137373434353434333733373400000000000000000000-2": "b8b31363df973e53b50826d21b6a62e5793d38d7afb8ac9541b75ac76ae65d82", - "6c6f6262795f32355f3137373434353434333733373400000000000000000000-1": "2608bfd60b74f9635f9d06f8b6a26ee5ae8a7bad96856294800563d3160b954e", - "6c6f6262795f32335f3137373434353632303131313900000000000000000000-1": "01c6e25e041eb7b4ffcff27139d91646bc0b148ada2c6691f91737c381c367cf", - "6c6f6262795f32335f3137373434353632303131313900000000000000000000-2": "95756404c278a18feec7c2b4d3a84402d533d2cc89252828291fca7b779df447", - "6c6f6262795f3235365f31373734343537313937393639000000000000000000-1": "8c791e50065e094e3802c3510f7edb3ce7473cd624e1069d422fc6d9ae078885", - "6c6f6262795f3235365f31373734343537313937393639000000000000000000-2": "6d6ff650abe249f23bbbd974befd3b69cee8a734671dc9dd126def89d3c2597c", - "6c6f6262795f335f313737343436363733343732330000000000000000000000-2": "943d5c6908e96ce84feeeaf58d9b92d5ff445a7cacac6cffe78dd747e4568b43", - "6c6f6262795f335f313737343436363733343732330000000000000000000000-1": "064d9eb207dfe94d4febf81c1e6526a683fc8031d8d622aa1a896263b92933ed", - "6c6f6262795f355f313737343533353136333530380000000000000000000000-2": "13d5232bd74a32009901d25648f348a5d9e4fbffbd6ed30fadd3126d84e99bcc", - "6c6f6262795f355f313737343533353136333530380000000000000000000000-1": "70e56b4f5da537fb76e0cc0319a7c756a9f6d56ef43b223431f6937ae5eab666", - "6c6f6262795f365f313737343533353838333037330000000000000000000000-1": "c332cdd8e43d2d5106654b9823e708033c5037fb12a1f43e93d91bd496649b4d", - "6c6f6262795f365f313737343533353838333037330000000000000000000000-2": "137f139a4baa306975b02e8b321acf07d766ba879b77e36fd57e225812989574", - "6c6f6262795f31365f3137373435333633383937383400000000000000000000-1": "3e6d61543f033aace2eb5e3a9498c9703687f8cd38bd816fd876977762b3439c", - "6c6f6262795f31365f3137373435333633383937383400000000000000000000-2": "f8a407bd6fe60ba0346f55c6ff7282d6a394e48ea68b7e11285e277ddac27f32", - "6c6f6262795f35335f3137373435333730353830363800000000000000000000-2": "3a8bb68ec48fcb538f7fa2ed184102ffa199f7ea0691aba66b897641f1df275a", - "6c6f6262795f35335f3137373435333730353830363800000000000000000000-1": "58165c5c907646bc075b17b6932dbd049edd47fba8f1eef7586933e6e74f40da", - "6c6f6262795f395f313737343533393239393434370000000000000000000000-1": "a3001df6ce10b5a1a6af5f55a26d129f604106e6ec1277ea8aef641e13dc1398", - "6c6f6262795f395f313737343533393239393434370000000000000000000000-2": "afa61e320c28736cb287215dbdbe7750e7860d5422206bee8c0d2681982143f9", - "6c6f6262795f31305f3137373435343038363237323400000000000000000000-2": "7c4917f0a85d2a56c481aa6b638ffffed129ca6e2d6685f52ee6f80fe6da736b", - "6c6f6262795f31305f3137373435343038363237323400000000000000000000-1": "c5c4e303bd0a608f5e61da32294f7b8189e3037700d0f92755218559d57bd84f", - "6c6f6262795f3139335f31373734353431303435373932000000000000000000-2": "29db814642f40827bf635f72802b981d1dbf62774ccc4381980582281e9d364e", - "6c6f6262795f3139335f31373734353431303435373932000000000000000000-1": "07c3b3a725fc623e63827de456ab7598f4cbf2d028f36cd259361fda70bcf679", - "6c6f6262795f34305f3137373435343937343333313600000000000000000000-2": "3d2915c386815017cccc0567a3548044afe9b9d0bd0e05f4ea1c775cb020d63d", - "6c6f6262795f34305f3137373435343937343333313600000000000000000000-1": "f6c5a3733232427df74e3dfb1e8532e91ea4fe486a3ab171b2554cc40c2cb245", - "6c6f6262795f365f313737343535303631373431330000000000000000000000-1": "a584684452d41f6cec432fedc2052c5805a752ce584bb9727312c6a6602f5ee7", - "6c6f6262795f365f313737343535303631373431330000000000000000000000-2": "adaa5069e9dd415ed5e060ca8d8d6ec86a5f49f4724216baa62a022b7debbe71", - "6c6f6262795f31305f3137373435353138363639383600000000000000000000-1": "20a08c42178e526593841f6699e99da43537a225a3063b4d4b177c3ff1a226a2", - "6c6f6262795f31305f3137373435353138363639383600000000000000000000-2": "30c5e772b7703b1aacdc6115f7eff6b82e940e276233f9fcab688efebc31ee79", - "6c6f6262795f33315f3137373435353631333733383800000000000000000000-2": "b9f8cc7f1e8405456b8f2fb51c0adf6a92a324531e026cd94e353679dd5a9e06", - "6c6f6262795f33315f3137373435353631333733383800000000000000000000-1": "20fa1f41d531fc579e88db82fe90aae151a2bdb61c4ae51d49265682bc96a66e", - "6c6f6262795f325f313737343535393030353130380000000000000000000000-2": "1af0a97894119295857993a181dd27dd4ad6cfc9a45af08a18e4bcf07c0c1fbf", - "6c6f6262795f325f313737343535393030353130380000000000000000000000-1": "8ad8918d8ca431a71b8c705d9f567b1681d8930854a29630f324b785f64719b0", - "6c6f6262795f31365f3137373436313934323235303100000000000000000000-2": "7e390b2ca2aa9733e4ad4fde6f6a77e90cff6bf4bb3e469151e849034157da04", - "6c6f6262795f31365f3137373436313934323235303100000000000000000000-1": "95afd20c6f67efa0c65986a58c0a7c59309074ecc9ef3b61ea07654e1d66fb61", - "6c6f6262795f345f313737343632333830333138380000000000000000000000-2": "aa5235ebaa998b56193617e4c0100f8b147bc07e182edc9a2d241a2836da0135", - "6c6f6262795f345f313737343632333830333138380000000000000000000000-1": "17a74c5c7dc57965767e256481d6c8f3b063907e286e1d0e56804013c154eec2", - "6c6f6262795f31355f3137373436323535363738353700000000000000000000-2": "e17723ac1222ca3f3b1fa5f1ed9af0a06aed252aff8356867b2353657625df37", - "6c6f6262795f31355f3137373436323535363738353700000000000000000000-1": "87ce80938b38c05ea91ba1d935a9bbf4f5914775cc2c2d47d1a90a7692c8f7d5", - "6c6f6262795f355f313737343632363236303334310000000000000000000000-2": "3e4182a9a0ab8b8c8af7788b4908cb08edebef8c6fa6159f17b5baa9c72567d6", - "6c6f6262795f355f313737343632363236303334310000000000000000000000-1": "178c9d1f16de8487a423695b924db0a003e61faed42b29042c53473352f3edbc", - "6c6f6262795f3139395f31373734363238353734353236000000000000000000-2": "640f84cbed7b0622bb631ba4be1a304e66dcde317914bf2ada0e65400d5631a3", - "6c6f6262795f3139395f31373734363238353734353236000000000000000000-1": "ee9a031439eeaf186899ab111fe755607aa405f5db08da3ac0275d0a2bcdad1c", - "6c6f6262795f365f313737343838303537313332350000000000000000000000-2": "2d4da247e0bb5bcfac2c6a282bcf8245193b2113f68aa1a5583dc61614624838", - "6c6f6262795f365f313737343838303537313332350000000000000000000000-1": "270257882e4e631ed5837da30991fffacf2186ca15bcdbd8f02809570d5a98ee", - "6c6f6262795f385f313737343838313134353736300000000000000000000000-1": "3889ce6cacc1c1d2fd57dea2a3273805034716606be04cb2411bcb0239d52593", - "6c6f6262795f385f313737343838313134353736300000000000000000000000-2": "d2f359559aaf9e895daf99d14005e1ee0e5d2f9de70fd45ba5fb92e6ac8fedf4", - "6c6f6262795f385f313737343838313838383635350000000000000000000000-1": "c5d85c3c50bf532fbd1f9f83d3ee683cf4983c6b31bd723e6ffa5eb837d24563", - "6c6f6262795f385f313737343838313838383635350000000000000000000000-2": "3316b94eb06217deebb87c7a76ee25976ed65d0f87b5b462e47f2725e784ca40", - "6c6f6262795f32325f3137373438383637333232333100000000000000000000-2": "cc0cf1e9b2dabcd9988e9d21cb604dca9d2fc52c776ed3fa316cac4992ea7e9d", - "6c6f6262795f32325f3137373438383637333232333100000000000000000000-1": "f52fcde1038346d371932ca4534d9e8b6d6db0f779cafb988a268921127409c7", - "6c6f6262795f355f313737343838373437353738360000000000000000000000-1": "5237abc4129c11dafc4cc86db88c7b5a53fe88d9a2d4e6e958921839a970d508", - "6c6f6262795f355f313737343838373437353738360000000000000000000000-2": "277d8faaa68b471a120581ec2102b1b495ea4badf4209ddcd07fed6dca769018", - "6c6f6262795f32385f3137373438383835373932383000000000000000000000-1": "766faf1d1c796618efea5eed278eaed0a07883fbc9ddd1e34e39e2e5334effc8", - "6c6f6262795f32385f3137373438383835373932383000000000000000000000-2": "5c94174f96e7295c4da1334d488ad0f67d40edff1fc1811cd7bb23b25a37425f", - "6c6f6262795f31355f3137373438393030353237383800000000000000000000-1": "7bf27e470df5a1aa095a85504e86208a3ea0781232b5096bee9a6de1e59b1bdf", - "6c6f6262795f31355f3137373438393030353237383800000000000000000000-2": "e654aeb5132cf1120f957c93fccc00c5522909e695f23d7ee7651043ee9ff236", - "6c6f6262795f385f313737343839303432383536330000000000000000000000-2": "a2134a2b12bfa5be9b4caba69d25fc822a2bd0e76ce77ad9bd5f1c8007296b3f", - "6c6f6262795f385f313737343839303432383536330000000000000000000000-1": "677e8ed23e29266537bbe70c2b2e8de638768d278f7978c0f7994f82b52d498f" - } -} \ No newline at end of file diff --git a/packages/client/node/data/setup-state.json b/packages/client/node/data/setup-state.json deleted file mode 100644 index 68b10f8..0000000 --- a/packages/client/node/data/setup-state.json +++ /dev/null @@ -1,534 +0,0 @@ -{ - "lobby_4_1773178701542:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773178701542:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_109_1773241521987:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_109_1773241521987:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773258199415:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773258199415:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773261312565:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773261312565:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773322240663:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773322240663:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_21_1773325594312:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_21_1773325594312:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773340756840:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773340756840:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_11_1773343085449:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_11_1773343085449:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_12_1773345043163:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_12_1773345043163:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_8_1773412316021:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_8_1773412316021:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_17_1773416326876:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_17_1773416326876:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_34_1773423382346:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_34_1773423382346:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_19_1773428336292:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_19_1773428336292:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1773672635718:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1773672635718:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_12_1773675913709:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1773677045816:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_8_1773677045816:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_24_1773682917895:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_24_1773682917895:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773683879292:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1773683879292:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773685165806:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773685165806:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_18_1773686218386:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_18_1773686218386:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_53_1773688341647:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_53_1773688341647:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_5_1773690426109:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_5_1773690426109:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_22_1773757915223:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_22_1773757915223:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_1967_1773781181561:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_1967_1773781181561:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_11_1773847924872:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_11_1773847924872:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_20_1773850023146:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_20_1773850023146:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_14_1773852367571:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_14_1773852367571:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_5_1773853428729:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_5_1773853428729:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_10_1773856012585:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_10_1773856012585:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773857286675:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_3_1773857286675:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_13_1773862412269:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_13_1773862412269:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_14_1773864280525:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_14_1773864280525:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_9_1773923699366:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_9_1773923699366:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1773926182437:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1773926182437:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773933035870:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773933035870:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_30_1773950839758:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_30_1773950839758:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773951966346:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_4_1773951966346:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1774018541198:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_7_1774018541198:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1774021831213:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1774021831213:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_31_1774024362395:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_31_1774024362395:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_57_1774031966710:2": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_57_1774031966710:1": { - "hasMaskApplied": true, - "hasDealt": true - }, - "lobby_6_1774291232389:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774291232389:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774292285204:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774292285204:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_198_1774298292678:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_11_1774301107619:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_11_1774301107619:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "LOBBY_ID_HERE:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_667_1774304629983:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_667_1774304629983:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_11_1774358844928:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_11_1774358844928:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774376680705:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774376680705:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_9_1774539299447:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_9_1774539299447:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_193_1774541045792:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_193_1774541045792:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_40_1774549743316:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_40_1774549743316:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774550617413:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774550617413:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_10_1774551866986:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_10_1774551866986:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_2_1774559005108:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_2_1774559005108:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_16_1774619422501:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_16_1774619422501:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_4_1774623803188:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_4_1774623803188:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_15_1774625567857:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_15_1774625567857:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774626260341:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774626260341:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_199_1774628574526:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_199_1774628574526:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774880571325:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_6_1774880571325:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774881145760:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774881145760:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774881888655:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774881888655:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_22_1774886732231:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_22_1774886732231:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774887475786:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_5_1774887475786:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_28_1774888579280:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_28_1774888579280:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_15_1774890052788:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_15_1774890052788:2": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774890428563:1": { - "hasMaskApplied": true, - "hasDealt": false - }, - "lobby_8_1774890428563:2": { - "hasMaskApplied": true, - "hasDealt": false - } -} \ No newline at end of file diff --git a/packages/client/node/deno.json b/packages/client/node/deno.json index 83bafc7..3cf65b8 100644 --- a/packages/client/node/deno.json +++ b/packages/client/node/deno.json @@ -28,21 +28,21 @@ "@go-fish/data-types": "../../shared/data-types/src/mod.ts", // Paima packages - "@paimaexample/concise": "jsr:@paimaexample/concise@0.9.0", - "@paimaexample/config": "jsr:@paimaexample/config@0.9.0", - "@paimaexample/log": "jsr:@paimaexample/log@0.9.0", - "@paimaexample/orchestrator": "jsr:@paimaexample/orchestrator@0.9.0", - "@paimaexample/runtime": "jsr:@paimaexample/runtime@0.9.0", - "@paimaexample/tui": "jsr:@paimaexample/tui@0.9.0", - "@paimaexample/utils": "jsr:@paimaexample/utils@0.9.0", - "@paimaexample/sm": "jsr:@paimaexample/sm@0.9.0", - "@paimaexample/coroutine": "jsr:@paimaexample/coroutine@0.9.0", - "@paimaexample/db": "jsr:@paimaexample/db@0.9.0", + "@paimaexample/concise": "jsr:@paimaexample/concise@0.10.24", + "@paimaexample/config": "jsr:@paimaexample/config@0.10.24", + "@paimaexample/log": "jsr:@paimaexample/log@0.10.24", + "@paimaexample/orchestrator": "jsr:@paimaexample/orchestrator@0.10.24", + "@paimaexample/runtime": "jsr:@paimaexample/runtime@0.10.24", + "@paimaexample/tui": "jsr:@paimaexample/tui@0.10.24", + "@paimaexample/utils": "jsr:@paimaexample/utils@0.10.24", + "@paimaexample/sm": "jsr:@paimaexample/sm@0.10.24", + "@paimaexample/coroutine": "jsr:@paimaexample/coroutine@0.10.24", + "@paimaexample/db": "jsr:@paimaexample/db@0.10.24", "fastify": "npm:fastify@^5.4.0", "pg": "npm:pg@^8.14.0", "@sinclair/typebox": "npm:@sinclair/typebox@^0.34.30", "effection": "npm:effection@^3.5.0", - "@paimaexample/explorer": "npm:@paimaexample/explorer@0.9.0", + "@paimaexample/explorer": "npm:@paimaexample/explorer@0.10.24", "@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v3@3.0.0", "@midnight-ntwrk/compact-runtime": "npm:@midnight-ntwrk/compact-runtime@0.15.0", "@midnight-ntwrk/wallet-sdk-address-format": "npm:@midnight-ntwrk/wallet-sdk-address-format@3.1.0", diff --git a/packages/client/node/runtime-config.json b/packages/client/node/runtime-config.json deleted file mode 100644 index 1f73b70..0000000 --- a/packages/client/node/runtime-config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "useTypescriptContract": false, - "useBatcherMode": true -} \ No newline at end of file diff --git a/packages/client/node/scripts/graphql-proxy.ts b/packages/client/node/scripts/graphql-proxy.ts deleted file mode 100644 index 74dda31..0000000 --- a/packages/client/node/scripts/graphql-proxy.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * GraphQL Proxy - Handles SDK to Indexer v3 compatibility - * - * The midnight-js SDK expects certain fields that the indexer v3 doesn't have. - * Specifically, the SDK expects on Transaction type: - * - transactionResult (for tracking transaction status) - * - identifiers (for transaction identifiers) - * - * The indexer v3 schema: - * - Query: contractAction(address: HexEncoded!) -> ContractAction (matches SDK) - * - Transaction: id, hash, protocolVersion, raw, block, contractActions, etc. - * (missing transactionResult, identifiers) - * - * This proxy: - * 1. Removes unsupported fields from queries before forwarding to indexer - * 2. Adds mock values for those fields in responses - */ - -import { serve } from "https://deno.land/std@0.208.0/http/server.ts"; - -const PROXY_PORT = 8089; -const INDEXER_HTTP_URL = Deno.env.get("INDEXER_HTTP_URL") || "http://127.0.0.1:8088/api/v3/graphql"; -const INDEXER_WS_URL = Deno.env.get("INDEXER_WS_URL") || "ws://127.0.0.1:8088/api/v3/graphql/ws"; - -// CORS headers -const corsHeaders = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Sec-WebSocket-Protocol", -}; - -// Fields that SDK expects but indexer v3 doesn't have -const UNSUPPORTED_TRANSACTION_FIELDS = ["transactionResult", "identifiers", "applyStage"]; - -/** - * Remove unsupported fields from GraphQL query - * This is a simple approach - removes field references from the query string - */ -function removeUnsupportedFields(query: string): string { - let cleaned = query; - - for (const field of UNSUPPORTED_TRANSACTION_FIELDS) { - // Remove field with optional alias (e.g., "applyStage" or "applyStage: transactionResult") - // Match patterns like: - // - fieldName - // - fieldName { ... } - // - alias: fieldName - // - alias: fieldName { ... } - - // Simple field reference - cleaned = cleaned.replace(new RegExp(`\\b${field}\\b\\s*(?![:{(])`, 'g'), ''); - - // Field with selection set - need to remove the whole block - // This is tricky with regex, so we'll do a simple approach - const fieldWithBlockRegex = new RegExp(`\\b${field}\\s*\\{[^}]*\\}`, 'g'); - cleaned = cleaned.replace(fieldWithBlockRegex, ''); - - // Aliased field references (e.g., "foo: transactionResult") - cleaned = cleaned.replace(new RegExp(`\\w+\\s*:\\s*${field}\\b`, 'g'), ''); - } - - // Clean up any double spaces or empty lines - cleaned = cleaned.replace(/\s+/g, ' '); - - // Clean up empty selection sets that might result from removing fields - cleaned = cleaned.replace(/\{\s*\}/g, '{ __typename }'); - - return cleaned; -} - -/** - * Add mock values for unsupported fields to transaction objects in response - */ -function addMockFieldsToResponse(data: any): any { - if (!data) return data; - - // Deep clone - const result = JSON.parse(JSON.stringify(data)); - - function processObject(obj: any): any { - if (obj === null || obj === undefined) return obj; - if (typeof obj !== "object") return obj; - - if (Array.isArray(obj)) { - return obj.map(processObject); - } - - // Check if this looks like a Transaction object (has 'hash' or 'id' field typical of transactions) - const processed: any = {}; - for (const [key, value] of Object.entries(obj)) { - processed[key] = processObject(value); - } - - // If this object has transaction-like fields, add the mock fields - if (processed.hash !== undefined || processed.id !== undefined) { - // Add mock fields that SDK might expect - if (!processed.transactionResult) { - processed.transactionResult = "applied"; // Mock: transaction was applied - } - if (!processed.identifiers) { - processed.identifiers = processed.hash ? [processed.hash] : []; - } - if (!processed.applyStage) { - processed.applyStage = "applied"; - } - } - - return processed; - } - - if (result.data) { - result.data = processObject(result.data); - } - - return result; -} - -/** - * Proxy HTTP GraphQL requests - */ -async function proxyHttpRequest(request: Request): Promise { - const url = new URL(request.url); - - // Handle CORS preflight - if (request.method === "OPTIONS") { - return new Response(null, { status: 204, headers: corsHeaders }); - } - - // Health check - if (url.pathname === "/health") { - return new Response(JSON.stringify({ status: "ok", proxy: true }), { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); - } - - // Only proxy GraphQL endpoints - if (!url.pathname.includes("/graphql")) { - return new Response(JSON.stringify({ error: "Not found" }), { - status: 404, - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); - } - - try { - const body = await request.json(); - const { query, variables = {}, operationName } = body; - - console.log("[GraphQL-Proxy] Incoming query:", query?.substring(0, 150)); - - // Remove unsupported fields from query - const cleanedQuery = removeUnsupportedFields(query); - console.log("[GraphQL-Proxy] Cleaned query:", cleanedQuery?.substring(0, 150)); - - // Forward to indexer - const indexerResponse = await fetch(INDEXER_HTTP_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: cleanedQuery, - variables, - operationName, - }), - }); - - const indexerData = await indexerResponse.json(); - - // Check for errors - if (indexerData.errors) { - console.log("[GraphQL-Proxy] Indexer returned errors:", JSON.stringify(indexerData.errors)); - - // For contractAction queries where contract doesn't exist, return null - const firstError = indexerData.errors[0]?.message || ""; - if (firstError.includes("Unknown field") && query.includes("contractAction")) { - console.log("[GraphQL-Proxy] Contract not found or schema issue, returning null"); - return new Response(JSON.stringify({ - data: { contractAction: null }, - extensions: { proxied: true, fallback: true } - }), { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); - } - } - - // Add mock values for unsupported fields - const enrichedResponse = addMockFieldsToResponse(indexerData); - enrichedResponse.extensions = { ...enrichedResponse.extensions, proxied: true }; - - console.log("[GraphQL-Proxy] Response processed successfully"); - - return new Response(JSON.stringify(enrichedResponse), { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - }); - } catch (error) { - console.error("[GraphQL-Proxy] Error:", error); - return new Response( - JSON.stringify({ errors: [{ message: `Proxy error: ${error}` }] }), - { - status: 500, - headers: { ...corsHeaders, "Content-Type": "application/json" }, - } - ); - } -} - -/** - * Wait for the indexer to be ready - */ -async function waitForIndexer(): Promise { - console.log("[GraphQL-Proxy] Waiting for indexer at", INDEXER_HTTP_URL); - - for (let i = 0; i < 60; i++) { - try { - const response = await fetch(INDEXER_HTTP_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ query: "{ __typename }" }), - }); - - if (response.ok) { - const data = await response.json(); - if (data.data?.__typename) { - console.log("[GraphQL-Proxy] Indexer is ready!"); - return; - } - } - } catch { - // Connection refused, keep retrying - } - - if (i % 5 === 0) { - console.log(`[GraphQL-Proxy] Still waiting for indexer... (${i + 1}/60)`); - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - throw new Error("Indexer not ready after 60 seconds"); -} - -/** - * Main entry point - */ -async function main() { - console.log("[GraphQL-Proxy] SDK to Indexer v3 Compatibility Proxy"); - console.log("[GraphQL-Proxy] =========================================="); - console.log(`[GraphQL-Proxy] Upstream indexer: ${INDEXER_HTTP_URL}`); - console.log(`[GraphQL-Proxy] Proxy port: ${PROXY_PORT}`); - - // Wait for indexer - await waitForIndexer(); - - console.log(`[GraphQL-Proxy] Starting proxy server on port ${PROXY_PORT}...`); - - await serve(proxyHttpRequest, { - port: PROXY_PORT, - onListen: () => { - console.log(`[GraphQL-Proxy] Proxy running at http://localhost:${PROXY_PORT}`); - console.log("[GraphQL-Proxy] SDK should connect to:"); - console.log(`[GraphQL-Proxy] HTTP: http://127.0.0.1:${PROXY_PORT}/api/v1/graphql`); - console.log(`[GraphQL-Proxy] WS: ws://127.0.0.1:${PROXY_PORT}/api/v1/graphql/ws`); - }, - }); -} - -main().catch((error) => { - console.error("[GraphQL-Proxy] Fatal error:", error); - Deno.exit(1); -}); diff --git a/packages/client/node/scripts/start.dev.ts b/packages/client/node/scripts/start.dev.ts index e76078a..6bd2fa0 100644 --- a/packages/client/node/scripts/start.dev.ts +++ b/packages/client/node/scripts/start.dev.ts @@ -9,47 +9,12 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const midnightContractsDir = path.resolve(__dirname, "../../../shared/contracts/midnight"); const indexerConfigPath = path.join(midnightContractsDir, "indexer-standalone/config.yaml"); - -// Check if we should skip Midnight infrastructure (when using TypeScript contract) -const useTypescriptContract = Deno.env.get("USE_TYPESCRIPT_CONTRACT") === "true"; - -// Check if batcher mode is enabled (run frontend in batcher mode, no Lace wallet needed) -const useBatcherMode = Deno.env.get("USE_BATCHER_MODE") === "true"; - -// Check if we should deploy the Midnight contract at startup (takes ~6 minutes) -// Set DEPLOY_MIDNIGHT_CONTRACT=true to deploy, otherwise assumes contract is already deployed -const deployMidnightContract = Deno.env.get("DEPLOY_MIDNIGHT_CONTRACT") === "true"; - -// Check if Midnight infrastructure is already running (via midnight:setup) -// Set SKIP_MIDNIGHT_INFRA=true to skip starting node/indexer/proof-server -const skipMidnightInfra = Deno.env.get("SKIP_MIDNIGHT_INFRA") === "true"; - -// Check if EVM/Hardhat is already running externally -// Set SKIP_EVM_LAUNCH=true to skip launching Hardhat node and deployment -// This is useful when multiple programs share a single Hardhat instance -// Example: SKIP_EVM_LAUNCH=true USE_TYPESCRIPT_CONTRACT=true deno task dev -const skipEvmLaunch = Deno.env.get("SKIP_EVM_LAUNCH") === "true"; - -// Check if pglite database should be skipped (for shared infrastructure environments) -// Set SKIP_PGLITE=true to skip launching pglite database -const skipPglite = Deno.env.get("SKIP_PGLITE") === "true"; - -// Suppress noisy infrastructure logs (hardhat-evmMain, effectstream-sync-block-merge, etc.) -// Set QUIET_LOGS=true to hide these and keep only batcher/node/game-relevant output -const quietLogs = Deno.env.get("QUIET_LOGS") === "true"; - -// Debug logging -console.log(`[Orchestrator] USE_TYPESCRIPT_CONTRACT=${useTypescriptContract}, USE_BATCHER_MODE=${useBatcherMode}, DEPLOY_MIDNIGHT_CONTRACT=${deployMidnightContract}, SKIP_MIDNIGHT_INFRA=${skipMidnightInfra}, SKIP_EVM_LAUNCH=${skipEvmLaunch}, SKIP_PGLITE=${skipPglite}`); - -// Path to GraphQL proxy (translates SDK v2.0.0 queries to indexer v3 schema) -// Note: With SDK v3, this may no longer be needed -const graphqlProxyScript = path.resolve(__dirname, "graphql-proxy.ts"); - // Path to cleanup script for indexer database const cleanupIndexerScript = path.resolve(__dirname, "cleanup-indexer-db.ts"); + // Midnight infrastructure processes (skipped when using TypeScript contract or SKIP_MIDNIGHT_INFRA=true) -const midnightProcesses = (useTypescriptContract || skipMidnightInfra) ? [] : [ +const midnightProcesses = [ /** MIDNIGHT-NODE-BLOCK */ { name: "midnight-node", @@ -133,30 +98,6 @@ const midnightProcesses = (useTypescriptContract || skipMidnightInfra) ? [] : [ /** MIDNIGHT-PROOF-SERVER-BLOCK */ ]; -// GraphQL proxy (translates SDK v2.0.0 queries to indexer v3 schema) -// This is necessary because SDK v2.0.0 expects 'contractAction' but indexer v3 has 'contract' -const graphqlProxyProcesses = useBatcherMode && !useTypescriptContract ? [ - /** GRAPHQL-PROXY-BLOCK */ - { - name: "graphql-proxy", - args: [ - "run", "-A", "--unstable-detect-cjs", - graphqlProxyScript, - ], - env: { - INDEXER_HTTP_URL: "http://127.0.0.1:8088/api/v3/graphql", - INDEXER_WS_URL: "ws://127.0.0.1:8088/api/v3/graphql/ws", - }, - waitToExit: false, - type: "system-dependency", - link: "http://localhost:8089", - stopProcessAtPort: [8089], - // Only depend on indexer if we're managing it here - dependsOn: skipMidnightInfra ? [] : ["midnight-indexer"], - }, - /** GRAPHQL-PROXY-BLOCK */ -] : []; - // Note: The old midnight-batcher (ts-batcher on port 8000) has been removed. // Midnight transactions are now handled by the Paima batcher (@go-fish/batcher on port 3336) // which uses MidnightAdapter for Midnight blockchain integration. @@ -165,7 +106,7 @@ const graphqlProxyProcesses = useBatcherMode && !useTypescriptContract ? [ // Only deploys if DEPLOY_MIDNIGHT_CONTRACT=true AND we're managing the infra (not SKIP_MIDNIGHT_INFRA) // Using MIDNIGHT_DEPLOY_VERIFIER_KEYS_LIMIT=1 for faster deployment (only 1 verifier key) // Note: If using SKIP_MIDNIGHT_INFRA, the contract was already deployed by midnight:setup -const midnightContractDeployment = deployMidnightContract && useBatcherMode && !useTypescriptContract && !skipMidnightInfra ? [ +const midnightContractDeployment = [ { name: "midnight-contract-deploy", args: [ @@ -180,53 +121,48 @@ const midnightContractDeployment = deployMidnightContract && useBatcherMode && ! type: "system-dependency", dependsOn: ["midnight-proof-server", "midnight-indexer"], }, -] : []; +]; -// Debug: log which processes will be launched -console.log(`[Orchestrator] midnightProcesses: ${midnightProcesses.length} processes`); -console.log(`[Orchestrator] graphqlProxyProcesses: ${graphqlProxyProcesses.length} processes`); const customProcesses = [ // Midnight infrastructure (skipped when USE_TYPESCRIPT_CONTRACT=true) ...midnightProcesses, // Deploy Midnight contract after infrastructure is ready (only in batcher mode) ...midnightContractDeployment, - // GraphQL proxy for SDK v2 to indexer v3 translation (only when USE_BATCHER_MODE=true) - ...graphqlProxyProcesses, /** FRONTEND-BLOCK */ - { - name: "install-frontend", - command: "npm", - cwd: "../../frontend/", - args: ["install"], - waitToExit: true, - type: "system-dependency", - dependsOn: [], - }, - { - name: "serve-frontend", - command: "npm", - cwd: "../../frontend", - // Use batcher mode script when USE_BATCHER_MODE=true (no Lace wallet needed) - args: useBatcherMode ? ["run", "dev:batcher"] : ["run", "dev"], - waitToExit: false, - link: "http://localhost:3000", - type: "system-dependency", - dependsOn: useBatcherMode ? ["install-frontend", "batcher"] : ["install-frontend"], - logs: "none", - }, + // { + // name: "install-frontend", + // command: "npm", + // cwd: "../../frontend/", + // args: ["install"], + // waitToExit: true, + // type: "system-dependency", + // dependsOn: [], + // }, + // { + // name: "serve-frontend", + // command: "npm", + // cwd: "../../frontend", + // // Use batcher mode script when USE_BATCHER_MODE=true (no Lace wallet needed) + // args: useBatcherMode ? ["run", "dev:batcher"] : ["run", "dev"], + // waitToExit: false, + // link: "http://localhost:3000", + // type: "system-dependency", + // dependsOn: useBatcherMode ? ["install-frontend", "batcher"] : ["install-frontend"], + // logs: "none", + // }, /** FRONTEND-BLOCK */ /** EXPLORER-BLOCK */ - { - name: "explorer", - args: ["run", "-A", "--unstable-detect-cjs", "@paimaexample/explorer"], - waitToExit: false, - type: "system-dependency", - link: "http://localhost:10590", - stopProcessAtPort: [10590], - }, + // { + // name: "explorer", + // args: ["run", "-A", "--unstable-detect-cjs", "@paimaexample/explorer"], + // waitToExit: false, + // type: "system-dependency", + // link: "http://localhost:10590", + // stopProcessAtPort: [10590], + // }, /** EXPLORER-BLOCK */ /** BATCHER-BLOCK */ @@ -241,9 +177,9 @@ const customProcesses = [ // - If deploying contract: wait for deployment // - If midnight infra managed here: wait for proof server // - If midnight infra external (SKIP_MIDNIGHT_INFRA): no midnight dependencies - dependsOn: deployMidnightContract && useBatcherMode && !useTypescriptContract && !skipMidnightInfra - ? ["midnight-contract-deploy"] - : (useBatcherMode && !useTypescriptContract && !skipMidnightInfra ? ["midnight-proof-server"] : []), + dependsOn: + // ["midnight-proof-server"], + ["midnight-contract-deploy"], }, /** BATCHER-BLOCK */ ]; @@ -254,8 +190,7 @@ const config = Value.Parse(OrchestratorConfig, { processes: { [ComponentNames.TMUX]: true, [ComponentNames.TUI]: true, - // Launch Dev DB & Collector (skip pglite if SKIP_PGLITE=true) - [ComponentNames.EFFECTSTREAM_PGLITE]: !skipPglite, + [ComponentNames.EFFECTSTREAM_PGLITE]: true, [ComponentNames.COLLECTOR]: true, }, @@ -263,12 +198,7 @@ const config = Value.Parse(OrchestratorConfig, { processesToLaunch: [ // Launch EVM contracts (Hardhat node + deploy) // Skip if SKIP_EVM_LAUNCH=true (when using external Hardhat instance) - ...(skipEvmLaunch ? [] : launchEvm("@go-fish/evm-contracts").map(p => { - // QUIET_LOGS=true: suppress hardhat chain logs (evmMain, evmParallel, block-merge, etc.) - if (!quietLogs) return p; - const q: typeof p = { ...p, logs: "none" }; - return q; - })), + ...launchEvm("@go-fish/evm-contracts"), ...customProcesses, ], }); @@ -280,14 +210,4 @@ if (Deno.env.get("EFFECTSTREAM_STDOUT")) { config.processes[ComponentNames.COLLECTOR] = false; } -// Write runtime config file for the backend to read -// This is needed because env vars don't always propagate through the orchestrator -const runtimeConfig = { - useTypescriptContract: Deno.env.get("USE_TYPESCRIPT_CONTRACT") === "true", - useBatcherMode: Deno.env.get("USE_BATCHER_MODE") === "true", -}; -const configPath = new URL("../runtime-config.json", import.meta.url); -await Deno.writeTextFile(configPath, JSON.stringify(runtimeConfig, null, 2)); -console.log(`[Orchestrator] Runtime config written: useTypescriptContract=${runtimeConfig.useTypescriptContract}, useBatcherMode=${runtimeConfig.useBatcherMode}`); - await start(config); diff --git a/packages/client/node/src/api.ts b/packages/client/node/src/api.ts index 2f335a9..7513eb0 100644 --- a/packages/client/node/src/api.ts +++ b/packages/client/node/src/api.ts @@ -636,7 +636,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Get player's decrypted hand - server.get("/api/midnight/player_hand", async (request, reply) => { + server.get("/debug/midnight/player_hand", async (request, reply) => { const { lobby_id, player_id } = request.query as { lobby_id: string; player_id: string }; if (!lobby_id || !player_id) { @@ -655,7 +655,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d // Get player's real hand using their secret key (batcher mode) // The frontend passes its secret so the backend can run doesPlayerHaveSpecificCard // with the correct witness. The secret is never persisted — used only for this call. - server.post("/api/midnight/player_hand_with_secret", async (request, reply) => { + server.post("/debug/midnight/player_hand_with_secret", async (request, reply) => { const { lobby_id, player_id, player_secret, shuffle_seed, opponent_secret, opponent_shuffle_seed } = request.body as { lobby_id: string; player_id: number; @@ -691,7 +691,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Ask for card action - server.post("/api/midnight/ask_for_card", async (request, reply) => { + server.post("/debug/midnight/ask_for_card", async (request, reply) => { const { lobby_id, player_id, rank } = request.body as { lobby_id: string; player_id: number; @@ -712,7 +712,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Go Fish action - server.post("/api/midnight/go_fish", async (request, reply) => { + server.post("/debug/midnight/go_fish", async (request, reply) => { const { lobby_id, player_id } = request.body as { lobby_id: string; player_id: number; @@ -732,7 +732,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Apply Mask action (setup phase) - server.post("/api/midnight/apply_mask", async (request, reply) => { + server.post("/debug/midnight/apply_mask", async (request, reply) => { const { lobby_id, player_id, player_secret, shuffle_seed } = request.body as { lobby_id: string; player_id: number; @@ -754,7 +754,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Deal Cards action (setup phase) - server.post("/api/midnight/deal_cards", async (request, reply) => { + server.post("/debug/midnight/deal_cards", async (request, reply) => { const { lobby_id, player_id, player_secret, shuffle_seed } = request.body as { lobby_id: string; player_id: number; @@ -776,7 +776,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Respond to ask action (opponent responds to card request) - server.post("/api/midnight/respond_to_ask", async (request, reply) => { + server.post("/debug/midnight/respond_to_ask", async (request, reply) => { const { lobby_id, player_id } = request.body as { lobby_id: string; player_id: number; @@ -796,14 +796,14 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // After Go Fish action (complete the draw turn) - server.post("/api/midnight/after_go_fish", async (request, reply) => { + server.post("/debug/midnight/after_go_fish", async (request, reply) => { const { lobby_id, player_id, drew_requested_card } = request.body as { lobby_id: string; player_id: number; - drew_requested_card: boolean; + drew_requested_card?: boolean; // V3 contract no longer needs this — kept for backwards compat }; - if (!lobby_id || !player_id || drew_requested_card === undefined) { + if (!lobby_id || !player_id) { return reply.code(400).send({ error: 'Missing required fields' }); } @@ -817,7 +817,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d }); // Skip draw when deck is empty - ends turn without drawing - server.post("/api/midnight/skip_draw_deck_empty", async (request, reply) => { + server.post("/debug/midnight/skip_draw_deck_empty", async (request, reply) => { const { lobby_id, player_id } = request.body as { lobby_id: string; player_id: number; diff --git a/packages/client/node/src/midnight-actions.ts b/packages/client/node/src/midnight-actions.ts index 8d86321..bde7b71 100644 --- a/packages/client/node/src/midnight-actions.ts +++ b/packages/client/node/src/midnight-actions.ts @@ -240,9 +240,9 @@ const actionWitnesses: any = { const hexGameId = Array.from(new Uint8Array(gameId)).map(b => b.toString(16).padStart(2, '0')).join(''); const key = `${hexGameId}-${player}`; - // Check if we already have a secret for this player/game - if (playerSecrets.has(key)) { - const s = playerSecrets.get(key)!; + // Check volatile map first, then persistent store (survives hand queries that clear volatile map) + const s = playerSecrets.get(key) ?? persistentSecrets.get(key); + if (s !== undefined) { return [context.privateState, s]; } @@ -258,13 +258,12 @@ const actionWitnesses: any = { const hexGameId = Array.from(new Uint8Array(gameId)).map(b => b.toString(16).padStart(2, '0')).join(''); const key = `${hexGameId}-${player}`; - // Use injected shuffle seed if available (set before replay via injectShuffleSeed) - if (playerShuffleSeeds.has(key)) { - const seed = playerShuffleSeeds.get(key)!; + // Check volatile map first, then persistent store (survives hand queries that clear volatile map) + const cachedSeed = playerShuffleSeeds.get(key) ?? persistentShuffleSeeds.get(key); + if (cachedSeed) { console.log(`[MidnightActions] shuffle_seed: HIT key="${key}"`); - // Capture so get_sorted_deck_witness can generate deterministic weights - lastActionShuffleSeed = seed; - return [context.privateState, seed]; + lastActionShuffleSeed = cachedSeed; + return [context.privateState, cachedSeed]; } // Fallback: deterministic seed (only used when no real seed was provided) @@ -917,11 +916,13 @@ export async function askForCard( const gameId = lobbyIdToGameId(lobbyId); console.log(`[MidnightActions] askForCard(gameId: ${lobbyId}, playerId: ${playerId}, rank: ${rank})`); + const now = BigInt(Date.now()); const result = actionContract.provableCircuits.askForCard( actionContext, gameId, BigInt(playerId), - BigInt(rank) + BigInt(rank), + now ); actionContext = result.context; @@ -948,10 +949,12 @@ export async function goFish( const gameId = lobbyIdToGameId(lobbyId); console.log(`[MidnightActions] goFish(gameId: ${lobbyId}, playerId: ${playerId})`); + const now = BigInt(Date.now()); const result = actionContract.provableCircuits.goFish( actionContext, gameId, - BigInt(playerId) + BigInt(playerId), + now ); actionContext = result.context; @@ -1146,10 +1149,12 @@ export async function respondToAsk( let result; try { + const now = BigInt(Date.now()); result = actionContract.provableCircuits.respondToAsk( actionContext, gameId, - BigInt(playerId) + BigInt(playerId), + now ); actionContext = result.context; } catch (respondError: any) { @@ -1206,11 +1211,13 @@ export async function afterGoFish( const gameId = lobbyIdToGameId(lobbyId); console.log(`[MidnightActions] afterGoFish(gameId: ${lobbyId}, playerId: ${playerId}, drewRequestedCard: ${drewRequestedCard})`); + // V3 contract: afterGoFish no longer takes drewRequestedCard — it decrypts and checks internally + const now = BigInt(Date.now()); const result = actionContract.provableCircuits.afterGoFish( actionContext, gameId, BigInt(playerId), - drewRequestedCard + now ); actionContext = result.context; diff --git a/packages/frontend/.env.batcher b/packages/frontend/.env.batcher deleted file mode 100644 index dbe0573..0000000 --- a/packages/frontend/.env.batcher +++ /dev/null @@ -1,16 +0,0 @@ -# Batcher Mode Configuration -# Copy this to .env.local and set VITE_BATCHER_MODE_ENABLED=true to use batcher mode -# Batcher mode allows users to play without installing Lace wallet - -# Enable batcher mode (set to "true" to enable) -VITE_BATCHER_MODE_ENABLED=true - -# Paima batcher service URL (handles transaction submission) -# Uses the @paimaexample/batcher with MidnightAdapter -# Leave empty to use Vite dev server proxy (avoids CORS). The proxy forwards to localhost:3336. -# VITE_BATCHER_URL=http://localhost:3336 - -# Indexer URLs (for reading blockchain state) -# SDK v3 uses /api/v3/graphql endpoints -VITE_INDEXER_HTTP_URL=http://127.0.0.1:8088/api/v3/graphql -VITE_INDEXER_WS_URL=ws://127.0.0.1:8088/api/v3/graphql/ws diff --git a/packages/frontend/src/screens/WalletScreen.ts b/packages/frontend/src/screens/WalletScreen.ts deleted file mode 100644 index 567f4e6..0000000 --- a/packages/frontend/src/screens/WalletScreen.ts +++ /dev/null @@ -1,432 +0,0 @@ -/** - * WalletScreen - Handles wallet connection before entering the game - * Only requires Lace wallet for Midnight ZK operations. - * EVM wallet is auto-generated locally (no MetaMask needed). - * - * In mock mode (USE_TYPESCRIPT_CONTRACT=true): No wallet needed, auto-proceed - * In production mode: Only Lace wallet required for Midnight transactions - */ - -import * as EffectstreamBridge from '../effectstreamBridge'; -import * as LaceWalletBridge from '../laceWalletBridge'; -import { MidnightService } from '../services/MidnightService'; -import { isBatcherModeEnabled } from '../proving/batcher-providers'; - -// Config from backend -interface AppConfig { - useMockedMidnight: boolean; - requiresLaceWallet: boolean; - requiresEvmWallet: boolean; -} - -export class WalletScreen { - private container: HTMLElement; - private connectLaceButton: HTMLButtonElement | null = null; - private statusText: HTMLElement | null = null; - private errorText: HTMLElement | null = null; - - // Connection state - only tracking Lace now - private laceConnected: boolean = false; - - // Config from backend - // Default to production mode (require Lace wallet) - private config: AppConfig = { - useMockedMidnight: false, - requiresLaceWallet: true, - requiresEvmWallet: false, // No longer required - using local wallet - }; - - constructor(container: HTMLElement) { - this.container = container; - } - - async show() { - // Fetch config from backend to determine which wallets are needed - await this.fetchConfig(); - - // Check existing Lace connection - this.laceConnected = LaceWalletBridge.isLaceConnected(); - - // Check if batcher mode is enabled (no Lace wallet required) - const batcherMode = isBatcherModeEnabled(); - if (batcherMode) { - console.log('[WalletScreen] Batcher mode enabled - no Lace wallet required'); - } - - // Initialize the local EVM wallet automatically (no user interaction needed) - console.log('[WalletScreen] Initializing local EVM wallet...'); - await EffectstreamBridge.userWalletLogin(); - console.log('[WalletScreen] Local EVM wallet ready'); - - // If in mock mode, batcher mode, or Lace already connected, proceed - if (this.config.useMockedMidnight || batcherMode || this.laceConnected) { - console.log('[WalletScreen] Proceeding to game...'); - - // If batcher mode is enabled, initialize on-chain service now - // (in wallet mode, this happens after Lace connection) - if (batcherMode && !this.config.useMockedMidnight) { - console.log('[WalletScreen] Initializing batcher mode on-chain service...'); - MidnightService.tryInitializeOnChain().then((initialized) => { - if (initialized) { - console.log('[WalletScreen] Batcher mode on-chain service initialized'); - } else { - console.log('[WalletScreen] Batcher mode on-chain service not ready yet'); - } - }).catch((error) => { - console.error('[WalletScreen] Batcher mode initialization error:', error); - }); - } - - this.dispatchNavigate('name-entry'); - return; - } - - this.render(); - } - - hide() { - // No cleanup needed - } - - private async fetchConfig(): Promise { - try { - const { API_BASE_URL } = await import('../apiConfig'); - const response = await fetch(`${API_BASE_URL}/api/config`); - if (response.ok) { - const serverConfig = await response.json(); - // Override EVM requirement - we always use local wallet now - this.config = { - ...serverConfig, - requiresEvmWallet: false, - }; - console.log('[WalletScreen] Config loaded:', this.config); - } - } catch (error) { - console.warn('[WalletScreen] Could not fetch config, using defaults:', error); - // Keep defaults - } - } - - private render() { - this.container.innerHTML = ` - - -
-
-

Go Fish

-

A blockchain card game with ZK privacy

- -
- No MetaMask needed!
- A local wallet has been automatically created for game transactions. -
- -
-

Connect your Lace wallet to enable Midnight ZK privacy features

-

-
- -
-
-
Midnight Network (ZK Privacy)
- -
- - ${this.laceConnected ? 'Connected' : 'Not connected'} -
-
-
- -

- -
-

Requirements:

-

• Lace Midnight Preview wallet extension

-

• tDUST tokens for transaction fees

-
-
-
- `; - this.attachEventListeners(); - } - - private attachEventListeners(): void { - this.connectLaceButton = document.getElementById('connect-lace-btn') as HTMLButtonElement; - this.statusText = document.getElementById('wallet-status'); - this.errorText = document.getElementById('error-text'); - - this.connectLaceButton?.addEventListener('click', () => { - if (!this.laceConnected) { - this.handleConnectLaceWallet(); - } - }); - } - - private async handleConnectLaceWallet(): Promise { - if (!this.connectLaceButton || !this.statusText || !this.errorText) return; - - // Disable button and show loading state - this.setButtonLoading(this.connectLaceButton, true); - this.errorText.textContent = ''; - this.statusText.textContent = 'Opening Lace wallet...'; - - try { - // Attempt to connect Lace wallet - const result = await LaceWalletBridge.laceWalletLogin(); - - if (result.success) { - const address = result.address || LaceWalletBridge.getLaceAddress(); - console.log('Lace wallet connected successfully:', address); - - this.laceConnected = true; - - // Try to initialize on-chain service now that Lace is connected - // This enables real Midnight blockchain transactions in production mode - console.log('[WalletScreen] Attempting to initialize on-chain service...'); - MidnightService.tryInitializeOnChain().then((initialized) => { - if (initialized) { - console.log('[WalletScreen] On-chain service initialized successfully'); - } else { - console.log('[WalletScreen] On-chain service not initialized (mock mode or not ready)'); - } - }).catch((error) => { - console.error('[WalletScreen] On-chain service initialization error:', error); - }); - - // Navigate to game - this.statusText.textContent = `Connected! Proceeding...`; - this.connectLaceButton.textContent = 'Lace Connected'; - this.connectLaceButton.classList.remove('connecting'); - this.connectLaceButton.classList.add('btn-connected'); - - setTimeout(() => { - this.dispatchNavigate('name-entry'); - }, 300); - } else { - // Connection failed - this.handleConnectionError(result.errorMessage || 'Failed to connect Lace wallet'); - } - } catch (error) { - console.error('Error connecting Lace wallet:', error); - this.handleConnectionError( - error instanceof Error ? error.message : 'Unknown error occurred' - ); - } - } - - private setButtonLoading(button: HTMLButtonElement, loading: boolean): void { - if (loading) { - button.disabled = true; - button.textContent = 'Connecting...'; - button.classList.add('connecting'); - } else { - button.disabled = false; - button.textContent = 'Connect Lace Wallet'; - button.classList.remove('connecting'); - } - } - - private handleConnectionError(message: string): void { - // Re-render to reset button states - this.render(); - - this.errorText = document.getElementById('error-text'); - if (!this.errorText) return; - - // Show user-friendly error messages - if (message.includes('user rejected') || message.includes('User rejected')) { - this.errorText.textContent = 'Connection cancelled. Please try again.'; - } else if (message.includes('Lace wallet not found') || message.includes('extension installed')) { - this.errorText.textContent = 'Please install the Lace Midnight Preview wallet extension.'; - } else if (message.includes('No compatible Midnight wallet')) { - this.errorText.textContent = 'Please update your Lace wallet to a compatible version.'; - } else if (message.includes('Network ID mismatch')) { - this.errorText.textContent = 'Network mismatch. Please switch to the correct network in Lace.'; - } else { - this.errorText.textContent = `Error: ${message}`; - } - } - - private dispatchNavigate(screen: string, data: any = {}) { - const event = new CustomEvent('navigate', { - detail: { screen, ...data }, - bubbles: true, - }); - this.container.dispatchEvent(event); - } -} diff --git a/packages/shared/contracts/evm/deno.json b/packages/shared/contracts/evm/deno.json index a67bdae..06ce2f7 100644 --- a/packages/shared/contracts/evm/deno.json +++ b/packages/shared/contracts/evm/deno.json @@ -8,9 +8,9 @@ "version": "0.1.0", "license": "MIT", "imports": { - "@paimaexample/evm-hardhat": "jsr:@paimaexample/evm-hardhat@0.9.0", - "@paimaexample/log": "jsr:@paimaexample/log@0.9.0", - "@paimaexample/evm-contracts": "jsr:@paimaexample/evm-contracts@0.9.0", + "@paimaexample/evm-hardhat": "jsr:@paimaexample/evm-hardhat@0.10.24", + "@paimaexample/log": "jsr:@paimaexample/log@0.10.24", + "@paimaexample/evm-contracts": "jsr:@paimaexample/evm-contracts@0.10.24", "@wagmi/cli": "npm:@wagmi/cli@2.3.1", "hardhat": "npm:hardhat@3.0.4", "@nomicfoundation/hardhat-ignition-viem": "npm:@nomicfoundation/hardhat-ignition-viem@3.0.2", diff --git a/packages/shared/contracts/evm/package.json b/packages/shared/contracts/evm/package.json index 53cd8a5..b298419 100644 --- a/packages/shared/contracts/evm/package.json +++ b/packages/shared/contracts/evm/package.json @@ -3,7 +3,7 @@ "dependencies": { "@openzeppelin/contracts": "5.1.0", "@openzeppelin/contracts-upgradeable": "^5.1.0", - "@paimaexample/evm-contracts": "0.7.0" + "@paimaexample/evm-contracts": "0.10.24" }, "devDependencies": { "jsonc-parser": "3.3.1", diff --git a/packages/shared/contracts/midnight/deno.json b/packages/shared/contracts/midnight/deno.json index f2966b6..5639e73 100644 --- a/packages/shared/contracts/midnight/deno.json +++ b/packages/shared/contracts/midnight/deno.json @@ -15,16 +15,16 @@ "midnight-indexer:wait": "wait-on tcp:8088", "midnight-proof-server:start": "LEDGER_NETWORK_ID=\"Undeployed\" RUST_BACKTRACE=full SUBSTRATE_NODE_WS_URL=\"ws://localhost:9944\" deno run -A --unstable-detect-cjs npm:@paimaexample/npm-midnight-proof-server", "midnight-proof-server:wait": "wait-on tcp:6300", - "midnight-contract:clean": "rm -rf midnight-level-db && rm -f contract.json contract-go-fish.undeployed.json", + "midnight-contract:clean": "rm -rf midnight-level-db midnight-level-db-deploy && rm -f contract.json contract-go-fish.undeployed.json deployment-state.json", "midnight-contract:deploy": "deno task midnight-contract:clean && deno --unstable-detect-cjs -A deploy.ts", "midnight-contract:deploy-quick": "deno task midnight-contract:clean && MIDNIGHT_DEPLOY_VERIFIER_KEYS_LIMIT=1 deno --unstable-detect-cjs -A deploy.ts", "midnight-faucet:start": "deno run -A faucet.ts", "test": "deno run -A example.test.ts" }, "imports": { - "@paimaexample/npm-midnight-indexer": "npm:@paimaexample/npm-midnight-indexer@0.10.2", - "@paimaexample/npm-midnight-node": "npm:@paimaexample/npm-midnight-node@0.10.2", - "@paimaexample/npm-midnight-proof-server": "npm:@paimaexample/npm-midnight-proof-server@0.10.2", + "@paimaexample/npm-midnight-indexer": "npm:@paimaexample/npm-midnight-indexer@0.10.24", + "@paimaexample/npm-midnight-node": "npm:@paimaexample/npm-midnight-node@0.10.24", + "@paimaexample/npm-midnight-proof-server": "npm:@paimaexample/npm-midnight-proof-server@0.10.24", "@midnight-ntwrk/ledger": "npm:@midnight-ntwrk/ledger-v8@8.0.3", "@midnight-ntwrk/compact-runtime": "npm:@midnight-ntwrk/compact-runtime@0.15.0", "@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v3@3.0.0", diff --git a/packages/shared/contracts/midnight/deploy.midnight.ts b/packages/shared/contracts/midnight/deploy.midnight.ts index b7d42ac..9fe03b2 100644 --- a/packages/shared/contracts/midnight/deploy.midnight.ts +++ b/packages/shared/contracts/midnight/deploy.midnight.ts @@ -233,50 +233,80 @@ async function buildWalletAndWaitForFunds( seed: string, networkId: NetworkId.NetworkId, ): Promise { - log.info("Building wallet using modular SDK"); - const result = await buildWalletFacade(networkUrls, seed, networkId); - - const initialState = await getInitialShieldedState(result.wallet.shielded); - const address = initialState.address.coinPublicKeyString(); - log.info(`Wallet seed: ${seed}`); - log.info(`Wallet address: ${address}`); - log.info(`Dust address: ${result.dustAddress}`); - - let balance = initialState.balances[shieldedToken().tag] ?? 0n; - log.info("initialState " + safeStringifyProgress(initialState)); - const syncTimeoutMs = resolveWalletSyncTimeoutMs(); - if (balance === 0n) { - const skipWait = - Deno.env.get("MIDNIGHT_SKIP_WAIT_FOR_FUNDS")?.toLowerCase() === "true"; - log.info("Wallet shielded balance: 0"); - log.info( - `Waiting to receive tokens... (timeout ${syncTimeoutMs}ms${ - skipWait ? ", skip on timeout enabled" : "" - })`, - ); + const maxRetries = 5; + const baseDelayMs = 3000; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + log.info(`Building wallet using modular SDK (attempt ${attempt}/${maxRetries})`); + let result: WalletResult | null = null; try { - const { shieldedBalance, unshieldedBalance } = await syncAndWaitForFunds( - result.wallet, - ); - balance = shieldedBalance; - if (unshieldedBalance > 0n) { - log.info(`Unshielded balance available: ${unshieldedBalance}`); + result = await buildWalletFacade(networkUrls, seed, networkId); + + const initialState = await getInitialShieldedState(result.wallet.shielded); + const address = initialState.address.coinPublicKeyString(); + log.info(`Wallet seed: ${seed}`); + log.info(`Wallet address: ${address}`); + log.info(`Dust address: ${result.dustAddress}`); + + let balance = initialState.balances[shieldedToken().tag] ?? 0n; + log.info("initialState " + safeStringifyProgress(initialState)); + const syncTimeoutMs = resolveWalletSyncTimeoutMs(); + if (balance === 0n) { + const skipWait = + Deno.env.get("MIDNIGHT_SKIP_WAIT_FOR_FUNDS")?.toLowerCase() === "true"; + log.info("Wallet shielded balance: 0"); + log.info( + `Waiting to receive tokens... (timeout ${syncTimeoutMs}ms${ + skipWait ? ", skip on timeout enabled" : "" + })`, + ); + try { + const { shieldedBalance, unshieldedBalance } = await syncAndWaitForFunds( + result.wallet, + ); + balance = shieldedBalance; + if (unshieldedBalance > 0n) { + log.info(`Unshielded balance available: ${unshieldedBalance}`); + } + } catch (e) { + if (skipWait) { + log.warn( + `Skipping wait for shielded funds after timeout: ${ + (e as Error).message + }`, + ); + } else { + throw e; + } + } } + log.info(`Wallet balance: ${balance}`); + + return result; } catch (e) { - if (skipWait) { + const tag = (e as any)?._tag; + const msg = e instanceof Error ? e.message : String(e); + const isWalletSyncError = tag === "Wallet.Sync" || msg.includes("Wallet.Sync"); + const isConnectionError = msg.includes("ErrorEvent") || + msg.includes("WebSocket") || msg.includes("ECONNREFUSED") || + msg.includes("[object ErrorEvent]"); + + if ((isWalletSyncError || isConnectionError) && attempt < maxRetries) { + // Clean up the failed wallet before retrying + if (result) { + try { await result.wallet.stop(); } catch { /* ignore cleanup errors */ } + } + const delayMs = baseDelayMs * attempt; log.warn( - `Skipping wait for shielded funds after timeout: ${ - (e as Error).message - }`, + `Wallet sync failed (attempt ${attempt}/${maxRetries}), retrying in ${delayMs}ms: ${String(e)}`, ); - } else { - throw e; + await new Promise((r) => setTimeout(r, delayMs)); + continue; } + throw e; } } - log.info(`Wallet balance: ${balance}`); - - return result; + throw new Error("buildWalletAndWaitForFunds: unreachable"); } async function ensureDustBalance(walletResult: WalletResult): Promise { @@ -505,10 +535,48 @@ async function submitInsertVerifierKeyTxLocal( ); const finalizedTx = await walletResult.wallet.finalizeRecipe(signedRecipe); - const txId = await walletResult.wallet.submitTransaction(finalizedTx); + const txId = await submitWithRetry(walletResult.wallet, finalizedTx); return await providers.publicDataProvider.watchForTxData(txId); } +/** + * Submit a finalized transaction with retry on WS disconnect. + * + * The Midnight wallet SDK's PolkadotNodeClient disconnects after each + * operation by design. This can race with the submitAndWatchExtrinsic + * callback, causing a "Transaction submission error" with WS close code + * 1000 (Normal Closure). Retrying typically succeeds on the next connection. + */ +async function submitWithRetry( + wallet: WalletFacade, + // deno-lint-ignore no-explicit-any + tx: any, + maxAttempts = 3, + delayMs = 5_000, + // deno-lint-ignore no-explicit-any +): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await wallet.submitTransaction(tx); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + const isDisconnect = msg.includes("submission error") || + msg.includes("disconnected") || + msg.includes("Normal Closure"); + + if (isDisconnect && attempt < maxAttempts) { + log.warn( + `Submit attempt ${attempt}/${maxAttempts} failed (WS disconnect). Retrying in ${delayMs}ms...`, + ); + await new Promise((r) => setTimeout(r, delayMs)); + continue; + } + throw error; + } + } + throw new Error("submitWithRetry: unreachable"); +} + async function deployWithLimitedVerifierKeys( providers: ReturnType, // deno-lint-ignore no-explicit-any @@ -522,14 +590,29 @@ async function deployWithLimitedVerifierKeys( // We deploy with NO verifier keys (stripped contract state), then insert // each VK individually via submitInsertVerifierKeyTx after deployment. // - // Steps: - // 1. Run ContractExecutable.initialize() with the real zkConfigProvider - // (so VKs are validated and the contract state is correctly initialized) - // 2. Convert the resulting contract state to ledger format - // 3. Create a STRIPPED copy with only data + maintenanceAuthority (no operations) - // 4. Build the deploy transaction from the stripped state - // 5. After deployment, insert all VKs individually + // Deployment state is persisted to deployment-state.json so the process + // can resume after a crash (e.g., mid-VK-insert). + + const stateFilePath = path.join(Deno.cwd(), "deployment-state.json"); + let deploymentState: { contractAddress: string; deployedCircuits: string[] } = { + contractAddress: "", + deployedCircuits: [], + }; + + try { + const content = await Deno.readTextFile(stateFilePath); + deploymentState = JSON.parse(content); + log.info( + `Found existing deployment state. Resuming deployment for contract: ${deploymentState.contractAddress}`, + ); + } catch (_error) { + // No existing state, start fresh + } + let contractAddress = deploymentState.contractAddress; + + // Always run local initialization to get circuit IDs and contract state. + // This is purely local (no network), so it's fast even on resume. const signingKey = sampleSigningKey(); const coinPublicKey = parseCoinPublicKeyToHex( @@ -537,7 +620,6 @@ async function deployWithLimitedVerifierKeys( getNetworkId(), ); - // Step 1: Initialize the contract with the real provider to get valid state const contractExec = ContractExecutable.make(compiledContract); const contractRuntime = makeContractExecutableRuntime( providers.zkConfigProvider, @@ -573,109 +655,119 @@ async function deployWithLimitedVerifierKeys( private: { privateState, signingKey: derivedSigningKey }, } = initResult; - // Step 2: Convert compact-runtime ContractState to ledger ContractState + // Convert compact-runtime ContractState to ledger ContractState const fullLedgerState = LedgerContractState.deserialize( fullContractState.serialize(), ); - // Log all operations (circuits) found in the initialized state const allOperationIds = fullLedgerState.operations() as string[]; log.info( `Contract initialized with ${allOperationIds.length} operations: ${allOperationIds.join(", ")}`, ); - // Step 3: Create a stripped ContractState with NO operations (no VKs) - const strippedState = new LedgerContractState(); - strippedState.data = fullLedgerState.data; - strippedState.maintenanceAuthority = fullLedgerState.maintenanceAuthority; - // Deliberately skip copying operations — this is the key to reducing tx size - - log.info("Created stripped contract state (no operations/verifier keys)"); - - // Step 4: Build the deploy transaction - const contractDeploy = new ContractDeploy(strippedState); - const contractAddress = contractDeploy.address; - - // Build the unproven transaction with Intent containing the deploy - const intent = Intent.new(createTtl()).addDeploy(contractDeploy); - const unprovenTx = Transaction.fromParts( - getNetworkId(), - undefined, // no guaranteed zswap offer needed for a stripped deploy - undefined, // no fallible offer - intent, - ); + if (!contractAddress) { + // Step 3: Create a stripped ContractState with NO operations (no VKs) + const strippedState = new LedgerContractState(); + strippedState.data = fullLedgerState.data; + strippedState.maintenanceAuthority = fullLedgerState.maintenanceAuthority; + // Deliberately skip copying operations — this is the key to reducing tx size + + log.info("Created stripped contract state (no operations/verifier keys)"); + + // Step 4: Build the deploy transaction + const contractDeploy = new ContractDeploy(strippedState); + contractAddress = contractDeploy.address; + + // Build the unproven transaction with Intent containing the deploy + const intent = Intent.new(createTtl()).addDeploy(contractDeploy); + const unprovenTx = Transaction.fromParts( + getNetworkId(), + undefined, // no guaranteed zswap offer needed for a stripped deploy + undefined, // no fallible offer + intent, + ); - log.info(`Deploy tx built for contract address: ${contractAddress}`); + log.info(`Deploy tx built for contract address: ${contractAddress}`); + + // Step 5: Balance, sign, finalize, and submit the deploy transaction. + // We bypass submitTx (which calls proveTx first) because our stripped deploy + // has no circuit calls — there are no proofs to generate. + const { wallet, walletZswapSecretKeys, walletDustSecretKey, unshieldedKeystore } = walletResult; + const balanceSecretKeys = { + shieldedSecretKeys: walletZswapSecretKeys, + dustSecretKey: walletDustSecretKey, + }; + + log.info("Balancing deploy transaction (unproven)..."); + const recipe = await wallet.balanceUnprovenTransaction( + unprovenTx, + balanceSecretKeys, + { ttl: createTtl() }, + ); - // Step 5: Balance, sign, finalize, and submit the deploy transaction. - // We bypass submitTx (which calls proveTx first) because our stripped deploy - // has no circuit calls — there are no proofs to generate. The proof provider - // would convert PreProof markers to Proof markers, causing the wallet to fail - // when trying to deserialize the intent. - const { wallet, walletZswapSecretKeys, walletDustSecretKey, unshieldedKeystore } = walletResult; - const balanceSecretKeys = { - shieldedSecretKeys: walletZswapSecretKeys, - dustSecretKey: walletDustSecretKey, - }; + const signedRecipe = await wallet.signRecipe( + recipe, + (payload) => unshieldedKeystore.signData(payload), + ); - log.info("Balancing deploy transaction (unproven)..."); - const recipe = await wallet.balanceUnprovenTransaction( - unprovenTx, - balanceSecretKeys, - { ttl: createTtl() }, - ); + const finalizedTx = await wallet.finalizeRecipe(signedRecipe); - const signedRecipe = await wallet.signRecipe( - recipe, - (payload) => unshieldedKeystore.signData(payload), - ); + log.info("Submitting deploy transaction..."); + const txId = await submitWithRetry(wallet, finalizedTx); + log.info(`Deploy transaction submitted, txId: ${txId}`); - const finalizedTx = await wallet.finalizeRecipe(signedRecipe); + // Wait for the transaction to be finalized on-chain + const finalizedTxData = await providers.publicDataProvider.watchForTxData(txId); + if (finalizedTxData.status !== SucceedEntirely) { + throw new Error( + `Deployment failed with status ${finalizedTxData.status}`, + ); + } - log.info("Submitting deploy transaction..."); - const txId = await wallet.submitTransaction(finalizedTx); - log.info(`Deploy transaction submitted, txId: ${txId}`); + log.info("Deploy transaction finalized on-chain."); - // Wait for the transaction to be finalized on-chain - const finalizedTxData = await providers.publicDataProvider.watchForTxData(txId); - if (finalizedTxData.status !== SucceedEntirely) { - throw new Error( - `Deployment failed with status ${finalizedTxData.status}`, + // Save deployment state so we can resume VK insertion if interrupted + deploymentState.contractAddress = contractAddress; + await Deno.writeTextFile( + stateFilePath, + JSON.stringify(deploymentState, null, 2), ); - } - - log.info("Deploy transaction finalized on-chain."); - // Scope the private state provider to this contract address (required in 3.2.0) - providers.privateStateProvider.setContractAddress(contractAddress); + // Scope the private state provider to this contract address (required in 3.2.0) + providers.privateStateProvider.setContractAddress(contractAddress); - // Save private state and signing key - if (config.privateStateId) { - await providers.privateStateProvider.set( - config.privateStateId, - privateState, + // Save private state and signing key + if (config.privateStateId) { + await providers.privateStateProvider.set( + config.privateStateId, + privateState, + ); + } + await providers.privateStateProvider.setSigningKey( + contractAddress, + derivedSigningKey, ); } - await providers.privateStateProvider.setSigningKey( - contractAddress, - derivedSigningKey, - ); // Step 6: Insert all verifier keys individually if (!resolveSkipInsertRemainingVks()) { - // Collect all verifier keys from the real zkConfigProvider const allVerifierKeys = await providers.zkConfigProvider.getVerifierKeys( allOperationIds as any, ); log.info( - `Inserting ${allVerifierKeys.length} verifier keys individually...`, + `Inserting verifier keys (${deploymentState.deployedCircuits.length} already done)...`, ); const VK_INSERT_MAX_RETRIES = 3; const VK_INSERT_RETRY_DELAY_MS = 10_000; const VK_INSERT_PAUSE_MS = 2_000; for (const [circuitId, verifierKey] of allVerifierKeys) { + if (deploymentState.deployedCircuits.includes(circuitId as string)) { + log.info(`Skipping already deployed circuit: ${circuitId}`); + continue; + } + log.info(`Inserting verifier key for circuit: ${circuitId}`); let lastError: unknown; @@ -715,10 +807,24 @@ async function deployWithLimitedVerifierKeys( throw lastError; } + // Track deployed circuit and persist state + deploymentState.deployedCircuits.push(circuitId as string); + await Deno.writeTextFile( + stateFilePath, + JSON.stringify(deploymentState, null, 2), + ); + // Brief pause between VK inserts to let the dust wallet state refresh await new Promise((r) => setTimeout(r, VK_INSERT_PAUSE_MS)); } log.info("All verifier keys inserted successfully."); + + // Clean up deployment state file on success + try { + await Deno.remove(stateFilePath); + } catch { + // Ignore if already removed + } } else { log.warn( "Skipping verifier key insertion (MIDNIGHT_DEPLOY_SKIP_INSERT_REMAINING_VKS=true)", diff --git a/packages/shared/data-types/deno.json b/packages/shared/data-types/deno.json index 9a86fde..fe6041c 100644 --- a/packages/shared/data-types/deno.json +++ b/packages/shared/data-types/deno.json @@ -9,11 +9,11 @@ "./types": "./src/types.ts" }, "imports": { - "@paimaexample/concise": "jsr:@paimaexample/concise@0.9.0", - "@paimaexample/config": "jsr:@paimaexample/config@0.9.0", - "@paimaexample/utils": "jsr:@paimaexample/utils@0.9.0", - "@paimaexample/db": "jsr:@paimaexample/db@0.9.0", - "@paimaexample/sm": "jsr:@paimaexample/sm@0.9.0", + "@paimaexample/concise": "jsr:@paimaexample/concise@0.10.24", + "@paimaexample/config": "jsr:@paimaexample/config@0.10.24", + "@paimaexample/utils": "jsr:@paimaexample/utils@0.10.24", + "@paimaexample/db": "jsr:@paimaexample/db@0.10.24", + "@paimaexample/sm": "jsr:@paimaexample/sm@0.10.24", "@sinclair/typebox": "npm:@sinclair/typebox@0.34.41", "viem": "npm:viem@2.37.3", "viem/chains": "npm:viem@2.37.3/chains" From e26452d28641eb875f9720cd494140fbcfbe231e Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 17:33:21 -0400 Subject: [PATCH 02/78] Continue cleanup --- .../batcher/src/adapter-effectstreaml2.ts | 2 +- packages/client/node/src/api.ts | 757 +----------------- .../contracts/evm/contracts/GoFishLobby.sol | 236 ------ packages/shared/contracts/evm/deploy.ts | 2 +- .../shared/contracts/evm/forge-to-hardhat.ts | 1 - .../artifacts/GoFishModule#GoFishLobby.json | 476 ----------- .../GoFishModule#PaimaL2Contract.json | 169 ++-- ...15f593671aebede96af01bc3114417d98eab0.json | 47 ++ .../build-info/stub-build-info.json | 27 - .../chain-31337/deployed_addresses.json | 3 +- .../deployments/chain-31337/journal.jsonl | 16 +- .../evm/ignition/modules/GoFishLobby.ts | 4 +- packages/shared/data-types/src/config.dev.ts | 19 +- 13 files changed, 166 insertions(+), 1593 deletions(-) delete mode 100644 packages/shared/contracts/evm/contracts/GoFishLobby.sol delete mode 100644 packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#GoFishLobby.json create mode 100644 packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0.json delete mode 100644 packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/stub-build-info.json diff --git a/packages/client/batcher/src/adapter-effectstreaml2.ts b/packages/client/batcher/src/adapter-effectstreaml2.ts index 0902043..5e78efe 100644 --- a/packages/client/batcher/src/adapter-effectstreaml2.ts +++ b/packages/client/batcher/src/adapter-effectstreaml2.ts @@ -21,7 +21,7 @@ if (isMainnet) { // PaimaL2Contract address from deployment -const paimaL2Address = contractAddressesEvmMain()[chainNameId]["effectstreaml2Module#effectstreaml2"] as `0x${string}`; +const paimaL2Address = contractAddressesEvmMain()[chainNameId]["GoFishModule#PaimaL2Contract"] as `0x${string}`; if (!paimaL2Address) { throw new Error("EffectstreamL2 address not found"); } diff --git a/packages/client/node/src/api.ts b/packages/client/node/src/api.ts index 7513eb0..8d014b2 100644 --- a/packages/client/node/src/api.ts +++ b/packages/client/node/src/api.ts @@ -7,246 +7,15 @@ import type { StartConfigApiRouter } from "@paimaexample/runtime"; import type { Pool } from "pg"; import { getGameState as getMidnightGameState, - queryHasMaskApplied, - queryHasDealt, } from "./midnight-query.ts"; import { - markMaskApplied, - markDealtComplete, isValidLobbyId, - queryOnChainSetupStatuses, } from "./midnight-onchain.ts"; -import { - getPlayerHand as getMidnightPlayerHand, - getPlayerHandWithSecret as getMidnightPlayerHandWithSecret, - ensureGameReplayedIfNeeded as midnightEnsureGameReplayedIfNeeded, - askForCard as midnightAskForCard, - goFish as midnightGoFish, - applyMask as midnightApplyMask, - dealCards as midnightDealCards, - respondToAsk as midnightRespondToAsk, - afterGoFish as midnightAfterGoFish, - skipDrawDeckEmpty as midnightSkipDrawDeckEmpty, - getStoredPlayerSecret, - getStoredShuffleSeed, - storePlayerSecret, -} from "./midnight-actions.ts"; -import { calculateAndPersistScores } from "./leaderboard.ts"; + // Database connection pool - set by apiRouter from the runtime-provided connection let dbPool: Pool | null = null; -// Rank names for display — must match the Rank type in go-fish-types.ts -const RANK_NAMES = ['A', '2', '3', '4', '5', '6', '7'] as const; - -/** - * Parse and validate a player_id parameter from an API request. - * Returns 1 | 2 or null (caller must return 400 when null). - */ -function parsePlayerId(raw: unknown): 1 | 2 | null { - const n = parseInt(String(raw), 10); - return n === 1 || n === 2 ? n : null; -} - -/** - * Persistent game log storage - maintains full log history per game - * Key: lobbyId, Value: { logs: string[], lastState: state snapshot } - */ -interface GameLogState { - logs: string[]; - lastPhase: string | null; - lastTurn: number | null; - lastAskedRank: number | null; - lastAskingPlayer: number | null; - lastScores: [number, number]; - playerNames: [string, string]; -} - -const gameLogStorage = new Map(); - -// Tracks lobbies where leaderboard scores have already been persisted. -// Prevents double-counting when multiple game_state polls see phase="finished". -const leaderboardProcessed = new Set(); - -/** - * Get or initialize game log state - */ -function getGameLogState(lobbyId: string, players: Array<{ player_name: string }>): GameLogState { - let state = gameLogStorage.get(lobbyId); - if (!state) { - const player1Name = players[0]?.player_name || 'Player 1'; - const player2Name = players[1]?.player_name || 'Player 2'; - state = { - logs: ['Game started'], - lastPhase: null, - lastTurn: null, - lastAskedRank: null, - lastAskingPlayer: null, - lastScores: [0, 0], - playerNames: [player1Name, player2Name], - }; - gameLogStorage.set(lobbyId, state); - } - return state; -} - -/** - * Update game log based on state changes (appends new entries) - */ -function updateGameLog( - lobbyId: string, - midnightState: { - phase: string; - currentTurn: number; - scores: [number, number]; - lastAskedRank: number | null; - lastAskingPlayer: number | null; - isGameOver: boolean; - }, - players: Array<{ account_id: number; player_name: string }> -): string[] { - const state = getGameLogState(lobbyId, players); - const [player1Name, player2Name] = state.playerNames; - const getPlayerName = (id: number) => id === 1 ? player1Name : player2Name; - const getOpponentName = (id: number) => id === 1 ? player2Name : player1Name; - - // Detect state changes and append appropriate log entries - const phaseChanged = state.lastPhase !== midnightState.phase; - const turnChanged = state.lastTurn !== midnightState.currentTurn; - const askChanged = state.lastAskedRank !== midnightState.lastAskedRank || - state.lastAskingPlayer !== midnightState.lastAskingPlayer; - - // Handle phase transitions - if (phaseChanged || askChanged) { - const askerName = midnightState.lastAskingPlayer ? getPlayerName(midnightState.lastAskingPlayer) : null; - const targetName = midnightState.lastAskingPlayer ? getOpponentName(midnightState.lastAskingPlayer) : null; - const rankName = midnightState.lastAskedRank !== null - ? RANK_NAMES[midnightState.lastAskedRank] || `rank ${midnightState.lastAskedRank}` - : null; - - // Log new ask action - if (askChanged && midnightState.lastAskedRank !== null && midnightState.phase === 'wait_response') { - state.logs.push(`${askerName} asked ${targetName} for ${rankName}s`); - } - - // Log response/go fish events based on phase transitions - if (phaseChanged && state.lastPhase === 'wait_response') { - if (midnightState.phase === 'wait_transfer') { - state.logs.push(`${targetName} has ${rankName}s!`); - } else if (midnightState.phase === 'wait_draw') { - state.logs.push(`${targetName} says "Go Fish!"`); - } - } - - // Log draw event - if (phaseChanged && state.lastPhase === 'wait_draw' && midnightState.phase === 'wait_draw_check') { - state.logs.push(`${askerName} drew a card from the deck`); - } - - // Log transfer completion - if (phaseChanged && state.lastPhase === 'wait_transfer' && midnightState.phase === 'turn_start') { - const prevAskerName = state.lastAskingPlayer ? getPlayerName(state.lastAskingPlayer) : askerName; - const prevRankName = state.lastAskedRank !== null - ? RANK_NAMES[state.lastAskedRank] || `rank ${state.lastAskedRank}` - : rankName; - state.logs.push(`${prevAskerName} received ${prevRankName}s!`); - } - - // Log turn change after go fish - if (phaseChanged && state.lastPhase === 'wait_draw_check' && midnightState.phase === 'turn_start') { - const prevAskerName = state.lastAskingPlayer ? getPlayerName(state.lastAskingPlayer) : null; - if (turnChanged && prevAskerName) { - state.logs.push(`${prevAskerName} didn't get the requested card`); - } else if (!turnChanged && prevAskerName) { - state.logs.push(`${prevAskerName} got the requested card! Another turn!`); - } - } - - // Log turn changes - if (turnChanged && midnightState.phase === 'turn_start') { - state.logs.push(`${getPlayerName(midnightState.currentTurn)}'s turn`); - } - - // Log game over - if (midnightState.isGameOver && state.lastPhase !== 'finished' && midnightState.phase === 'finished') { - const winner = midnightState.scores[0] > midnightState.scores[1] ? player1Name : - midnightState.scores[1] > midnightState.scores[0] ? player2Name : 'Tie'; - state.logs.push(`Game Over! ${winner === 'Tie' ? "It's a tie!" : `${winner} wins!`}`); - state.logs.push(`Final scores: ${player1Name}: ${midnightState.scores[0]}, ${player2Name}: ${midnightState.scores[1]}`); - - // Fire-and-forget leaderboard scoring (deduplicated — only runs once per game) - if (dbPool && !leaderboardProcessed.has(lobbyId)) { - leaderboardProcessed.add(lobbyId); - const winnerPlayerId = midnightState.scores[0] > midnightState.scores[1] ? 1 - : midnightState.scores[1] > midnightState.scores[0] ? 2 : 0; - if (winnerPlayerId !== 0) { - calculateAndPersistScores(lobbyId, winnerPlayerId as 1 | 2, Date.now(), dbPool) - .catch(err => console.error('[leaderboard] Score persistence failed:', err)); - } - } - } - } - - // Log book completions (score changes) - if (state.lastScores) { - const player1ScoreDiff = midnightState.scores[0] - state.lastScores[0]; - const player2ScoreDiff = midnightState.scores[1] - state.lastScores[1]; - - if (player1ScoreDiff > 0) { - state.logs.push(`📚 ${player1Name} completed a book! (${midnightState.scores[0]} total)`); - } - if (player2ScoreDiff > 0) { - state.logs.push(`📚 ${player2Name} completed a book! (${midnightState.scores[1]} total)`); - } - } - - // Update tracked state - state.lastPhase = midnightState.phase; - state.lastTurn = midnightState.currentTurn; - state.lastAskedRank = midnightState.lastAskedRank; - state.lastAskingPlayer = midnightState.lastAskingPlayer; - state.lastScores = [...midnightState.scores] as [number, number]; - - // Note: No limit on log size - a typical Go Fish game has ~100-200 log entries - // which is negligible memory usage. The log is cleared when the game ends. - - return [...state.logs]; // Return a copy -} - -/** - * Clear game log (call when game ends or lobby is deleted) - */ -export function clearGameLog(lobbyId: string): void { - gameLogStorage.delete(lobbyId); -} - -// Check if we're using the TypeScript-compiled contract (mock mode) -// First check env var, then fall back to runtime config file written by orchestrator -function getUseTypescriptContract(): boolean { - // Check env var first - const envValue = Deno.env.get("USE_TYPESCRIPT_CONTRACT"); - if (envValue !== undefined) { - const result = envValue === "true"; - console.log(`[API] USE_TYPESCRIPT_CONTRACT from env: "${envValue}" -> mock mode: ${result}`); - return result; - } - - // Fall back to runtime config file (written by start.dev.ts) - try { - const configPath = new URL("../runtime-config.json", import.meta.url); - const configText = Deno.readTextFileSync(configPath); - const config = JSON.parse(configText); - const result = config.useTypescriptContract === true; - console.log(`[API] USE_TYPESCRIPT_CONTRACT from config file -> mock mode: ${result}`); - return result; - } catch { - // Config file doesn't exist or is invalid - default to production mode - console.log(`[API] USE_TYPESCRIPT_CONTRACT: no env or config file -> mock mode: false (production default)`); - return false; - } -} - -const USE_TYPESCRIPT_CONTRACT = getUseTypescriptContract(); export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, dbConn: Pool) => { // Use the runtime-provided database connection (works with PGLite in dev mode) @@ -303,46 +72,6 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d } ); - /** - * Config endpoint - tells frontend which mode we're running in - * USE_TYPESCRIPT_CONTRACT=true: Mock mode, only EVM wallet needed - * USE_TYPESCRIPT_CONTRACT=false: Production mode, both EVM and Lace wallets needed - */ - server.get("/api/config", async (request, reply) => { - return { - useMockedMidnight: USE_TYPESCRIPT_CONTRACT, - requiresLaceWallet: !USE_TYPESCRIPT_CONTRACT, - requiresEvmWallet: true, // Always need EVM for Paima Engine - }; - }); - - /** - * Get deployed contract address (for on-chain mode) - * Returns the Midnight contract address if a deployment file exists - */ - server.get("/api/midnight/contract_address", async (request, reply) => { - try { - // Try to read the deployment file - const deploymentPath = new URL( - "../../../shared/contracts/midnight/contract-go-fish.undeployed.json", - import.meta.url - ); - const deploymentText = await Deno.readTextFile(deploymentPath); - const deployment = JSON.parse(deploymentText); - return { - contractAddress: deployment.contractAddress || null, - networkId: deployment.networkId || "undeployed", - }; - } catch { - // No deployment file exists yet - return { - contractAddress: null, - networkId: null, - message: "Contract not deployed. Run: deno task midnight:deploy", - }; - } - }); - /** * Get open lobbies (for lobby list) */ @@ -350,6 +79,9 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d const { page = 0, count = 10, wallet } = request.query as { page?: number; count?: number; wallet?: string }; const db = dbPool!; + if (!db) { + return reply.code(503).send({ error: 'Database not ready' }); + } const offset = page * count; try { @@ -448,6 +180,9 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d const { lobby_id } = request.query as { lobby_id: string }; const db = dbPool!; + if (!db) { + return reply.code(503).send({ error: 'Database not ready' }); + } try { // Get lobby info @@ -513,6 +248,9 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d } const db = dbPool!; + if (!db) { + return reply.code(503).send({ error: 'Database not ready' }); + } // Get lobby info to verify it's in_progress const lobbyResult = await db.query(` @@ -616,482 +354,11 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d myBooks: [], // Dynamic game log - persisted across state changes - gameLog: updateGameLog(lobby_id, midnightState, players), - }; - }); - - /** - * Midnight Actions API - Backend proxy for Midnight contract calls - */ - - // Get the last asked rank for a lobby (public game state — no wallet required). - // Used by the frontend in batcher mode to determine hasCards for respondToAsk. - server.get("/api/midnight/last_asked_rank", async (request, reply) => { - const { lobby_id } = request.query as { lobby_id: string }; - if (!lobby_id || !isValidLobbyId(lobby_id)) { - return reply.code(400).send({ error: 'Missing or invalid lobby_id' }); - } - const midnightState = await getMidnightGameState(lobby_id); - return { lastAskedRank: midnightState.lastAskedRank ?? null }; - }); - - // Get player's decrypted hand - server.get("/debug/midnight/player_hand", async (request, reply) => { - const { lobby_id, player_id } = request.query as { lobby_id: string; player_id: string }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing lobby_id or player_id' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const hand = await getMidnightPlayerHand(lobby_id, playerId); - return { hand }; - }); - - // Get player's real hand using their secret key (batcher mode) - // The frontend passes its secret so the backend can run doesPlayerHaveSpecificCard - // with the correct witness. The secret is never persisted — used only for this call. - server.post("/debug/midnight/player_hand_with_secret", async (request, reply) => { - const { lobby_id, player_id, player_secret, shuffle_seed, opponent_secret, opponent_shuffle_seed } = request.body as { - lobby_id: string; - player_id: number; - player_secret: string; // hex-encoded bigint, no 0x prefix - shuffle_seed?: string; // hex-encoded 32 bytes — used for setup replay - opponent_secret?: string; // opponent's secret — used for setup replay - opponent_shuffle_seed?: string; // opponent's shuffle seed — used for setup replay - }; - - if (!lobby_id || !player_id || !player_secret) { - return reply.code(400).send({ error: 'Missing lobby_id, player_id, or player_secret' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - // If the local simulation doesn't have this game's state (e.g. node restarted), - // replay the setup sequence using the provided secrets before querying the hand. - // This is a no-op when the game is already in the actionContext. - await midnightEnsureGameReplayedIfNeeded( - lobby_id, - playerId, - player_secret, - shuffle_seed, - opponent_secret, - opponent_shuffle_seed, - ); - - const hand = await getMidnightPlayerHandWithSecret(lobby_id, playerId, player_secret, opponent_secret); - return { hand }; - }); - - // Ask for card action - server.post("/debug/midnight/ask_for_card", async (request, reply) => { - const { lobby_id, player_id, rank } = request.body as { - lobby_id: string; - player_id: number; - rank: number; - }; - - if (!lobby_id || !player_id || rank === undefined) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightAskForCard(lobby_id, playerId, rank); - return result; - }); - - // Go Fish action - server.post("/debug/midnight/go_fish", async (request, reply) => { - const { lobby_id, player_id } = request.body as { - lobby_id: string; - player_id: number; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightGoFish(lobby_id, playerId); - return result; - }); - - // Apply Mask action (setup phase) - server.post("/debug/midnight/apply_mask", async (request, reply) => { - const { lobby_id, player_id, player_secret, shuffle_seed } = request.body as { - lobby_id: string; - player_id: number; - player_secret?: string; - shuffle_seed?: string; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightApplyMask(lobby_id, playerId, player_secret, shuffle_seed); - return result; - }); - - // Deal Cards action (setup phase) - server.post("/debug/midnight/deal_cards", async (request, reply) => { - const { lobby_id, player_id, player_secret, shuffle_seed } = request.body as { - lobby_id: string; - player_id: number; - player_secret?: string; - shuffle_seed?: string; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightDealCards(lobby_id, playerId, player_secret, shuffle_seed); - return result; - }); - - // Respond to ask action (opponent responds to card request) - server.post("/debug/midnight/respond_to_ask", async (request, reply) => { - const { lobby_id, player_id } = request.body as { - lobby_id: string; - player_id: number; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightRespondToAsk(lobby_id, playerId); - return result; - }); - - // After Go Fish action (complete the draw turn) - server.post("/debug/midnight/after_go_fish", async (request, reply) => { - const { lobby_id, player_id, drew_requested_card } = request.body as { - lobby_id: string; - player_id: number; - drew_requested_card?: boolean; // V3 contract no longer needs this — kept for backwards compat - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightAfterGoFish(lobby_id, playerId, drew_requested_card); - return result; - }); - - // Skip draw when deck is empty - ends turn without drawing - server.post("/debug/midnight/skip_draw_deck_empty", async (request, reply) => { - const { lobby_id, player_id } = request.body as { - lobby_id: string; - player_id: number; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const result = await midnightSkipDrawDeckEmpty(lobby_id, playerId); - return result; - }); - - // Check setup status (for automatic setup coordination) - server.get("/api/midnight/setup_status", async (request, reply) => { - const { lobby_id, player_id } = request.query as { - lobby_id: string; - player_id: string; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - let hasMaskApplied = await queryHasMaskApplied(lobby_id, playerId); - let hasDealt = await queryHasDealt(lobby_id, playerId); - - // Also check opponent's status for coordination - const opponentId = (playerId === 1 ? 2 : 1) as 1 | 2; - let opponentHasMaskApplied = await queryHasMaskApplied(lobby_id, opponentId); - let opponentHasDealt = await queryHasDealt(lobby_id, opponentId); - - // Always cross-check against the real on-chain state via the batcher query. - // This handles the case where notify_setup was missed (e.g. browser timing, network failure). - // Run in parallel with the map reads above, so latency is hidden. - { - const onChainSetup = await queryOnChainSetupStatuses(lobby_id); - if (onChainSetup) { - const [p1Mask, p2Mask] = onChainSetup.maskApplied; - const [p1Dealt, p2Dealt] = onChainSetup.hasDealt; - - // Back-populate setupStateMap for any player whose on-chain state is ahead of local map - if (p1Mask && !await queryHasMaskApplied(lobby_id, 1)) { - markMaskApplied(lobby_id, 1); - console.log(`[API] setup_status: back-populated maskApplied for lobby=${lobby_id} player=1 from on-chain`); - } - if (p2Mask && !await queryHasMaskApplied(lobby_id, 2)) { - markMaskApplied(lobby_id, 2); - console.log(`[API] setup_status: back-populated maskApplied for lobby=${lobby_id} player=2 from on-chain`); - } - if (p1Dealt && !await queryHasDealt(lobby_id, 1)) { - markDealtComplete(lobby_id, 1); - console.log(`[API] setup_status: back-populated hasDealt for lobby=${lobby_id} player=1 from on-chain`); - } - if (p2Dealt && !await queryHasDealt(lobby_id, 2)) { - markDealtComplete(lobby_id, 2); - console.log(`[API] setup_status: back-populated hasDealt for lobby=${lobby_id} player=2 from on-chain`); - } - - // Re-read from map after potential updates - hasMaskApplied = await queryHasMaskApplied(lobby_id, playerId); - hasDealt = await queryHasDealt(lobby_id, playerId); - opponentHasMaskApplied = await queryHasMaskApplied(lobby_id, opponentId); - opponentHasDealt = await queryHasDealt(lobby_id, opponentId); - } - } - - console.log(`[API] setup_status response: lobby=${lobby_id} player=${playerId} mask=${hasMaskApplied} dealt=${hasDealt} oppMask=${opponentHasMaskApplied} oppDealt=${opponentHasDealt}`); - return { - hasMaskApplied, - hasDealt, - opponentHasMaskApplied, - opponentHasDealt, - }; - }); - - // Get a stored player secret — called by the batcher adapter to retrieve the opponent's - // secret for circuits that require both players' secrets (askForCard, respondToAsk, etc.). - // The secret is only available after notify_setup has been processed for this player. - // This endpoint is intentionally NOT authenticated — it's only reachable from localhost - // (the batcher runs on the same host as the node). - server.get("/api/midnight/player_secret", async (request, reply) => { - const { lobby_id, player_id } = request.query as { - lobby_id: string; - player_id: string; - }; - - if (!lobby_id || !player_id) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - const secret = getStoredPlayerSecret(lobby_id, playerId); - const shuffleSeed = getStoredShuffleSeed(lobby_id, playerId); - - if (secret === null) { - return reply.code(404).send({ error: 'No secret stored for this player/game' }); - } - - console.log(`[API] player_secret: returning secret for lobby=${lobby_id} player=${playerId} secret=0x${secret.slice(0, 16)}...`); - return { secret, shuffleSeed }; - }); - - // Register (or refresh) a player's secret outside of setup replay. - // The frontend calls this on game reconnect / page load so the backend always has - // the latest secrets for batcher-side proof generation, even after a node restart. - server.post("/api/midnight/register_secret", async (request, reply) => { - const { lobby_id, player_id, player_secret, shuffle_seed } = request.body as { - lobby_id: string; - player_id: number; - player_secret: string; // hex-encoded bigint, no 0x prefix - shuffle_seed?: string; // hex-encoded 32 bytes, no 0x prefix + // gameLog: updateGameLog(lobby_id, midnightState, players), }; - - if (!lobby_id || !player_id || !player_secret) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - // Store directly in the persistent map (no circuit execution). - storePlayerSecret(lobby_id, playerId, player_secret, shuffle_seed); - - console.log(`[API] register_secret: stored secret for lobby=${lobby_id} player=${playerId}`); - return { success: true }; }); - // Register a player's Midnight shielded address for leaderboard attribution. - // Called by the frontend on game start so scores can be persisted at game end. - server.post("/api/midnight/register_address", async (request, reply) => { - const { lobby_id, player_id, midnight_address } = request.body as { - lobby_id: string; - player_id: number; - midnight_address: string; - }; - - if (!lobby_id || !player_id || !midnight_address) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - if (!isValidLobbyId(lobby_id)) { - return reply.code(400).send({ error: 'Invalid lobby_id format' }); - } - - // Midnight shielded addresses are hex strings, 64–200 chars - if (!/^[0-9a-fA-F]{64,200}$/.test(midnight_address)) { - return reply.code(400).send({ error: 'Invalid midnight_address format' }); - } - - const db = dbPool!; - - // Determine which account_id corresponds to this player (by join order) - const playersResult = await db.query<{ account_id: number }>( - `SELECT account_id FROM lobby_players WHERE lobby_id = $1 ORDER BY joined_at ASC`, - [lobby_id] - ); - const target = playersResult.rows[playerId - 1]; // player 1 = index 0 - if (!target) { - return reply.code(404).send({ error: 'Player not found in lobby' }); - } - - await db.query( - `UPDATE lobby_players SET midnight_address = $1 WHERE lobby_id = $2 AND account_id = $3`, - [midnight_address, lobby_id, target.account_id] - ); - - console.log(`[API] register_address: lobby=${lobby_id} player=${playerId} addr=${midnight_address.slice(0, 16)}…`); - return { success: true }; - }); - - // Notify setup complete (called by batcher after on-chain transaction succeeds). - // When player_secret is included, also replays the circuit on the local actionContract - // so the node's in-memory state stays in sync with the real Midnight chain state. - server.post("/api/midnight/notify_setup", async (request, reply) => { - console.log(`[API] notify_setup RAW body: player_id=${(request.body as any)?.player_id} action=${(request.body as any)?.action} lobby=${(request.body as any)?.lobby_id}`); - const { lobby_id, player_id, action, player_secret, shuffle_seed, opponent_secret, opponent_shuffle_seed } = request.body as { - lobby_id: string; - player_id: number; - action: "mask_applied" | "dealt_complete"; - player_secret?: string; // hex-encoded bigint, no 0x prefix - shuffle_seed?: string; // hex-encoded 32 bytes, no 0x prefix - opponent_secret?: string; // hex-encoded bigint, no 0x prefix - opponent_shuffle_seed?: string; // hex-encoded 32 bytes, no 0x prefix - }; - - if (!lobby_id || !player_id || !action) { - return reply.code(400).send({ error: 'Missing required fields' }); - } - - const playerId = parsePlayerId(player_id); - if (!playerId) { - return reply.code(400).send({ error: 'Invalid player_id (must be 1 or 2)' }); - } - - if (action !== "mask_applied" && action !== "dealt_complete") { - return reply.code(400).send({ error: 'Invalid action' }); - } - - const opponentId = (playerId === 1 ? 2 : 1) as 1 | 2; - - // Eagerly store all secrets into persistentSecrets BEFORE the async replay so that - // fetchSecretFromBackend (called by the batcher for game-phase circuits) can find them - // immediately, even if the circuit replay queue hasn't started yet. - if (player_secret) storePlayerSecret(lobby_id, playerId, player_secret, shuffle_seed); - if (opponent_secret) storePlayerSecret(lobby_id, opponentId, opponent_secret, opponent_shuffle_seed); - - // Update local state tracking - if (action === "mask_applied") { - markMaskApplied(lobby_id, playerId); - console.log(`[API] notify_setup: setupStateMap now has mask=true for lobby=${lobby_id} player=${playerId}`); - // Replay applyMask on local actionContract so getPlayerHandWithSecret works later. - // IMPORTANT: the contract requires P1 to act before P2. Always replay P1 first. - // Pass shuffle seeds so the local simulation's shuffle matches the on-chain transaction. - const p1Secret = playerId === 1 ? player_secret : opponent_secret; - const p1Seed = playerId === 1 ? shuffle_seed : opponent_shuffle_seed; - const p2Secret = playerId === 2 ? player_secret : opponent_secret; - const p2Seed = playerId === 2 ? shuffle_seed : opponent_shuffle_seed; - const replayMasks = async () => { - for (const [pid, sec, seed] of [[1, p1Secret, p1Seed], [2, p2Secret, p2Seed]] as const) { - if (sec) { - await midnightApplyMask(lobby_id, pid, sec, seed).catch((err: Error) => { - console.warn(`[API] Local applyMask P${pid} replay failed (non-critical):`, err?.message); - }); - } - } - }; - replayMasks().catch(() => {}); - } else if (action === "dealt_complete") { - markDealtComplete(lobby_id, playerId); - // Replay dealCards on local actionContract so getPlayerHandWithSecret works later. - // Both player_secret AND shuffle_seed are required for the local replay to produce - // the same cardOwnership ledger as the real on-chain transaction. - // IMPORTANT: the contract requires P1 to deal before P2. Always replay P1 first. - const p1Secret = playerId === 1 ? player_secret : opponent_secret; - const p1Seed = playerId === 1 ? shuffle_seed : opponent_shuffle_seed; - const p2Secret = playerId === 2 ? player_secret : opponent_secret; - const p2Seed = playerId === 2 ? shuffle_seed : opponent_shuffle_seed; - const replayDeals = async () => { - for (const [pid, sec, seed] of [[1, p1Secret, p1Seed], [2, p2Secret, p2Seed]] as const) { - if (sec) { - await midnightDealCards(lobby_id, pid, sec, seed).catch((err: Error) => { - console.warn(`[API] Local dealCards P${pid} replay failed (non-critical):`, err?.message); - }); - } - } - }; - replayDeals().catch(() => {}); - } - - console.log(`[API] Setup notification received: ${action} for lobby ${lobby_id} player ${playerId}`); - return { success: true }; - }); + // ============================================================================ // PRC-6: Midnight dApp Integration API diff --git a/packages/shared/contracts/evm/contracts/GoFishLobby.sol b/packages/shared/contracts/evm/contracts/GoFishLobby.sol deleted file mode 100644 index 109321b..0000000 --- a/packages/shared/contracts/evm/contracts/GoFishLobby.sol +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -/** - * @title GoFishLobby - * @dev Manages game lobbies and matchmaking for Go Fish - */ -contract GoFishLobby { - struct Lobby { - uint256 id; - address host; - uint8 maxPlayers; - uint8 currentPlayers; - address[] players; - bool isActive; - uint256 createdAt; - } - - struct PlayerStats { - uint256 gamesPlayed; - uint256 gamesWon; - uint256 totalBooks; - uint256 rating; - } - - // State variables - uint256 private lobbyCounter; - mapping(uint256 => Lobby) public lobbies; - mapping(address => PlayerStats) public playerStats; - mapping(uint256 => mapping(address => bool)) public lobbyPlayers; - - // Active lobbies list - uint256[] public activeLobbies; - mapping(uint256 => uint256) private lobbyIndexInActive; - - // Events - event LobbyCreated(uint256 indexed lobbyId, address indexed host, uint8 maxPlayers); - event PlayerJoined(uint256 indexed lobbyId, address indexed player); - event PlayerLeft(uint256 indexed lobbyId, address indexed player); - event LobbyClosed(uint256 indexed lobbyId); - event GameStarted(uint256 indexed lobbyId); - event StatsUpdated(address indexed player, uint256 gamesPlayed, uint256 gamesWon); - - /** - * @dev Create a new lobby - * @param maxPlayers Maximum number of players (2-6) - */ - function createLobby(uint8 maxPlayers) external returns (uint256) { - require(maxPlayers >= 2 && maxPlayers <= 6, "Invalid player count"); - - lobbyCounter++; - uint256 lobbyId = lobbyCounter; - - address[] memory initialPlayers = new address[](1); - initialPlayers[0] = msg.sender; - - lobbies[lobbyId] = Lobby({ - id: lobbyId, - host: msg.sender, - maxPlayers: maxPlayers, - currentPlayers: 1, - players: initialPlayers, - isActive: true, - createdAt: block.timestamp - }); - - lobbyPlayers[lobbyId][msg.sender] = true; - - // Add to active lobbies - activeLobbies.push(lobbyId); - lobbyIndexInActive[lobbyId] = activeLobbies.length - 1; - - emit LobbyCreated(lobbyId, msg.sender, maxPlayers); - emit PlayerJoined(lobbyId, msg.sender); - - return lobbyId; - } - - /** - * @dev Join an existing lobby - * @param lobbyId ID of the lobby to join - */ - function joinLobby(uint256 lobbyId) external { - Lobby storage lobby = lobbies[lobbyId]; - - require(lobby.isActive, "Lobby not active"); - require(lobby.currentPlayers < lobby.maxPlayers, "Lobby full"); - require(!lobbyPlayers[lobbyId][msg.sender], "Already in lobby"); - - lobby.players.push(msg.sender); - lobby.currentPlayers++; - lobbyPlayers[lobbyId][msg.sender] = true; - - emit PlayerJoined(lobbyId, msg.sender); - } - - /** - * @dev Leave a lobby - * @param lobbyId ID of the lobby to leave - */ - function leaveLobby(uint256 lobbyId) external { - Lobby storage lobby = lobbies[lobbyId]; - - require(lobby.isActive, "Lobby not active"); - require(lobbyPlayers[lobbyId][msg.sender], "Not in lobby"); - - lobbyPlayers[lobbyId][msg.sender] = false; - lobby.currentPlayers--; - - // Remove player from array - for (uint256 i = 0; i < lobby.players.length; i++) { - if (lobby.players[i] == msg.sender) { - lobby.players[i] = lobby.players[lobby.players.length - 1]; - lobby.players.pop(); - break; - } - } - - emit PlayerLeft(lobbyId, msg.sender); - - // Close lobby if empty or host left - if (lobby.currentPlayers == 0 || msg.sender == lobby.host) { - closeLobby(lobbyId); - } - } - - /** - * @dev Start a game (host only) - * @param lobbyId ID of the lobby - */ - function startGame(uint256 lobbyId) external { - Lobby storage lobby = lobbies[lobbyId]; - - require(lobby.isActive, "Lobby not active"); - require(msg.sender == lobby.host, "Only host can start"); - require(lobby.currentPlayers >= 2, "Need at least 2 players"); - - lobby.isActive = false; - removeFromActiveLobbies(lobbyId); - - emit GameStarted(lobbyId); - } - - /** - * @dev Close a lobby - * @param lobbyId ID of the lobby to close - */ - function closeLobby(uint256 lobbyId) internal { - Lobby storage lobby = lobbies[lobbyId]; - lobby.isActive = false; - - removeFromActiveLobbies(lobbyId); - - emit LobbyClosed(lobbyId); - } - - /** - * @dev Remove lobby from active list - */ - function removeFromActiveLobbies(uint256 lobbyId) internal { - uint256 index = lobbyIndexInActive[lobbyId]; - uint256 lastIndex = activeLobbies.length - 1; - - if (index != lastIndex) { - uint256 lastLobbyId = activeLobbies[lastIndex]; - activeLobbies[index] = lastLobbyId; - lobbyIndexInActive[lastLobbyId] = index; - } - - activeLobbies.pop(); - delete lobbyIndexInActive[lobbyId]; - } - - /** - * @dev Update player stats after game ends - * @param player Address of the player - * @param won Whether the player won - * @param books Number of books collected - */ - function updatePlayerStats(address player, bool won, uint256 books) external { - // In production, this would be called by a trusted game contract - // For now, we'll allow anyone to call it (for testing) - - PlayerStats storage stats = playerStats[player]; - stats.gamesPlayed++; - stats.totalBooks += books; - - if (won) { - stats.gamesWon++; - stats.rating += 10; // Simple rating system - } else { - if (stats.rating > 5) { - stats.rating -= 5; - } - } - - emit StatsUpdated(player, stats.gamesPlayed, stats.gamesWon); - } - - // View functions - - /** - * @dev Get lobby details - */ - function getLobby(uint256 lobbyId) external view returns (Lobby memory) { - return lobbies[lobbyId]; - } - - /** - * @dev Get all active lobbies - */ - function getActiveLobbies() external view returns (uint256[] memory) { - return activeLobbies; - } - - /** - * @dev Get player stats - */ - function getPlayerStats(address player) external view returns (PlayerStats memory) { - return playerStats[player]; - } - - /** - * @dev Check if player is in lobby - */ - function isPlayerInLobby(uint256 lobbyId, address player) external view returns (bool) { - return lobbyPlayers[lobbyId][player]; - } - - /** - * @dev Get number of active lobbies - */ - function getActiveLobbiesCount() external view returns (uint256) { - return activeLobbies.length; - } -} diff --git a/packages/shared/contracts/evm/deploy.ts b/packages/shared/contracts/evm/deploy.ts index 46ee6c7..81cf52f 100644 --- a/packages/shared/contracts/evm/deploy.ts +++ b/packages/shared/contracts/evm/deploy.ts @@ -40,7 +40,7 @@ export async function deploy(): Promise { messages.push( `${deployment.module.id.substring(0, 16).padEnd(16)} @ ${ deployment.network.substring(0, 16).padEnd(16) - } deployed to ${result.paimaL2Contract.address} (PaimaL2) & ${result.goFishLobby.address} (GoFishLobby)`, + } deployed to ${result.paimaL2Contract.address} (PaimaL2)` ); } console.log("Deployed contracts:\n", messages.join("\n")); diff --git a/packages/shared/contracts/evm/forge-to-hardhat.ts b/packages/shared/contracts/evm/forge-to-hardhat.ts index 7f81ee3..8145159 100644 --- a/packages/shared/contracts/evm/forge-to-hardhat.ts +++ b/packages/shared/contracts/evm/forge-to-hardhat.ts @@ -12,7 +12,6 @@ await Deno.mkdir(`${__dirname}/build/artifacts/hardhat/build-info`, { recursive: const contracts = [ { name: "PaimaL2Contract", source: "contracts/PaimaL2Contract.sol" }, - { name: "GoFishLobby", source: "contracts/GoFishLobby.sol" }, ]; const buildInfoId = "stub-build-info"; diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#GoFishLobby.json b/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#GoFishLobby.json deleted file mode 100644 index 8a01278..0000000 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#GoFishLobby.json +++ /dev/null @@ -1,476 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "GoFishLobby", - "sourceName": "contracts/GoFishLobby.sol", - "abi": [ - { - "type": "function", - "name": "activeLobbies", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "createLobby", - "inputs": [ - { - "name": "maxPlayers", - "type": "uint8", - "internalType": "uint8" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getActiveLobbies", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getActiveLobbiesCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getLobby", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct GoFishLobby.Lobby", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "host", - "type": "address", - "internalType": "address" - }, - { - "name": "maxPlayers", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "currentPlayers", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "players", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "isActive", - "type": "bool", - "internalType": "bool" - }, - { - "name": "createdAt", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getPlayerStats", - "inputs": [ - { - "name": "player", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct GoFishLobby.PlayerStats", - "components": [ - { - "name": "gamesPlayed", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gamesWon", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalBooks", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rating", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isPlayerInLobby", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "player", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "joinLobby", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "leaveLobby", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "lobbies", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "host", - "type": "address", - "internalType": "address" - }, - { - "name": "maxPlayers", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "currentPlayers", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "isActive", - "type": "bool", - "internalType": "bool" - }, - { - "name": "createdAt", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "lobbyPlayers", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "playerStats", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "gamesPlayed", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gamesWon", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "totalBooks", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "rating", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "startGame", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "updatePlayerStats", - "inputs": [ - { - "name": "player", - "type": "address", - "internalType": "address" - }, - { - "name": "won", - "type": "bool", - "internalType": "bool" - }, - { - "name": "books", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "GameStarted", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "LobbyClosed", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "LobbyCreated", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "host", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "maxPlayers", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "PlayerJoined", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "player", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "PlayerLeft", - "inputs": [ - { - "name": "lobbyId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "player", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "StatsUpdated", - "inputs": [ - { - "name": "player", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "gamesPlayed", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "gamesWon", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - } - ], - "bytecode": "0x6080604052348015600f57600080fd5b506122228061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c8063948428f01161008c578063cdafbbb611610066578063cdafbbb614610288578063e5ed1d59146102bb578063e9173b3a146102d7578063ff465523146102f3576100ea565b8063948428f01461022057806396edd5001461023c578063bb214aaa1461026c576100ea565b8063559ab8cc116100c8578063559ab8cc1461017f5780635b7c2a14146101af5780635c95e301146101cd5780636e8fb4c6146101eb576100ea565b80631eab6ee8146100ef5780631faae2c91461011f5780634fd66eae1461014f575b600080fd5b610109600480360381019061010491906116fa565b610323565b6040516101169190611736565b60405180910390f35b6101396004803603810190610134919061178a565b610347565b6040516101469190611736565b60405180910390f35b61016960048036038101906101649190611815565b6106f5565b60405161017691906118a6565b60405180910390f35b610199600480360381019061019491906118c1565b610776565b6040516101a6919061191c565b60405180910390f35b6101b76107de565b6040516101c49190611736565b60405180910390f35b6101d56107eb565b6040516101e291906119e6565b60405180910390f35b610205600480360381019061020091906116fa565b610843565b60405161021796959493929190611a26565b60405180910390f35b61023a600480360381019061023591906116fa565b6108c6565b005b610256600480360381019061025191906118c1565b610cca565b604051610263919061191c565b60405180910390f35b61028660048036038101906102819190611ab3565b610cf9565b005b6102a2600480360381019061029d9190611815565b610e3b565b6040516102b29493929190611b06565b60405180910390f35b6102d560048036038101906102d091906116fa565b610e6b565b005b6102f160048036038101906102ec91906116fa565b611014565b005b61030d600480360381019061030891906116fa565b6112d4565b60405161031a9190611cc3565b60405180910390f35b6004818154811061033357600080fd5b906000526020600020016000915090505481565b600060028260ff1610158015610361575060068260ff1611155b6103a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161039790611d42565b60405180910390fd5b6000808154809291906103b290611d91565b91905055506000805490506000600167ffffffffffffffff8111156103da576103d9611dd9565b5b6040519080825280602002602001820160405280156104085781602001602082028036833780820191505090505b50905033816000815181106104205761041f611e08565b5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506040518060e001604052808381526020013373ffffffffffffffffffffffffffffffffffffffff1681526020018560ff168152602001600160ff16815260200182815260200160011515815260200142815250600160008481526020019081526020016000206000820151816000015560208201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060408201518160010160146101000a81548160ff021916908360ff16021790555060608201518160010160156101000a81548160ff021916908360ff160217905550608082015181600201908051906020019061056f929190611595565b5060a08201518160030160006101000a81548160ff02191690831515021790555060c0820151816004015590505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550600482908060018154018082558091505060019003906000526020600020016000909190919091505560016004805490506106419190611e37565b60056000848152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff16827f41cb1a5f5e43e6d3b0bc486e69de8bef216000ccad8a43cbb787f85d495ccdb38660405161069f9190611e6b565b60405180910390a33373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a38192505050919050565b6106fd61161f565b600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206040518060800160405290816000820154815260200160018201548152602001600282015481526020016003820154815250509050919050565b60006003600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b6000600480549050905090565b6060600480548060200260200160405190810160405280929190818152602001828054801561083957602002820191906000526020600020905b815481526020019060010190808311610825575b5050505050905090565b60016020528060005260406000206000915090508060000154908060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010160149054906101000a900460ff16908060010160159054906101000a900460ff16908060030160009054906101000a900460ff16908060040154905086565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661092e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161092590611ed2565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff166109cb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109c290611f3e565b60405180910390fd5b60006003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555080600101601581819054906101000a900460ff1680929190610a5590611f5e565b91906101000a81548160ff021916908360ff1602179055505060005b8160020180549050811015610c00573373ffffffffffffffffffffffffffffffffffffffff16826002018281548110610aad57610aac611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1603610bf3578160020160018360020180549050610b0b9190611e37565b81548110610b1c57610b1b611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16826002018281548110610b5d57610b5c611e08565b5b9060005260206000200160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555081600201805480610bb957610bb8611f87565b5b6001900381819060005260206000200160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690559055610c00565b8080600101915050610a71565b503373ffffffffffffffffffffffffffffffffffffffff16827fb2bd643202385c64d9814c6c3fa61b1ba098d6652292e9d9a6f1a0cdae6eb82460405160405180910390a360008160010160159054906101000a900460ff1660ff161480610cb757508060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16145b15610cc657610cc58261144e565b5b5050565b60036020528160005260406000206020528060005260406000206000915091509054906101000a900460ff1681565b6000600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209050806000016000815480929190610d5190611d91565b919050555081816002016000828254610d6a9190611fb6565b925050819055508215610db257806001016000815480929190610d8c90611d91565b9190505550600a816003016000828254610da69190611fb6565b92505081905550610ddd565b600581600301541115610ddc576005816003016000828254610dd49190611e37565b925050819055505b5b8373ffffffffffffffffffffffffffffffffffffffff167f80140d6bcee83a25999242a95412ea6b9754c623fc171e8d4c86c8392c053ad982600001548360010154604051610e2d929190611fea565b60405180910390a250505050565b60026020528060005260406000206000915090508060000154908060010154908060020154908060030154905084565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff16610ed3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610eca90611ed2565b60405180910390fd5b8060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610f65576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f5c9061205f565b60405180910390fd5b60028160010160159054906101000a900460ff1660ff161015610fbd576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610fb4906120cb565b60405180910390fd5b60008160030160006101000a81548160ff021916908315150217905550610fe3826114bc565b817f50ad08f58a27f2851d7e3a1b3a6a46b290f2ce677e99642d30ff639721e7779060405160405180910390a25050565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661107c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161107390611ed2565b60405180910390fd5b8060010160149054906101000a900460ff1660ff168160010160159054906101000a900460ff1660ff16106110e6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110dd90612137565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1615611184576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161117b906121a3565b60405180910390fd5b80600201339080600181540180825580915050600190039060005260206000200160009091909190916101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600101601581819054906101000a900460ff168092919061120a906121c3565b91906101000a81548160ff021916908360ff1602179055505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055503373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a35050565b6112dc611647565b600160008381526020019081526020016000206040518060e0016040529081600082015481526020016001820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020016001820160149054906101000a900460ff1660ff1660ff1681526020016001820160159054906101000a900460ff1660ff1660ff1681526020016002820180548060200260200160405190810160405280929190818152602001828054801561141957602002820191906000526020600020905b8160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190600101908083116113cf575b505050505081526020016003820160009054906101000a900460ff161515151581526020016004820154815250509050919050565b600060016000838152602001908152602001600020905060008160030160006101000a81548160ff02191690831515021790555061148b826114bc565b817f9f95e402f4fc9f7176ebc701895fe5aab299ae1ac65742f07ad5536d5339554f60405160405180910390a25050565b600060056000838152602001908152602001600020549050600060016004805490506114e89190611e37565b90508082146115515760006004828154811061150757611506611e08565b5b90600052602060002001549050806004848154811061152957611528611e08565b5b9060005260206000200181905550826005600083815260200190815260200160002081905550505b600480548061156357611562611f87565b5b600190038181906000526020600020016000905590556005600084815260200190815260200160002060009055505050565b82805482825590600052602060002090810192821561160e579160200282015b8281111561160d5782518260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550916020019190600101906115b5565b5b50905061161b91906116a2565b5090565b6040518060800160405280600081526020016000815260200160008152602001600081525090565b6040518060e0016040528060008152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001600060ff168152602001600060ff16815260200160608152602001600015158152602001600081525090565b5b808211156116bb5760008160009055506001016116a3565b5090565b600080fd5b6000819050919050565b6116d7816116c4565b81146116e257600080fd5b50565b6000813590506116f4816116ce565b92915050565b6000602082840312156117105761170f6116bf565b5b600061171e848285016116e5565b91505092915050565b611730816116c4565b82525050565b600060208201905061174b6000830184611727565b92915050565b600060ff82169050919050565b61176781611751565b811461177257600080fd5b50565b6000813590506117848161175e565b92915050565b6000602082840312156117a05761179f6116bf565b5b60006117ae84828501611775565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006117e2826117b7565b9050919050565b6117f2816117d7565b81146117fd57600080fd5b50565b60008135905061180f816117e9565b92915050565b60006020828403121561182b5761182a6116bf565b5b600061183984828501611800565b91505092915050565b61184b816116c4565b82525050565b6080820160008201516118676000850182611842565b50602082015161187a6020850182611842565b50604082015161188d6040850182611842565b5060608201516118a06060850182611842565b50505050565b60006080820190506118bb6000830184611851565b92915050565b600080604083850312156118d8576118d76116bf565b5b60006118e6858286016116e5565b92505060206118f785828601611800565b9150509250929050565b60008115159050919050565b61191681611901565b82525050565b6000602082019050611931600083018461190d565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600061196f8383611842565b60208301905092915050565b6000602082019050919050565b600061199382611937565b61199d8185611942565b93506119a883611953565b8060005b838110156119d95781516119c08882611963565b97506119cb8361197b565b9250506001810190506119ac565b5085935050505092915050565b60006020820190508181036000830152611a008184611988565b905092915050565b611a11816117d7565b82525050565b611a2081611751565b82525050565b600060c082019050611a3b6000830189611727565b611a486020830188611a08565b611a556040830187611a17565b611a626060830186611a17565b611a6f608083018561190d565b611a7c60a0830184611727565b979650505050505050565b611a9081611901565b8114611a9b57600080fd5b50565b600081359050611aad81611a87565b92915050565b600080600060608486031215611acc57611acb6116bf565b5b6000611ada86828701611800565b9350506020611aeb86828701611a9e565b9250506040611afc868287016116e5565b9150509250925092565b6000608082019050611b1b6000830187611727565b611b286020830186611727565b611b356040830185611727565b611b426060830184611727565b95945050505050565b611b54816117d7565b82525050565b611b6381611751565b82525050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6000611ba18383611b4b565b60208301905092915050565b6000602082019050919050565b6000611bc582611b69565b611bcf8185611b74565b9350611bda83611b85565b8060005b83811015611c0b578151611bf28882611b95565b9750611bfd83611bad565b925050600181019050611bde565b5085935050505092915050565b611c2181611901565b82525050565b600060e083016000830151611c3f6000860182611842565b506020830151611c526020860182611b4b565b506040830151611c656040860182611b5a565b506060830151611c786060860182611b5a565b5060808301518482036080860152611c908282611bba565b91505060a0830151611ca560a0860182611c18565b5060c0830151611cb860c0860182611842565b508091505092915050565b60006020820190508181036000830152611cdd8184611c27565b905092915050565b600082825260208201905092915050565b7f496e76616c696420706c6179657220636f756e74000000000000000000000000600082015250565b6000611d2c601483611ce5565b9150611d3782611cf6565b602082019050919050565b60006020820190508181036000830152611d5b81611d1f565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000611d9c826116c4565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611dce57611dcd611d62565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000611e42826116c4565b9150611e4d836116c4565b9250828203905081811115611e6557611e64611d62565b5b92915050565b6000602082019050611e806000830184611a17565b92915050565b7f4c6f626279206e6f742061637469766500000000000000000000000000000000600082015250565b6000611ebc601083611ce5565b9150611ec782611e86565b602082019050919050565b60006020820190508181036000830152611eeb81611eaf565b9050919050565b7f4e6f7420696e206c6f6262790000000000000000000000000000000000000000600082015250565b6000611f28600c83611ce5565b9150611f3382611ef2565b602082019050919050565b60006020820190508181036000830152611f5781611f1b565b9050919050565b6000611f6982611751565b915060008203611f7c57611f7b611d62565b5b600182039050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6000611fc1826116c4565b9150611fcc836116c4565b9250828201905080821115611fe457611fe3611d62565b5b92915050565b6000604082019050611fff6000830185611727565b61200c6020830184611727565b9392505050565b7f4f6e6c7920686f73742063616e20737461727400000000000000000000000000600082015250565b6000612049601383611ce5565b915061205482612013565b602082019050919050565b600060208201905081810360008301526120788161203c565b9050919050565b7f4e656564206174206c65617374203220706c6179657273000000000000000000600082015250565b60006120b5601783611ce5565b91506120c08261207f565b602082019050919050565b600060208201905081810360008301526120e4816120a8565b9050919050565b7f4c6f6262792066756c6c00000000000000000000000000000000000000000000600082015250565b6000612121600a83611ce5565b915061212c826120eb565b602082019050919050565b6000602082019050818103600083015261215081612114565b9050919050565b7f416c726561647920696e206c6f62627900000000000000000000000000000000600082015250565b600061218d601083611ce5565b915061219882612157565b602082019050919050565b600060208201905081810360008301526121bc81612180565b9050919050565b60006121ce82611751565b915060ff82036121e1576121e0611d62565b5b60018201905091905056fea264697066735822122050310a74d557c4df02164f6b3a7450fbb4d952130b8866cdf6a985b6d896986064736f6c634300081e0033", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100ea5760003560e01c8063948428f01161008c578063cdafbbb611610066578063cdafbbb614610288578063e5ed1d59146102bb578063e9173b3a146102d7578063ff465523146102f3576100ea565b8063948428f01461022057806396edd5001461023c578063bb214aaa1461026c576100ea565b8063559ab8cc116100c8578063559ab8cc1461017f5780635b7c2a14146101af5780635c95e301146101cd5780636e8fb4c6146101eb576100ea565b80631eab6ee8146100ef5780631faae2c91461011f5780634fd66eae1461014f575b600080fd5b610109600480360381019061010491906116fa565b610323565b6040516101169190611736565b60405180910390f35b6101396004803603810190610134919061178a565b610347565b6040516101469190611736565b60405180910390f35b61016960048036038101906101649190611815565b6106f5565b60405161017691906118a6565b60405180910390f35b610199600480360381019061019491906118c1565b610776565b6040516101a6919061191c565b60405180910390f35b6101b76107de565b6040516101c49190611736565b60405180910390f35b6101d56107eb565b6040516101e291906119e6565b60405180910390f35b610205600480360381019061020091906116fa565b610843565b60405161021796959493929190611a26565b60405180910390f35b61023a600480360381019061023591906116fa565b6108c6565b005b610256600480360381019061025191906118c1565b610cca565b604051610263919061191c565b60405180910390f35b61028660048036038101906102819190611ab3565b610cf9565b005b6102a2600480360381019061029d9190611815565b610e3b565b6040516102b29493929190611b06565b60405180910390f35b6102d560048036038101906102d091906116fa565b610e6b565b005b6102f160048036038101906102ec91906116fa565b611014565b005b61030d600480360381019061030891906116fa565b6112d4565b60405161031a9190611cc3565b60405180910390f35b6004818154811061033357600080fd5b906000526020600020016000915090505481565b600060028260ff1610158015610361575060068260ff1611155b6103a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161039790611d42565b60405180910390fd5b6000808154809291906103b290611d91565b91905055506000805490506000600167ffffffffffffffff8111156103da576103d9611dd9565b5b6040519080825280602002602001820160405280156104085781602001602082028036833780820191505090505b50905033816000815181106104205761041f611e08565b5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506040518060e001604052808381526020013373ffffffffffffffffffffffffffffffffffffffff1681526020018560ff168152602001600160ff16815260200182815260200160011515815260200142815250600160008481526020019081526020016000206000820151816000015560208201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060408201518160010160146101000a81548160ff021916908360ff16021790555060608201518160010160156101000a81548160ff021916908360ff160217905550608082015181600201908051906020019061056f929190611595565b5060a08201518160030160006101000a81548160ff02191690831515021790555060c0820151816004015590505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550600482908060018154018082558091505060019003906000526020600020016000909190919091505560016004805490506106419190611e37565b60056000848152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff16827f41cb1a5f5e43e6d3b0bc486e69de8bef216000ccad8a43cbb787f85d495ccdb38660405161069f9190611e6b565b60405180910390a33373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a38192505050919050565b6106fd61161f565b600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206040518060800160405290816000820154815260200160018201548152602001600282015481526020016003820154815250509050919050565b60006003600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b6000600480549050905090565b6060600480548060200260200160405190810160405280929190818152602001828054801561083957602002820191906000526020600020905b815481526020019060010190808311610825575b5050505050905090565b60016020528060005260406000206000915090508060000154908060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010160149054906101000a900460ff16908060010160159054906101000a900460ff16908060030160009054906101000a900460ff16908060040154905086565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661092e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161092590611ed2565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff166109cb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109c290611f3e565b60405180910390fd5b60006003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555080600101601581819054906101000a900460ff1680929190610a5590611f5e565b91906101000a81548160ff021916908360ff1602179055505060005b8160020180549050811015610c00573373ffffffffffffffffffffffffffffffffffffffff16826002018281548110610aad57610aac611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1603610bf3578160020160018360020180549050610b0b9190611e37565b81548110610b1c57610b1b611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16826002018281548110610b5d57610b5c611e08565b5b9060005260206000200160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555081600201805480610bb957610bb8611f87565b5b6001900381819060005260206000200160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690559055610c00565b8080600101915050610a71565b503373ffffffffffffffffffffffffffffffffffffffff16827fb2bd643202385c64d9814c6c3fa61b1ba098d6652292e9d9a6f1a0cdae6eb82460405160405180910390a360008160010160159054906101000a900460ff1660ff161480610cb757508060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16145b15610cc657610cc58261144e565b5b5050565b60036020528160005260406000206020528060005260406000206000915091509054906101000a900460ff1681565b6000600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209050806000016000815480929190610d5190611d91565b919050555081816002016000828254610d6a9190611fb6565b925050819055508215610db257806001016000815480929190610d8c90611d91565b9190505550600a816003016000828254610da69190611fb6565b92505081905550610ddd565b600581600301541115610ddc576005816003016000828254610dd49190611e37565b925050819055505b5b8373ffffffffffffffffffffffffffffffffffffffff167f80140d6bcee83a25999242a95412ea6b9754c623fc171e8d4c86c8392c053ad982600001548360010154604051610e2d929190611fea565b60405180910390a250505050565b60026020528060005260406000206000915090508060000154908060010154908060020154908060030154905084565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff16610ed3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610eca90611ed2565b60405180910390fd5b8060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610f65576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f5c9061205f565b60405180910390fd5b60028160010160159054906101000a900460ff1660ff161015610fbd576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610fb4906120cb565b60405180910390fd5b60008160030160006101000a81548160ff021916908315150217905550610fe3826114bc565b817f50ad08f58a27f2851d7e3a1b3a6a46b290f2ce677e99642d30ff639721e7779060405160405180910390a25050565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661107c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161107390611ed2565b60405180910390fd5b8060010160149054906101000a900460ff1660ff168160010160159054906101000a900460ff1660ff16106110e6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110dd90612137565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1615611184576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161117b906121a3565b60405180910390fd5b80600201339080600181540180825580915050600190039060005260206000200160009091909190916101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600101601581819054906101000a900460ff168092919061120a906121c3565b91906101000a81548160ff021916908360ff1602179055505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055503373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a35050565b6112dc611647565b600160008381526020019081526020016000206040518060e0016040529081600082015481526020016001820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020016001820160149054906101000a900460ff1660ff1660ff1681526020016001820160159054906101000a900460ff1660ff1660ff1681526020016002820180548060200260200160405190810160405280929190818152602001828054801561141957602002820191906000526020600020905b8160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190600101908083116113cf575b505050505081526020016003820160009054906101000a900460ff161515151581526020016004820154815250509050919050565b600060016000838152602001908152602001600020905060008160030160006101000a81548160ff02191690831515021790555061148b826114bc565b817f9f95e402f4fc9f7176ebc701895fe5aab299ae1ac65742f07ad5536d5339554f60405160405180910390a25050565b600060056000838152602001908152602001600020549050600060016004805490506114e89190611e37565b90508082146115515760006004828154811061150757611506611e08565b5b90600052602060002001549050806004848154811061152957611528611e08565b5b9060005260206000200181905550826005600083815260200190815260200160002081905550505b600480548061156357611562611f87565b5b600190038181906000526020600020016000905590556005600084815260200190815260200160002060009055505050565b82805482825590600052602060002090810192821561160e579160200282015b8281111561160d5782518260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550916020019190600101906115b5565b5b50905061161b91906116a2565b5090565b6040518060800160405280600081526020016000815260200160008152602001600081525090565b6040518060e0016040528060008152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001600060ff168152602001600060ff16815260200160608152602001600015158152602001600081525090565b5b808211156116bb5760008160009055506001016116a3565b5090565b600080fd5b6000819050919050565b6116d7816116c4565b81146116e257600080fd5b50565b6000813590506116f4816116ce565b92915050565b6000602082840312156117105761170f6116bf565b5b600061171e848285016116e5565b91505092915050565b611730816116c4565b82525050565b600060208201905061174b6000830184611727565b92915050565b600060ff82169050919050565b61176781611751565b811461177257600080fd5b50565b6000813590506117848161175e565b92915050565b6000602082840312156117a05761179f6116bf565b5b60006117ae84828501611775565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006117e2826117b7565b9050919050565b6117f2816117d7565b81146117fd57600080fd5b50565b60008135905061180f816117e9565b92915050565b60006020828403121561182b5761182a6116bf565b5b600061183984828501611800565b91505092915050565b61184b816116c4565b82525050565b6080820160008201516118676000850182611842565b50602082015161187a6020850182611842565b50604082015161188d6040850182611842565b5060608201516118a06060850182611842565b50505050565b60006080820190506118bb6000830184611851565b92915050565b600080604083850312156118d8576118d76116bf565b5b60006118e6858286016116e5565b92505060206118f785828601611800565b9150509250929050565b60008115159050919050565b61191681611901565b82525050565b6000602082019050611931600083018461190d565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600061196f8383611842565b60208301905092915050565b6000602082019050919050565b600061199382611937565b61199d8185611942565b93506119a883611953565b8060005b838110156119d95781516119c08882611963565b97506119cb8361197b565b9250506001810190506119ac565b5085935050505092915050565b60006020820190508181036000830152611a008184611988565b905092915050565b611a11816117d7565b82525050565b611a2081611751565b82525050565b600060c082019050611a3b6000830189611727565b611a486020830188611a08565b611a556040830187611a17565b611a626060830186611a17565b611a6f608083018561190d565b611a7c60a0830184611727565b979650505050505050565b611a9081611901565b8114611a9b57600080fd5b50565b600081359050611aad81611a87565b92915050565b600080600060608486031215611acc57611acb6116bf565b5b6000611ada86828701611800565b9350506020611aeb86828701611a9e565b9250506040611afc868287016116e5565b9150509250925092565b6000608082019050611b1b6000830187611727565b611b286020830186611727565b611b356040830185611727565b611b426060830184611727565b95945050505050565b611b54816117d7565b82525050565b611b6381611751565b82525050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6000611ba18383611b4b565b60208301905092915050565b6000602082019050919050565b6000611bc582611b69565b611bcf8185611b74565b9350611bda83611b85565b8060005b83811015611c0b578151611bf28882611b95565b9750611bfd83611bad565b925050600181019050611bde565b5085935050505092915050565b611c2181611901565b82525050565b600060e083016000830151611c3f6000860182611842565b506020830151611c526020860182611b4b565b506040830151611c656040860182611b5a565b506060830151611c786060860182611b5a565b5060808301518482036080860152611c908282611bba565b91505060a0830151611ca560a0860182611c18565b5060c0830151611cb860c0860182611842565b508091505092915050565b60006020820190508181036000830152611cdd8184611c27565b905092915050565b600082825260208201905092915050565b7f496e76616c696420706c6179657220636f756e74000000000000000000000000600082015250565b6000611d2c601483611ce5565b9150611d3782611cf6565b602082019050919050565b60006020820190508181036000830152611d5b81611d1f565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000611d9c826116c4565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611dce57611dcd611d62565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000611e42826116c4565b9150611e4d836116c4565b9250828203905081811115611e6557611e64611d62565b5b92915050565b6000602082019050611e806000830184611a17565b92915050565b7f4c6f626279206e6f742061637469766500000000000000000000000000000000600082015250565b6000611ebc601083611ce5565b9150611ec782611e86565b602082019050919050565b60006020820190508181036000830152611eeb81611eaf565b9050919050565b7f4e6f7420696e206c6f6262790000000000000000000000000000000000000000600082015250565b6000611f28600c83611ce5565b9150611f3382611ef2565b602082019050919050565b60006020820190508181036000830152611f5781611f1b565b9050919050565b6000611f6982611751565b915060008203611f7c57611f7b611d62565b5b600182039050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6000611fc1826116c4565b9150611fcc836116c4565b9250828201905080821115611fe457611fe3611d62565b5b92915050565b6000604082019050611fff6000830185611727565b61200c6020830184611727565b9392505050565b7f4f6e6c7920686f73742063616e20737461727400000000000000000000000000600082015250565b6000612049601383611ce5565b915061205482612013565b602082019050919050565b600060208201905081810360008301526120788161203c565b9050919050565b7f4e656564206174206c65617374203220706c6179657273000000000000000000600082015250565b60006120b5601783611ce5565b91506120c08261207f565b602082019050919050565b600060208201905081810360008301526120e4816120a8565b9050919050565b7f4c6f6262792066756c6c00000000000000000000000000000000000000000000600082015250565b6000612121600a83611ce5565b915061212c826120eb565b602082019050919050565b6000602082019050818103600083015261215081612114565b9050919050565b7f416c726561647920696e206c6f62627900000000000000000000000000000000600082015250565b600061218d601083611ce5565b915061219882612157565b602082019050919050565b600060208201905081810360008301526121bc81612180565b9050919050565b60006121ce82611751565b915060ff82036121e1576121e0611d62565b5b60018201905091905056fea264697066735822122050310a74d557c4df02164f6b3a7450fbb4d952130b8866cdf6a985b6d896986064736f6c634300081e0033", - "linkReferences": {}, - "deployedLinkReferences": {}, - "immutableReferences": {}, - "buildInfoId": "stub-build-info" -} \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#PaimaL2Contract.json b/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#PaimaL2Contract.json index 573626e..187bc17 100644 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#PaimaL2Contract.json +++ b/packages/shared/contracts/evm/ignition/deployments/chain-31337/artifacts/GoFishModule#PaimaL2Contract.json @@ -1,147 +1,148 @@ { - "_format": "hh-sol-artifact-1", + "_format": "hh3-artifact-1", "contractName": "PaimaL2Contract", "sourceName": "contracts/PaimaL2Contract.sol", "abi": [ { - "type": "constructor", "inputs": [ { + "internalType": "address", "name": "_owner", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "uint256", "name": "_fee", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "type": "function", - "name": "fee", "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "InsufficientBalance", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "userAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "PaimaGameInteraction", + "type": "event" + }, + { + "inputs": [], + "name": "fee", "outputs": [ { + "internalType": "uint256", "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "owner", "inputs": [], + "name": "owner", "outputs": [ { + "internalType": "address", "name": "", - "type": "address", - "internalType": "address" + "type": "address" } ], - "stateMutability": "view" + "stateMutability": "view", + "type": "function" }, { - "type": "function", - "name": "paimaSubmitGameInput", "inputs": [ { + "internalType": "bytes", "name": "data", - "type": "bytes", - "internalType": "bytes" + "type": "bytes" } ], + "name": "paimaSubmitGameInput", "outputs": [], - "stateMutability": "payable" + "stateMutability": "payable", + "type": "function" }, { - "type": "function", - "name": "setFee", "inputs": [ { + "internalType": "uint256", "name": "newFee", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], + "name": "setFee", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "setOwner", "inputs": [ { + "internalType": "address", "name": "newOwner", - "type": "address", - "internalType": "address" + "type": "address" } ], + "name": "setOwner", "outputs": [], - "stateMutability": "nonpayable" + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "function", - "name": "withdrawFunds", "inputs": [], + "name": "withdrawFunds", "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "PaimaGameInteraction", - "inputs": [ - { - "name": "userAddress", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "data", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "FailedCall", - "inputs": [] - }, - { - "type": "error", - "name": "InsufficientBalance", - "inputs": [ - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] + "stateMutability": "nonpayable", + "type": "function" } ], - "bytecode": "0x608060405234801561001057600080fd5b50604051610bba380380610bba83398181016040528101906100329190610119565b816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550806001819055505050610159565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100b082610085565b9050919050565b6100c0816100a5565b81146100cb57600080fd5b50565b6000815190506100dd816100b7565b92915050565b6000819050919050565b6100f6816100e3565b811461010157600080fd5b50565b600081519050610113816100ed565b92915050565b600080604083850312156101305761012f610080565b5b600061013e858286016100ce565b925050602061014f85828601610104565b9150509250929050565b610a52806101686000396000f3fe6080604052600436106100555760003560e01c806313af40351461005a57806324600fc3146100835780633804df031461009a57806369fe0e2d146100b65780638da5cb5b146100df578063ddca3f431461010a575b600080fd5b34801561006657600080fd5b50610081600480360381019061007c91906105a0565b610135565b005b34801561008f57600080fd5b50610098610206565b005b6100b460048036038101906100af9190610632565b6102ec565b005b3480156100c257600080fd5b506100dd60048036038101906100d891906106b5565b610387565b005b3480156100eb57600080fd5b506100f461041f565b60405161010191906106f1565b60405180910390f35b34801561011657600080fd5b5061011f610443565b60405161012c919061071b565b60405180910390f35b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101c3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ba90610793565b60405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610294576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161028b906107ff565b60405180910390fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905060004790506102e8818373ffffffffffffffffffffffffffffffffffffffff1661044990919063ffffffff16565b5050565b600154341015610331576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161032890610891565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc51783833460405161037b9392919061090f565b60405180910390a25050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610415576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040c9061098d565b60405180910390fd5b8060018190555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104905747816040517fcf4791810000000000000000000000000000000000000000000000000000000081526004016104879291906109ad565b60405180910390fd5b60008273ffffffffffffffffffffffffffffffffffffffff16826040516104b690610a07565b60006040518083038185875af1925050503d80600081146104f3576040519150601f19603f3d011682016040523d82523d6000602084013e6104f8565b606091505b5050905080610533576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061056d82610542565b9050919050565b61057d81610562565b811461058857600080fd5b50565b60008135905061059a81610574565b92915050565b6000602082840312156105b6576105b5610538565b5b60006105c48482850161058b565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126105f2576105f16105cd565b5b8235905067ffffffffffffffff81111561060f5761060e6105d2565b5b60208301915083600182028301111561062b5761062a6105d7565b5b9250929050565b6000806020838503121561064957610648610538565b5b600083013567ffffffffffffffff8111156106675761066661053d565b5b610673858286016105dc565b92509250509250929050565b6000819050919050565b6106928161067f565b811461069d57600080fd5b50565b6000813590506106af81610689565b92915050565b6000602082840312156106cb576106ca610538565b5b60006106d9848285016106a0565b91505092915050565b6106eb81610562565b82525050565b600060208201905061070660008301846106e2565b92915050565b6107158161067f565b82525050565b6000602082019050610730600083018461070c565b92915050565b600082825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e65720000000000600082015250565b600061077d601b83610736565b915061078882610747565b602082019050919050565b600060208201905081810360008301526107ac81610770565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e6473000000600082015250565b60006107e9601d83610736565b91506107f4826107b3565b602082019050919050565b60006020820190508181036000830152610818816107dc565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f2073756260008201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b600061087b602e83610736565b91506108868261081f565b604082019050919050565b600060208201905081810360008301526108aa8161086e565b9050919050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006108ee83856108b1565b93506108fb8385846108c2565b610904836108d1565b840190509392505050565b6000604082019050818103600083015261092a8185876108e2565b9050610939602083018461070c565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e67652066656500000000000000600082015250565b6000610977601983610736565b915061098282610941565b602082019050919050565b600060208201905081810360008301526109a68161096a565b9050919050565b60006040820190506109c2600083018561070c565b6109cf602083018461070c565b9392505050565b600081905092915050565b50565b60006109f16000836109d6565b91506109fc826109e1565b600082019050919050565b6000610a12826109e4565b915081905091905056fea26469706673582212205c31f824432e6edaf50c5909ad07b00497c5d4a7f9b8f122f337476c7a11bae664736f6c634300081e0033", - "deployedBytecode": "0x6080604052600436106100555760003560e01c806313af40351461005a57806324600fc3146100835780633804df031461009a57806369fe0e2d146100b65780638da5cb5b146100df578063ddca3f431461010a575b600080fd5b34801561006657600080fd5b50610081600480360381019061007c91906105a0565b610135565b005b34801561008f57600080fd5b50610098610206565b005b6100b460048036038101906100af9190610632565b6102ec565b005b3480156100c257600080fd5b506100dd60048036038101906100d891906106b5565b610387565b005b3480156100eb57600080fd5b506100f461041f565b60405161010191906106f1565b60405180910390f35b34801561011657600080fd5b5061011f610443565b60405161012c919061071b565b60405180910390f35b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101c3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ba90610793565b60405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610294576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161028b906107ff565b60405180910390fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905060004790506102e8818373ffffffffffffffffffffffffffffffffffffffff1661044990919063ffffffff16565b5050565b600154341015610331576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161032890610891565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc51783833460405161037b9392919061090f565b60405180910390a25050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610415576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040c9061098d565b60405180910390fd5b8060018190555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104905747816040517fcf4791810000000000000000000000000000000000000000000000000000000081526004016104879291906109ad565b60405180910390fd5b60008273ffffffffffffffffffffffffffffffffffffffff16826040516104b690610a07565b60006040518083038185875af1925050503d80600081146104f3576040519150601f19603f3d011682016040523d82523d6000602084013e6104f8565b606091505b5050905080610533576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061056d82610542565b9050919050565b61057d81610562565b811461058857600080fd5b50565b60008135905061059a81610574565b92915050565b6000602082840312156105b6576105b5610538565b5b60006105c48482850161058b565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126105f2576105f16105cd565b5b8235905067ffffffffffffffff81111561060f5761060e6105d2565b5b60208301915083600182028301111561062b5761062a6105d7565b5b9250929050565b6000806020838503121561064957610648610538565b5b600083013567ffffffffffffffff8111156106675761066661053d565b5b610673858286016105dc565b92509250509250929050565b6000819050919050565b6106928161067f565b811461069d57600080fd5b50565b6000813590506106af81610689565b92915050565b6000602082840312156106cb576106ca610538565b5b60006106d9848285016106a0565b91505092915050565b6106eb81610562565b82525050565b600060208201905061070660008301846106e2565b92915050565b6107158161067f565b82525050565b6000602082019050610730600083018461070c565b92915050565b600082825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e65720000000000600082015250565b600061077d601b83610736565b915061078882610747565b602082019050919050565b600060208201905081810360008301526107ac81610770565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e6473000000600082015250565b60006107e9601d83610736565b91506107f4826107b3565b602082019050919050565b60006020820190508181036000830152610818816107dc565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f2073756260008201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b600061087b602e83610736565b91506108868261081f565b604082019050919050565b600060208201905081810360008301526108aa8161086e565b9050919050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006108ee83856108b1565b93506108fb8385846108c2565b610904836108d1565b840190509392505050565b6000604082019050818103600083015261092a8185876108e2565b9050610939602083018461070c565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e67652066656500000000000000600082015250565b6000610977601983610736565b915061098282610941565b602082019050919050565b600060208201905081810360008301526109a68161096a565b9050919050565b60006040820190506109c2600083018561070c565b6109cf602083018461070c565b9392505050565b600081905092915050565b50565b60006109f16000836109d6565b91506109fc826109e1565b600082019050919050565b6000610a12826109e4565b915081905091905056fea26469706673582212205c31f824432e6edaf50c5909ad07b00497c5d4a7f9b8f122f337476c7a11bae664736f6c634300081e0033", + "bytecode": "0x608060405234801561000f575f5ffd5b50604051610b6c380380610b6c83398181016040528101906100319190610113565b8181815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060018190555050505050610151565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100af82610086565b9050919050565b6100bf816100a5565b81146100c9575f5ffd5b50565b5f815190506100da816100b6565b92915050565b5f819050919050565b6100f2816100e0565b81146100fc575f5ffd5b50565b5f8151905061010d816100e9565b92915050565b5f5f6040838503121561012957610128610082565b5b5f610136858286016100cc565b9250506020610147858286016100ff565b9150509250929050565b610a0e8061015e5f395ff3fe608060405260043610610054575f3560e01c806313af40351461005857806324600fc3146100805780633804df031461009657806369fe0e2d146100b25780638da5cb5b146100da578063ddca3f4314610104575b5f5ffd5b348015610063575f5ffd5b5061007e6004803603810190610079919061058b565b61012e565b005b34801561008b575f5ffd5b506100946101fe565b005b6100b060048036038101906100ab9190610617565b6102e1565b005b3480156100bd575f5ffd5b506100d860048036038101906100d39190610695565b61037c565b005b3480156100e5575f5ffd5b506100ee610414565b6040516100fb91906106cf565b60405180910390f35b34801561010f575f5ffd5b50610118610438565b60405161012591906106f7565b60405180910390f35b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101bc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101b39061076a565b60405180910390fd5b805f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461028c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610283906107d2565b60405180910390fd5b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505f4790506102dd818373ffffffffffffffffffffffffffffffffffffffff1661043e90919063ffffffff16565b5050565b600154341015610326576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161031d90610860565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc517838334604051610370939291906108d8565b60405180910390a25050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461040a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040190610952565b60405180910390fd5b8060018190555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104855747816040517fcf47918100000000000000000000000000000000000000000000000000000000815260040161047c929190610970565b60405180910390fd5b5f8273ffffffffffffffffffffffffffffffffffffffff16826040516104aa906109c4565b5f6040518083038185875af1925050503d805f81146104e4576040519150601f19603f3d011682016040523d82523d5f602084013e6104e9565b606091505b5050905080610524576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61055a82610531565b9050919050565b61056a81610550565b8114610574575f5ffd5b50565b5f8135905061058581610561565b92915050565b5f602082840312156105a05761059f610529565b5b5f6105ad84828501610577565b91505092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126105d7576105d66105b6565b5b8235905067ffffffffffffffff8111156105f4576105f36105ba565b5b6020830191508360018202830111156106105761060f6105be565b5b9250929050565b5f5f6020838503121561062d5761062c610529565b5b5f83013567ffffffffffffffff81111561064a5761064961052d565b5b610656858286016105c2565b92509250509250929050565b5f819050919050565b61067481610662565b811461067e575f5ffd5b50565b5f8135905061068f8161066b565b92915050565b5f602082840312156106aa576106a9610529565b5b5f6106b784828501610681565b91505092915050565b6106c981610550565b82525050565b5f6020820190506106e25f8301846106c0565b92915050565b6106f181610662565b82525050565b5f60208201905061070a5f8301846106e8565b92915050565b5f82825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e657200000000005f82015250565b5f610754601b83610710565b915061075f82610720565b602082019050919050565b5f6020820190508181035f83015261078181610748565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e64730000005f82015250565b5f6107bc601d83610710565b91506107c782610788565b602082019050919050565b5f6020820190508181035f8301526107e9816107b0565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f207375625f8201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b5f61084a602e83610710565b9150610855826107f0565b604082019050919050565b5f6020820190508181035f8301526108778161083e565b9050919050565b5f82825260208201905092915050565b828183375f83830152505050565b5f601f19601f8301169050919050565b5f6108b7838561087e565b93506108c483858461088e565b6108cd8361089c565b840190509392505050565b5f6040820190508181035f8301526108f18185876108ac565b905061090060208301846106e8565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e676520666565000000000000005f82015250565b5f61093c601983610710565b915061094782610908565b602082019050919050565b5f6020820190508181035f83015261096981610930565b9050919050565b5f6040820190506109835f8301856106e8565b61099060208301846106e8565b9392505050565b5f81905092915050565b50565b5f6109af5f83610997565b91506109ba826109a1565b5f82019050919050565b5f6109ce826109a4565b915081905091905056fea26469706673582212204996e7c4d7c04b30cc59e3ac0a05e259eaa1e10fffdba7a316a1f8233c93dddb64736f6c634300081b0033", + "deployedBytecode": "0x608060405260043610610054575f3560e01c806313af40351461005857806324600fc3146100805780633804df031461009657806369fe0e2d146100b25780638da5cb5b146100da578063ddca3f4314610104575b5f5ffd5b348015610063575f5ffd5b5061007e6004803603810190610079919061058b565b61012e565b005b34801561008b575f5ffd5b506100946101fe565b005b6100b060048036038101906100ab9190610617565b6102e1565b005b3480156100bd575f5ffd5b506100d860048036038101906100d39190610695565b61037c565b005b3480156100e5575f5ffd5b506100ee610414565b6040516100fb91906106cf565b60405180910390f35b34801561010f575f5ffd5b50610118610438565b60405161012591906106f7565b60405180910390f35b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101bc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101b39061076a565b60405180910390fd5b805f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461028c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610283906107d2565b60405180910390fd5b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505f4790506102dd818373ffffffffffffffffffffffffffffffffffffffff1661043e90919063ffffffff16565b5050565b600154341015610326576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161031d90610860565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc517838334604051610370939291906108d8565b60405180910390a25050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461040a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040190610952565b60405180910390fd5b8060018190555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104855747816040517fcf47918100000000000000000000000000000000000000000000000000000000815260040161047c929190610970565b60405180910390fd5b5f8273ffffffffffffffffffffffffffffffffffffffff16826040516104aa906109c4565b5f6040518083038185875af1925050503d805f81146104e4576040519150601f19603f3d011682016040523d82523d5f602084013e6104e9565b606091505b5050905080610524576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61055a82610531565b9050919050565b61056a81610550565b8114610574575f5ffd5b50565b5f8135905061058581610561565b92915050565b5f602082840312156105a05761059f610529565b5b5f6105ad84828501610577565b91505092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126105d7576105d66105b6565b5b8235905067ffffffffffffffff8111156105f4576105f36105ba565b5b6020830191508360018202830111156106105761060f6105be565b5b9250929050565b5f5f6020838503121561062d5761062c610529565b5b5f83013567ffffffffffffffff81111561064a5761064961052d565b5b610656858286016105c2565b92509250509250929050565b5f819050919050565b61067481610662565b811461067e575f5ffd5b50565b5f8135905061068f8161066b565b92915050565b5f602082840312156106aa576106a9610529565b5b5f6106b784828501610681565b91505092915050565b6106c981610550565b82525050565b5f6020820190506106e25f8301846106c0565b92915050565b6106f181610662565b82525050565b5f60208201905061070a5f8301846106e8565b92915050565b5f82825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e657200000000005f82015250565b5f610754601b83610710565b915061075f82610720565b602082019050919050565b5f6020820190508181035f83015261078181610748565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e64730000005f82015250565b5f6107bc601d83610710565b91506107c782610788565b602082019050919050565b5f6020820190508181035f8301526107e9816107b0565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f207375625f8201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b5f61084a602e83610710565b9150610855826107f0565b604082019050919050565b5f6020820190508181035f8301526108778161083e565b9050919050565b5f82825260208201905092915050565b828183375f83830152505050565b5f601f19601f8301169050919050565b5f6108b7838561087e565b93506108c483858461088e565b6108cd8361089c565b840190509392505050565b5f6040820190508181035f8301526108f18185876108ac565b905061090060208301846106e8565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e676520666565000000000000005f82015250565b5f61093c601983610710565b915061094782610908565b602082019050919050565b5f6020820190508181035f83015261096981610930565b9050919050565b5f6040820190506109835f8301856106e8565b61099060208301846106e8565b9392505050565b5f81905092915050565b50565b5f6109af5f83610997565b91506109ba826109a1565b5f82019050919050565b5f6109ce826109a4565b915081905091905056fea26469706673582212204996e7c4d7c04b30cc59e3ac0a05e259eaa1e10fffdba7a316a1f8233c93dddb64736f6c634300081b0033", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": {}, - "buildInfoId": "stub-build-info" + "inputSourceName": "project/contracts/PaimaL2Contract.sol", + "buildInfoId": "solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0" } \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0.json b/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0.json new file mode 100644 index 0000000..647296b --- /dev/null +++ b/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0.json @@ -0,0 +1,47 @@ +{ + "_format": "hh3-sol-build-info-1", + "id": "solc-0_8_27-fba15f593671aebede96af01bc3114417d98eab0", + "solcVersion": "0.8.27", + "solcLongVersion": "0.8.27+commit.40a35a09", + "userSourceNameMap": { + "contracts/PaimaL2Contract.sol": "project/contracts/PaimaL2Contract.sol" + }, + "input": { + "language": "Solidity", + "settings": { + "evmVersion": "cancun", + "outputSelection": { + "*": { + "": [ + "ast" + ], + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata" + ] + } + }, + "remappings": [ + "npm/@paimaexample/evm-contracts@0.10.24/:@openzeppelin/contracts/=npm/@openzeppelin/contracts@5.1.0/", + "project/:@paimaexample/evm-contracts/=npm/@paimaexample/evm-contracts@0.10.24/" + ] + }, + "sources": { + "npm/@openzeppelin/contracts@5.1.0/utils/Address.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Address.sol)\n\npragma solidity ^0.8.20;\n\nimport {Errors} from \"./Errors.sol\";\n\n/**\n * @dev Collection of functions related to the address type\n */\nlibrary Address {\n /**\n * @dev There's no code at `target` (it is not a contract).\n */\n error AddressEmptyCode(address target);\n\n /**\n * @dev Replacement for Solidity's `transfer`: sends `amount` wei to\n * `recipient`, forwarding all available gas and reverting on errors.\n *\n * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost\n * of certain opcodes, possibly making contracts go over the 2300 gas limit\n * imposed by `transfer`, making them unable to receive funds via\n * `transfer`. {sendValue} removes this limitation.\n *\n * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].\n *\n * IMPORTANT: because control is transferred to `recipient`, care must be\n * taken to not create reentrancy vulnerabilities. Consider using\n * {ReentrancyGuard} or the\n * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].\n */\n function sendValue(address payable recipient, uint256 amount) internal {\n if (address(this).balance < amount) {\n revert Errors.InsufficientBalance(address(this).balance, amount);\n }\n\n (bool success, ) = recipient.call{value: amount}(\"\");\n if (!success) {\n revert Errors.FailedCall();\n }\n }\n\n /**\n * @dev Performs a Solidity function call using a low level `call`. A\n * plain `call` is an unsafe replacement for a function call: use this\n * function instead.\n *\n * If `target` reverts with a revert reason or custom error, it is bubbled\n * up by this function (like regular Solidity function calls). However, if\n * the call reverted with no returned reason, this function reverts with a\n * {Errors.FailedCall} error.\n *\n * Returns the raw returned data. To convert to the expected return value,\n * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].\n *\n * Requirements:\n *\n * - `target` must be a contract.\n * - calling `target` with `data` must not revert.\n */\n function functionCall(address target, bytes memory data) internal returns (bytes memory) {\n return functionCallWithValue(target, data, 0);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but also transferring `value` wei to `target`.\n *\n * Requirements:\n *\n * - the calling contract must have an ETH balance of at least `value`.\n * - the called Solidity function must be `payable`.\n */\n function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {\n if (address(this).balance < value) {\n revert Errors.InsufficientBalance(address(this).balance, value);\n }\n (bool success, bytes memory returndata) = target.call{value: value}(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a static call.\n */\n function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {\n (bool success, bytes memory returndata) = target.staticcall(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a delegate call.\n */\n function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {\n (bool success, bytes memory returndata) = target.delegatecall(data);\n return verifyCallResultFromTarget(target, success, returndata);\n }\n\n /**\n * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target\n * was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case\n * of an unsuccessful call.\n */\n function verifyCallResultFromTarget(\n address target,\n bool success,\n bytes memory returndata\n ) internal view returns (bytes memory) {\n if (!success) {\n _revert(returndata);\n } else {\n // only check if target is a contract if the call was successful and the return data is empty\n // otherwise we already know that it was a contract\n if (returndata.length == 0 && target.code.length == 0) {\n revert AddressEmptyCode(target);\n }\n return returndata;\n }\n }\n\n /**\n * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the\n * revert reason or with a default {Errors.FailedCall} error.\n */\n function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {\n if (!success) {\n _revert(returndata);\n } else {\n return returndata;\n }\n }\n\n /**\n * @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedCall}.\n */\n function _revert(bytes memory returndata) private pure {\n // Look for revert reason and bubble it up if present\n if (returndata.length > 0) {\n // The easiest way to bubble the revert reason is using memory via assembly\n assembly (\"memory-safe\") {\n let returndata_size := mload(returndata)\n revert(add(32, returndata), returndata_size)\n }\n } else {\n revert Errors.FailedCall();\n }\n }\n}\n" + }, + "npm/@openzeppelin/contracts@5.1.0/utils/Errors.sol": { + "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Collection of common custom errors used in multiple contracts\n *\n * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.\n * It is recommended to avoid relying on the error API for critical functionality.\n *\n * _Available since v5.1._\n */\nlibrary Errors {\n /**\n * @dev The ETH balance of the account is not enough to perform the operation.\n */\n error InsufficientBalance(uint256 balance, uint256 needed);\n\n /**\n * @dev A call to an address target failed. The target may have reverted.\n */\n error FailedCall();\n\n /**\n * @dev The deployment failed.\n */\n error FailedDeployment();\n\n /**\n * @dev A necessary precompile is missing.\n */\n error MissingPrecompile(address);\n}\n" + }, + "npm/@paimaexample/evm-contracts@0.10.24/src/contracts/PaimaL2Contract.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\nimport \"@openzeppelin/contracts/utils/Address.sol\";\n\n/// @dev The main L2 contract for a Paima L2.\ncontract PaimaL2Contract {\n using Address for address payable;\n\n /// @dev Emitted when `paimaSubmitGameInput` function is called with `data`.\n /// `userAddress` is the transaction sender and `value` is the transaction value.\n event PaimaGameInteraction(address indexed userAddress, bytes data, uint256 value);\n\n /// @dev Contract owner.\n address public owner;\n /// @dev Amount in wei that is required to be paid when calling `paimaSubmitGameInput`.\n uint256 public fee; // in wei\n\n /// @dev Sets the contract owner to `_owner` and payment fee to `_fee`.\n constructor(address _owner, uint256 _fee) {\n owner = _owner;\n fee = _fee;\n }\n\n /// @dev Emits the `PaimaGameInteraction` event, logging the `msg.sender`, `data`, and `msg.value`.\n /// Revert if `msg.value` is less than set `fee`.\n function paimaSubmitGameInput(bytes calldata data) public payable {\n require(msg.value >= fee, \"Sufficient funds required to submit game input\");\n emit PaimaGameInteraction(msg.sender, data, msg.value);\n }\n\n /// @dev Withdraws the contract balance to the `owner`.\n /// Callable only by the contract owner.\n function withdrawFunds() public {\n require(msg.sender == owner, \"Only owner can withdraw funds\");\n address payable to = payable(owner);\n uint256 balance = address(this).balance;\n to.sendValue(balance);\n }\n\n /// @dev Sets the `newOwner` as the contract owner.\n /// Callable only by the contract owner.\n function setOwner(address newOwner) public {\n require(msg.sender == owner, \"Only owner can change owner\");\n owner = newOwner;\n }\n\n /// @dev Sets the `newFee` as the required payment fee.\n /// Callable only by the contract owner.\n function setFee(uint256 newFee) public {\n require(msg.sender == owner, \"Only owner can change fee\");\n fee = newFee;\n }\n}\n" + }, + "project/contracts/PaimaL2Contract.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.27;\n\nimport {PaimaL2Contract as BasePaimaL2Contract} from \"@paimaexample/evm-contracts/src/contracts/PaimaL2Contract.sol\";\n\n/**\n * @title PaimaL2Contract\n * @dev Thin wrapper around Paima's base L2 contract for Go Fish game\n *\n * This contract serves as the entry point for all game inputs submitted to the L2.\n * Players submit concise game commands (create lobby, join, ask for card, etc.)\n * which are then processed by the Paima Engine state machine.\n */\ncontract PaimaL2Contract is BasePaimaL2Contract {\n /**\n * @dev Constructor\n * @param _owner Address that will own this contract (can update fees)\n * @param _fee Fee in wei required to submit game inputs (can be 0 for free-to-play)\n */\n constructor(address _owner, uint256 _fee) BasePaimaL2Contract(_owner, _fee) {}\n\n // All functionality inherited from BasePaimaL2Contract:\n // - paimaSubmitGameInput(bytes data): Main function for submitting game inputs\n // - setFee(uint256 newFee): Update submission fee (owner only)\n // - Events: SubmittedGameInput(address indexed from, bytes data)\n}\n" + } + } + } +} \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/stub-build-info.json b/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/stub-build-info.json deleted file mode 100644 index e309652..0000000 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/build-info/stub-build-info.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "_format": "hh-sol-build-info-1", - "id": "stub-build-info", - "solcVersion": "0.8.27", - "solcLongVersion": "0.8.27", - "input": { - "language": "Solidity", - "sources": {}, - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "*" - ] - } - } - } - }, - "output": { - "contracts": {}, - "sources": {} - } -} \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/deployed_addresses.json b/packages/shared/contracts/evm/ignition/deployments/chain-31337/deployed_addresses.json index f52f1c0..b1e7ac4 100644 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/deployed_addresses.json +++ b/packages/shared/contracts/evm/ignition/deployments/chain-31337/deployed_addresses.json @@ -1,4 +1,3 @@ { - "GoFishModule#GoFishLobby": "0x5FbDB2315678afecb367f032d93F642f64180aa3", - "GoFishModule#PaimaL2Contract": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "GoFishModule#PaimaL2Contract": "0x5FbDB2315678afecb367f032d93F642f64180aa3" } \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl b/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl index a91f95b..40cb038 100644 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl +++ b/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl @@ -1,14 +1,8 @@ {"chainId":31337,"type":"DEPLOYMENT_INITIALIZE"} -{"artifactId":"GoFishModule#GoFishLobby","constructorArgs":[],"contractName":"GoFishLobby","dependencies":[],"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","futureId":"GoFishModule#GoFishLobby","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"0"}} -{"futureId":"GoFishModule#GoFishLobby","networkInteraction":{"data":"0x6080604052348015600f57600080fd5b506122228061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c8063948428f01161008c578063cdafbbb611610066578063cdafbbb614610288578063e5ed1d59146102bb578063e9173b3a146102d7578063ff465523146102f3576100ea565b8063948428f01461022057806396edd5001461023c578063bb214aaa1461026c576100ea565b8063559ab8cc116100c8578063559ab8cc1461017f5780635b7c2a14146101af5780635c95e301146101cd5780636e8fb4c6146101eb576100ea565b80631eab6ee8146100ef5780631faae2c91461011f5780634fd66eae1461014f575b600080fd5b610109600480360381019061010491906116fa565b610323565b6040516101169190611736565b60405180910390f35b6101396004803603810190610134919061178a565b610347565b6040516101469190611736565b60405180910390f35b61016960048036038101906101649190611815565b6106f5565b60405161017691906118a6565b60405180910390f35b610199600480360381019061019491906118c1565b610776565b6040516101a6919061191c565b60405180910390f35b6101b76107de565b6040516101c49190611736565b60405180910390f35b6101d56107eb565b6040516101e291906119e6565b60405180910390f35b610205600480360381019061020091906116fa565b610843565b60405161021796959493929190611a26565b60405180910390f35b61023a600480360381019061023591906116fa565b6108c6565b005b610256600480360381019061025191906118c1565b610cca565b604051610263919061191c565b60405180910390f35b61028660048036038101906102819190611ab3565b610cf9565b005b6102a2600480360381019061029d9190611815565b610e3b565b6040516102b29493929190611b06565b60405180910390f35b6102d560048036038101906102d091906116fa565b610e6b565b005b6102f160048036038101906102ec91906116fa565b611014565b005b61030d600480360381019061030891906116fa565b6112d4565b60405161031a9190611cc3565b60405180910390f35b6004818154811061033357600080fd5b906000526020600020016000915090505481565b600060028260ff1610158015610361575060068260ff1611155b6103a0576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161039790611d42565b60405180910390fd5b6000808154809291906103b290611d91565b91905055506000805490506000600167ffffffffffffffff8111156103da576103d9611dd9565b5b6040519080825280602002602001820160405280156104085781602001602082028036833780820191505090505b50905033816000815181106104205761041f611e08565b5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506040518060e001604052808381526020013373ffffffffffffffffffffffffffffffffffffffff1681526020018560ff168152602001600160ff16815260200182815260200160011515815260200142815250600160008481526020019081526020016000206000820151816000015560208201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060408201518160010160146101000a81548160ff021916908360ff16021790555060608201518160010160156101000a81548160ff021916908360ff160217905550608082015181600201908051906020019061056f929190611595565b5060a08201518160030160006101000a81548160ff02191690831515021790555060c0820151816004015590505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550600482908060018154018082558091505060019003906000526020600020016000909190919091505560016004805490506106419190611e37565b60056000848152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff16827f41cb1a5f5e43e6d3b0bc486e69de8bef216000ccad8a43cbb787f85d495ccdb38660405161069f9190611e6b565b60405180910390a33373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a38192505050919050565b6106fd61161f565b600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206040518060800160405290816000820154815260200160018201548152602001600282015481526020016003820154815250509050919050565b60006003600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b6000600480549050905090565b6060600480548060200260200160405190810160405280929190818152602001828054801561083957602002820191906000526020600020905b815481526020019060010190808311610825575b5050505050905090565b60016020528060005260406000206000915090508060000154908060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010160149054906101000a900460ff16908060010160159054906101000a900460ff16908060030160009054906101000a900460ff16908060040154905086565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661092e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161092590611ed2565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff166109cb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109c290611f3e565b60405180910390fd5b60006003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff02191690831515021790555080600101601581819054906101000a900460ff1680929190610a5590611f5e565b91906101000a81548160ff021916908360ff1602179055505060005b8160020180549050811015610c00573373ffffffffffffffffffffffffffffffffffffffff16826002018281548110610aad57610aac611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1603610bf3578160020160018360020180549050610b0b9190611e37565b81548110610b1c57610b1b611e08565b5b9060005260206000200160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16826002018281548110610b5d57610b5c611e08565b5b9060005260206000200160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555081600201805480610bb957610bb8611f87565b5b6001900381819060005260206000200160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690559055610c00565b8080600101915050610a71565b503373ffffffffffffffffffffffffffffffffffffffff16827fb2bd643202385c64d9814c6c3fa61b1ba098d6652292e9d9a6f1a0cdae6eb82460405160405180910390a360008160010160159054906101000a900460ff1660ff161480610cb757508060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16145b15610cc657610cc58261144e565b5b5050565b60036020528160005260406000206020528060005260406000206000915091509054906101000a900460ff1681565b6000600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209050806000016000815480929190610d5190611d91565b919050555081816002016000828254610d6a9190611fb6565b925050819055508215610db257806001016000815480929190610d8c90611d91565b9190505550600a816003016000828254610da69190611fb6565b92505081905550610ddd565b600581600301541115610ddc576005816003016000828254610dd49190611e37565b925050819055505b5b8373ffffffffffffffffffffffffffffffffffffffff167f80140d6bcee83a25999242a95412ea6b9754c623fc171e8d4c86c8392c053ad982600001548360010154604051610e2d929190611fea565b60405180910390a250505050565b60026020528060005260406000206000915090508060000154908060010154908060020154908060030154905084565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff16610ed3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610eca90611ed2565b60405180910390fd5b8060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610f65576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f5c9061205f565b60405180910390fd5b60028160010160159054906101000a900460ff1660ff161015610fbd576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610fb4906120cb565b60405180910390fd5b60008160030160006101000a81548160ff021916908315150217905550610fe3826114bc565b817f50ad08f58a27f2851d7e3a1b3a6a46b290f2ce677e99642d30ff639721e7779060405160405180910390a25050565b60006001600083815260200190815260200160002090508060030160009054906101000a900460ff1661107c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161107390611ed2565b60405180910390fd5b8060010160149054906101000a900460ff1660ff168160010160159054906101000a900460ff1660ff16106110e6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110dd90612137565b60405180910390fd5b6003600083815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1615611184576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161117b906121a3565b60405180910390fd5b80600201339080600181540180825580915050600190039060005260206000200160009091909190916101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600101601581819054906101000a900460ff168092919061120a906121c3565b91906101000a81548160ff021916908360ff1602179055505060016003600084815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055503373ffffffffffffffffffffffffffffffffffffffff16827f87969bc7faf902221a147b95ceba76e011c5efb0339a0a8ee7a2bb82d9cfbbd660405160405180910390a35050565b6112dc611647565b600160008381526020019081526020016000206040518060e0016040529081600082015481526020016001820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020016001820160149054906101000a900460ff1660ff1660ff1681526020016001820160159054906101000a900460ff1660ff1660ff1681526020016002820180548060200260200160405190810160405280929190818152602001828054801561141957602002820191906000526020600020905b8160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190600101908083116113cf575b505050505081526020016003820160009054906101000a900460ff161515151581526020016004820154815250509050919050565b600060016000838152602001908152602001600020905060008160030160006101000a81548160ff02191690831515021790555061148b826114bc565b817f9f95e402f4fc9f7176ebc701895fe5aab299ae1ac65742f07ad5536d5339554f60405160405180910390a25050565b600060056000838152602001908152602001600020549050600060016004805490506114e89190611e37565b90508082146115515760006004828154811061150757611506611e08565b5b90600052602060002001549050806004848154811061152957611528611e08565b5b9060005260206000200181905550826005600083815260200190815260200160002081905550505b600480548061156357611562611f87565b5b600190038181906000526020600020016000905590556005600084815260200190815260200160002060009055505050565b82805482825590600052602060002090810192821561160e579160200282015b8281111561160d5782518260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550916020019190600101906115b5565b5b50905061161b91906116a2565b5090565b6040518060800160405280600081526020016000815260200160008152602001600081525090565b6040518060e0016040528060008152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001600060ff168152602001600060ff16815260200160608152602001600015158152602001600081525090565b5b808211156116bb5760008160009055506001016116a3565b5090565b600080fd5b6000819050919050565b6116d7816116c4565b81146116e257600080fd5b50565b6000813590506116f4816116ce565b92915050565b6000602082840312156117105761170f6116bf565b5b600061171e848285016116e5565b91505092915050565b611730816116c4565b82525050565b600060208201905061174b6000830184611727565b92915050565b600060ff82169050919050565b61176781611751565b811461177257600080fd5b50565b6000813590506117848161175e565b92915050565b6000602082840312156117a05761179f6116bf565b5b60006117ae84828501611775565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006117e2826117b7565b9050919050565b6117f2816117d7565b81146117fd57600080fd5b50565b60008135905061180f816117e9565b92915050565b60006020828403121561182b5761182a6116bf565b5b600061183984828501611800565b91505092915050565b61184b816116c4565b82525050565b6080820160008201516118676000850182611842565b50602082015161187a6020850182611842565b50604082015161188d6040850182611842565b5060608201516118a06060850182611842565b50505050565b60006080820190506118bb6000830184611851565b92915050565b600080604083850312156118d8576118d76116bf565b5b60006118e6858286016116e5565b92505060206118f785828601611800565b9150509250929050565b60008115159050919050565b61191681611901565b82525050565b6000602082019050611931600083018461190d565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600061196f8383611842565b60208301905092915050565b6000602082019050919050565b600061199382611937565b61199d8185611942565b93506119a883611953565b8060005b838110156119d95781516119c08882611963565b97506119cb8361197b565b9250506001810190506119ac565b5085935050505092915050565b60006020820190508181036000830152611a008184611988565b905092915050565b611a11816117d7565b82525050565b611a2081611751565b82525050565b600060c082019050611a3b6000830189611727565b611a486020830188611a08565b611a556040830187611a17565b611a626060830186611a17565b611a6f608083018561190d565b611a7c60a0830184611727565b979650505050505050565b611a9081611901565b8114611a9b57600080fd5b50565b600081359050611aad81611a87565b92915050565b600080600060608486031215611acc57611acb6116bf565b5b6000611ada86828701611800565b9350506020611aeb86828701611a9e565b9250506040611afc868287016116e5565b9150509250925092565b6000608082019050611b1b6000830187611727565b611b286020830186611727565b611b356040830185611727565b611b426060830184611727565b95945050505050565b611b54816117d7565b82525050565b611b6381611751565b82525050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6000611ba18383611b4b565b60208301905092915050565b6000602082019050919050565b6000611bc582611b69565b611bcf8185611b74565b9350611bda83611b85565b8060005b83811015611c0b578151611bf28882611b95565b9750611bfd83611bad565b925050600181019050611bde565b5085935050505092915050565b611c2181611901565b82525050565b600060e083016000830151611c3f6000860182611842565b506020830151611c526020860182611b4b565b506040830151611c656040860182611b5a565b506060830151611c786060860182611b5a565b5060808301518482036080860152611c908282611bba565b91505060a0830151611ca560a0860182611c18565b5060c0830151611cb860c0860182611842565b508091505092915050565b60006020820190508181036000830152611cdd8184611c27565b905092915050565b600082825260208201905092915050565b7f496e76616c696420706c6179657220636f756e74000000000000000000000000600082015250565b6000611d2c601483611ce5565b9150611d3782611cf6565b602082019050919050565b60006020820190508181036000830152611d5b81611d1f565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000611d9c826116c4565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611dce57611dcd611d62565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6000611e42826116c4565b9150611e4d836116c4565b9250828203905081811115611e6557611e64611d62565b5b92915050565b6000602082019050611e806000830184611a17565b92915050565b7f4c6f626279206e6f742061637469766500000000000000000000000000000000600082015250565b6000611ebc601083611ce5565b9150611ec782611e86565b602082019050919050565b60006020820190508181036000830152611eeb81611eaf565b9050919050565b7f4e6f7420696e206c6f6262790000000000000000000000000000000000000000600082015250565b6000611f28600c83611ce5565b9150611f3382611ef2565b602082019050919050565b60006020820190508181036000830152611f5781611f1b565b9050919050565b6000611f6982611751565b915060008203611f7c57611f7b611d62565b5b600182039050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6000611fc1826116c4565b9150611fcc836116c4565b9250828201905080821115611fe457611fe3611d62565b5b92915050565b6000604082019050611fff6000830185611727565b61200c6020830184611727565b9392505050565b7f4f6e6c7920686f73742063616e20737461727400000000000000000000000000600082015250565b6000612049601383611ce5565b915061205482612013565b602082019050919050565b600060208201905081810360008301526120788161203c565b9050919050565b7f4e656564206174206c65617374203220706c6179657273000000000000000000600082015250565b60006120b5601783611ce5565b91506120c08261207f565b602082019050919050565b600060208201905081810360008301526120e4816120a8565b9050919050565b7f4c6f6262792066756c6c00000000000000000000000000000000000000000000600082015250565b6000612121600a83611ce5565b915061212c826120eb565b602082019050919050565b6000602082019050818103600083015261215081612114565b9050919050565b7f416c726561647920696e206c6f62627900000000000000000000000000000000600082015250565b600061218d601083611ce5565b915061219882612157565b602082019050919050565b600060208201905081810360008301526121bc81612180565b9050919050565b60006121ce82611751565b915060ff82036121e1576121e0611d62565b5b60018201905091905056fea264697066735822122050310a74d557c4df02164f6b3a7450fbb4d952130b8866cdf6a985b6d896986064736f6c634300081e0033","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} -{"futureId":"GoFishModule#GoFishLobby","networkInteractionId":1,"nonce":0,"type":"TRANSACTION_PREPARE_SEND"} -{"futureId":"GoFishModule#GoFishLobby","networkInteractionId":1,"nonce":0,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"2172363282"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000000"}},"hash":"0xc2eb2d4de5c179f81c4fa00c1826627a5deaf94ba2ceda02e94e8f7c9bf2a078"},"type":"TRANSACTION_SEND"} -{"futureId":"GoFishModule#GoFishLobby","hash":"0xc2eb2d4de5c179f81c4fa00c1826627a5deaf94ba2ceda02e94e8f7c9bf2a078","networkInteractionId":1,"receipt":{"blockHash":"0xc6e1a68e88bd8799c09bd9c5aea36251d2bafe5895aaca3a3b5977a3e66f9e5b","blockNumber":5,"contractAddress":"0x5FbDB2315678afecb367f032d93F642f64180aa3","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} -{"futureId":"GoFishModule#GoFishLobby","result":{"address":"0x5FbDB2315678afecb367f032d93F642f64180aa3","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} {"artifactId":"GoFishModule#PaimaL2Contract","constructorArgs":["0xEFfE522D441d971dDC7153439a7d10235Ae6301f",0],"contractName":"PaimaL2Contract","dependencies":[],"from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","futureId":"GoFishModule#PaimaL2Contract","futureType":"NAMED_ARTIFACT_CONTRACT_DEPLOYMENT","libraries":{},"strategy":"basic","strategyConfig":{},"type":"DEPLOYMENT_EXECUTION_STATE_INITIALIZE","value":{"_kind":"bigint","value":"0"}} -{"futureId":"GoFishModule#PaimaL2Contract","networkInteraction":{"data":"0x608060405234801561001057600080fd5b50604051610bba380380610bba83398181016040528101906100329190610119565b816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550806001819055505050610159565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006100b082610085565b9050919050565b6100c0816100a5565b81146100cb57600080fd5b50565b6000815190506100dd816100b7565b92915050565b6000819050919050565b6100f6816100e3565b811461010157600080fd5b50565b600081519050610113816100ed565b92915050565b600080604083850312156101305761012f610080565b5b600061013e858286016100ce565b925050602061014f85828601610104565b9150509250929050565b610a52806101686000396000f3fe6080604052600436106100555760003560e01c806313af40351461005a57806324600fc3146100835780633804df031461009a57806369fe0e2d146100b65780638da5cb5b146100df578063ddca3f431461010a575b600080fd5b34801561006657600080fd5b50610081600480360381019061007c91906105a0565b610135565b005b34801561008f57600080fd5b50610098610206565b005b6100b460048036038101906100af9190610632565b6102ec565b005b3480156100c257600080fd5b506100dd60048036038101906100d891906106b5565b610387565b005b3480156100eb57600080fd5b506100f461041f565b60405161010191906106f1565b60405180910390f35b34801561011657600080fd5b5061011f610443565b60405161012c919061071b565b60405180910390f35b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101c3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ba90610793565b60405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610294576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161028b906107ff565b60405180910390fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905060004790506102e8818373ffffffffffffffffffffffffffffffffffffffff1661044990919063ffffffff16565b5050565b600154341015610331576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161032890610891565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc51783833460405161037b9392919061090f565b60405180910390a25050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610415576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040c9061098d565b60405180910390fd5b8060018190555050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104905747816040517fcf4791810000000000000000000000000000000000000000000000000000000081526004016104879291906109ad565b60405180910390fd5b60008273ffffffffffffffffffffffffffffffffffffffff16826040516104b690610a07565b60006040518083038185875af1925050503d80600081146104f3576040519150601f19603f3d011682016040523d82523d6000602084013e6104f8565b606091505b5050905080610533576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061056d82610542565b9050919050565b61057d81610562565b811461058857600080fd5b50565b60008135905061059a81610574565b92915050565b6000602082840312156105b6576105b5610538565b5b60006105c48482850161058b565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126105f2576105f16105cd565b5b8235905067ffffffffffffffff81111561060f5761060e6105d2565b5b60208301915083600182028301111561062b5761062a6105d7565b5b9250929050565b6000806020838503121561064957610648610538565b5b600083013567ffffffffffffffff8111156106675761066661053d565b5b610673858286016105dc565b92509250509250929050565b6000819050919050565b6106928161067f565b811461069d57600080fd5b50565b6000813590506106af81610689565b92915050565b6000602082840312156106cb576106ca610538565b5b60006106d9848285016106a0565b91505092915050565b6106eb81610562565b82525050565b600060208201905061070660008301846106e2565b92915050565b6107158161067f565b82525050565b6000602082019050610730600083018461070c565b92915050565b600082825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e65720000000000600082015250565b600061077d601b83610736565b915061078882610747565b602082019050919050565b600060208201905081810360008301526107ac81610770565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e6473000000600082015250565b60006107e9601d83610736565b91506107f4826107b3565b602082019050919050565b60006020820190508181036000830152610818816107dc565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f2073756260008201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b600061087b602e83610736565b91506108868261081f565b604082019050919050565b600060208201905081810360008301526108aa8161086e565b9050919050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006108ee83856108b1565b93506108fb8385846108c2565b610904836108d1565b840190509392505050565b6000604082019050818103600083015261092a8185876108e2565b9050610939602083018461070c565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e67652066656500000000000000600082015250565b6000610977601983610736565b915061098282610941565b602082019050919050565b600060208201905081810360008301526109a68161096a565b9050919050565b60006040820190506109c2600083018561070c565b6109cf602083018461070c565b9392505050565b600081905092915050565b50565b60006109f16000836109d6565b91506109fc826109e1565b600082019050919050565b6000610a12826109e4565b915081905091905056fea26469706673582212205c31f824432e6edaf50c5909ad07b00497c5d4a7f9b8f122f337476c7a11bae664736f6c634300081e0033000000000000000000000000effe522d441d971ddc7153439a7d10235ae6301f0000000000000000000000000000000000000000000000000000000000000000","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} -{"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":1,"type":"TRANSACTION_PREPARE_SEND"} -{"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":1,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"2025817872"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000000"}},"hash":"0x651df73a20af665aa6041869137951049df8a2ab566933c30601cc6b7b2557f6"},"type":"TRANSACTION_SEND"} -{"futureId":"GoFishModule#PaimaL2Contract","hash":"0x651df73a20af665aa6041869137951049df8a2ab566933c30601cc6b7b2557f6","networkInteractionId":1,"receipt":{"blockHash":"0x8312b1c4e466013e2b671219f59b42345548c5059047ab1801bff9e44a2f6621","blockNumber":6,"contractAddress":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} -{"futureId":"GoFishModule#PaimaL2Contract","result":{"address":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file +{"futureId":"GoFishModule#PaimaL2Contract","networkInteraction":{"data":"0x608060405234801561000f575f5ffd5b50604051610b6c380380610b6c83398181016040528101906100319190610113565b8181815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060018190555050505050610151565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100af82610086565b9050919050565b6100bf816100a5565b81146100c9575f5ffd5b50565b5f815190506100da816100b6565b92915050565b5f819050919050565b6100f2816100e0565b81146100fc575f5ffd5b50565b5f8151905061010d816100e9565b92915050565b5f5f6040838503121561012957610128610082565b5b5f610136858286016100cc565b9250506020610147858286016100ff565b9150509250929050565b610a0e8061015e5f395ff3fe608060405260043610610054575f3560e01c806313af40351461005857806324600fc3146100805780633804df031461009657806369fe0e2d146100b25780638da5cb5b146100da578063ddca3f4314610104575b5f5ffd5b348015610063575f5ffd5b5061007e6004803603810190610079919061058b565b61012e565b005b34801561008b575f5ffd5b506100946101fe565b005b6100b060048036038101906100ab9190610617565b6102e1565b005b3480156100bd575f5ffd5b506100d860048036038101906100d39190610695565b61037c565b005b3480156100e5575f5ffd5b506100ee610414565b6040516100fb91906106cf565b60405180910390f35b34801561010f575f5ffd5b50610118610438565b60405161012591906106f7565b60405180910390f35b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101bc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101b39061076a565b60405180910390fd5b805f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461028c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610283906107d2565b60405180910390fd5b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505f4790506102dd818373ffffffffffffffffffffffffffffffffffffffff1661043e90919063ffffffff16565b5050565b600154341015610326576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161031d90610860565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc517838334604051610370939291906108d8565b60405180910390a25050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461040a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040190610952565b60405180910390fd5b8060018190555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104855747816040517fcf47918100000000000000000000000000000000000000000000000000000000815260040161047c929190610970565b60405180910390fd5b5f8273ffffffffffffffffffffffffffffffffffffffff16826040516104aa906109c4565b5f6040518083038185875af1925050503d805f81146104e4576040519150601f19603f3d011682016040523d82523d5f602084013e6104e9565b606091505b5050905080610524576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61055a82610531565b9050919050565b61056a81610550565b8114610574575f5ffd5b50565b5f8135905061058581610561565b92915050565b5f602082840312156105a05761059f610529565b5b5f6105ad84828501610577565b91505092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126105d7576105d66105b6565b5b8235905067ffffffffffffffff8111156105f4576105f36105ba565b5b6020830191508360018202830111156106105761060f6105be565b5b9250929050565b5f5f6020838503121561062d5761062c610529565b5b5f83013567ffffffffffffffff81111561064a5761064961052d565b5b610656858286016105c2565b92509250509250929050565b5f819050919050565b61067481610662565b811461067e575f5ffd5b50565b5f8135905061068f8161066b565b92915050565b5f602082840312156106aa576106a9610529565b5b5f6106b784828501610681565b91505092915050565b6106c981610550565b82525050565b5f6020820190506106e25f8301846106c0565b92915050565b6106f181610662565b82525050565b5f60208201905061070a5f8301846106e8565b92915050565b5f82825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e657200000000005f82015250565b5f610754601b83610710565b915061075f82610720565b602082019050919050565b5f6020820190508181035f83015261078181610748565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e64730000005f82015250565b5f6107bc601d83610710565b91506107c782610788565b602082019050919050565b5f6020820190508181035f8301526107e9816107b0565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f207375625f8201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b5f61084a602e83610710565b9150610855826107f0565b604082019050919050565b5f6020820190508181035f8301526108778161083e565b9050919050565b5f82825260208201905092915050565b828183375f83830152505050565b5f601f19601f8301169050919050565b5f6108b7838561087e565b93506108c483858461088e565b6108cd8361089c565b840190509392505050565b5f6040820190508181035f8301526108f18185876108ac565b905061090060208301846106e8565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e676520666565000000000000005f82015250565b5f61093c601983610710565b915061094782610908565b602082019050919050565b5f6020820190508181035f83015261096981610930565b9050919050565b5f6040820190506109835f8301856106e8565b61099060208301846106e8565b9392505050565b5f81905092915050565b50565b5f6109af5f83610997565b91506109ba826109a1565b5f82019050919050565b5f6109ce826109a4565b915081905091905056fea26469706673582212204996e7c4d7c04b30cc59e3ac0a05e259eaa1e10fffdba7a316a1f8233c93dddb64736f6c634300081b0033000000000000000000000000effe522d441d971ddc7153439a7d10235ae6301f0000000000000000000000000000000000000000000000000000000000000000","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} +{"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":0,"type":"TRANSACTION_PREPARE_SEND"} +{"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":0,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"2339843750"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000000"}},"hash":"0x5d635d19fe69ab100184e6c723616d9f529925e18b3d0bcae481a650d07ab7dd"},"type":"TRANSACTION_SEND"} +{"futureId":"GoFishModule#PaimaL2Contract","hash":"0x5d635d19fe69ab100184e6c723616d9f529925e18b3d0bcae481a650d07ab7dd","networkInteractionId":1,"receipt":{"blockHash":"0x63c89b70c533230f66b52735158f52c8768f44d4c65c91d214a52aabc5bf9419","blockNumber":4,"contractAddress":"0x5FbDB2315678afecb367f032d93F642f64180aa3","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"GoFishModule#PaimaL2Contract","result":{"address":"0x5FbDB2315678afecb367f032d93F642f64180aa3","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/modules/GoFishLobby.ts b/packages/shared/contracts/evm/ignition/modules/GoFishLobby.ts index 05cdc4b..37ba11a 100644 --- a/packages/shared/contracts/evm/ignition/modules/GoFishLobby.ts +++ b/packages/shared/contracts/evm/ignition/modules/GoFishLobby.ts @@ -11,10 +11,8 @@ const GoFishModule = buildModule("GoFishModule", (m) => { // Deploy PaimaL2Contract const paimaL2Contract = m.contract("PaimaL2Contract", [owner, fee]); - // Deploy GoFishLobby contract - const goFishLobby = m.contract("GoFishLobby"); - return { paimaL2Contract, goFishLobby }; + return { paimaL2Contract }; }); export default GoFishModule; diff --git a/packages/shared/data-types/src/config.dev.ts b/packages/shared/data-types/src/config.dev.ts index cfce4f9..a60fb3b 100644 --- a/packages/shared/data-types/src/config.dev.ts +++ b/packages/shared/data-types/src/config.dev.ts @@ -11,9 +11,18 @@ import { import { PrimitiveTypeEVMPaimaL2 } from "@paimaexample/sm/builtin"; import { hardhat } from "viem/chains"; import { grammar } from "@go-fish/data-types/grammar"; +import { contractAddressesEvmMain } from "@go-fish/evm-contracts"; const mainSyncProtocolName = "mainNtp"; +const chainNameId = "chain31337" as keyof typeof contractAddressesEvmMain; + +// PaimaL2Contract address from deployment +const paimaL2Address = contractAddressesEvmMain()[chainNameId]["GoFishModule#PaimaL2Contract"] as `0x${string}`; +if (!paimaL2Address) { + throw new Error("EffectstreamL2 address not found"); +} + export const config = new ConfigBuilder() .setNamespace((builder) => builder.setSecurityNamespace("[go-fish]")) .buildNetworks((builder) => @@ -39,9 +48,7 @@ export const config = new ConfigBuilder() type: ConfigSyncProtocolType.NTP_MAIN, chainUri: "", startBlockHeight: 1, - // Increased to 5000ms to allow CPU-intensive Midnight WASM operations - // to complete without blocking sync processes - pollingInterval: 5000, + pollingInterval: 1000, }) ) .addParallel( @@ -54,7 +61,7 @@ export const config = new ConfigBuilder() // Increased to 5000ms to reduce mutex contention // Midnight circuit operations are CPU-intensive and block the event loop // This gives sync processes time to complete between operations - pollingInterval: 5000, + pollingInterval: 1000, confirmationDepth: 1, }) ) @@ -65,8 +72,8 @@ export const config = new ConfigBuilder() (network, deployments, syncProtocol) => ({ name: "GoFish_PaimaL2", type: PrimitiveTypeEVMPaimaL2, - startBlockHeight: Number(Deno.env.get("PAIMA_L2_START_BLOCK") || "0"), - contractAddress: Deno.env.get("PAIMA_L2_CONTRACT_ADDRESS") || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + startBlockHeight: 0, + contractAddress: paimaL2Address, paimaL2Grammar: grammar, }) ) From ff9bdb3f5fbee70ed6a76f1cf64127f4d9aebc8f Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 18:24:48 -0400 Subject: [PATCH 03/78] Simplify EVM lobby flow to create/join with auto-start and TTL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the 5-command lobby grammar to 3 (createdLobby, joinedLobby, closedLobby) so users have a single action to create or join. The second join auto-starts the game in the same state-machine transition, and open lobbies drop off /open_lobbies 10 minutes after creation. - Grammar: drop toggledReady, startedGame, leftLobby, askedForCard; drop maxPlayers from createdLobby (always 2 players). - State machine: joinedLobby validates open status + not-full, inserts the player, and fires the startGame query in the same transition on real insert. closedLobby verifies host + alone + status=open, then deletes the lobby and its lobby_players rows. Lobby IDs are now deterministic (blockHeight + addr slug) instead of Date.now(). - DB: drop lobbies.max_players and lobby_players.is_ready via new migration 3_simplify_lobbies. Rewrite lobby-queries.sql with new CountLobbyPlayers, DeleteLobbyPlayers, DeleteLobby; regenerate pgtyped bindings. - API: /open_lobbies filters created_at > NOW() - INTERVAL '10 minutes' and drops max_players from SELECT. - Frontend: remove ready/start/leave UI. LobbyListScreen has a single Create/Join flow; LobbyScreen shows Cancel only to the host while alone and auto-navigates on status=in_progress. effectstreamBridge exports closeLobby instead of toggleReady/startGame/leaveLobby. - Tests: api.test.ts adds /open_lobbies shape, TTL (9min keep, 11min hide), and no-max_players assertions; mock pool extended accordingly. - e2e: runLobbyFlow simplified to create → join → wait for in_progress. - Docs: README and CLAUDE.md reflect the 3-command set. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 140 ++ EVM_UPDATE.md | 149 +++ README.md | 23 +- e2e/game-round.test.ts | 78 ++ e2e/smoke/_helpers.ts | 1168 +++++++++++++++++ e2e/smoke/lobby-batcher.test.ts | 179 +++ frontend/src/effectstreamBridge.ts | 90 +- frontend/src/screens/LobbyListScreen.ts | 21 +- frontend/src/screens/LobbyScreen.ts | 391 ++---- frontend/src/services/GoFishGameService.ts | 94 +- .../client/database/migrations/database.sql | 4 +- .../database/src/lobby-queries.queries.ts | 221 ++-- .../client/database/src/lobby-queries.sql | 40 +- packages/client/database/src/mod.ts | 13 +- packages/client/node/src/api.test.ts | 117 ++ packages/client/node/src/api.ts | 9 +- packages/client/node/src/state-machine.ts | 292 ++--- .../shared/data-types/src/go-fish-types.ts | 4 +- packages/shared/data-types/src/grammar.ts | 60 +- 19 files changed, 2231 insertions(+), 862 deletions(-) create mode 100644 CLAUDE.md create mode 100644 EVM_UPDATE.md create mode 100644 e2e/game-round.test.ts create mode 100644 e2e/smoke/_helpers.ts create mode 100644 e2e/smoke/lobby-batcher.test.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d3353db --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,140 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Go Fish card game built on **Paima Engine (Effectstream)** with a hybrid blockchain architecture: +- **EVM (Solidity/Hardhat)**: Lobbies, matchmaking, player stats +- **Midnight (Compact language)**: Private game logic, card shuffling/dealing with ZK proofs +- **Paima Node (Deno)**: State machine processing blockchain events, REST API +- **Frontend**: Three.js 3D game scene with HTML overlay screens (Vite + TypeScript) + +## Build & Run Commands + +```bash +# First time setup +deno install --allow-scripts + +# Build EVM contracts (required before first run) +deno task build:evm + +# Full stack dev (launches frontend:3000, API:9996, EVM:8545, explorer:10590) +deno task dev + +# Dev with batcher mode (no Lace wallet needed) +deno task dev:batcher + +# Dev with TypeScript contract (no Midnight infrastructure needed) +deno task dev:typescript + +# Frontend only +deno task frontend:dev + +# Build Midnight contracts +deno task build:midnight +``` + +### Testing + +```bash +# Run node tests (Deno test runner, api.test.ts) +deno task test + +# Watch mode +deno task --filter @go-fish/node test:watch + +# Run a single test file directly +deno test -A --no-check --unstable-raw-imports packages/client/node/src/api.test.ts + +# Midnight contract tests +cd packages/shared/contracts/midnight && deno task test + +# EVM contract tests +cd packages/shared/contracts/evm && npx hardhat test +``` + +### EVM Contract Build Details + +```bash +# Full compile (forge + hardhat + deploy + artifact generation) +deno task --filter @go-fish/evm-contracts contract:compile + +# Individual steps +cd packages/shared/contracts/evm +deno task build:forge # Forge only +deno task build:hardhat # Hardhat only +deno task deploy:standalone # Start chain, deploy, stop +``` + +### Midnight Infrastructure (for full Midnight dev) + +```bash +# Terminal 1: Start all Midnight services +EFFECTSTREAM_STDOUT=true deno task -f @go-fish/node midnight:setup + +# Terminal 2: Dev server connecting to running infra +USE_TYPESCRIPT_CONTRACT=false EFFECTSTREAM_STDOUT=true USE_BATCHER_MODE=true SKIP_MIDNIGHT_INFRA=true deno task dev +``` + +## Architecture + +``` +Frontend (Vite/Three.js :3000) + ├─ HTTP /api ──► Paima Node (:9996) ──► PGLite DB + │ ├─ EVM read (Hardhat :8545) + │ └─ Midnight query (Indexer :8088) + └─ HTTP /batcher-query ──► Batcher (:3334) ──► Midnight +``` + +**Data flow by phase:** +1. **Lobby** (EVM): Frontend → PaimaL2Contract → Paima state machine → PGLite +2. **Game** (Midnight): Frontend → Midnight contract (ZK proofs) → Indexer → Paima queries +3. **Scoring**: Game completion → leaderboard calculation → PGLite persistence + +### Package Structure + +- `packages/frontend/` — Vite SPA: Three.js scenes, screen overlays, wallet bridges +- `packages/client/node/` — Paima node: state machine (`state-machine.ts`), REST API (`api.ts`), Midnight integration +- `packages/client/batcher/` — Transaction batching service for Midnight +- `packages/client/database/` — PGLite schema, migrations (`mod.ts`), SQL queries (pgtyped) +- `packages/shared/data-types/` — Shared types, Paima concise grammar, config +- `packages/shared/contracts/evm/` — Solidity: `GoFishLobby.sol`, `PaimaL2Contract.sol` +- `packages/shared/contracts/midnight/` — Compact: `GoFish.compact`, `Deck.compact`, `Hand.compact`, etc. + +### Key Environment Variables + +- `USE_TYPESCRIPT_CONTRACT=true` — Local TypeScript contract (skip Midnight infra) +- `USE_BATCHER_MODE=true` — Batcher mode (no Lace wallet needed) +- `SKIP_EVM_LAUNCH=true` — Skip Hardhat if already running +- `SKIP_MIDNIGHT_INFRA=true` — Skip Midnight services +- `EFFECTSTREAM_STDOUT=true` — Log to stdout instead of tmux + +## Game Domain + +Simplified 21-card deck: 7 ranks (A,2-7) x 3 suits (hearts, diamonds, clubs). Book = 3 cards of same rank. 4 cards dealt per player. Card index formula: `rank_index + (suit_index * 7)` → 0-20. + +Paima concise grammar commands (defined in `data-types/src/grammar.ts`): +- Lobby: `createdLobby`, `joinedLobby`, `closedLobby` +- `joinedLobby` auto-starts the game: the 2nd player always fills the lobby, so + the state machine flips `status='open'` → `'in_progress'` in the same transition. +- `closedLobby` is host-only and only valid while the host is alone. +- Open lobbies disappear from `/open_lobbies` 10 minutes after creation (soft TTL). +- Game-round actions (ask for card, respond, go fish, score books) run on the + Midnight contract as ZK circuits, not the EVM grammar. + +## Critical: Midnight ec_mul Guard Bug + +**Never put `std_ecMul` (or any function calling it) inside a Compact `if/else` branch.** The ZKIR compiler wraps branch inputs with guards that produce `(0,0)` on inactive branches, and `ec_mul` has no guard support — it panics on invalid curve points. + +**Fix pattern:** Fetch all secrets unconditionally, use ternary (`? :`) to select the right value (compiles to safe `cond_select`), then call `ec_mul` once unconditionally. + +**Verify after compiling:** Run the detection script in `ZKIR-EC-MUL-GUARD-BUG.md` after every `deno task compact` to ensure no circuit has both `ec_mul` and guarded `public_input` ops. + +## Lint Config + +Deno lint with these rules excluded: `no-this-alias`, `require-yield`, `no-explicit-any`, `ban-types`, `no-unused-vars`, `no-slow-types`. + +## Compact Point Comparison + +When comparing `JubjubPoint` values in Compact 0.30.0, do **not** use `===` (it will silently fail). Instead, hash both points and compare the hashes. diff --git a/EVM_UPDATE.md b/EVM_UPDATE.md new file mode 100644 index 0000000..9ac14a1 --- /dev/null +++ b/EVM_UPDATE.md @@ -0,0 +1,149 @@ +# EVM Lobby Simplification Spec + +Collapse the EVM-side lobby flow to **two user actions** — *Create* and *Join* — with auto-start on join, host cancel, and a 10-minute TTL on stale lobbies. + +## Goals + +1. Two user actions only: **Create** and **Join** (join auto-starts the game). +2. Host can **Cancel** an open lobby while no one else has joined. +3. Open lobbies disappear from the UI after **10 minutes**. +4. Max players is a constant (**2**) — always two players. +5. Drop all dead or now-unused commands from the EVM grammar. + +## Final command set + +After this refactor, the EVM `goFishL2Grammar` has exactly three commands: + +| Command | Fields | Semantics | +|---|---|---| +| `createdLobby` | `playerName`, `lobbyName` | Creates a new lobby, host inserted as sole player. | +| `joinedLobby` | `playerName`, `lobbyID` | Second player joins. Handler unconditionally flips `status='in_progress'` and sets `started_at`. | +| `closedLobby` | `lobbyID` | Host cancels while still alone. Deletes the lobby row (cascades to `lobby_players`). | + +Commands **deleted**: `toggledReady`, `startedGame`, `leftLobby`, `askedForCard`. + +Note on `askedForCard`: this is the Paima grammar stub (past tense). It has no state-machine handler, no frontend encoder, no test — only two doc references. **Do not confuse** with the Midnight circuit `askForCard` (imperative, no "ed") at `packages/shared/contracts/midnight/go-fish-contract/src/game.compact:279`, which is the actual ZK gameplay circuit and must not be touched. + +## File-by-file changes + +### `packages/shared/data-types/src/grammar.ts` +- Delete `toggledReady`, `startedGame`, `leftLobby`, `askedForCard` entries. +- Keep `closedLobby` (already defined) — just ensure it survives. +- Modify `createdLobby`: remove the `maxPlayers` field. Final shape: + ```ts + createdLobby: [ + ['playerName', PlayerName], + ['lobbyName', LobbyName], + ], + ``` +- Final exported `goFishL2Grammar` contains only `createdLobby`, `joinedLobby`, `closedLobby`. + +### `packages/client/node/src/state-machine.ts` +- **Delete handlers**: `toggledReady` (lines ~208–237), `startedGame` (~242–271), `leftLobby` (~276–305). +- **`createdLobby` handler (~22–118)**: + - Remove the `data.parsedInput.maxPlayers` read. + - Drop the `is_ready` column write when inserting the host into `lobby_players`. + - Replace `lobby_${data.blockHeight}_${Date.now()}` with a deterministic ID (see *Determinism* below). +- **`joinedLobby` handler (~123–203)**: + - After the `lobby_players` insert, unconditionally run the existing `startGame` update (set `status='in_progress'`, `started_at=`) in the same transition. No player-count check needed — the second `joinedLobby` is always the final player. + - Remove any host-only check (never applied here anyway). +- **Add new `closedLobby` handler**: + - Resolve signer → account_id. + - `SELECT host_account_id, status FROM lobbies WHERE lobby_id = ?`. + - Assert `host_account_id == signer_account_id`. + - Assert `status = 'open'`. + - Assert `(SELECT COUNT(*) FROM lobby_players WHERE lobby_id = ?) = 1` — only the host present. + - `DELETE FROM lobbies WHERE lobby_id = ?`. Relies on `ON DELETE CASCADE` on the `lobby_players.lobby_id` FK (see schema section). + - Any failed assertion: log and drop the transition (no-op), do not throw. + +### `packages/client/database/migrations/database.sql` +- **Drop column** `lobbies.max_players`. +- **Drop column** `lobby_players.is_ready`. +- **Keep** `lobbies.created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` — the TTL filter depends on it. +- **Pre-flight check**: verify the `lobby_players.lobby_id` FK has `ON DELETE CASCADE`. If missing, add it in the same migration. Otherwise the `closedLobby` handler needs a second explicit `DELETE FROM lobby_players WHERE lobby_id = ?` before the lobby delete. +- Indexes on `status` and `created_at DESC` stay — they support the TTL-filtered list query. + +### `packages/client/database/src/lobby-queries.sql` (and generated pgtyped output) +- **Delete** the `togglePlayerReady` query. +- **Delete** the `startGame` query if it's still standalone — or keep it and call it from `joinedLobby`. Decide during implementation based on whether pgtyped reuse is cleaner than inlining. +- **Delete** any query that references `max_players` or `is_ready`. +- **Add** a `deleteLobby` query: `DELETE FROM lobbies WHERE lobby_id = :lobby_id`. +- **Add** a `countLobbyPlayers` query for the `closedLobby` assertion. +- Update the `openLobbies` SELECT — drop `max_players` from the column list. + +### `packages/client/node/src/api.ts` +- `/open_lobbies` (~lines 78–125): + - Modify the WHERE clause at ~line 112: + ```sql + WHERE l.status = 'open' + AND l.created_at > NOW() - INTERVAL '10 minutes' + ``` + - Drop `max_players` from the SELECT list. + - Drop `max_players` from the TypeScript response type. +- Keep pagination, ordering (`created_at DESC`), and the optional `?wallet=` + `is_player_in_lobby` subquery (harmless, frontend may still want it). + +### Shared TS types +- Wherever the `LobbySummary` / `OpenLobbyRow` type lives (likely `packages/shared/data-types/src/`), remove `maxPlayers` / `max_players`. + +### Frontend (`packages/frontend/src/`) +- `screens/LobbyListScreen.ts`: + - Per row: **Join** button for others' lobbies, **Cancel** button for your own open lobby (submits `closedLobby`). No ready toggle, no start button, no max-players display. + - Keep the 4000ms poll at line ~24. + - On poll tick, if the current player is a member of a lobby whose status flipped to `in_progress`, navigate to the game screen. The host, still on the waiting screen, routes through the same mechanism. +- `screens/CreateLobbyScreen.ts` (or equivalent): + - Remove any max-players selector. Submit `createdLobby(playerName, lobbyName)` only. +- `services/GoFishGameService.ts` (~line 133 area): + - Delete encoders/helpers for `toggledReady`, `startedGame`, `leftLobby`, and the Paima-grammar `askedForCard` if present. **Do not touch** any Midnight `askForCard` helpers. + - Update the `createLobby(...)` helper signature to drop `maxPlayers`. +- Update the `/open_lobbies` fetch response type to match the API change. + +### Docs +- `README.md` line ~190: remove the `askedForCard` bullet, update the commands list to the new three-command set. +- `CLAUDE.md` line ~119: update the "Paima concise grammar commands" section. The **Lobby** group becomes `createdLobby`, `joinedLobby`, `closedLobby`. Delete the **Game: `askedForCard`** line entirely. + +### Tests (`packages/client/node/src/api.test.ts`) +- **Delete / rewrite** any test referencing `toggledReady`, `startedGame`, `leftLobby`, or `maxPlayers`. +- **Add** — auto-start: submit `createdLobby` then `joinedLobby`; assert `lobbies.status = 'in_progress'` and `started_at IS NOT NULL`. +- **Add** — `closedLobby` happy path: host alone, submits close, lobby row gone, `lobby_players` rows gone. +- **Add** — `closedLobby` rejection: non-host signer → no-op; second player present → no-op; `status != 'open'` → no-op. +- **Add** — TTL filter: insert a lobby with `created_at = NOW() - INTERVAL '11 minutes'`, `GET /open_lobbies`, assert absent. Insert a sibling with `created_at = NOW() - INTERVAL '9 minutes'`, assert present. + +## Determinism fix (separate commit, recommended) + +Today `state-machine.ts:29` uses `Date.now()` in the lobby ID, and the DB defaults `created_at` to `CURRENT_TIMESTAMP`. Both are non-deterministic on state-machine replay. The TTL filter now makes this matter: two replays could land a lobby on different sides of the 10-minute cutoff. + +- Replace the lobby ID suffix with the block timestamp from the event payload, if Paima exposes one. Fallback: `data.blockHeight` alone is sufficient for uniqueness. +- Write `created_at` from the handler (parameterised) using the same block timestamp, instead of relying on `DEFAULT CURRENT_TIMESTAMP`. +- Pre-flight: verify whether the Paima runtime surfaces a block timestamp on the event `data` object. If not, document the limitation and defer the fix. + +This can ship as an independent commit before or after the main refactor. + +## Implementation order + +1. **Grammar** deletions + `createdLobby` field drop. Expect compile errors to pinpoint every call site that still references the old commands — they guide steps 2 and 5. +2. **State machine**: delete dead handlers, modify `createdLobby` and `joinedLobby`, add `closedLobby`. +3. **Database**: schema migration (drop columns, verify cascade), update `lobby-queries.sql`, regenerate pgtyped. +4. **API**: `/open_lobbies` TTL filter and response type. +5. **Frontend**: button cleanup, create-lobby form, auto-nav on status flip, response type update. +6. **Tests**: delete obsolete, add new (auto-start, close, TTL). +7. **Docs**: `README.md`, `CLAUDE.md`. +8. **(Optional)** Determinism fix as its own commit. + +## Out of scope + +- Midnight-side circuits, including `askForCard` / `respondToAsk` / `goFish` / `afterGoFish`. None of those are touched. +- Hard TTL (cron/state-machine sweep that writes an `expired` status). Soft query-time filter is sufficient for the UI requirement. +- Leaderboard / stats / scoring flows. +- Any `PaimaL2Contract.sol` change — the contract stays a thin `paimaSubmitGameInput(bytes)` wrapper. + +## Acceptance checklist + +- [ ] `goFishL2Grammar` exports exactly `createdLobby`, `joinedLobby`, `closedLobby`. +- [ ] Submitting `joinedLobby` as the second player transitions `lobbies.status` to `'in_progress'` in one state-machine step. +- [ ] Host submitting `closedLobby` while alone deletes the lobby and its `lobby_players` rows. +- [ ] Non-host `closedLobby`, or `closedLobby` with a second player present, is a no-op. +- [ ] `GET /open_lobbies` never returns rows older than 10 minutes. +- [ ] `lobbies.max_players` and `lobby_players.is_ready` columns no longer exist. +- [ ] Frontend shows only **Create**, **Join**, **Cancel** buttons in the lobby flow; no ready/start UI. +- [ ] No reference to `askedForCard` anywhere in `packages/` or docs. References to Midnight `askForCard` are untouched. +- [ ] `api.test.ts` passes with the new auto-start, close, and TTL tests. diff --git a/README.md b/README.md index 44fb5fc..a70b8ad 100644 --- a/README.md +++ b/README.md @@ -176,19 +176,20 @@ Queries are defined in SQL files and auto-generated using pgtyped: ## Game Commands -Blockchain commands (Paima concise grammar format): +Blockchain commands (Paima concise grammar format). + +Go Fish is a fixed 2-player game with auto-start on the second join, so the +EVM grammar is deliberately minimal — just three commands: **Lobby Management (EVM):** -- `createdLobby|playerName|maxPlayers` - Create game lobby -- `joinedLobby|playerName|lobbyID` - Join lobby -- `toggledReady|lobbyID` - Toggle ready status -- `startedGame|lobbyID` - Start game (host only) -- `leftLobby|lobbyID` - Leave lobby -- `closedLobby|lobbyID` - Close lobby (host only) - -**Game Actions (Midnight - planned):** -- `askedForCard|lobbyID|targetPlayerID|rank` - Ask player for cards -- Game logic will be handled by Midnight contracts for privacy +- `createdLobby|playerName|lobbyName` — Create a new lobby. Host is added as the first player. +- `joinedLobby|playerName|lobbyID` — Second player joins. The state machine flips the lobby to `in_progress` in the same transition. +- `closedLobby|lobbyID` — Host cancels an open lobby while still alone. Deletes the lobby row. + +**Game Actions:** +Game-round logic (asking for cards, drawing, booking) runs on the Midnight +contract as ZK circuits. See `packages/shared/contracts/midnight/`. None of +those actions touch the EVM grammar. See [packages/shared/data-types/src/grammar.ts](packages/shared/data-types/src/grammar.ts) diff --git a/e2e/game-round.test.ts b/e2e/game-round.test.ts new file mode 100644 index 0000000..f3d0835 --- /dev/null +++ b/e2e/game-round.test.ts @@ -0,0 +1,78 @@ +/** + * E2E test — full Go Fish game via real prover + batcher. + * + * Reference implementation for the frontend: drives the entire lifecycle + * through the SAME pipeline the frontend will use once `preferBatchedMode` + * flips in `frontend/src/effectstreamBridge.ts`. + * + * Pipeline: + * 1. Lobby (EVM via batcher /send-input → effectstreaml2 target): + * createdLobby → joinedLobby (auto-starts the game) + * 2. Midnight setup (batcher /send-input → midnight_balancing target): + * applyMask ×2 → dealCards ×2 (init_deck only on first deploy) + * 3. Turn loop until phase == GameOver, scores sum == 7, or MAX_TURNS: + * askForCard → respondToAsk → (afterGoFish if Go Fish) → autoScoreBooks + * + * All the mechanics live in `smoke/_helpers.ts`, which doubles as the + * reference module the frontend will port from. This file is just the + * orchestration shell. + * + * Prereqs (full stack, see GAME_ROUND_PLAN.md): + * - Hardhat (:8545), Paima node (:9996), batcher (:3336) + * - Midnight node + indexer (:8088) + proof server (:6300) + * - go-fish contract deployed (address in go-fish-contract.undeployed.json) + * + * Run: deno task test:e2e + */ + +import { ethers } from "ethers"; +import { + clearAllSecrets, + createSmokeSession, + EVM_RPC_URL, + HARDHAT_KEYS, + lobbyIdToGameId, + PHASE, + runFullGame, + runFullSetup, + runLobbyFlow, +} from "./smoke/_helpers.ts"; + +Deno.test({ + name: "e2e: full Go Fish game via batcher (lobby + Midnight)", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + // Two EVM wallets sign lobby transactions for the batcher. + const provider = new ethers.providers.JsonRpcProvider(EVM_RPC_URL); + const p1Wallet = new ethers.Wallet(HARDHAT_KEYS[1], provider); + const p2Wallet = new ethers.Wallet(HARDHAT_KEYS[2], provider); + + // 1. Lobby flow — create, join, ready ×2, start. Returns Paima lobbyId. + const lobbyId = await runLobbyFlow(p1Wallet, p2Wallet, `E2E-${Date.now()}`); + const gameId = lobbyIdToGameId(lobbyId); + + // 2. Midnight session + per-game setup (applyMask ×2, dealCards ×2). + const session = await createSmokeSession(`e2e-${lobbyId}`); + const setup = await runFullSetup(session, { gameId }); + + try { + // 3. Turn loop until GameOver / sum==7 / MAX_TURNS (20). + const result = await runFullGame(session, gameId, 20); + + console.log(`\n══════════════════════════════════`); + console.log(` E2E result`); + console.log(`══════════════════════════════════`); + console.log(` exitReason: ${result.exitReason}`); + console.log(` turnsPlayed: ${result.turnsPlayed}`); + console.log(` finalScores: P1=${result.finalScores[0]} P2=${result.finalScores[1]}`); + console.log(` finalHandSizes: P1=${result.finalHandSizes[0]} P2=${result.finalHandSizes[1]}`); + console.log(` totalBooks: ${result.totalBooksScored}/7`); + console.log(` divergences: ${result.divergenceCount}`); + console.log(` finalPhase: ${result.finalPhase} (GameOver=${PHASE.GameOver})`); + console.log(` winner: ${result.winner ?? "(not set)"}`); + } finally { + clearAllSecrets(session, setup.gameIdHex); + } + }, +}); diff --git a/e2e/smoke/_helpers.ts b/e2e/smoke/_helpers.ts new file mode 100644 index 0000000..b2a1d8d --- /dev/null +++ b/e2e/smoke/_helpers.ts @@ -0,0 +1,1168 @@ +/** + * Shared helpers for the Go Fish smoke tests. + * + * Factored out of the one-off smokes once we hit 3+ files with 90% overlap. + * Existing smokes (midnight-imports, contract-config, ledger-subscription, + * lobby-batcher, init-deck-circuit, apply-mask, setup-sequence) predate + * this and duplicate most of the content — they're left alone to not + * risk breaking green tests. Future smokes import from here. + */ + +import { resolve } from "jsr:@std/path"; +import { ethers } from "ethers"; +import { generateStmInput } from "@paimaexample/concise"; +import { grammar } from "@go-fish/data-types"; +import { + toHex, + QueryContext, + CostModel, + sampleContractAddress, + createConstructorContext, + CompactTypeBoolean, + CompactTypeUnsignedInteger, +} from "@midnight-ntwrk/compact-runtime"; +import { CompiledContract } from "@midnight-ntwrk/compact-js"; +import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts"; +import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; +import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; + +import { Contract as GoFishContract } from "@go-fish/midnight-contract/contract"; +import { createInMemoryPrivateStateProvider } from "../../frontend/src/services/midnightInMemoryPrivateStateProvider.ts"; +import { makeTestWitnesses, WitnessState } from "./test-witnesses.ts"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +export const REPO_ROOT = + Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); +export const BATCHER_URL = Deno.env.get("BATCHER_URL") ?? "http://localhost:3336"; +export const PROOF_URL = Deno.env.get("PROOF_URL") ?? "http://127.0.0.1:6300"; +export const INDEXER_HTTP_URL = + Deno.env.get("INDEXER_HTTP_URL") ?? "http://127.0.0.1:8088/api/v3/graphql"; +export const INDEXER_WS_URL = + Deno.env.get("INDEXER_WS_URL") ?? "ws://127.0.0.1:8088/api/v3/graphql/ws"; +export const PAIMA_API_URL = Deno.env.get("API_URL") ?? "http://localhost:9996"; +export const EVM_RPC_URL = Deno.env.get("EVM_RPC_URL") ?? "http://localhost:8545"; + +export const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); +export const ZK_CONFIG_PATH = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract/src/managed", +); + +export const DELEGATED_SENTINEL = "GoFish: delegated to midnight_balancing batcher"; + +/** Jubjub prime-order subgroup order — secrets must be in [1, r) */ +export const JUBJUB_R = + 0x0e7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7n; + +/** + * Wait timing budgets — based on observed backend latencies. + * EVM (lobby): ~5s typical → 30s headroom. + * Midnight (indexer / API / DB): up to 2 minutes for state confirmation. + */ +export const EVM_WAIT_MS = 30_000; +export const MIDNIGHT_WAIT_MS = 120_000; + +/** Hardhat default test accounts — index 0 is reserved for the batcher. */ +export const HARDHAT_KEYS = [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // #0 batcher + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", // #1 P1 + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // #2 P2 +]; + +/** Address-type enum value for EVM (matches paima-engine CryptoManager). */ +export const ADDRESS_TYPE_EVM = 0; + +/** + * GamePhase enum values (from GoFish.compact:4 — authoritative order). + * `WaitForDraw` is deprecated (index 4 reserved for enum stability). + */ +export const PHASE = { + Setup: 0, + TurnStart: 1, + WaitForResponse: 2, + WaitForTransfer: 3, + WaitForDraw: 4, // deprecated, merged into respondToAsk + WaitForDrawCheck: 5, + GameOver: 6, +} as const; + +/** Simplified 21-card deck: 7 ranks (A,2-7) × 3 suits (h,d,c) */ +export const RANKS = ["A", "2", "3", "4", "5", "6", "7"] as const; +export const SUITS = ["h", "d", "c"] as const; +export function cardName(idx: number): string { + return `${RANKS[idx % 7]}${SUITS[Math.floor(idx / 7)]}`; +} + +// --------------------------------------------------------------------------- +// Filesystem + crypto helpers +// --------------------------------------------------------------------------- + +export function loadContractAddress(): string { + return JSON.parse(Deno.readTextFileSync(CONTRACT_ADDRESS_FILE)) + .contractAddress.replace(/^0x/, ""); +} + +export function generatePlayerSecret(): bigint { + while (true) { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + const n = BigInt( + "0x" + Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join(""), + ); + if (n >= 1n && n < JUBJUB_R) return n; + } +} + +export function generateShuffleSeed(): Uint8Array { + const seed = new Uint8Array(32); + crypto.getRandomValues(seed); + return seed; +} + +export function randomGameId(): Uint8Array { + const id = new Uint8Array(32); + crypto.getRandomValues(id); + return id; +} + +export function gameIdToHex(gameId: Uint8Array): string { + return ( + "0x" + Array.from(gameId).map(b => b.toString(16).padStart(2, "0")).join("") + ); +} + +/** Contract's `now` parameter is seconds since epoch (matches frontend). */ +export function nowSeconds(): bigint { + return BigInt(Math.floor(Date.now() / 1000)); +} + +/** + * Encode a Paima-issued lobbyId string into the contract's 32-byte gameId. + * UTF-8 encode the lobbyId, copy up to 32 bytes, zero-pad the rest. + * Matches the convention in the old in-process simulator. + */ +export function lobbyIdToGameId(lobbyId: string): Uint8Array { + const enc = new TextEncoder().encode(lobbyId); + const g = new Uint8Array(32); + g.set(enc.slice(0, 32)); + return g; +} + +// --------------------------------------------------------------------------- +// Lobby POST — batcher /send-input with effectstreaml2 target (signed) +// --------------------------------------------------------------------------- + +/** + * Port of paima-engine's `createMessageForBatcher` (jsr:@paimaexample/concise + * — `batcher.ts`). The batcher and the Paima node both rebuild this exact + * string and verify the signature against it. + * + * IMPORTANT — both `namespace` and `target` must be omitted (treated as "") + * for the message to match what the Paima node reconstructs: + * - The batcher config sets `namespace: ""` (config.ts:13) + * - The Paima node calls `createMessageForBatcher(null, ...)` (null → "") + * - `extractBatches` does NOT persist the `target` field into the on-chain + * subunit, so the Paima node reconstructs with `target=undefined → ""` + * Routing still works because the batcher uses `defaultTarget` (effectstreaml2). + */ +export function createMessageForBatcher( + namespace: string | null, + millisecondTimestamp: string, + walletAddress: string, + _addressType: number, + inputData: string, + target: string | undefined = undefined, +): string { + return ((namespace ?? "") + (target ?? "") + millisecondTimestamp + walletAddress + inputData) + .replace(/[^a-zA-Z0-9]/g, "-") + .toLocaleLowerCase(); +} + +/** + * POST a concise lobby command to the batcher. Signs with an ethers wallet + * and lets the batcher route to its `defaultTarget` (effectstreaml2) by + * omitting the `target` field — see notes on createMessageForBatcher. + * + * Use generateStmInput(grammar, "createdLobby", { ... }) to construct the + * conciseData tuple from the typed grammar. + */ +export async function postLobby( + wallet: ethers.Wallet, + conciseData: unknown[], +): Promise { + const timestamp = Date.now().toString(); + const inputStr = JSON.stringify(conciseData); + const message = createMessageForBatcher( + "", // namespace empty (matches batcher + Paima) + timestamp, + wallet.address, + ADDRESS_TYPE_EVM, + inputStr, + undefined, // target omitted (paima-engine 0.10.24 quirk) + ); + const signature = await wallet.signMessage(message); + + const body = { + data: { + // target intentionally omitted — batcher uses defaultTarget + addressType: ADDRESS_TYPE_EVM, + address: wallet.address, + signature, + input: inputStr, + timestamp, + }, + confirmationLevel: "no-wait", + }; + + console.log(` [batcher] lobby ${conciseData[0]} (${wallet.address.slice(0, 8)}…)`); + const res = await fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`batcher rejected lobby ${conciseData[0]}: ${text}`); + } +} + +// --------------------------------------------------------------------------- +// Paima REST API — read EVM lobby metadata +// --------------------------------------------------------------------------- + +export async function getUserLobbies(walletAddress: string): Promise { + const res = await fetch( + `${PAIMA_API_URL}/user_lobbies?wallet=${walletAddress.toLowerCase()}&page=0&count=50`, + ); + if (!res.ok) throw new Error(`user_lobbies: ${res.status}`); + return (await res.json()).lobbies ?? []; +} + +export async function getLobbyState(lobbyId: string): Promise { + const res = await fetch(`${PAIMA_API_URL}/lobby_state?lobby_id=${lobbyId}`); + if (!res.ok) throw new Error(`lobby_state: ${res.status}`); + return res.json(); +} + +// --------------------------------------------------------------------------- +// Lobby flow — createdLobby → joinedLobby (auto-starts the game) +// --------------------------------------------------------------------------- + +/** + * Drive the full EVM lobby flow through the batcher. Returns the lobbyId + * once the game is `in_progress`. + * + * Go Fish is a 2-player game with auto-start: the second joinedLobby + * transition flips the lobby to in_progress in the same state-machine step. + * There are no separate ready/start commands. + */ +export async function runLobbyFlow( + p1Wallet: ethers.Wallet, + p2Wallet: ethers.Wallet, + lobbyName: string, +): Promise { + console.log(`\n── Lobby flow: "${lobbyName}" ──`); + + // Snapshot existing lobbies so we can detect the new one + const before = await getUserLobbies(p1Wallet.address); + const beforeIds = new Set(before.map((l: any) => l.lobby_id)); + + // P1 creates + const conciseCreate = generateStmInput(grammar, "createdLobby", { + playerName: "P1", + lobbyName, + }); + await postLobby(p1Wallet, conciseCreate as unknown as unknown[]); + + const newLobby = await waitFor( + "lobby created", + EVM_WAIT_MS, + async () => { + const lobbies = await getUserLobbies(p1Wallet.address); + return lobbies.find((l: any) => !beforeIds.has(l.lobby_id)) ?? null; + }, + v => v != null, + ); + const lobbyId = String((newLobby as any).lobby_id); + console.log(` ✓ lobby created: ${lobbyId}`); + + // P2 joins — this auto-starts the game in the same state transition. + const conciseJoin = generateStmInput(grammar, "joinedLobby", { + playerName: "P2", + lobbyID: lobbyId, + }); + await postLobby(p2Wallet, conciseJoin as unknown as unknown[]); + + await waitFor( + "game in_progress", + EVM_WAIT_MS, + async () => (await getLobbyState(lobbyId)).status, + v => v === "in_progress", + ); + + console.log(` ✓ game in_progress (lobbyId=${lobbyId})`); + return lobbyId; +} + +// --------------------------------------------------------------------------- +// Batcher POST — midnight_balancing +// --------------------------------------------------------------------------- + +export function detectTxStage( + serializedTx: string, +): "unproven" | "unbound" | "finalized" { + const prefixBytes = new Uint8Array( + serializedTx + .slice(0, 600) + .padEnd(600, "0") + .match(/.{2}/g)! + .map(b => parseInt(b, 16)), + ); + const header = new TextDecoder().decode(prefixBytes); + const m = header.match( + /midnight:(?:transaction|intent)\[v\d+\]\(signature\[v\d+\],([^,]+),([^)]+)\):/, + ); + if (!m) throw new Error(`detectTxStage: cannot parse: ${header.slice(0, 80)}`); + if (m[1]!.includes("proof-preimage")) return "unproven"; + if (m[2]!.includes("embedded-fr")) return "unbound"; + if (m[2]!.includes("pedersen-schnorr")) return "finalized"; + throw new Error(`detectTxStage: unknown: ${m[1]} / ${m[2]}`); +} + +export async function postMidnightTx( + serializedTx: string, + circuitId: string, + meta: { playerId?: number; lobbyId?: string } = {}, +): Promise { + const txStage = detectTxStage(serializedTx); + console.log( + ` [batcher] ${circuitId}: txStage=${txStage}, len=${serializedTx.length / 2}B`, + ); + + const res = await fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + data: { + target: "midnight_balancing", + addressType: 0, + address: "go_fish_player", + input: JSON.stringify({ tx: serializedTx, txStage, circuitId, ...meta }), + timestamp: Date.now(), + }, + confirmationLevel: "no-wait", + }), + }); + const text = await res.text(); + console.log(` [batcher] ${circuitId} → ${res.status} ${text.slice(0, 120)}`); + if (!res.ok) throw new Error(`batcher rejected ${circuitId}: ${text}`); +} + +// --------------------------------------------------------------------------- +// Delegating provider — intercepts balanceTx, POSTs to batcher, throws sentinel +// --------------------------------------------------------------------------- + +export function makeDelegatingProvider() { + const provider: any = { + getCoinPublicKey: () => "00".repeat(32), + getEncryptionPublicKey: () => "00".repeat(32), + async balanceTx(tx: any) { + if (typeof provider.__delegatedBalanceHook === "function") { + await provider.__delegatedBalanceHook(tx); + throw new Error(DELEGATED_SENTINEL); + } + throw new Error("balanceTx called without hook"); + }, + submitTx(_tx: any) { + throw new Error("submitTx should never be called — delegated"); + }, + __delegatedBalanceHook: undefined as undefined | ((tx: any) => Promise), + }; + return provider; +} + +export function isDelegationError(err: unknown): boolean { + if (!(err instanceof Error)) return false; + let e: Error | undefined = err; + while (e) { + if (e.message.includes(DELEGATED_SENTINEL)) return true; + e = e.cause instanceof Error ? e.cause : undefined; + } + return false; +} + +export async function callDelegated( + provider: any, + circuitId: string, + invoke: () => Promise, + meta: { playerId?: number; lobbyId?: string } = {}, +): Promise { + let delegated = false; + provider.__delegatedBalanceHook = async (tx: any) => { + const serializedTx = toHex((tx as any).serialize()); + await postMidnightTx(serializedTx, circuitId, meta); + delegated = true; + }; + try { + await invoke(); + } catch (err) { + if (isDelegationError(err) || delegated) return; + throw err; + } finally { + delete provider.__delegatedBalanceHook; + } +} + +// --------------------------------------------------------------------------- +// Local Contract reader (runs impure circuits against indexer state) +// --------------------------------------------------------------------------- + +/** + * Build a local Contract instance for read-only impureCircuit evaluation. + * Both this local contract AND the deployed contract (via CompiledContract) + * MUST share the same witness state — otherwise reads and submissions can + * see different secrets, which is exactly BACKEND_ISSUES #1. + */ +export function buildLocalContract(witnessState: WitnessState) { + const localContract: any = new GoFishContract(makeTestWitnesses(witnessState) as any); + const init = localContract.initialState( + createConstructorContext({}, "0".repeat(64)), + ); + return { + localContract, + initialPrivateState: init.currentPrivateState, + initialZswap: init.currentZswapLocalState, + }; +} + +/** Sentinel returned when a map lookup fails because the key doesn't exist. */ +export type Missing = { __missing: true }; +export function isMissing(v: T | Missing): v is Missing { + return typeof v === "object" && v !== null && (v as any).__missing === true; +} + +export async function readCircuit( + publicDataProvider: any, + contractAddress: string, + localContract: any, + initialPrivateState: any, + initialZswap: any, + circuitName: string, + ...args: any[] +): Promise { + const cs = await publicDataProvider.queryContractState(contractAddress); + if (!cs) throw new Error("queryContractState returned null"); + const ctx = { + currentPrivateState: initialPrivateState, + currentZswapLocalState: initialZswap, + currentQueryContext: new QueryContext(cs.data, sampleContractAddress()), + costModel: CostModel.initialCostModel(), + }; + try { + return localContract.impureCircuits[circuitName](ctx, ...args).result as T; + } catch (err) { + const msg = (err as Error).message ?? ""; + if ( + msg.includes("expected a cell, received null") || + msg.includes("not present") + ) { + return { __missing: true }; + } + throw err; + } +} + +// --------------------------------------------------------------------------- +// waitFor — poll a fetcher until a predicate matches +// --------------------------------------------------------------------------- + +export async function waitFor( + label: string, + timeoutMs: number, + fetcher: () => Promise, + predicate: (v: T) => boolean, + intervalMs = 2000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const v = await fetcher(); + if (predicate(v)) { + console.log(` ✓ ${label}: matched (${safeJson(v)})`); + return v; + } + const remaining = Math.round((deadline - Date.now()) / 1000); + console.log(` [poll] ${label} pending (${safeJson(v)}, ${remaining}s left)`); + await new Promise(r => setTimeout(r, intervalMs)); + } + throw new Error(`Timeout: ${label}`); +} + +function safeJson(v: unknown): string { + try { + return JSON.stringify(v, (_k, val) => + typeof val === "bigint" ? val.toString() + "n" : val, + ); + } catch { + return String(v); + } +} + +// --------------------------------------------------------------------------- +// One-shot session bootstrapper — sets up providers, joins contract, +// returns a `read` function and the deployed contract + delegating provider. +// --------------------------------------------------------------------------- + +export interface SmokeSession { + contractAddress: string; + contract: any; + walletProvider: any; + publicDataProvider: any; + read: (name: string, ...args: any[]) => Promise; + /** Test-owned witness state — set/clear secrets here, never via the + * default `@go-fish/midnight-contract` setPlayerSecrets module global. */ + witnessState: WitnessState; +} + +export async function createSmokeSession( + privateStateId: string, +): Promise { + setNetworkId("undeployed"); + + const contractAddress = loadContractAddress(); + const zkConfigProvider = new NodeZkConfigProvider(ZK_CONFIG_PATH); + const proofProvider = httpClientProofProvider(PROOF_URL, zkConfigProvider); + const publicDataProvider = indexerPublicDataProvider( + INDEXER_HTTP_URL, + INDEXER_WS_URL, + ); + const privateStateProvider = createInMemoryPrivateStateProvider(privateStateId); + const walletProvider = makeDelegatingProvider(); + + // Test-owned witness state. Both the deployed contract and the local + // read contract use the SAME instance — they cannot diverge on what a + // player's secret is. + const witnessState = new WitnessState(); + const testWitnesses = makeTestWitnesses(witnessState); + + const compiledContract = CompiledContract.make("go-fish", GoFishContract as any) + .pipe( + CompiledContract.withWitnesses(testWitnesses as any), + CompiledContract.withCompiledFileAssets(ZK_CONFIG_PATH), + ); + + console.log(" joining deployed contract..."); + const contract: any = await findDeployedContract( + { + privateStateProvider, + zkConfigProvider, + proofProvider, + publicDataProvider, + walletProvider, + midnightProvider: walletProvider, + } as any, + { + contractAddress: contractAddress as any, + compiledContract: compiledContract as any, + privateStateId: privateStateId as any, + initialPrivateState: {}, + } as any, + ); + console.log(" ✓ contract joined"); + + const { localContract, initialPrivateState, initialZswap } = + buildLocalContract(witnessState); + + const read = async (name: string, ...args: any[]) => + readCircuit( + publicDataProvider, + contractAddress, + localContract, + initialPrivateState, + initialZswap, + name, + ...args, + ); + + return { + contractAddress, + contract, + walletProvider, + publicDataProvider, + read, + witnessState, + }; +} + +// --------------------------------------------------------------------------- +// One-shot deploy-time init_deck — must run once per contract deployment +// --------------------------------------------------------------------------- + +/** + * Query the `staticDeckInitialized` Boolean via raw VM query ops. The + * go-fish contract uses module-scoped ledger storage (module `GoFish`, + * `GoFish.compact:4`), so we can't reach the field through the typed + * `ledger()` decoder (which is empty for module-based contracts). Reading + * it as VM ops on the indexer's `ContractState` is how the frontend's + * `GoFishContractService.queryIsStaticDeckInitialized` does it. + * + * Path [1n, 0n] is the compiled accessor path for + * `Deck::staticDeckInitialized` in the current contract build. + */ +async function queryStaticDeckInitialized( + publicDataProvider: any, + contractAddress: string, +): Promise { + const contractState = await publicDataProvider.queryContractState(contractAddress); + if (!contractState) return false; + + const keyType = new CompactTypeUnsignedInteger(255n, 1); + const key1 = { value: keyType.toValue(1n), alignment: keyType.alignment() }; + const key0 = { value: keyType.toValue(0n), alignment: keyType.alignment() }; + + try { + const results = contractState.query( + [ + { dup: { n: 0 } }, + { + idx: { + cached: false, + pushPath: false, + path: [ + { tag: "value" as const, value: key1 }, + { tag: "value" as const, value: key0 }, + ], + }, + }, + { popeq: { cached: false, result: null } }, + ], + CostModel.initialCostModel(), + ); + const gatherResults = (results as any)[1]; + const first = Array.isArray(gatherResults) ? gatherResults[0] : null; + if (!first || first.tag !== "read") return false; + return CompactTypeBoolean.fromValue(first.content.value); + } catch { + return false; + } +} + +/** + * Make sure the contract's global `staticDeckInitialized` flag is true. + * Called once at the start of the e2e — on a fresh deploy the flag is + * false and `applyMask` would fail its internal assert "Static deck not + * initialized - call init_static_deck first". + * + * `init_deck()` is a one-shot wrapper around `init_static_deck()`: the + * body is idempotent internally (it checks the flag and returns early), + * but calling it unconditionally still costs a full proof round-trip, + * so we skip when the flag is already true. + */ +export async function ensureStaticDeckInitialized( + session: SmokeSession, +): Promise { + const already = await queryStaticDeckInitialized( + session.publicDataProvider, + session.contractAddress, + ); + if (already) { + console.log(" staticDeckInitialized: already true (skipping init_deck)"); + return; + } + + console.log("\n── init_deck() (first run on this deploy) ──"); + await callDelegated( + session.walletProvider, + "init_deck", + () => session.contract.callTx.init_deck(), + ); + await waitFor( + "staticDeckInitialized: false → true", + MIDNIGHT_WAIT_MS, + () => + queryStaticDeckInitialized( + session.publicDataProvider, + session.contractAddress, + ), + v => v === true, + ); +} + +// --------------------------------------------------------------------------- +// Full per-game setup: applyMask×2 → dealCards×2, ending at TurnStart +// --------------------------------------------------------------------------- + +export interface SetupResult { + gameId: Uint8Array; + gameIdHex: string; + p1Secret: bigint; + p2Secret: bigint; + p1Seed: Uint8Array; + p2Seed: Uint8Array; +} + +export async function runFullSetup( + session: SmokeSession, + options: { + gameId?: Uint8Array; + p1Secret?: bigint; + p2Secret?: bigint; + p1Seed?: Uint8Array; + p2Seed?: Uint8Array; + } = {}, +): Promise { + const gameId = options.gameId ?? randomGameId(); + const gameIdHex = gameIdToHex(gameId); + const p1Secret = options.p1Secret ?? generatePlayerSecret(); + const p2Secret = options.p2Secret ?? generatePlayerSecret(); + const p1Seed = options.p1Seed ?? generateShuffleSeed(); + const p2Seed = options.p2Seed ?? generateShuffleSeed(); + + console.log(` gameId = ${gameIdHex.slice(0, 22)}...`); + + // Test-owned witness state: explicit set, no global, no fallback. + // Both `session.contract` (deployed) and the local read contract share + // this WitnessState, so reads and submissions cannot diverge. + session.witnessState.set(gameIdHex, 1, p1Secret, p1Seed); + session.witnessState.set(gameIdHex, 2, p2Secret, p2Seed); + + // Deploy-time one-shot: initialize the static deck if it hasn't been + // done yet. Needed for applyMask's internal assert to pass. + await ensureStaticDeckInitialized(session); + + const { contract, walletProvider, read } = session; + + console.log("\n── applyMask(gameId, 1n) ──"); + await callDelegated( + walletProvider, + "applyMask:1", + () => contract.callTx.applyMask(gameId, 1n), + { playerId: 1 }, + ); + await waitFor( + "hasMaskApplied(1)", + MIDNIGHT_WAIT_MS, + () => read("hasMaskApplied", gameId, 1n), + v => v === true, + ); + + console.log("\n── applyMask(gameId, 2n) ──"); + await callDelegated( + walletProvider, + "applyMask:2", + () => contract.callTx.applyMask(gameId, 2n), + { playerId: 2 }, + ); + await waitFor( + "hasMaskApplied(2)", + MIDNIGHT_WAIT_MS, + () => read("hasMaskApplied", gameId, 2n), + v => v === true, + ); + + console.log("\n── dealCards(gameId, 1n) ──"); + await callDelegated( + walletProvider, + "dealCards:1", + () => contract.callTx.dealCards(gameId, 1n), + { playerId: 1 }, + ); + await waitFor( + "hasDealt(1)", + MIDNIGHT_WAIT_MS, + () => read("hasDealt", gameId, 1n), + v => v === true, + ); + + console.log("\n── dealCards(gameId, 2n) ──"); + await callDelegated( + walletProvider, + "dealCards:2", + () => contract.callTx.dealCards(gameId, 2n), + { playerId: 2 }, + ); + await waitFor( + "phase == TurnStart", + MIDNIGHT_WAIT_MS, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) === PHASE.TurnStart, + ); + + return { gameId, gameIdHex, p1Secret, p2Secret, p1Seed, p2Seed }; +} + +/** + * Clear both players' witness secrets from the session-owned WitnessState. + * Call from a `finally` block. After this, any further circuit call that + * needs P1 or P2's secret for `gameIdHex` will throw immediately rather + * than silently using a fallback. + */ +export function clearAllSecrets(session: SmokeSession, gameIdHex: string): void { + session.witnessState.clearAll(gameIdHex); +} + +// --------------------------------------------------------------------------- +// Hand reading — iterate 0..20 calling doesPlayerHaveSpecificCard +// --------------------------------------------------------------------------- + +/** + * Read a player's hand (their own private cards) by enumerating all 21 + * possible card indices via `doesPlayerHaveSpecificCard`. Requires the + * player's secret to be set in the witness module before calling. + */ +export async function readHand( + session: SmokeSession, + gameId: Uint8Array, + playerId: 1 | 2, +): Promise { + const hand: number[] = []; + for (let i = 0; i < 21; i++) { + const has = await session.read( + "doesPlayerHaveSpecificCard", + gameId, + BigInt(playerId), + BigInt(i), + ); + if (has === true) hand.push(i); + } + return hand; +} + +export function handRanks(cards: number[]): number[] { + return [...new Set(cards.map(c => c % 7))].sort((a, b) => a - b); +} + +// --------------------------------------------------------------------------- +// Turn loop primitives — playOneTurn, autoScoreBooks, runFullGame +// --------------------------------------------------------------------------- + +export interface TurnResult { + turn: number; + playerId: 1 | 2; + opponent: 1 | 2; + askRank: number | null; + handBefore: number[]; + oppHandBefore: number[]; + handAfter: number[]; + oppHandAfter: number[]; + gotCards: boolean; + wentFishing: boolean; + ended: boolean; + endReason?: string; +} + +/** + * Drive one full turn: read state → ask → respond → (afterGoFish if Go Fish). + * + * Returns a TurnResult describing what happened. When `ended === true`, the + * caller should stop the loop. Reasons include: phase already GameOver, + * empty hand, unexpected phase transition, or askForCard rejected by the + * contract (BACKEND_ISSUES #1: divergence between local hand read and the + * contract's internal rank check). + * + * The function does NOT score books — that's `autoScoreBooks`'s job. + */ +export async function playOneTurn( + session: SmokeSession, + gameId: Uint8Array, + turnNumber: number, +): Promise { + const { contract, walletProvider, read } = session; + + const empty: TurnResult = { + turn: turnNumber, + playerId: 1, + opponent: 2, + askRank: null, + handBefore: [], + oppHandBefore: [], + handAfter: [], + oppHandAfter: [], + gotCards: false, + wentFishing: false, + ended: true, + }; + + // Phase check + const phaseRaw = await read("getGamePhase", gameId); + if (isMissing(phaseRaw)) { + return { ...empty, endReason: "getGamePhase missing" }; + } + const phase = Number(phaseRaw); + if (phase === PHASE.GameOver) { + return { ...empty, endReason: "phase == GameOver" }; + } + if (phase !== PHASE.TurnStart) { + return { ...empty, endReason: `unexpected phase ${phase} at turn start` }; + } + + // Whose turn? + const playerId = Number(await read("getCurrentTurn", gameId)) as 1 | 2; + const opponent = (playerId === 1 ? 2 : 1) as 1 | 2; + + // Read both hands (debug visibility — prints on the test stdout) + const handBefore = await readHand(session, gameId, playerId); + const oppHandBefore = await readHand(session, gameId, opponent); + + console.log(`\n══ Turn ${turnNumber} ══`); + console.log(` P${playerId} hand: ${handBefore.map(cardName).join(" ") || "(empty)"} [${handBefore.length}]`); + console.log(` P${opponent} hand: ${oppHandBefore.map(cardName).join(" ") || "(empty)"} [${oppHandBefore.length}]`); + + if (handBefore.length === 0) { + return { + ...empty, + playerId, + opponent, + handBefore, + oppHandBefore, + handAfter: handBefore, + oppHandAfter: oppHandBefore, + endReason: `P${playerId} has empty hand`, + }; + } + + // Pick the first rank in our hand (simple heuristic; fine for testing) + const askRank = handRanks(handBefore)[0]!; + console.log(` → P${playerId} asks P${opponent} for ${RANKS[askRank]}`); + + // Ask + await callDelegated( + walletProvider, + `askForCard:t${turnNumber}`, + () => contract.callTx.askForCard(gameId, BigInt(playerId), BigInt(askRank), nowSeconds()), + { playerId }, + ); + await waitFor( + "phase == WaitForResponse", + MIDNIGHT_WAIT_MS, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) === PHASE.WaitForResponse, + ); + + // Opponent responds + await callDelegated( + walletProvider, + `respondToAsk:t${turnNumber}`, + () => contract.callTx.respondToAsk(gameId, BigInt(opponent), nowSeconds()), + { playerId: opponent }, + ); + const postRespondRaw = await waitFor( + "phase != WaitForResponse", + MIDNIGHT_WAIT_MS, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) !== PHASE.WaitForResponse, + ); + const postRespond = Number(postRespondRaw); + + let gotCards = false; + let wentFishing = false; + + if (postRespond === PHASE.WaitForDrawCheck) { + // Go Fish: respondToAsk auto-drew a card from the deck for the asker. + // afterGoFish decrypts it and decides whether to switchTurn. + wentFishing = true; + console.log(` → Go Fish (P${playerId} drew from deck)`); + await callDelegated( + walletProvider, + `afterGoFish:t${turnNumber}`, + () => contract.callTx.afterGoFish(gameId, BigInt(playerId), nowSeconds()), + { playerId }, + ); + await waitFor( + "phase out of WaitForDrawCheck", + MIDNIGHT_WAIT_MS, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) !== PHASE.WaitForDrawCheck, + ); + } else if (postRespond === PHASE.TurnStart) { + gotCards = true; + console.log(` → P${opponent} had cards, transferred`); + } else if (postRespond === PHASE.GameOver) { + return { + ...empty, + playerId, + opponent, + askRank, + handBefore, + oppHandBefore, + endReason: "GameOver via respondToAsk", + }; + } else { + console.log(` [warn] unexpected postRespond phase: ${postRespond}`); + } + + // Read final hands for this turn + const handAfter = await readHand(session, gameId, playerId); + const oppHandAfter = await readHand(session, gameId, opponent); + + return { + turn: turnNumber, + playerId, + opponent, + askRank, + handBefore, + oppHandBefore, + handAfter, + oppHandAfter, + gotCards, + wentFishing, + ended: false, + }; +} + +/** + * After a hand-mutating action, look for any rank with ≥3 cards in the + * given player's hand and submit `checkAndScoreBook` for it. Each attempt + * is wrapped in try/catch so divergence (BACKEND_ISSUES #1) doesn't halt + * the loop — failures are logged and recorded. + * + * Waits for the player's hand size to shrink by 3 after each successful + * book scoring (book consumes exactly 3 cards from the hand). + */ +export async function autoScoreBooks( + session: SmokeSession, + gameId: Uint8Array, + playerId: 1 | 2, + turnNumber: number, +): Promise<{ scored: number[]; failed: number[] }> { + const { contract, walletProvider, read } = session; + + const hand = await readHand(session, gameId, playerId); + const counts = new Map(); + for (const c of hand) counts.set(c % 7, (counts.get(c % 7) ?? 0) + 1); + + const scored: number[] = []; + const failed: number[] = []; + + for (const [rank, count] of counts) { + if (count < 3) continue; + console.log(` ★ P${playerId} has ${count} of ${RANKS[rank]} — attempting book scoring`); + + // Read pre-call hand size so we can wait for the −3 shrink + const [h1Pre, h2Pre] = (await read<[any, any]>("getHandSizes", gameId)) as [any, any]; + const sizePre = playerId === 1 ? Number(h1Pre) : Number(h2Pre); + + try { + await callDelegated( + walletProvider, + `checkAndScoreBook:t${turnNumber}:r${rank}`, + () => contract.callTx.checkAndScoreBook(gameId, BigInt(playerId), BigInt(rank)), + { playerId }, + ); + await waitFor( + `P${playerId} hand shrinks after book of ${RANKS[rank]}`, + MIDNIGHT_WAIT_MS, + async () => { + const [h1, h2] = (await read<[any, any]>("getHandSizes", gameId)) as [any, any]; + return playerId === 1 ? Number(h1) : Number(h2); + }, + size => size <= sizePre - 3, + 2000, + ); + scored.push(rank); + console.log(` ✓ book of ${RANKS[rank]} scored`); + } catch (err) { + // BACKEND_ISSUES #1: doesPlayerHaveSpecificCard local read may give + // false positives that disagree with the contract's internal counting. + // Log and continue so the game still completes. + console.log(` [divergence] checkAndScoreBook(P${playerId}, ${RANKS[rank]}) failed: ${(err as Error).message}`); + failed.push(rank); + } + } + + return { scored, failed }; +} + +export interface GameResult { + turnsPlayed: number; + finalPhase: number; + finalScores: [number, number]; + finalHandSizes: [number, number]; + totalBooksScored: number; + divergenceCount: number; + exitReason: string; + winner: number | null; +} + +/** + * Run the full game loop: playOneTurn + autoScoreBooks until the game + * ends or we hit MAX_TURNS. Exit reasons: + * - phase == GameOver (the contract auto-ends at 7 books) + * - sum of scores == 7 (same condition, defensive) + * - playOneTurn returned `ended: true` (empty hand, etc.) + * - turn >= maxTurns (safety cap; the user's design says 20 is enough + * for a natural game) + */ +export async function runFullGame( + session: SmokeSession, + gameId: Uint8Array, + maxTurns: number = 20, +): Promise { + const { read } = session; + let turn = 0; + let exitReason = `MAX_TURNS (${maxTurns}) reached`; + let divergenceCount = 0; + + while (turn < maxTurns) { + turn++; + const result = await playOneTurn(session, gameId, turn); + + if (result.ended) { + exitReason = result.endReason ?? "playOneTurn ended"; + break; + } + + // Score any books now in the current player's hand + const books = await autoScoreBooks(session, gameId, result.playerId, turn); + divergenceCount += books.failed.length; + + // Check end-of-game conditions + const [s1, s2] = (await read<[any, any]>("getScores", gameId)) as [any, any]; + const sum = Number(s1) + Number(s2); + console.log(` scores=${Number(s1)}-${Number(s2)} (total ${sum}/7)`); + + if (sum >= 7) { + exitReason = `all 7 books scored (final ${Number(s1)}-${Number(s2)})`; + break; + } + + const phaseRaw = await read("getGamePhase", gameId); + if (!isMissing(phaseRaw) && Number(phaseRaw) === PHASE.GameOver) { + exitReason = "phase → GameOver"; + break; + } + } + + // Final state snapshot + const [fs1, fs2] = (await read<[any, any]>("getScores", gameId)) as [any, any]; + const [fh1, fh2] = (await read<[any, any]>("getHandSizes", gameId)) as [any, any]; + const finalPhase = Number(await read("getGamePhase", gameId)); + + // Winner is recorded on-chain when GameOver is reached. getWinner returns + // 0 for tie/unset, 1 for P1, 2 for P2. + let winner: number | null = null; + try { + const w = await read("getWinner", gameId); + if (!isMissing(w)) winner = Number(w); + } catch { + /* getWinner not available before GameOver — leave null */ + } + + return { + turnsPlayed: turn, + finalPhase, + finalScores: [Number(fs1), Number(fs2)], + finalHandSizes: [Number(fh1), Number(fh2)], + totalBooksScored: Number(fs1) + Number(fs2), + divergenceCount, + exitReason, + winner, + }; +} diff --git a/e2e/smoke/lobby-batcher.test.ts b/e2e/smoke/lobby-batcher.test.ts new file mode 100644 index 0000000..4c32392 --- /dev/null +++ b/e2e/smoke/lobby-batcher.test.ts @@ -0,0 +1,179 @@ +/** + * Step 4 smoke test — lobby POST to batcher (effectstreaml2, signed). + * + * Verifies we can: + * 1. Build the batcher message with createMessageForBatcher (ported from + * paima-engine's packages/effectstream-sdk/concise/src/batcher.ts) + * 2. Sign it with an ethers Wallet using a Hardhat key + * 3. POST to ${BATCHER_URL}/send-input with target="effectstreaml2" + * 4. Poll GET ${API_URL}/user_lobbies until the new lobby appears + * + * PREREQS: + * - Hardhat running (:8545) + * - Paima node running (:9996) + * - Batcher running (default :3336 — matches the repo config, not the + * upstream default of 3334) + * + * Run: deno task --filter @go-fish/e2e smoke:lobby + */ + +import { assertEquals, assertExists } from "jsr:@std/assert"; +import { ethers } from "ethers"; +import { generateStmInput } from "@paimaexample/concise"; +import { grammar } from "@go-fish/data-types"; + +const BATCHER_URL = Deno.env.get("BATCHER_URL") ?? "http://localhost:3336"; +const API_URL = Deno.env.get("API_URL") ?? "http://localhost:9996"; +const EVM_RPC_URL = Deno.env.get("EVM_RPC_URL") ?? "http://localhost:8545"; + +// Hardhat test account #1 (account #0 is typically the deployer) +const HARDHAT_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; + +// AddressType 0 = EVM. Matches paima-engine's CryptoManager registry. +const ADDRESS_TYPE_EVM = 0; + +/** + * Port of paima-engine's createMessageForBatcher + * (packages/effectstream-sdk/concise/src/batcher.ts:63-78). + * + * Concatenates the signable fields, lowercases, and replaces non-alphanumerics + * with "-". The batcher reconstructs the same string server-side and verifies + * the signature. + */ +function createMessageForBatcher( + namespace: string | null, + millisecondTimestamp: string, + walletAddress: string, + _addressType: number, + inputData: string, + target: string | undefined = undefined, +): string { + return ((namespace ?? "") + (target ?? "") + millisecondTimestamp + walletAddress + inputData) + .replace(/[^a-zA-Z0-9]/g, "-") + .toLocaleLowerCase(); +} + +async function postLobby(wallet: ethers.Wallet, conciseData: unknown[]): Promise { + const timestamp = Date.now().toString(); + const inputStr = JSON.stringify(conciseData); + + // IMPORTANT: leave `target` out of the signed message and the POST body. + // + // Why: paima-engine 0.10.24 `extractBatches` (jsr:@paimaexample/concise) + // does NOT persist the target field into the on-chain subunit. When the + // Paima node later reconstructs the signed message for verification it + // passes target=undefined → "". The batcher's own verifier uses + // `(input.target ?? "")`, so if we omit target in the POST the batcher + // also builds "" there. Both sides agree, signature verifies on both + // ends. Routing still works because the batcher uses `defaultTarget` + // when `input.target` is absent, and `effectstreaml2` is set as default + // in main.ts:42. + // + // The batcher's namespace is also "" (config.ts:13), so namespace agrees + // across: client ("") ↔ batcher ("") ↔ Paima (null → ""). + const message = createMessageForBatcher( + "", + timestamp, + wallet.address, + ADDRESS_TYPE_EVM, + inputStr, + undefined, + ); + const signature = await wallet.signMessage(message); + + const body = { + data: { + // target intentionally omitted — batcher uses defaultTarget + addressType: ADDRESS_TYPE_EVM, + address: wallet.address, + signature, + input: inputStr, + timestamp, + }, + // Use wait-receipt for the smoke so errors surface; the real e2e test + // will use no-wait and rely on the indexer/REST API for confirmation. + confirmationLevel: "wait-receipt", + }; + + console.log(` → POST ${BATCHER_URL}/send-input`); + console.log(` input=${inputStr}`); + console.log(` message=${message.slice(0, 80)}${message.length > 80 ? "..." : ""}`); + console.log(` signature=${signature.slice(0, 20)}...`); + + return fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +async function getJson(url: string): Promise { + const r = await fetch(url); + if (!r.ok) throw new Error(`GET ${url} → ${r.status}`); + return r.json(); +} + +async function pollUntil( + label: string, + fn: () => Promise, + timeoutMs: number, + intervalMs: number, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const r = await fn(); + if (r) return r; + console.log(` [poll] ${label} ... (${Math.round((deadline - Date.now()) / 1000)}s left)`); + await new Promise(res => setTimeout(res, intervalMs)); + } + throw new Error(`Timeout: ${label}`); +} + +Deno.test({ + name: "lobby: createdLobby via batcher effectstreaml2 target", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + const provider = new ethers.providers.JsonRpcProvider(EVM_RPC_URL); + const wallet = new ethers.Wallet(HARDHAT_KEY, provider); + console.log(` wallet address = ${wallet.address}`); + + // Snapshot existing lobbies to detect the newly-created one + const before = await getJson( + `${API_URL}/user_lobbies?wallet=${wallet.address.toLowerCase()}&page=0&count=50`, + ); + const existingIds = new Set((before.lobbies || []).map((l: any) => l.lobby_id)); + console.log(` existing lobbies: ${existingIds.size}`); + + // Build the concise tuple with the grammar helper so typebox validates + // our fields against the contract-side schema (minLength / maxLength / + // numeric ranges etc.). Result: ["createdLobby", "SmokeP1", "E2E-…"]. + const lobbyName = `E2E-SMOKE-${Date.now()}`; + const conciseData = generateStmInput(grammar, "createdLobby", { + playerName: "SmokeP1", + lobbyName, + }); + console.log(` concise tuple: ${JSON.stringify(conciseData)}`); + + const res = await postLobby(wallet, conciseData as unknown as unknown[]); + console.log(` batcher responded: ${res.status}`); + const bodyText = await res.text(); + console.log(` body: ${bodyText.slice(0, 200)}`); + assertEquals(res.ok, true, `batcher rejected: ${bodyText}`); + + // Poll Paima REST until the new lobby appears + const newLobby = await pollUntil( + "new lobby appears", + async () => { + const d = await getJson( + `${API_URL}/user_lobbies?wallet=${wallet.address.toLowerCase()}&page=0&count=50`, + ); + return (d.lobbies || []).find((l: any) => !existingIds.has(l.lobby_id)) || null; + }, + 30_000, + 2_000, + ); + assertExists(newLobby, "new lobby should exist"); + console.log(` ✓ lobby created via batcher: id=${newLobby.lobby_id} name=${newLobby.lobby_name ?? newLobby.name}`); + }, +}); diff --git a/frontend/src/effectstreamBridge.ts b/frontend/src/effectstreamBridge.ts index bc6d6dc..1f9062c 100644 --- a/frontend/src/effectstreamBridge.ts +++ b/frontend/src/effectstreamBridge.ts @@ -224,12 +224,12 @@ export function isWalletConnected(): boolean { } /** - * Create a new game lobby on-chain + * Create a new game lobby on-chain. + * Go Fish is always a 2-player game; no maxPlayers argument is accepted. */ export async function createLobby( playerName: string, lobbyName: string, - maxPlayers: number ): Promise<{ success: boolean; lobbyId?: string; errorMessage?: string }> { const currentWallet = await ensureWallet(); if (!currentWallet) { @@ -237,9 +237,8 @@ export async function createLobby( } try { - // Submit transaction to Paima L2 contract - // Format: createdLobby command with playerName, lobbyName, and maxPlayers - const params = ["createdLobby", playerName, lobbyName, maxPlayers]; + // Grammar expects: createdLobby|playerName|lobbyName + const params = ["createdLobby", playerName, lobbyName]; // Send transaction without waiting for processing (to avoid timeout) const result = await sendTransaction(currentWallet, params, paimaEngineConfig, "no-wait"); @@ -318,9 +317,10 @@ export async function joinLobby( } /** - * Toggle ready status in a lobby + * Close a lobby (host only, while still alone). + * Deletes the lobby on-chain when no second player has joined yet. */ -export async function toggleReady( +export async function closeLobby( lobbyId: string ): Promise<{ success: boolean; errorMessage?: string }> { const currentWallet = await ensureWallet(); @@ -329,80 +329,18 @@ export async function toggleReady( } try { - // Grammar expects: toggledReady|lobbyID - const params = ["toggledReady", lobbyId]; + // Grammar expects: closedLobby|lobbyID + const params = ["closedLobby", lobbyId]; const result = await sendTransaction(currentWallet, params, paimaEngineConfig, "no-wait"); if (!result.success) { - return { success: false, errorMessage: "Failed to toggle ready" }; + return { success: false, errorMessage: "Failed to close lobby" }; } - console.log('Toggle ready transaction submitted:', result); + console.log('Close lobby transaction submitted:', result); return { success: true }; } catch (error) { - console.error('Error toggling ready:', error); - return { - success: false, - errorMessage: error instanceof Error ? error.message : 'Unknown error', - }; - } -} - -/** - * Start game (host only) - */ -export async function startGame( - lobbyId: string -): Promise<{ success: boolean; errorMessage?: string }> { - const currentWallet = await ensureWallet(); - if (!currentWallet) { - return { success: false, errorMessage: "Failed to initialize wallet" }; - } - - try { - // Grammar expects: startedGame|lobbyID - const params = ["startedGame", lobbyId]; - const result = await sendTransaction(currentWallet, params, paimaEngineConfig, "no-wait"); - - if (!result.success) { - return { success: false, errorMessage: "Failed to start game" }; - } - - console.log('Start game transaction submitted:', result); - return { success: true }; - } catch (error) { - console.error('Error starting game:', error); - return { - success: false, - errorMessage: error instanceof Error ? error.message : 'Unknown error', - }; - } -} - -/** - * Leave a lobby - */ -export async function leaveLobby( - lobbyId: string -): Promise<{ success: boolean; errorMessage?: string }> { - const currentWallet = await ensureWallet(); - if (!currentWallet) { - return { success: false, errorMessage: "Failed to initialize wallet" }; - } - - try { - // Grammar expects: leftLobby|lobbyID - const params = ["leftLobby", lobbyId]; - const result = await sendTransaction(currentWallet, params, paimaEngineConfig, "no-wait"); - - if (!result.success) { - return { success: false, errorMessage: "Failed to leave lobby" }; - } - - console.log('Leave lobby transaction submitted:', result); - return { success: true }; - } catch (error) { - console.error('Error leaving lobby:', error); + console.error('Error closing lobby:', error); return { success: false, errorMessage: error instanceof Error ? error.message : 'Unknown error', @@ -528,9 +466,7 @@ export const EffectstreamBridge = { isWalletConnected, createLobby, joinLobby, - toggleReady, - startGame, - leaveLobby, + closeLobby, getLobbyState, getOpenLobbies, getUserLobbies, diff --git a/frontend/src/screens/LobbyListScreen.ts b/frontend/src/screens/LobbyListScreen.ts index d0bc4e3..7ed29cb 100644 --- a/frontend/src/screens/LobbyListScreen.ts +++ b/frontend/src/screens/LobbyListScreen.ts @@ -109,15 +109,7 @@ export class LobbyListScreen {
- - - 3+ players coming in a future version + Go Fish is a 2-player game. Your game will start automatically when someone joins.
`; @@ -246,11 +239,9 @@ export class LobbyListScreen { // Confirm button document.getElementById('confirm-create-btn')?.addEventListener('click', async () => { const nameInput = document.getElementById('lobby-name') as HTMLInputElement; - const maxPlayersInput = document.getElementById('max-players') as HTMLSelectElement; const confirmBtn = document.getElementById('confirm-create-btn') as HTMLButtonElement; const lobbyName = nameInput.value.trim() || `${this.gameService.getPlayerName()}'s Lobby`; - const maxPlayers = parseInt(maxPlayersInput.value); if (!this.gameService.getPlayerName()) { alert('Please enter your name first!'); @@ -265,7 +256,7 @@ export class LobbyListScreen { try { // Create lobby on-chain - const lobby = await this.gameService.createLobby(lobbyName, maxPlayers); + const lobby = await this.gameService.createLobby(lobbyName); if (!lobby) { alert('Failed to create lobby. Please try again.'); diff --git a/frontend/src/screens/LobbyScreen.ts b/frontend/src/screens/LobbyScreen.ts index 8b71495..9f797f4 100644 --- a/frontend/src/screens/LobbyScreen.ts +++ b/frontend/src/screens/LobbyScreen.ts @@ -1,20 +1,25 @@ /** - * Lobby Screen - Waiting room before game starts + * Lobby Screen - Waiting room before game starts. + * + * Go Fish is always 2 players. The second `joinedLobby` auto-starts the game, + * so this screen is almost always shown to the host while they wait for an + * opponent. It also appears briefly to a joiner in the window between + * submitting their join tx and the state machine flipping the lobby to + * `in_progress`. + * + * Actions: + * - Host, alone: "Cancel Lobby" → closedLobby (deletes the lobby). + * - Anyone: auto-navigate to the game screen when status becomes in_progress. */ import { GoFishGameService } from '../services/GoFishGameService'; -import type { GoFishGameState, GoFishPlayer } from '../../../shared/data-types/src/go-fish-types'; -import * as EffectstreamBridge from '../effectstreamBridge'; -// Type for lobby state response interface LobbyStateResponse { players: Array<{ wallet_address: string; account_id: number; player_name: string; - is_ready: boolean; }>; - max_players: number; host_account_id: number; lobby_name: string; status: string; @@ -25,15 +30,12 @@ export class LobbyScreen { private gameService: GoFishGameService; private lobbyId: string = ''; private refreshInterval?: number; - private previousLobbyState: LobbyStateResponse | null = null; - private hasRenderedOnce: boolean = false; - - // Track pending transactions to prevent UI updates while processing - private pendingReady: boolean = false; - private _pendingLeave: boolean = false; - private _pendingStart: boolean = false; private fetchFailCount: number = 0; - private static readonly MAX_FETCH_FAILS = 5; // Allow up to 5 consecutive 404s before navigating away + private cancelPending: boolean = false; + + // Tolerate a few consecutive failures while the lobby row is being indexed. + // A sustained fail likely means the lobby was deleted (host cancelled). + private static readonly MAX_FETCH_FAILS = 5; constructor(container: HTMLElement) { this.container = container; @@ -42,67 +44,41 @@ export class LobbyScreen { async show(lobbyId: string) { this.lobbyId = lobbyId; - this.hasRenderedOnce = false; - this.previousLobbyState = null; this.fetchFailCount = 0; + this.cancelPending = false; await this.render(); - // Poll every 3 seconds to reduce database pressure - // This prevents mutex deadlocks during block processing and Midnight queries + // Poll every 3 seconds to pick up the status flip without hammering the DB. this.refreshInterval = window.setInterval(() => this.render(), 3000); } hide() { if (this.refreshInterval) { clearInterval(this.refreshInterval); + this.refreshInterval = undefined; } } private async render() { - // Fetch lobby state from API const { API_BASE_URL } = await import('../apiConfig'); const response = await fetch(`${API_BASE_URL}/lobby_state?lobby_id=${this.lobbyId}`); + if (!response.ok) { this.fetchFailCount++; - // After lobby creation, EffectStream may not have processed the transaction yet. - // Tolerate several consecutive failures before giving up. if (this.fetchFailCount >= LobbyScreen.MAX_FETCH_FAILS) { - console.error(`Failed to fetch lobby state ${this.fetchFailCount} times, navigating away`); + console.warn('[LobbyScreen] Lobby no longer available, returning to list'); this.dispatchEvent('navigate', { screen: 'lobby-list' }); } else { - console.warn(`Lobby state not available yet (attempt ${this.fetchFailCount}/${LobbyScreen.MAX_FETCH_FAILS}), will retry...`); - // Show a loading state if we haven't rendered anything yet - if (!this.hasRenderedOnce) { - this.container.innerHTML = ` -
-

Loading lobby...

-
- `; - } + this.renderLoading(); } return; } - // Reset fail counter on success this.fetchFailCount = 0; const lobbyData: LobbyStateResponse = await response.json(); - if (!lobbyData) { - this.dispatchEvent('navigate', { screen: 'lobby-list' }); - return; - } - - // Extract data from API response - const players = lobbyData.players || []; - const playerCount = players.length; - const maxPlayers = lobbyData.max_players; - const hostAccountId = lobbyData.host_account_id; - const lobbyName = lobbyData.lobby_name; - const status = lobbyData.status; - - // If game started, navigate to game screen - if (status === 'in_progress') { - // Stop polling before navigating to prevent duplicate navigation events + // Auto-navigate once the state machine has flipped status to in_progress. + if (lobbyData.status === 'in_progress') { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = undefined; @@ -111,319 +87,130 @@ export class LobbyScreen { return; } - // Find current player + const players = lobbyData.players || []; const myWalletAddress = this.gameService.getPlayerId(); - const currentPlayer = players.find((p: any) => p.wallet_address.toLowerCase() === myWalletAddress.toLowerCase()); - - // Check if current user is host (compare account IDs) - const isHost = currentPlayer && currentPlayer.account_id === hostAccountId; - - // Check if all players are ready and there are at least 2 players - const allReady = playerCount >= 2 && players.every((p: any) => p.is_ready || p.account_id === hostAccountId); - - // Check if we need a full render or can do selective updates - if (!this.hasRenderedOnce || this.needsFullRender(lobbyData)) { - this.fullRender(players, playerCount, maxPlayers, hostAccountId, lobbyName, currentPlayer, isHost ?? false, allReady); - this.hasRenderedOnce = true; - } else { - this.selectiveUpdate(players, playerCount, maxPlayers, hostAccountId, currentPlayer, isHost ?? false, allReady); - } - - // Store for comparison on next render - this.previousLobbyState = lobbyData; + const currentPlayer = players.find( + (p: any) => p.wallet_address?.toLowerCase() === myWalletAddress.toLowerCase() + ); + const isHost = currentPlayer?.account_id === lobbyData.host_account_id; + + this.renderWaiting( + lobbyData.lobby_name, + players, + lobbyData.host_account_id, + isHost, + ); } - /** - * Check if state changes require a full re-render - */ - private needsFullRender(currentState: LobbyStateResponse): boolean { - if (!this.previousLobbyState) return true; - - // Full render needed if player count changed - if (this.previousLobbyState.players.length !== currentState.players.length) return true; - - // Full render needed if host status changed (shouldn't happen but just in case) - if (this.previousLobbyState.host_account_id !== currentState.host_account_id) return true; - - return false; + private renderLoading() { + this.container.innerHTML = ` +
+

Loading lobby…

+
+ `; } - /** - * Perform a full DOM render - */ - private fullRender( + private renderWaiting( + lobbyName: string, players: any[], - playerCount: number, - maxPlayers: number, hostAccountId: number, - lobbyName: string, - currentPlayer: any, isHost: boolean, - allReady: boolean ) { const myWalletAddress = this.gameService.getPlayerId(); + const playerCount = players.length; + + // The only surface action is Cancel, and only the host sees it. + const cancelButton = isHost + ? `` + : ''; + + const infoText = isHost + ? (playerCount < 2 ? 'Waiting for an opponent to join…' : 'Starting game…') + : 'Starting game…'; this.container.innerHTML = `

🎣 ${lobbyName}

- + ${cancelButton}
-
-

Players (${playerCount}/${maxPlayers})

+

Players (${playerCount}/2)

- ${players.map((p: any) => this.renderPlayerFromAPI(p, hostAccountId, myWalletAddress)).join('')} + ${players.map((p: any) => this.renderPlayer(p, hostAccountId, myWalletAddress)).join('')}
- ${!isHost ? ` -
- -
- ` : ''} - - ${isHost ? ` -
-

- ${allReady - ? 'All players are ready!' - : `Need ${playerCount < 2 ? (2 - playerCount) + ' more player(s) and ' : ''}all players to ready up` - } -

- -
- ` : ''} +
+

${infoText}

+
-

How to Play Go Fish

    -
  • Objective: Collect the most "books" (sets of 4 cards of the same rank)
  • -
  • Your Turn: Ask any player for cards of a specific rank you have in your hand
  • -
  • Success: If they have cards of that rank, they give them all to you and you go again
  • -
  • Go Fish!: If they don't have any, you draw a card from the deck
  • -
  • Books: When you collect all 4 cards of a rank, they automatically form a book
  • +
  • Objective: Collect the most "books" (sets of 3 cards of the same rank).
  • +
  • Your Turn: Ask the other player for cards of a rank you already hold.
  • +
  • Success: If they have cards of that rank, they give them all to you and you go again.
  • +
  • Go Fish!: If they don't, you draw a card from the deck.
  • Winning: The player with the most books when the deck runs out wins!
`; - this.attachEventListeners(); + this.attachEventListeners(isHost); } - /** - * Perform selective DOM updates (preserves existing DOM structure) - */ - private selectiveUpdate( - players: any[], - playerCount: number, - maxPlayers: number, - hostAccountId: number, - currentPlayer: any, - isHost: boolean, - allReady: boolean - ) { - const myWalletAddress = this.gameService.getPlayerId(); - - // Update player count header - const playersHeader = this.container.querySelector('.players-panel h2'); - if (playersHeader) { - playersHeader.textContent = `Players (${playerCount}/${maxPlayers})`; - } - - // Update players list - const playersList = this.container.querySelector('.players-list'); - if (playersList) { - playersList.innerHTML = players.map((p: any) => this.renderPlayerFromAPI(p, hostAccountId, myWalletAddress)).join(''); - } - - // Update ready button state - const readyBtn = document.getElementById('toggle-ready-btn') as HTMLButtonElement; - if (readyBtn && !isHost) { - // Check if ready state changed from previous (transaction completed) - if (this.pendingReady && this.previousLobbyState) { - const prevPlayer = this.previousLobbyState.players.find( - (p: any) => p.wallet_address.toLowerCase() === myWalletAddress.toLowerCase() - ); - if (prevPlayer && prevPlayer.is_ready !== currentPlayer?.is_ready) { - // State changed - transaction completed - this.pendingReady = false; - } - } - - // Only update button if not pending - if (!this.pendingReady) { - readyBtn.className = `btn ${currentPlayer?.is_ready ? 'btn-secondary' : 'btn-primary'}`; - readyBtn.textContent = currentPlayer?.is_ready ? 'Not Ready' : 'Ready Up!'; - readyBtn.disabled = false; - } - } - - // Update host section info text - if (isHost) { - const infoText = this.container.querySelector('.host-section .info-text'); - if (infoText) { - infoText.textContent = allReady - ? 'All players are ready!' - : `Need ${playerCount < 2 ? (2 - playerCount) + ' more player(s) and ' : ''}all players to ready up`; - } - - const startBtn = document.getElementById('start-game-btn') as HTMLButtonElement; - if (startBtn) { - startBtn.disabled = !allReady; - } - } - } - - private _renderPlayer(player: GoFishPlayer, game: GoFishGameState): string { - const isHost = player.id === game.hostId; - const isCurrentPlayer = player.id === this.gameService.getPlayerId(); - - return ` -
- - ${player.name} - ${isHost ? 'Host' : ''} - ${isCurrentPlayer ? 'You' : ''} - - - ${player.isReady ? '✓ Ready' : '○ Not Ready'} - -
- `; - } - - private renderPlayerFromAPI(player: any, hostAccountId: number, myWalletAddress: string): string { - const isHost = player.account_id === hostAccountId; - const isCurrentPlayer = player.wallet_address.toLowerCase() === myWalletAddress.toLowerCase(); - + private renderPlayer(player: any, hostAccountId: number, myWalletAddress: string): string { + const playerIsHost = player.account_id === hostAccountId; + const isMe = player.wallet_address?.toLowerCase() === myWalletAddress.toLowerCase(); return ` -
+
${player.player_name} - ${isHost ? 'Host' : ''} - ${isCurrentPlayer ? 'You' : ''} - - - ${player.is_ready ? '✓ Ready' : '○ Not Ready'} + ${playerIsHost ? 'Host' : ''} + ${isMe ? 'You' : ''}
`; } - private attachEventListeners() { - // Leave lobby - document.getElementById('leave-lobby-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('leave-lobby-btn') as HTMLButtonElement; - if (btn) { - btn.disabled = true; - btn.textContent = 'Leaving...'; - } - this._pendingLeave = true; + private attachEventListeners(isHost: boolean) { + if (!isHost) return; - try { - const result = await this.gameService.leaveLobby(this.lobbyId); - if (result) { - this.dispatchEvent('navigate', { screen: 'lobby-list' }); - } else { - console.error('Failed to leave lobby'); - alert('Failed to leave lobby. Please try again.'); - if (btn) { - btn.disabled = false; - btn.textContent = 'Leave Lobby'; - } - } - } catch (error) { - console.error('Error leaving lobby:', error); - alert('Failed to leave lobby. Please try again.'); - if (btn) { - btn.disabled = false; - btn.textContent = 'Leave Lobby'; - } - } finally { - this._pendingLeave = false; - } - }); + document.getElementById('cancel-lobby-btn')?.addEventListener('click', async () => { + if (this.cancelPending) return; + this.cancelPending = true; - // Ready toggle - document.getElementById('toggle-ready-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('toggle-ready-btn') as HTMLButtonElement; + const btn = document.getElementById('cancel-lobby-btn') as HTMLButtonElement; if (btn) { btn.disabled = true; - btn.textContent = 'Processing...'; + btn.textContent = 'Cancelling…'; } - this.pendingReady = true; try { - const result = await EffectstreamBridge.toggleReady(this.lobbyId); - if (!result.success) { - console.error('Failed to toggle ready:', result.errorMessage); - alert('Failed to toggle ready status. Please try again.'); - // Re-enable button on error - if (btn) { - btn.disabled = false; - btn.textContent = 'Ready Up!'; - } - this.pendingReady = false; - } - // On success, pendingReady stays true until the next render detects the state change - // Then selectiveUpdate will re-enable the button with the correct text - } catch (error) { - console.error('Error toggling ready:', error); - if (btn) { - btn.disabled = false; - btn.textContent = 'Ready Up!'; - } - this.pendingReady = false; - } - }); - - // Start game - document.getElementById('start-game-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('start-game-btn') as HTMLButtonElement; - if (btn) { - btn.disabled = true; - btn.textContent = 'Starting...'; - } - this._pendingStart = true; - - try { - const result = await EffectstreamBridge.startGame(this.lobbyId); - if (result.success) { - // Game started - the lobby status will update to 'in_progress' - // and the screen will automatically navigate when it refreshes - console.log('Game started successfully'); - // Keep pendingStart true - we'll navigate away soon + const ok = await this.gameService.closeLobby(this.lobbyId); + if (ok) { + this.dispatchEvent('navigate', { screen: 'lobby-list' }); } else { - console.error('Failed to start game:', result.errorMessage); - alert('Failed to start game. Please try again.'); - // Re-enable button on error + alert('Failed to cancel the lobby. It may already have a second player.'); if (btn) { btn.disabled = false; - btn.textContent = 'Start Game'; + btn.textContent = 'Cancel Lobby'; } - this._pendingStart = false; + this.cancelPending = false; } - } catch (error) { - console.error('Error starting game:', error); + } catch (err) { + console.error('[LobbyScreen] Error cancelling lobby:', err); + alert('Failed to cancel the lobby. Please try again.'); if (btn) { btn.disabled = false; - btn.textContent = 'Start Game'; + btn.textContent = 'Cancel Lobby'; } - this._pendingStart = false; + this.cancelPending = false; } }); } diff --git a/frontend/src/services/GoFishGameService.ts b/frontend/src/services/GoFishGameService.ts index 5c0bf99..85c9c1a 100644 --- a/frontend/src/services/GoFishGameService.ts +++ b/frontend/src/services/GoFishGameService.ts @@ -12,8 +12,6 @@ import type { } from '../../../shared/data-types/src/go-fish-types'; import { - createDeck, - shuffleDeck, checkForBook, removeBook, getCardsOfRank, @@ -71,11 +69,9 @@ export class GoFishGameService { return this.playerName; } - // Lobby management - async createLobby(lobbyName: string, maxPlayers: number): Promise { - // Call blockchain middleware to create lobby - // Pass both player name and lobby name - const result = await EffectstreamBridge.createLobby(this.playerName, lobbyName, maxPlayers); + // Lobby management — Go Fish is always a 2-player game. + async createLobby(lobbyName: string): Promise { + const result = await EffectstreamBridge.createLobby(this.playerName, lobbyName); if (!result.success) { console.error('Failed to create lobby:', result.errorMessage); @@ -86,25 +82,24 @@ export class GoFishGameService { const lobbyId = result.lobbyId || `lobby_${Date.now()}`; const lobby: Lobby = { id: lobbyId, - name: lobbyName, // Use the lobby name from user input for local state + name: lobbyName, hostId: this.playerId, hostName: this.playerName, playerCount: 0, - maxPlayers, status: 'waiting', createdAt: Date.now(), }; this.lobbies.set(lobbyId, lobby); - // Create game state + // Create local game state (in-memory mirror; actual game runs on Midnight). const game: GoFishGameState = { id: lobbyId, status: 'waiting', phase: 'lobby', round: 0, players: [], - maxPlayers, + maxPlayers: 2, hostId: this.playerId, currentTurnIndex: 0, deck: [], @@ -144,7 +139,6 @@ export class GoFishGameService { hostId: apiLobby.host_account_id?.toString() || '', hostName: apiLobby.host_name || 'Unknown', playerCount: parseInt(apiLobby.player_count) || 0, - maxPlayers: apiLobby.max_players, status: apiLobby.status === 'open' ? 'waiting' as const : 'in_progress' as const, createdAt: new Date(apiLobby.created_at).getTime(), isPlayerInLobby: apiLobby.is_player_in_lobby === true, @@ -179,80 +173,18 @@ export class GoFishGameService { return true; } - async leaveLobby(lobbyId: string): Promise { - // Submit leave transaction to blockchain - const result = await EffectstreamBridge.leaveLobby(lobbyId); + /** + * Host-only: cancel an open lobby before a second player has joined. + */ + async closeLobby(lobbyId: string): Promise { + const result = await EffectstreamBridge.closeLobby(lobbyId); if (!result.success) { - console.error('Failed to leave lobby:', result.errorMessage); + console.error('Failed to close lobby:', result.errorMessage); return false; } - console.log('Leave lobby transaction submitted successfully'); - return true; - } - - toggleReady(lobbyId: string): void { - const game = this.games.get(lobbyId); - if (!game) return; - - const player = game.players.find(p => p.id === this.playerId); - if (player) { - player.isReady = !player.isReady; - } - } - - canStartGame(lobbyId: string): boolean { - const game = this.games.get(lobbyId); - if (!game) return false; - - return ( - game.players.length >= 2 && - game.players.every(p => p.isReady || p.id === game.hostId) - ); - } - - startGame(lobbyId: string): boolean { - const game = this.games.get(lobbyId); - const lobby = this.lobbies.get(lobbyId); - - if (!game || !lobby || !this.canStartGame(lobbyId)) { - return false; - } - - // Initialize deck and deal cards - game.deck = shuffleDeck(createDeck()); - game.deckCount = game.deck.length; - game.status = 'in_progress'; - game.phase = 'dealing'; - game.startedAt = Date.now(); - game.round = 1; - game.currentTurnIndex = 0; - - lobby.status = 'in_progress'; - - // Deal cards (5-7 cards per player depending on player count) - const cardsPerPlayer = game.players.length <= 3 ? 7 : 5; - - for (const player of game.players) { - for (let i = 0; i < cardsPerPlayer; i++) { - const card = game.deck.pop(); - if (card) { - player.hand.push(card); - } - } - player.hand = sortCards(player.hand); - player.cardCount = player.hand.length; - - // Check for initial books - this.checkAndCompleteBooks(game, player); - } - - game.deckCount = game.deck.length; - game.phase = 'playing'; - - this.addSystemMessage(lobbyId, `Game started! ${game.players[0].name}'s turn.`); - + console.log('Close lobby transaction submitted successfully'); return true; } diff --git a/packages/client/database/migrations/database.sql b/packages/client/database/migrations/database.sql index f61f866..029ca17 100644 --- a/packages/client/database/migrations/database.sql +++ b/packages/client/database/migrations/database.sql @@ -13,11 +13,11 @@ CREATE TABLE IF NOT EXISTS user_game_state ( ); -- Lobbies table (game lobbies before the game starts) +-- Go Fish is always a 2-player game; max_players is a code constant, not a column. CREATE TABLE IF NOT EXISTS lobbies ( lobby_id TEXT PRIMARY KEY, lobby_name TEXT NOT NULL, host_account_id INTEGER NOT NULL, - max_players INTEGER NOT NULL DEFAULT 4, status TEXT NOT NULL DEFAULT 'open', -- open, in_progress, finished created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, started_at TIMESTAMP, @@ -25,11 +25,11 @@ CREATE TABLE IF NOT EXISTS lobbies ( ); -- Lobby players table (tracks players in each lobby) +-- No ready flag: join auto-starts the game when the lobby fills. CREATE TABLE IF NOT EXISTS lobby_players ( lobby_id TEXT NOT NULL, account_id INTEGER NOT NULL, player_name TEXT NOT NULL, - is_ready BOOLEAN DEFAULT false, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (lobby_id, account_id) ); diff --git a/packages/client/database/src/lobby-queries.queries.ts b/packages/client/database/src/lobby-queries.queries.ts index 8af749e..8188749 100644 --- a/packages/client/database/src/lobby-queries.queries.ts +++ b/packages/client/database/src/lobby-queries.queries.ts @@ -1,115 +1,97 @@ /** Types generated for queries found in "src/lobby-queries.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; -/** Query 'CreateLobby' is invalid, so its result is assigned type 'never'. - * */ -export type CreateLobbyResult = never; +/** 'CreateLobby' parameters type */ +export type ICreateLobbyParams = { + lobbyId: string; + lobbyName: string; + hostAccountId: number; +}; -/** Query 'CreateLobby' is invalid, so its parameters are assigned type 'never'. - * */ -export type CreateLobbyParams = never; +/** 'CreateLobby' return type */ +export type ICreateLobbyResult = any; -const createLobbyIR: any = {"usedParamSet":{"lobbyId":true,"lobbyName":true,"hostAccountId":true,"maxPlayers":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":89,"b":96}]},{"name":"lobbyName","required":false,"transform":{"type":"scalar"},"locs":[{"a":99,"b":108}]},{"name":"hostAccountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":111,"b":124}]},{"name":"maxPlayers","required":false,"transform":{"type":"scalar"},"locs":[{"a":127,"b":137}]}],"statement":"INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, max_players, status)\nVALUES (:lobbyId, :lobbyName, :hostAccountId, :maxPlayers, 'open')\nRETURNING *"}; +const createLobbyIR: any = {"usedParamSet":{"lobbyId":true,"lobbyName":true,"hostAccountId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":76,"b":83}]},{"name":"lobbyName","required":false,"transform":{"type":"scalar"},"locs":[{"a":86,"b":95}]},{"name":"hostAccountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":98,"b":111}]}],"statement":"INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, status)\nVALUES (:lobbyId, :lobbyName, :hostAccountId, 'open')\nRETURNING *"}; /** - * Query generated from SQL: * ``` - * INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, max_players, status) - * VALUES (:lobbyId, :lobbyName, :hostAccountId, :maxPlayers, 'open') + * INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, status) + * VALUES (:lobbyId, :lobbyName, :hostAccountId, 'open') * RETURNING * * ``` */ -export const createLobby = new PreparedQuery(createLobbyIR); +export const createLobby = new PreparedQuery(createLobbyIR); -/** Query 'JoinLobby' is invalid, so its result is assigned type 'never'. - * */ -export type JoinLobbyResult = never; +/** 'JoinLobby' parameters type */ +export type IJoinLobbyParams = { + lobbyId: string; + accountId: number; + playerName: string; +}; -/** Query 'JoinLobby' is invalid, so its parameters are assigned type 'never'. - * */ -export type JoinLobbyParams = never; +/** 'JoinLobby' return type */ +export type IJoinLobbyResult = any; -const joinLobbyIR: any = {"usedParamSet":{"lobbyId":true,"accountId":true,"playerName":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":80,"b":87}]},{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":90,"b":99}]},{"name":"playerName","required":false,"transform":{"type":"scalar"},"locs":[{"a":102,"b":112}]}],"statement":"INSERT INTO lobby_players (lobby_id, account_id, player_name, is_ready)\nVALUES (:lobbyId, :accountId, :playerName, false)\nON CONFLICT (lobby_id, account_id) DO NOTHING\nRETURNING *"}; +const joinLobbyIR: any = {"usedParamSet":{"lobbyId":true,"accountId":true,"playerName":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":70,"b":77}]},{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":80,"b":89}]},{"name":"playerName","required":false,"transform":{"type":"scalar"},"locs":[{"a":92,"b":102}]}],"statement":"INSERT INTO lobby_players (lobby_id, account_id, player_name)\nVALUES (:lobbyId, :accountId, :playerName)\nON CONFLICT (lobby_id, account_id) DO NOTHING\nRETURNING *"}; /** - * Query generated from SQL: * ``` - * INSERT INTO lobby_players (lobby_id, account_id, player_name, is_ready) - * VALUES (:lobbyId, :accountId, :playerName, false) + * INSERT INTO lobby_players (lobby_id, account_id, player_name) + * VALUES (:lobbyId, :accountId, :playerName) * ON CONFLICT (lobby_id, account_id) DO NOTHING * RETURNING * * ``` */ -export const joinLobby = new PreparedQuery(joinLobbyIR); +export const joinLobby = new PreparedQuery(joinLobbyIR); -/** Query 'LeaveLobby' is invalid, so its result is assigned type 'never'. - * */ -export type LeaveLobbyResult = never; +/** 'GetOpenLobbies' parameters type */ +export type IGetOpenLobbiesParams = { + count: number; + offset: number; +}; -/** Query 'LeaveLobby' is invalid, so its parameters are assigned type 'never'. - * */ -export type LeaveLobbyParams = never; +/** 'GetOpenLobbies' return type */ +export type IGetOpenLobbiesResult = any; -const leaveLobbyIR: any = {"usedParamSet":{"lobbyId":true,"accountId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":43,"b":50}]},{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":69,"b":78}]}],"statement":"DELETE FROM lobby_players\nWHERE lobby_id = :lobbyId AND account_id = :accountId"}; +const getOpenLobbiesIR: any = {"usedParamSet":{"count":true,"offset":true},"params":[{"name":"count","required":false,"transform":{"type":"scalar"},"locs":[{"a":281,"b":286}]},{"name":"offset","required":false,"transform":{"type":"scalar"},"locs":[{"a":295,"b":301}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.status,\n l.created_at,\n (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count\nFROM lobbies l\nWHERE l.status = 'open'\n AND l.created_at > NOW() - INTERVAL '10 minutes'\nORDER BY l.created_at DESC\nLIMIT :count OFFSET :offset"}; /** - * Query generated from SQL: - * ``` - * DELETE FROM lobby_players - * WHERE lobby_id = :lobbyId AND account_id = :accountId - * ``` - */ -export const leaveLobby = new PreparedQuery(leaveLobbyIR); - - -/** Query 'GetOpenLobbies' is invalid, so its result is assigned type 'never'. - * */ -export type GetOpenLobbiesResult = never; - -/** Query 'GetOpenLobbies' is invalid, so its parameters are assigned type 'never'. - * */ -export type GetOpenLobbiesParams = never; - -const getOpenLobbiesIR: any = {"usedParamSet":{"count":true,"offset":true},"params":[{"name":"count","required":false,"transform":{"type":"scalar"},"locs":[{"a":249,"b":254}]},{"name":"offset","required":false,"transform":{"type":"scalar"},"locs":[{"a":263,"b":269}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.max_players,\n l.status,\n l.created_at,\n (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count\nFROM lobbies l\nWHERE l.status = 'open'\nORDER BY l.created_at DESC\nLIMIT :count OFFSET :offset"}; - -/** - * Query generated from SQL: * ``` * SELECT * l.lobby_id, * l.lobby_name, - * l.max_players, * l.status, * l.created_at, * (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count * FROM lobbies l * WHERE l.status = 'open' + * AND l.created_at > NOW() - INTERVAL '10 minutes' * ORDER BY l.created_at DESC * LIMIT :count OFFSET :offset * ``` */ -export const getOpenLobbies = new PreparedQuery(getOpenLobbiesIR); +export const getOpenLobbies = new PreparedQuery(getOpenLobbiesIR); -/** Query 'GetUserLobbies' is invalid, so its result is assigned type 'never'. - * */ -export type GetUserLobbiesResult = never; +/** 'GetUserLobbies' parameters type */ +export type IGetUserLobbiesParams = { + accountId: number; + count: number; + offset: number; +}; -/** Query 'GetUserLobbies' is invalid, so its parameters are assigned type 'never'. - * */ -export type GetUserLobbiesParams = never; +/** 'GetUserLobbies' return type */ +export type IGetUserLobbiesResult = any; -const getUserLobbiesIR: any = {"usedParamSet":{"accountId":true,"count":true,"offset":true},"params":[{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":270,"b":279}]},{"name":"count","required":false,"transform":{"type":"scalar"},"locs":[{"a":314,"b":319}]},{"name":"offset","required":false,"transform":{"type":"scalar"},"locs":[{"a":328,"b":334}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.max_players,\n l.status,\n l.created_at,\n (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count\nFROM lobbies l\nINNER JOIN lobby_players lp ON l.lobby_id = lp.lobby_id\nWHERE lp.account_id = :accountId\nORDER BY l.created_at DESC\nLIMIT :count OFFSET :offset"}; +const getUserLobbiesIR: any = {"usedParamSet":{"accountId":true,"count":true,"offset":true},"params":[{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":251,"b":260}]},{"name":"count","required":false,"transform":{"type":"scalar"},"locs":[{"a":295,"b":300}]},{"name":"offset","required":false,"transform":{"type":"scalar"},"locs":[{"a":309,"b":315}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.status,\n l.created_at,\n (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count\nFROM lobbies l\nINNER JOIN lobby_players lp ON l.lobby_id = lp.lobby_id\nWHERE lp.account_id = :accountId\nORDER BY l.created_at DESC\nLIMIT :count OFFSET :offset"}; /** - * Query generated from SQL: * ``` * SELECT * l.lobby_id, * l.lobby_name, - * l.max_players, * l.status, * l.created_at, * (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count @@ -120,27 +102,25 @@ const getUserLobbiesIR: any = {"usedParamSet":{"accountId":true,"count":true,"of * LIMIT :count OFFSET :offset * ``` */ -export const getUserLobbies = new PreparedQuery(getUserLobbiesIR); +export const getUserLobbies = new PreparedQuery(getUserLobbiesIR); -/** Query 'GetLobbyState' is invalid, so its result is assigned type 'never'. - * */ -export type GetLobbyStateResult = never; +/** 'GetLobbyState' parameters type */ +export type IGetLobbyStateParams = { + lobbyId: string; +}; -/** Query 'GetLobbyState' is invalid, so its parameters are assigned type 'never'. - * */ -export type GetLobbyStateParams = never; +/** 'GetLobbyState' return type */ +export type IGetLobbyStateResult = any; -const getLobbyStateIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":166,"b":173}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.host_account_id,\n l.max_players,\n l.status,\n l.created_at,\n l.started_at\nFROM lobbies l\nWHERE l.lobby_id = :lobbyId"}; +const getLobbyStateIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":147,"b":154}]}],"statement":"SELECT\n l.lobby_id,\n l.lobby_name,\n l.host_account_id,\n l.status,\n l.created_at,\n l.started_at\nFROM lobbies l\nWHERE l.lobby_id = :lobbyId"}; /** - * Query generated from SQL: * ``` * SELECT * l.lobby_id, * l.lobby_name, * l.host_account_id, - * l.max_players, * l.status, * l.created_at, * l.started_at @@ -148,26 +128,24 @@ const getLobbyStateIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name": * WHERE l.lobby_id = :lobbyId * ``` */ -export const getLobbyState = new PreparedQuery(getLobbyStateIR); +export const getLobbyState = new PreparedQuery(getLobbyStateIR); -/** Query 'GetLobbyPlayers' is invalid, so its result is assigned type 'never'. - * */ -export type GetLobbyPlayersResult = never; +/** 'GetLobbyPlayers' parameters type */ +export type IGetLobbyPlayersParams = { + lobbyId: string; +}; -/** Query 'GetLobbyPlayers' is invalid, so its parameters are assigned type 'never'. - * */ -export type GetLobbyPlayersParams = never; +/** 'GetLobbyPlayers' return type */ +export type IGetLobbyPlayersResult = any; -const getLobbyPlayersIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":222,"b":229}]}],"statement":"SELECT\n lp.account_id,\n lp.player_name,\n lp.is_ready,\n lp.joined_at,\n a.address as wallet_address\nFROM lobby_players lp\nINNER JOIN effectstream.accounts a ON lp.account_id = a.account_id\nWHERE lp.lobby_id = :lobbyId\nORDER BY lp.joined_at ASC"}; +const getLobbyPlayersIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":205,"b":212}]}],"statement":"SELECT\n lp.account_id,\n lp.player_name,\n lp.joined_at,\n a.address as wallet_address\nFROM lobby_players lp\nINNER JOIN effectstream.accounts a ON lp.account_id = a.account_id\nWHERE lp.lobby_id = :lobbyId\nORDER BY lp.joined_at ASC"}; /** - * Query generated from SQL: * ``` * SELECT * lp.account_id, * lp.player_name, - * lp.is_ready, * lp.joined_at, * a.address as wallet_address * FROM lobby_players lp @@ -176,72 +154,79 @@ const getLobbyPlayersIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name * ORDER BY lp.joined_at ASC * ``` */ -export const getLobbyPlayers = new PreparedQuery(getLobbyPlayersIR); +export const getLobbyPlayers = new PreparedQuery(getLobbyPlayersIR); -/** Query 'TogglePlayerReady' is invalid, so its result is assigned type 'never'. - * */ -export type TogglePlayerReadyResult = never; +/** 'CountLobbyPlayers' parameters type */ +export type ICountLobbyPlayersParams = { + lobbyId: string; +}; -/** Query 'TogglePlayerReady' is invalid, so its parameters are assigned type 'never'. - * */ -export type TogglePlayerReadyParams = never; +/** 'CountLobbyPlayers' return type */ +export type ICountLobbyPlayersResult = any; -const togglePlayerReadyIR: any = {"usedParamSet":{"lobbyId":true,"accountId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":66,"b":73}]},{"name":"accountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":92,"b":101}]}],"statement":"UPDATE lobby_players\nSET is_ready = NOT is_ready\nWHERE lobby_id = :lobbyId AND account_id = :accountId\nRETURNING is_ready"}; +const countLobbyPlayersIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":66,"b":73}]}],"statement":"SELECT COUNT(*)::int as count FROM lobby_players WHERE lobby_id = :lobbyId"}; /** - * Query generated from SQL: * ``` - * UPDATE lobby_players - * SET is_ready = NOT is_ready - * WHERE lobby_id = :lobbyId AND account_id = :accountId - * RETURNING is_ready + * SELECT COUNT(*)::int as count FROM lobby_players WHERE lobby_id = :lobbyId * ``` */ -export const togglePlayerReady = new PreparedQuery(togglePlayerReadyIR); +export const countLobbyPlayers = new PreparedQuery(countLobbyPlayersIR); -/** Query 'StartGame' is invalid, so its result is assigned type 'never'. - * */ -export type StartGameResult = never; +/** 'StartGame' parameters type */ +export type IStartGameParams = { + lobbyId: string; +}; -/** Query 'StartGame' is invalid, so its parameters are assigned type 'never'. - * */ -export type StartGameParams = never; +/** 'StartGame' return type */ +export type IStartGameResult = any; -const startGameIR: any = {"usedParamSet":{"lobbyId":true,"hostAccountId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":98}]},{"name":"hostAccountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":122,"b":135}]}],"statement":"UPDATE lobbies\nSET status = 'in_progress', started_at = CURRENT_TIMESTAMP\nWHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId\nRETURNING *"}; +const startGameIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":91,"b":98}]}],"statement":"UPDATE lobbies\nSET status = 'in_progress', started_at = CURRENT_TIMESTAMP\nWHERE lobby_id = :lobbyId AND status = 'open'\nRETURNING *"}; /** - * Query generated from SQL: * ``` * UPDATE lobbies * SET status = 'in_progress', started_at = CURRENT_TIMESTAMP - * WHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId + * WHERE lobby_id = :lobbyId AND status = 'open' * RETURNING * * ``` */ -export const startGame = new PreparedQuery(startGameIR); +export const startGame = new PreparedQuery(startGameIR); -/** Query 'CloseLobby' is invalid, so its result is assigned type 'never'. - * */ -export type CloseLobbyResult = never; +/** 'DeleteLobbyPlayers' parameters type */ +export type IDeleteLobbyPlayersParams = { + lobbyId: string; +}; -/** Query 'CloseLobby' is invalid, so its parameters are assigned type 'never'. - * */ -export type CloseLobbyParams = never; +/** 'DeleteLobbyPlayers' return type */ +export type IDeleteLobbyPlayersResult = any; -const closeLobbyIR: any = {"usedParamSet":{"lobbyId":true,"hostAccountId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":86,"b":93}]},{"name":"hostAccountId","required":false,"transform":{"type":"scalar"},"locs":[{"a":117,"b":130}]}],"statement":"UPDATE lobbies\nSET status = 'finished', ended_at = CURRENT_TIMESTAMP\nWHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId\nRETURNING *"}; +const deleteLobbyPlayersIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":43,"b":50}]}],"statement":"DELETE FROM lobby_players WHERE lobby_id = :lobbyId"}; /** - * Query generated from SQL: * ``` - * UPDATE lobbies - * SET status = 'finished', ended_at = CURRENT_TIMESTAMP - * WHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId - * RETURNING * + * DELETE FROM lobby_players WHERE lobby_id = :lobbyId * ``` */ -export const closeLobby = new PreparedQuery(closeLobbyIR); +export const deleteLobbyPlayers = new PreparedQuery(deleteLobbyPlayersIR); + + +/** 'DeleteLobby' parameters type */ +export type IDeleteLobbyParams = { + lobbyId: string; +}; + +/** 'DeleteLobby' return type */ +export type IDeleteLobbyResult = any; +const deleteLobbyIR: any = {"usedParamSet":{"lobbyId":true},"params":[{"name":"lobbyId","required":false,"transform":{"type":"scalar"},"locs":[{"a":37,"b":44}]}],"statement":"DELETE FROM lobbies WHERE lobby_id = :lobbyId"}; +/** + * ``` + * DELETE FROM lobbies WHERE lobby_id = :lobbyId + * ``` + */ +export const deleteLobby = new PreparedQuery(deleteLobbyIR); diff --git a/packages/client/database/src/lobby-queries.sql b/packages/client/database/src/lobby-queries.sql index 848b009..5d508ca 100644 --- a/packages/client/database/src/lobby-queries.sql +++ b/packages/client/database/src/lobby-queries.sql @@ -1,30 +1,24 @@ /* @name CreateLobby */ -INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, max_players, status) -VALUES (:lobbyId, :lobbyName, :hostAccountId, :maxPlayers, 'open') +INSERT INTO lobbies (lobby_id, lobby_name, host_account_id, status) +VALUES (:lobbyId, :lobbyName, :hostAccountId, 'open') RETURNING *; /* @name JoinLobby */ -INSERT INTO lobby_players (lobby_id, account_id, player_name, is_ready) -VALUES (:lobbyId, :accountId, :playerName, false) -ON CONFLICT (lobby_id, account_id) DO UPDATE SET - player_name = EXCLUDED.player_name, - is_ready = false +INSERT INTO lobby_players (lobby_id, account_id, player_name) +VALUES (:lobbyId, :accountId, :playerName) +ON CONFLICT (lobby_id, account_id) DO NOTHING RETURNING *; -/* @name LeaveLobby */ -DELETE FROM lobby_players -WHERE lobby_id = :lobbyId AND account_id = :accountId; - /* @name GetOpenLobbies */ SELECT l.lobby_id, l.lobby_name, - l.max_players, l.status, l.created_at, (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count FROM lobbies l WHERE l.status = 'open' + AND l.created_at > NOW() - INTERVAL '10 minutes' ORDER BY l.created_at DESC LIMIT :count OFFSET :offset; @@ -32,7 +26,6 @@ LIMIT :count OFFSET :offset; SELECT l.lobby_id, l.lobby_name, - l.max_players, l.status, l.created_at, (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count @@ -47,7 +40,6 @@ SELECT l.lobby_id, l.lobby_name, l.host_account_id, - l.max_players, l.status, l.created_at, l.started_at @@ -58,7 +50,6 @@ WHERE l.lobby_id = :lobbyId; SELECT lp.account_id, lp.player_name, - lp.is_ready, lp.joined_at, a.address as wallet_address FROM lobby_players lp @@ -66,20 +57,17 @@ INNER JOIN effectstream.accounts a ON lp.account_id = a.account_id WHERE lp.lobby_id = :lobbyId ORDER BY lp.joined_at ASC; -/* @name TogglePlayerReady */ -UPDATE lobby_players -SET is_ready = NOT is_ready -WHERE lobby_id = :lobbyId AND account_id = :accountId -RETURNING is_ready; +/* @name CountLobbyPlayers */ +SELECT COUNT(*)::int as count FROM lobby_players WHERE lobby_id = :lobbyId; /* @name StartGame */ UPDATE lobbies SET status = 'in_progress', started_at = CURRENT_TIMESTAMP -WHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId +WHERE lobby_id = :lobbyId AND status = 'open' RETURNING *; -/* @name CloseLobby */ -UPDATE lobbies -SET status = 'finished', ended_at = CURRENT_TIMESTAMP -WHERE lobby_id = :lobbyId AND host_account_id = :hostAccountId -RETURNING *; +/* @name DeleteLobbyPlayers */ +DELETE FROM lobby_players WHERE lobby_id = :lobbyId; + +/* @name DeleteLobby */ +DELETE FROM lobbies WHERE lobby_id = :lobbyId; diff --git a/packages/client/database/src/mod.ts b/packages/client/database/src/mod.ts index f5c4860..cb5675f 100644 --- a/packages/client/database/src/mod.ts +++ b/packages/client/database/src/mod.ts @@ -29,11 +29,11 @@ CREATE TABLE IF NOT EXISTS user_game_state ( ); -- Lobbies table (game lobbies before the game starts) +-- Go Fish is always a 2-player game; max_players is a code constant, not a column. CREATE TABLE IF NOT EXISTS lobbies ( lobby_id TEXT PRIMARY KEY, lobby_name TEXT NOT NULL, host_account_id INTEGER NOT NULL, - max_players INTEGER NOT NULL DEFAULT 4, status TEXT NOT NULL DEFAULT 'open', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, started_at TIMESTAMP, @@ -41,11 +41,11 @@ CREATE TABLE IF NOT EXISTS lobbies ( ); -- Lobby players table (tracks players in each lobby) +-- No ready flag: join auto-starts the game when the lobby fills. CREATE TABLE IF NOT EXISTS lobby_players ( lobby_id TEXT NOT NULL, account_id INTEGER NOT NULL, player_name TEXT NOT NULL, - is_ready BOOLEAN DEFAULT false, joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (lobby_id, account_id) ); @@ -110,4 +110,13 @@ ALTER TABLE lobby_players ADD COLUMN IF NOT EXISTS midnight_address TEXT; `, }, + { + name: "3_simplify_lobbies", + sql: ` +-- Simplify the lobby model to a fixed 2-player game with auto-start on join. +-- Drops the per-lobby max_players column and the per-player ready flag. +ALTER TABLE lobbies DROP COLUMN IF EXISTS max_players; +ALTER TABLE lobby_players DROP COLUMN IF EXISTS is_ready; + `, + }, ]; diff --git a/packages/client/node/src/api.test.ts b/packages/client/node/src/api.test.ts index be005a0..c46c75a 100644 --- a/packages/client/node/src/api.test.ts +++ b/packages/client/node/src/api.test.ts @@ -54,6 +54,52 @@ const FIXTURE_ROWS = [ type FixtureRow = (typeof FIXTURE_ROWS)[number]; +/** + * Fixture lobby rows for the /open_lobbies TTL test. + * `created_at` is computed at test time so the mock can apply the 10-minute + * cutoff the same way the real query does. + */ +const TEN_MINUTES_MS = 10 * 60 * 1000; +const NOW = Date.now(); + +const LOBBY_FIXTURES: Array<{ + lobby_id: string; + lobby_name: string; + status: string; + created_at: Date; + host_account_id: number; + player_count: number; + host_name: string; +}> = [ + { + lobby_id: "lobby_recent_alice", + lobby_name: "Alice's Game", + status: "open", + created_at: new Date(NOW - 2 * 60 * 1000), // 2 min old — fresh + host_account_id: 1, + player_count: 1, + host_name: "Alice", + }, + { + lobby_id: "lobby_edge_bob", + lobby_name: "Bob's Table", + status: "open", + created_at: new Date(NOW - 9 * 60 * 1000), // 9 min old — still fresh + host_account_id: 2, + player_count: 1, + host_name: "Bob", + }, + { + lobby_id: "lobby_stale_carol", + lobby_name: "Carol's Den", + status: "open", + created_at: new Date(NOW - 11 * 60 * 1000), // 11 min old — EXPIRED + host_account_id: 3, + player_count: 1, + host_name: "Carol", + }, +]; + /** * Build a minimal pg-Pool mock. * Intercepts SQL queries by pattern-matching against the query string and @@ -100,6 +146,29 @@ function makeMockPool(): Pool { return { rows: [{ rank }], rowCount: 1, command: "SELECT", oid: 0, fields: [] }; } + // ── open_lobbies: optional wallet lookup (is_player_in_lobby) ──────── + if (q.includes("from effectstream.addresses") && q.includes("where address =")) { + // No wallet is bound in these tests — return empty so the route + // takes the `false as is_player_in_lobby` path. + return { rows: [], rowCount: 0, command: "SELECT", oid: 0, fields: [] }; + } + + // ── open_lobbies: list query with 10-minute TTL cutoff ─────────────── + if (q.includes("from lobbies l") && q.includes("where l.status = 'open'")) { + const limit = Number(params?.[0] ?? 10); + const offset = Number(params?.[1] ?? 0); + const cutoff = Date.now() - TEN_MINUTES_MS; + const filtered = LOBBY_FIXTURES + .filter(l => l.status === "open" && l.created_at.getTime() > cutoff) + .sort((a, b) => b.created_at.getTime() - a.created_at.getTime()) + .slice(offset, offset + limit) + .map(l => ({ + ...l, + is_player_in_lobby: false, + })); + return { rows: filtered, rowCount: filtered.length, command: "SELECT", oid: 0, fields: [] }; + } + // Default: empty result (unrecognised query) return { rows: [], rowCount: 0, command: "SELECT", oid: 0, fields: [] }; }, @@ -474,6 +543,54 @@ Deno.test({ sanitizeResources: false, }); +// ── GET /open_lobbies ──────────────────────────────────────────────────────── + +Deno.test({ + name: "GET /open_lobbies → 200 with { lobbies: [...] }", + fn: async () => { + const { status, body } = await getJSON(server, "/open_lobbies"); + assertEquals(status, 200); + assertEquals(Array.isArray(body.lobbies), true); + }, + sanitizeOps: false, + sanitizeResources: false, +}); + +Deno.test({ + name: "GET /open_lobbies → hides lobbies older than 10 minutes", + fn: async () => { + const { status, body } = await getJSON(server, "/open_lobbies"); + assertEquals(status, 200); + + const ids = body.lobbies.map((l: { lobby_id: string }) => l.lobby_id); + // Fresh lobby (2 min old) — must be present + assertEquals(ids.includes("lobby_recent_alice"), true, "fresh lobby should be visible"); + // Edge-of-window lobby (9 min old) — must be present + assertEquals(ids.includes("lobby_edge_bob"), true, "9-minute lobby should still be visible"); + // Stale lobby (11 min old) — must be hidden by the TTL filter + assertEquals(ids.includes("lobby_stale_carol"), false, "11-minute lobby should be filtered out"); + }, + sanitizeOps: false, + sanitizeResources: false, +}); + +Deno.test({ + name: "GET /open_lobbies → response rows do not expose max_players", + fn: async () => { + const { status, body } = await getJSON(server, "/open_lobbies"); + assertEquals(status, 200); + for (const lobby of body.lobbies) { + assertEquals( + "max_players" in lobby, + false, + `lobby ${lobby.lobby_id} should not contain max_players`, + ); + } + }, + sanitizeOps: false, + sanitizeResources: false, +}); + // ── teardown ───────────────────────────────────────────────────────────────── Deno.test({ diff --git a/packages/client/node/src/api.ts b/packages/client/node/src/api.ts index 8d014b2..da2994a 100644 --- a/packages/client/node/src/api.ts +++ b/packages/client/node/src/api.ts @@ -96,12 +96,12 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d } } - // Query lobbies with optional membership check + // Query lobbies with optional membership check. + // Lobbies older than 10 minutes are hidden from the list (soft TTL). const result = await db.query(` SELECT l.lobby_id, l.lobby_name, - l.max_players, l.status, l.created_at, l.host_account_id, @@ -110,6 +110,7 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d ${accountId !== null ? `EXISTS(SELECT 1 FROM lobby_players WHERE lobby_id = l.lobby_id AND account_id = ${accountId})` : 'false'} as is_player_in_lobby FROM lobbies l WHERE l.status = 'open' + AND l.created_at > NOW() - INTERVAL '10 minutes' ORDER BY l.created_at DESC LIMIT $1 OFFSET $2 `, [count, offset]); @@ -153,7 +154,6 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d SELECT l.lobby_id, l.lobby_name, - l.max_players, l.status, l.created_at, (SELECT COUNT(*) FROM lobby_players WHERE lobby_id = l.lobby_id) as player_count @@ -191,7 +191,6 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d l.lobby_id, l.lobby_name, l.host_account_id, - l.max_players, l.status, l.created_at, l.started_at @@ -214,7 +213,6 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d SELECT lp.account_id, lp.player_name, - lp.is_ready, lp.joined_at, (SELECT addr.address FROM effectstream.addresses addr WHERE addr.account_id = lp.account_id LIMIT 1) as wallet_address @@ -258,7 +256,6 @@ export const apiRouter: StartConfigApiRouter = async (server: FastifyInstance, d l.lobby_id, l.lobby_name, l.host_account_id, - l.max_players, l.status, l.started_at FROM lobbies l diff --git a/packages/client/node/src/state-machine.ts b/packages/client/node/src/state-machine.ts index eac4d3c..309fa39 100644 --- a/packages/client/node/src/state-machine.ts +++ b/packages/client/node/src/state-machine.ts @@ -1,5 +1,10 @@ /** * State Machine - Defines how game state transitions based on blockchain events + * + * Grammar commands (see packages/shared/data-types/src/grammar.ts): + * createdLobby — host creates a new lobby (host is added as first player) + * joinedLobby — second player joins; auto-starts the game + * closedLobby — host cancels an open lobby while still alone */ import { PaimaSTM } from "@paimaexample/sm"; @@ -12,296 +17,243 @@ import { newAccount, updateAddressAccount } from "@paimaexample/db"; -import { createLobby, joinLobby, togglePlayerReady, startGame, leaveLobby } from "@go-fish/database"; +import { + createLobby, + joinLobby, + startGame, + getLobbyState, + countLobbyPlayers, + deleteLobbyPlayers, + deleteLobby, +} from "@go-fish/database"; const stm = new PaimaSTM(grammar); +// Go Fish is always a 2-player game. +const MAX_PLAYERS = 2; + /** - * Handle createdLobby command - create a new game lobby + * Handle createdLobby - host creates a new lobby */ stm.addStateTransition("createdLobby", function* (data) { - const { playerName, lobbyName, maxPlayers } = data.parsedInput; + const { playerName, lobbyName } = data.parsedInput; const walletAddress = data.signerAddress; + if (!walletAddress) { + console.error('[createdLobby] No signer address'); + return; + } - console.log(`🎮 [createdLobby] Creating lobby "${lobbyName}" - Player: ${playerName}, Max Players: ${maxPlayers}, Wallet: ${walletAddress}`); + console.log(`🎮 [createdLobby] "${lobbyName}" by ${playerName} @ ${walletAddress}`); - // Generate unique lobby ID based on block height and timestamp - const lobbyId = `lobby_${data.blockHeight}_${Date.now()}`; + // Deterministic lobby ID: block height + short address slice. + const addrSlug = walletAddress.slice(2, 10).toLowerCase(); + const lobbyId = `lobby_${data.blockHeight}_${addrSlug}`; - // Get or create account ID for this wallet address + // Resolve (or create) the account for this wallet. const addressResult = yield* World.resolve( getAddressByAddress, - { address: walletAddress! } + { address: walletAddress } ); - console.log('[createdLobby] addressResult:', addressResult); - let accountId: number | undefined; - // Check if address exists AND has a valid account_id if (addressResult && addressResult.length > 0 && addressResult[0].account_id !== null) { accountId = addressResult[0].account_id; - console.log('[createdLobby] Found existing account:', accountId); } else { - // Create new account (either address doesn't exist or account_id is null) - console.log('[createdLobby] Creating new account for:', walletAddress); const newAccountResult = yield* World.resolve( newAccount, - { primary_address: walletAddress! } + { primary_address: walletAddress } ); - - console.log('[createdLobby] newAccountResult:', newAccountResult); - if (!newAccountResult || newAccountResult.length === 0) { - console.error('[createdLobby] Failed to create account - empty result'); + console.error('[createdLobby] Failed to create account'); return; } - accountId = newAccountResult[0].id; - console.log('[createdLobby] Created new account:', accountId); - // If address already exists (but with null account_id), update it - // Otherwise, create new address record if (addressResult && addressResult.length > 0) { - console.log('[createdLobby] Updating existing address with new account_id'); - // Address exists but has null account_id - we need to update it yield* World.resolve( updateAddressAccount, - { - address: walletAddress!, - account_id: accountId - } + { address: walletAddress, account_id: accountId } ); } else { - console.log('[createdLobby] Creating new address record'); - // Link new address to account yield* World.resolve( newAddressWithId, - { - address: walletAddress!, - address_type: 0, - account_id: accountId - } + { address: walletAddress, address_type: 0, account_id: accountId } ); } } if (!accountId) { - console.error('[createdLobby] accountId is undefined after account resolution'); + console.error('[createdLobby] accountId undefined after resolution'); return; } - console.log('[createdLobby] Using accountId:', accountId); - - // Insert lobby using pgtyped query yield* World.resolve( createLobby, { - lobbyId: lobbyId, - lobbyName: lobbyName, + lobbyId, + lobbyName, hostAccountId: accountId, - maxPlayers: maxPlayers } ); - // Add host as first player using pgtyped query yield* World.resolve( joinLobby, { - lobbyId: lobbyId, - accountId: accountId, - playerName: playerName + lobbyId, + accountId, + playerName, } ); - console.log(`✅ [createdLobby] Lobby created in database: ${lobbyId}`); + console.log(`✅ [createdLobby] created ${lobbyId}`); }); /** - * Handle joinedLobby command - player joins an existing lobby + * Handle joinedLobby - second player joins; auto-starts the game when full. */ stm.addStateTransition("joinedLobby", function* (data) { const { playerName, lobbyID } = data.parsedInput; const walletAddress = data.signerAddress; + if (!walletAddress) { + console.error('[joinedLobby] No signer address'); + return; + } + + console.log(`🎮 [joinedLobby] ${playerName} → ${lobbyID} @ ${walletAddress}`); + + // Lobby must exist and still be open. + const lobbyState = yield* World.resolve(getLobbyState, { lobbyId: lobbyID }); + if (!lobbyState || lobbyState.length === 0) { + console.warn('[joinedLobby] Lobby not found:', lobbyID); + return; + } + if (lobbyState[0].status !== 'open') { + console.warn('[joinedLobby] Lobby not open:', lobbyID, lobbyState[0].status); + return; + } - console.log(`🎮 [joinedLobby] Player ${playerName} joining lobby ${lobbyID} with wallet ${walletAddress}`); + // Guard against joining a full lobby. + const countResult = yield* World.resolve(countLobbyPlayers, { lobbyId: lobbyID }); + const currentCount = Number(countResult?.[0]?.count ?? 0); + if (currentCount >= MAX_PLAYERS) { + console.warn('[joinedLobby] Lobby already full:', lobbyID, currentCount); + return; + } - // Get or create account ID for this wallet address + // Resolve (or create) the account for this wallet. const addressResult = yield* World.resolve( getAddressByAddress, - { address: walletAddress! } + { address: walletAddress } ); - console.log('[joinedLobby] addressResult:', addressResult); - let accountId: number | undefined; - // Check if address exists AND has a valid account_id if (addressResult && addressResult.length > 0 && addressResult[0].account_id !== null) { accountId = addressResult[0].account_id; - console.log('[joinedLobby] Found existing account:', accountId); } else { - // Create new account (either address doesn't exist or account_id is null) - console.log('[joinedLobby] Creating new account for:', walletAddress); const newAccountResult = yield* World.resolve( newAccount, - { primary_address: walletAddress! } + { primary_address: walletAddress } ); - - console.log('[joinedLobby] newAccountResult:', newAccountResult); - if (!newAccountResult || newAccountResult.length === 0) { - console.error('[joinedLobby] Failed to create account - empty result'); + console.error('[joinedLobby] Failed to create account'); return; } - accountId = newAccountResult[0].id; - console.log('[joinedLobby] Created new account:', accountId); - // If address already exists (but with null account_id), update it - // Otherwise, create new address record if (addressResult && addressResult.length > 0) { - console.log('[joinedLobby] Updating existing address with new account_id'); yield* World.resolve( updateAddressAccount, - { - address: walletAddress!, - account_id: accountId - } + { address: walletAddress, account_id: accountId } ); } else { - console.log('[joinedLobby] Creating new address record'); yield* World.resolve( newAddressWithId, - { - address: walletAddress!, - address_type: 0, - account_id: accountId - } + { address: walletAddress, address_type: 0, account_id: accountId } ); } } if (!accountId) { - console.error('[joinedLobby] accountId is undefined after account resolution'); + console.error('[joinedLobby] accountId undefined after resolution'); return; } - console.log('[joinedLobby] Using accountId:', accountId); - - // Add player to lobby using pgtyped query - yield* World.resolve( + // Insert the player. ON CONFLICT DO NOTHING → empty RETURNING means the + // player was already in the lobby, which we treat as a no-op. + const joinResult = yield* World.resolve( joinLobby, - { - lobbyId: lobbyID, - accountId: accountId, - playerName: playerName - } - ); - - console.log(`✅ [joinedLobby] Player ${playerName} joined lobby ${lobbyID}`); -}); - -/** - * Handle toggledReady command - player toggles ready status - */ -stm.addStateTransition("toggledReady", function* (data) { - const { lobbyID } = data.parsedInput; - const walletAddress = data.signerAddress; - - console.log(`🎮 [toggledReady] Player toggling ready in lobby ${lobbyID} with wallet ${walletAddress}`); - - // Get account ID for this wallet address - const addressResult = yield* World.resolve( - getAddressByAddress, - { address: walletAddress! } + { lobbyId: lobbyID, accountId, playerName } ); - if (!addressResult || addressResult.length === 0 || addressResult[0].account_id === null) { - console.error('[toggledReady] No account found for address:', walletAddress); + if (!joinResult || joinResult.length === 0) { + console.warn('[joinedLobby] Player already in lobby — no auto-start'); return; } - const accountId = addressResult[0].account_id; - - // Toggle ready status - yield* World.resolve( - togglePlayerReady, - { - lobbyId: lobbyID, - accountId: accountId - } - ); - - console.log(`✅ [toggledReady] Player ready status toggled in lobby ${lobbyID}`); + // Auto-start the game once the lobby fills. + const newCountResult = yield* World.resolve(countLobbyPlayers, { lobbyId: lobbyID }); + const newCount = Number(newCountResult?.[0]?.count ?? 0); + if (newCount >= MAX_PLAYERS) { + yield* World.resolve(startGame, { lobbyId: lobbyID }); + console.log(`✅ [joinedLobby] ${lobbyID} filled — auto-started`); + } else { + console.log(`✅ [joinedLobby] ${lobbyID} now has ${newCount} player(s)`); + } }); /** - * Handle startedGame command - host starts the game + * Handle closedLobby - host cancels an open lobby while still alone. */ -stm.addStateTransition("startedGame", function* (data) { +stm.addStateTransition("closedLobby", function* (data) { const { lobbyID } = data.parsedInput; const walletAddress = data.signerAddress; - - console.log(`🎮 [startedGame] Starting game for lobby ${lobbyID} by host ${walletAddress}`); - - // Get account ID for this wallet address - const addressResult = yield* World.resolve( - getAddressByAddress, - { address: walletAddress! } - ); - - if (!addressResult || addressResult.length === 0 || addressResult[0].account_id === null) { - console.error('[startedGame] No account found for address:', walletAddress); + if (!walletAddress) { + console.error('[closedLobby] No signer address'); return; } - const accountId = addressResult[0].account_id; - - // Update lobby status to 'in_progress' - yield* World.resolve( - startGame, - { - lobbyId: lobbyID, - hostAccountId: accountId - } - ); - - console.log(`✅ [startedGame] Game started for lobby ${lobbyID}`); -}); + console.log(`🎮 [closedLobby] ${lobbyID} by ${walletAddress}`); -/** - * Handle leftLobby command - player leaves a lobby - */ -stm.addStateTransition("leftLobby", function* (data) { - const { lobbyID } = data.parsedInput; - const walletAddress = data.signerAddress; - - console.log(`🎮 [leftLobby] Player leaving lobby ${lobbyID} with wallet ${walletAddress}`); + const lobbyState = yield* World.resolve(getLobbyState, { lobbyId: lobbyID }); + if (!lobbyState || lobbyState.length === 0) { + console.warn('[closedLobby] Lobby not found:', lobbyID); + return; + } + const lobby = lobbyState[0]; + if (lobby.status !== 'open') { + console.warn('[closedLobby] Lobby not open:', lobbyID, lobby.status); + return; + } - // Get account ID for this wallet address - const addressResult = yield* World.resolve( + // Only the host may close. + const signerResult = yield* World.resolve( getAddressByAddress, - { address: walletAddress! } + { address: walletAddress } ); - - if (!addressResult || addressResult.length === 0 || addressResult[0].account_id === null) { - console.error('[leftLobby] No account found for address:', walletAddress); + const signerAccountId = signerResult?.[0]?.account_id; + if (signerAccountId == null) { + console.warn('[closedLobby] No account for signer:', walletAddress); + return; + } + if (signerAccountId !== lobby.host_account_id) { + console.warn('[closedLobby] Non-host close attempt:', signerAccountId, '!=', lobby.host_account_id); return; } - const accountId = addressResult[0].account_id; + // Must be alone — cannot close once someone else has joined. + const countResult = yield* World.resolve(countLobbyPlayers, { lobbyId: lobbyID }); + const count = Number(countResult?.[0]?.count ?? 0); + if (count > 1) { + console.warn('[closedLobby] Lobby has other players:', count); + return; + } - // Remove player from lobby - yield* World.resolve( - leaveLobby, - { - lobbyId: lobbyID, - accountId: accountId - } - ); + yield* World.resolve(deleteLobbyPlayers, { lobbyId: lobbyID }); + yield* World.resolve(deleteLobby, { lobbyId: lobbyID }); - console.log(`✅ [leftLobby] Player left lobby ${lobbyID}`); + console.log(`✅ [closedLobby] ${lobbyID} deleted`); }); /** diff --git a/packages/shared/data-types/src/go-fish-types.ts b/packages/shared/data-types/src/go-fish-types.ts index 045a968..323969d 100644 --- a/packages/shared/data-types/src/go-fish-types.ts +++ b/packages/shared/data-types/src/go-fish-types.ts @@ -96,7 +96,6 @@ export interface GoFishPlayer { id: string; name: string; isAlive: boolean; // Keep for compatibility, always true in Go Fish - isReady: boolean; hand: Card[]; books: Rank[]; cardCount: number; @@ -121,14 +120,13 @@ export interface GoFishGameState { endedAt?: number; } -// Lobby types (reuse from game-types.ts) +// Lobby types. Go Fish is always 2 players — max is a code constant, not a field. export interface Lobby { id: string; name: string; hostId: string; hostName: string; playerCount: number; - maxPlayers: number; status: 'waiting' | 'in_progress' | 'finished'; createdAt: number; isPlayerInLobby?: boolean; // True if current user is already in this lobby diff --git a/packages/shared/data-types/src/grammar.ts b/packages/shared/data-types/src/grammar.ts index c373257..57779f9 100644 --- a/packages/shared/data-types/src/grammar.ts +++ b/packages/shared/data-types/src/grammar.ts @@ -6,41 +6,28 @@ import type { GrammarDefinition } from "@paimaexample/concise"; import { Type } from "@sinclair/typebox"; -// Custom types for Go Fish const PlayerName = Type.String({ minLength: 1, maxLength: 20 }); const LobbyName = Type.String({ minLength: 1, maxLength: 30 }); const LobbyID = Type.String({ minLength: 1, maxLength: 100 }); -const PlayerID = Type.String({ minLength: 1, maxLength: 100 }); -const Rank = Type.Union([ - Type.Literal('2'), - Type.Literal('3'), - Type.Literal('4'), - Type.Literal('5'), - Type.Literal('6'), - Type.Literal('7'), - Type.Literal('8'), - Type.Literal('9'), - Type.Literal('10'), - Type.Literal('J'), - Type.Literal('Q'), - Type.Literal('K'), - Type.Literal('A'), -]); export const goFishL2Grammar = { /** - * Create Lobby: c|playerName|lobbyName|maxPlayers - * Example: c|Alice|Alice's Game|4 + * Create Lobby: c|playerName|lobbyName + * Example: c|Alice|Alice's Game + * + * Max players is a constant (2) and is not transmitted. */ createdLobby: [ ['playerName', PlayerName], ['lobbyName', LobbyName], - ['maxPlayers', Type.Number({ minimum: 2, maximum: 6 })], ], /** * Join Lobby: j|playerName|lobbyID * Example: j|Bob|abc123def456 + * + * Auto-starts the game — the second joiner always fills the 2-player lobby, + * so the state machine flips status to 'in_progress' in the same transition. */ joinedLobby: [ ['playerName', PlayerName], @@ -48,36 +35,11 @@ export const goFishL2Grammar = { ], /** - * Leave Lobby: l|lobbyID - * Example: l|abc123def456 - */ - leftLobby: [['lobbyID', LobbyID]], - - /** - * Toggle Ready: r|lobbyID - * Example: r|abc123def456 - */ - toggledReady: [['lobbyID', LobbyID]], - - /** - * Start Game (Host only): start|lobbyID - * Example: start|abc123def456 - */ - startedGame: [['lobbyID', LobbyID]], - - /** - * Ask For Card: ask|lobbyID|targetPlayerID|rank - * Example: ask|abc123def456|player_123|K - */ - askedForCard: [ - ['lobbyID', LobbyID], - ['targetPlayerID', PlayerID], - ['rank', Rank], - ], - - /** - * Close Lobby (Host only): close|lobbyID + * Close Lobby (host only, while alone): close|lobbyID * Example: close|abc123def456 + * + * Host cancels an open lobby before a second player has joined. + * The state machine deletes the lobby row and its lobby_players row. */ closedLobby: [['lobbyID', LobbyID]], } as const satisfies GrammarDefinition; From f5ff6e41085830026583e2c4f7f2f59bb953ecd3 Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 18:36:28 -0400 Subject: [PATCH 04/78] Add e2e smoke tests and test-owned witness module Eight incremental smoke tests isolate each layer of the e2e pipeline (imports, fs config, indexer subscription, lobby via batcher, init_deck, applyMask with secrets, full per-game setup, one-turn play) so failures localize quickly without running the full ~20 min game. test-witnesses.ts is a self-contained witness implementation for the Go Fish contract. It replaces the default @go-fish/midnight-contract witnesses in the e2e path: explicit per-session WitnessState, throws on missing secrets instead of silently falling back to random Math.random static keys, and captures each player's shuffle seed during player_secret_key so the deck shuffle is deterministic per (game, player). deno.json adds the import-map entries needed by the smokes and the shared e2e helpers (rxjs, @paimaexample/concise, midnight-js providers, compact-js, local @go-fish/data-types and midnight-contract aliases). Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/deno.json | 24 +- e2e/smoke/apply-mask.test.ts | 370 +++++++++++++++++++++ e2e/smoke/contract-config.test.ts | 82 +++++ e2e/smoke/init-deck-circuit.test.ts | 333 +++++++++++++++++++ e2e/smoke/ledger-subscription.test.ts | 95 ++++++ e2e/smoke/midnight-imports.test.ts | 65 ++++ e2e/smoke/one-turn.test.ts | 162 ++++++++++ e2e/smoke/setup-sequence.test.ts | 449 ++++++++++++++++++++++++++ e2e/smoke/test-witnesses.ts | 270 ++++++++++++++++ 9 files changed, 1848 insertions(+), 2 deletions(-) create mode 100644 e2e/smoke/apply-mask.test.ts create mode 100644 e2e/smoke/contract-config.test.ts create mode 100644 e2e/smoke/init-deck-circuit.test.ts create mode 100644 e2e/smoke/ledger-subscription.test.ts create mode 100644 e2e/smoke/midnight-imports.test.ts create mode 100644 e2e/smoke/one-turn.test.ts create mode 100644 e2e/smoke/setup-sequence.test.ts create mode 100644 e2e/smoke/test-witnesses.ts diff --git a/e2e/deno.json b/e2e/deno.json index 98a95c9..5d225f3 100644 --- a/e2e/deno.json +++ b/e2e/deno.json @@ -2,10 +2,30 @@ "name": "@go-fish/e2e", "exports": "./game-round.test.ts", "tasks": { - "test": "deno test -A --no-check --unstable-raw-imports game-round.test.ts" + "test": "deno test -A --no-check --unstable-raw-imports game-round.test.ts", + "smoke:imports": "deno test -A --no-check --unstable-raw-imports smoke/midnight-imports.test.ts", + "smoke:config": "deno test -A --no-check --unstable-raw-imports smoke/contract-config.test.ts", + "smoke:ledger": "deno test -A --no-check --unstable-raw-imports smoke/ledger-subscription.test.ts", + "smoke:lobby": "deno test -A --no-check --unstable-raw-imports smoke/lobby-batcher.test.ts", + "smoke:init-deck": "deno test -A --no-check --unstable-raw-imports smoke/init-deck-circuit.test.ts", + "smoke:apply-mask": "deno test -A --no-check --unstable-raw-imports smoke/apply-mask.test.ts", + "smoke:setup": "deno test -A --no-check --unstable-raw-imports smoke/setup-sequence.test.ts", + "smoke:one-turn": "deno test -A --no-check --unstable-raw-imports smoke/one-turn.test.ts" }, "imports": { "ethers": "npm:ethers@5.7.2", - "@midnight-ntwrk/compact-runtime": "npm:@midnight-ntwrk/compact-runtime@0.15.0" + "rxjs": "npm:rxjs@7.8.1", + "@paimaexample/concise": "jsr:@paimaexample/concise@0.10.24", + "@go-fish/data-types": "../packages/shared/data-types/src/mod.ts", + "@midnight-ntwrk/compact-runtime": "npm:@midnight-ntwrk/compact-runtime@0.15.0", + "@midnight-ntwrk/midnight-js-contracts": "npm:@midnight-ntwrk/midnight-js-contracts@4.0.2", + "@midnight-ntwrk/midnight-js-http-client-proof-provider": "npm:@midnight-ntwrk/midnight-js-http-client-proof-provider@4.0.2", + "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "npm:@midnight-ntwrk/midnight-js-indexer-public-data-provider@4.0.2", + "@midnight-ntwrk/midnight-js-node-zk-config-provider": "npm:@midnight-ntwrk/midnight-js-node-zk-config-provider@4.0.2", + "@midnight-ntwrk/midnight-js-network-id": "npm:@midnight-ntwrk/midnight-js-network-id@4.0.2", + "@midnight-ntwrk/midnight-js-types": "npm:@midnight-ntwrk/midnight-js-types@4.0.2", + "@midnight-ntwrk/compact-js": "npm:@midnight-ntwrk/compact-js@2.5.0", + "@go-fish/midnight-contract": "../packages/shared/contracts/midnight/go-fish-contract/src/_index.ts", + "@go-fish/midnight-contract/contract": "../packages/shared/contracts/midnight/go-fish-contract/src/managed/contract/index.js" } } diff --git a/e2e/smoke/apply-mask.test.ts b/e2e/smoke/apply-mask.test.ts new file mode 100644 index 0000000..ed6d7ce --- /dev/null +++ b/e2e/smoke/apply-mask.test.ts @@ -0,0 +1,370 @@ +/** + * Step 7 smoke test — applyMask(gameId, 1) via setPlayerSecrets + + * real prover + batcher. First circuit that actually consumes player + * secrets through the witness module. + * + * Flow: + * 1. Generate a random 32-byte gameId (per run, so we always exercise + * the "first caller for a new game" branch of applyMask which + * implicitly calls go_fish_init_game + deck_init_deck + hand_init_hands) + * 2. Generate a P1 secret (rejection-sampled to [1, JUBJUB_R)) and shuffle seed + * 3. setPlayerSecrets(gameIdHex, 1, secret, seed) — writes to the shared + * witness module's dynamicSecrets map + * 4. contract.callTx.applyMask(gameId, 1n) — WASM simulates the circuit + * (consulting player_secret_key and shuffle_seed witnesses), prover + * generates the proof, balanceTx delegates to the batcher + * 5. Poll hasMaskApplied(gameId, 1) via VM ops until true + * 6. clearPlayerSecrets(gameIdHex, 1) in finally + * + * PREREQS: + * - Midnight node + indexer + proof server running + * - Batcher running with midnight_balancing adapter + * - staticDeckInitialized flag is true (otherwise applyMask's internal + * assertions may fail) + * + * Run: deno task --filter @go-fish/e2e smoke:apply-mask + */ + +import { assertEquals } from "jsr:@std/assert"; +import { resolve } from "jsr:@std/path"; +import { + toHex, + CompactTypeBoolean, + CompactTypeBytes, + CompactTypeUnsignedInteger, + CostModel, +} from "@midnight-ntwrk/compact-runtime"; +import { CompiledContract } from "@midnight-ntwrk/compact-js"; +import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts"; +import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; +import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; + +import { Contract as GoFishContract } from "@go-fish/midnight-contract/contract"; +import { + witnesses, + setPlayerSecrets, + clearPlayerSecrets, +} from "@go-fish/midnight-contract"; +import { createInMemoryPrivateStateProvider } from "../../frontend/src/services/midnightInMemoryPrivateStateProvider.ts"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const REPO_ROOT = Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); +const BATCHER_URL = Deno.env.get("BATCHER_URL") ?? "http://localhost:3336"; +const PROOF_URL = Deno.env.get("PROOF_URL") ?? "http://127.0.0.1:6300"; +const INDEXER_HTTP_URL = + Deno.env.get("INDEXER_HTTP_URL") ?? "http://127.0.0.1:8088/api/v3/graphql"; +const INDEXER_WS_URL = + Deno.env.get("INDEXER_WS_URL") ?? "ws://127.0.0.1:8088/api/v3/graphql/ws"; + +const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); +const ZK_CONFIG_PATH = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract/src/managed", +); + +const DELEGATED_SENTINEL = "GoFish: delegated to midnight_balancing batcher"; + +// Jubjub prime-order subgroup order — secrets must be in [1, r) +const JUBJUB_R = 0x0e7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7n; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function loadContractAddress(): string { + const parsed = JSON.parse(Deno.readTextFileSync(CONTRACT_ADDRESS_FILE)); + return parsed.contractAddress.replace(/^0x/, ""); +} + +function generatePlayerSecret(): bigint { + while (true) { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + const n = BigInt("0x" + Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("")); + if (n >= 1n && n < JUBJUB_R) return n; + } +} + +function generateShuffleSeed(): Uint8Array { + const seed = new Uint8Array(32); + crypto.getRandomValues(seed); + return seed; +} + +function randomGameId(): Uint8Array { + const id = new Uint8Array(32); + crypto.getRandomValues(id); + return id; +} + +function gameIdToHex(gameId: Uint8Array): string { + return "0x" + Array.from(gameId).map(b => b.toString(16).padStart(2, "0")).join(""); +} + +// --------------------------------------------------------------------------- +// Batcher POST — midnight_balancing +// --------------------------------------------------------------------------- + +function detectTxStage(serializedTx: string): "unproven" | "unbound" | "finalized" { + const prefixBytes = new Uint8Array( + serializedTx.slice(0, 600).padEnd(600, "0").match(/.{2}/g)!.map(b => parseInt(b, 16)), + ); + const header = new TextDecoder().decode(prefixBytes); + const m = header.match(/midnight:(?:transaction|intent)\[v\d+\]\(signature\[v\d+\],([^,]+),([^)]+)\):/); + if (!m) throw new Error(`detectTxStage: cannot parse: ${header.slice(0, 80)}`); + if (m[1]!.includes("proof-preimage")) return "unproven"; + if (m[2]!.includes("embedded-fr")) return "unbound"; + if (m[2]!.includes("pedersen-schnorr")) return "finalized"; + throw new Error(`detectTxStage: unknown stage: ${m[1]} / ${m[2]}`); +} + +async function postMidnightTx( + serializedTx: string, + circuitId: string, + meta: { lobbyId?: string; playerId?: number } = {}, +): Promise { + const txStage = detectTxStage(serializedTx); + console.log(` [batcher] ${circuitId}: txStage=${txStage}, len=${serializedTx.length / 2}B`); + + const res = await fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + data: { + target: "midnight_balancing", + addressType: 0, + address: "go_fish_player", + input: JSON.stringify({ tx: serializedTx, txStage, circuitId, ...meta }), + timestamp: Date.now(), + }, + confirmationLevel: "no-wait", + }), + }); + const text = await res.text(); + console.log(` [batcher] ${circuitId} → ${res.status} ${text.slice(0, 160)}`); + if (!res.ok) throw new Error(`batcher rejected ${circuitId}: ${text}`); +} + +// --------------------------------------------------------------------------- +// Delegating provider +// --------------------------------------------------------------------------- + +function makeDelegatingProvider() { + const provider: any = { + getCoinPublicKey: () => "00".repeat(32), + getEncryptionPublicKey: () => "00".repeat(32), + async balanceTx(tx: any) { + if (typeof provider.__delegatedBalanceHook === "function") { + await provider.__delegatedBalanceHook(tx); + throw new Error(DELEGATED_SENTINEL); + } + throw new Error("balanceTx called without hook"); + }, + submitTx(_tx: any) { + throw new Error("submitTx should never be called"); + }, + __delegatedBalanceHook: undefined as undefined | ((tx: any) => Promise), + }; + return provider; +} + +function isDelegationError(err: unknown): boolean { + if (!(err instanceof Error)) return false; + let e: Error | undefined = err; + while (e) { + if (e.message.includes(DELEGATED_SENTINEL)) return true; + e = e.cause instanceof Error ? e.cause : undefined; + } + return false; +} + +async function callDelegated( + provider: any, + circuitId: string, + invoke: () => Promise, + meta: { lobbyId?: string; playerId?: number } = {}, +): Promise { + let delegated = false; + provider.__delegatedBalanceHook = async (tx: any) => { + const serializedTx = toHex((tx as any).serialize()); + await postMidnightTx(serializedTx, circuitId, meta); + delegated = true; + }; + try { + await invoke(); + } catch (err) { + if (isDelegationError(err) || delegated) return; + throw err; + } finally { + delete provider.__delegatedBalanceHook; + } +} + +// --------------------------------------------------------------------------- +// Semantic reads (VM query ops) — ported from GoFishContractService.ts +// --------------------------------------------------------------------------- + +async function queryHasMaskApplied( + publicDataProvider: any, + contractAddress: string, + gameId: Uint8Array, + playerId: 1 | 2, +): Promise { + try { + const cs = await publicDataProvider.queryContractState(contractAddress); + if (!cs) return null; + + const keyType = new CompactTypeUnsignedInteger(255n, 1); + // maskAppliedP1 path = [2n, 5n][gameId], maskAppliedP2 = [2n, 6n][gameId] + const outerKey = { value: keyType.toValue(2n), alignment: keyType.alignment() }; + const innerKey = { + value: keyType.toValue(playerId === 1 ? 5n : 6n), + alignment: keyType.alignment(), + }; + const gameIdKey = { + value: new CompactTypeBytes(32).toValue(gameId), + alignment: new CompactTypeBytes(32).alignment(), + }; + + const results = cs.query( + [ + { dup: { n: 0 } }, + { + idx: { + cached: false, + pushPath: false, + path: [ + { tag: "value" as const, value: outerKey }, + { tag: "value" as const, value: innerKey }, + ], + }, + }, + { + idx: { + cached: false, + pushPath: false, + path: [{ tag: "value" as const, value: gameIdKey }], + }, + }, + { popeq: { cached: false, result: undefined } }, + ], + CostModel.initialCostModel(), + ); + + const gatherResults = (results as any)[1]; + const first = Array.isArray(gatherResults) ? gatherResults[0] : null; + if (!first || first.tag !== "read") return false; + return CompactTypeBoolean.fromValue(first.content.value); + } catch (err) { + // If the gameId key doesn't exist in the maskApplied map yet, the VM + // throws `expected a cell, received null` when it tries to idx into a + // missing entry. That's semantically "not yet applied" — return false. + const msg = (err as Error).message ?? ""; + if (msg.includes("expected a cell, received null") || msg.includes("not present")) { + return false; + } + console.warn(` queryHasMaskApplied error:`, msg); + return null; + } +} + +// --------------------------------------------------------------------------- +// The test +// --------------------------------------------------------------------------- + +Deno.test({ + name: "circuit: applyMask(gameId, 1) with setPlayerSecrets", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + setNetworkId("undeployed"); + + const contractAddress = loadContractAddress(); + const gameId = randomGameId(); + const gameIdHex = gameIdToHex(gameId); + const p1Secret = generatePlayerSecret(); + const p1Seed = generateShuffleSeed(); + + console.log(` contractAddress = ${contractAddress}`); + console.log(` gameId = ${gameIdHex.slice(0, 20)}...`); + console.log(` p1 secret = ${p1Secret.toString(16).slice(0, 16)}...`); + + const zkConfigProvider = new NodeZkConfigProvider(ZK_CONFIG_PATH); + const proofProvider = httpClientProofProvider(PROOF_URL, zkConfigProvider); + const publicDataProvider = indexerPublicDataProvider(INDEXER_HTTP_URL, INDEXER_WS_URL); + const privateStateProvider = createInMemoryPrivateStateProvider("smoke-apply-mask-p1"); + const walletProvider = makeDelegatingProvider(); + + const compiledContract = CompiledContract.make("go-fish", GoFishContract as any) + .pipe( + CompiledContract.withWitnesses(witnesses as any), + CompiledContract.withCompiledFileAssets(ZK_CONFIG_PATH), + ); + + console.log(" joining deployed contract..."); + const contract: any = await findDeployedContract( + { + privateStateProvider, + zkConfigProvider, + proofProvider, + publicDataProvider, + walletProvider, + midnightProvider: walletProvider, + } as any, + { + contractAddress: contractAddress as any, + compiledContract: compiledContract as any, + privateStateId: "smoke-apply-mask-p1" as any, + initialPrivateState: {}, + } as any, + ); + console.log(" ✓ contract joined"); + + // Precondition: hasMaskApplied(gameId, 1) should be false for a fresh gameId. + const before = await queryHasMaskApplied(publicDataProvider, contractAddress, gameId, 1); + console.log(` hasMaskApplied(p1) before: ${before}`); + assertEquals(before, false, "expected mask not yet applied for fresh gameId"); + + // Load player secrets into the shared witness module. The witness + // functions (player_secret_key, shuffle_seed) consult dynamicSecrets + // keyed by `${gameIdHex}-${playerId}`. + setPlayerSecrets(gameIdHex, 1, p1Secret, p1Seed); + + try { + console.log(" invoking contract.callTx.applyMask(gameId, 1n) ..."); + const t0 = Date.now(); + await callDelegated( + walletProvider, + "applyMask", + () => contract.callTx.applyMask(gameId, 1n), + { playerId: 1 }, + ); + console.log(` ✓ applyMask proven + submitted in ${Math.round((Date.now() - t0) / 1000)}s`); + } finally { + clearPlayerSecrets(gameIdHex, 1); + } + + // Poll until hasMaskApplied(gameId, 1) flips to true. 120s is generous + // for the batcher to balance, submit, and the indexer to catch up. + console.log(" polling indexer for hasMaskApplied(p1)==true (120s timeout)..."); + const deadline = Date.now() + 120_000; + while (Date.now() < deadline) { + await new Promise(r => setTimeout(r, 2_000)); + const flag = await queryHasMaskApplied(publicDataProvider, contractAddress, gameId, 1); + if (flag === true) { + console.log(` ✓ hasMaskApplied(p1): false → true`); + return; + } + console.log(` [poll] flag=${flag} (${Math.round((deadline - Date.now()) / 1000)}s left)`); + } + throw new Error("Timed out waiting for hasMaskApplied(p1) to flip to true"); + }, +}); diff --git a/e2e/smoke/contract-config.test.ts b/e2e/smoke/contract-config.test.ts new file mode 100644 index 0000000..89be505 --- /dev/null +++ b/e2e/smoke/contract-config.test.ts @@ -0,0 +1,82 @@ +/** + * Step 2 smoke test — filesystem loading for contract address + ZK keys. + * + * Verifies we can: + * 1. Read the deployed contract address from + * packages/shared/contracts/midnight/go-fish-contract.undeployed.json + * 2. Instantiate NodeZkConfigProvider against src/managed/ + * 3. Actually load prover/verifier/zkir for a real circuit — exercises the + * path resolution that Step 1 couldn't. + * + * No external services required. Must be run from the repo root (or set + * GO_FISH_REPO_ROOT). + * + * Run: deno task --filter @go-fish/e2e smoke:config + */ + +import { assertEquals, assertExists } from "jsr:@std/assert"; +import { resolve } from "jsr:@std/path"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; + +const REPO_ROOT = Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); + +const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); + +const ZK_CONFIG_PATH = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract/src/managed", +); + +// Known circuits the full test exercises. If these don't load, nothing will. +// Note: no separate `goFish` circuit exists — respondToAsk handles the draw +// automatically and transitions phase to WaitForDrawCheck, which afterGoFish +// then resolves. afterGoFish(gameId, playerId, now) — contract v3 signature. +const CIRCUITS = [ + "init_deck", + "applyMask", + "dealCards", + "askForCard", + "respondToAsk", + "afterGoFish", + "checkAndScoreBook", + "doesPlayerHaveSpecificCard", // used for local hand reads via provableCircuits +] as const; + +function loadContractAddress(): string { + const raw = Deno.readTextFileSync(CONTRACT_ADDRESS_FILE); + const parsed = JSON.parse(raw); + if (typeof parsed.contractAddress !== "string") { + throw new Error(`contractAddress missing from ${CONTRACT_ADDRESS_FILE}`); + } + // compact-runtime 0.14+ expects raw 64-hex without 0x prefix + return parsed.contractAddress.replace(/^0x/, ""); +} + +Deno.test("contract address loads from .undeployed.json", () => { + const addr = loadContractAddress(); + assertEquals(addr.length, 64, `expected 64 hex chars, got ${addr.length}`); + if (!/^[0-9a-f]{64}$/i.test(addr)) { + throw new Error(`not valid hex: ${addr}`); + } + console.log(` contractAddress = ${addr}`); +}); + +Deno.test("NodeZkConfigProvider loads prover/verifier/zkir for all circuits", async () => { + const provider = new NodeZkConfigProvider(ZK_CONFIG_PATH); + console.log(` ZK config path = ${ZK_CONFIG_PATH}`); + + for (const circuit of CIRCUITS) { + const [proverKey, verifierKey, zkir] = await Promise.all([ + provider.getProverKey(circuit), + provider.getVerifierKey(circuit), + provider.getZKIR(circuit), + ]); + assertExists(proverKey, `${circuit} prover key`); + assertExists(verifierKey, `${circuit} verifier key`); + assertExists(zkir, `${circuit} ZKIR`); + console.log(` ${circuit.padEnd(20)} prover=${proverKey.length}B verifier=${verifierKey.length}B zkir=${zkir.length}B`); + } +}); diff --git a/e2e/smoke/init-deck-circuit.test.ts b/e2e/smoke/init-deck-circuit.test.ts new file mode 100644 index 0000000..7644721 --- /dev/null +++ b/e2e/smoke/init-deck-circuit.test.ts @@ -0,0 +1,333 @@ +/** + * Step 6 smoke test — real init_deck circuit via HTTP prover + batcher. + * + * Exercises the full Midnight flow end-to-end for a single circuit: + * 1. findDeployedContract with NodeZkConfigProvider + httpClientProofProvider + * + indexerPublicDataProvider + in-memory private state + delegating + * wallet provider. + * 2. Call contract.callTx.init_deck() — compact-js WASM-simulates the + * circuit, httpClientProofProvider generates the ZK proof against the + * prover at :6300, produces an UnboundTransaction. + * 3. The delegating balanceTx hook intercepts the UnboundTransaction, + * serializes it, and POSTs to the batcher's /send-input with + * target="midnight_balancing". Then throws DELEGATED_SENTINEL to abort + * the SDK pipeline (we don't want it to try to call submitTx). + * 4. Wait for the WS contract-state subscription to emit a new state whose + * staticDeckInitialized flag is true — indicates the transaction was + * balanced, submitted, and indexed. + * + * PREREQS: + * - Midnight node + indexer (:8088) + proof server (:6300) running + * - Hardhat + Paima node + batcher (:3336) running + * - Contract deployed (go-fish-contract.undeployed.json up to date) + * + * Run: deno task --filter @go-fish/e2e smoke:init-deck + */ + +import { assertEquals } from "jsr:@std/assert"; +import { resolve } from "jsr:@std/path"; +import { + toHex, + CompactTypeBoolean, + CompactTypeUnsignedInteger, + CostModel, +} from "@midnight-ntwrk/compact-runtime"; +import { CompiledContract } from "@midnight-ntwrk/compact-js"; +import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts"; +import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; +import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; +import { firstValueFrom, timeout } from "rxjs"; + +import { Contract as GoFishContract } from "@go-fish/midnight-contract/contract"; +import { witnesses } from "@go-fish/midnight-contract"; +import { createInMemoryPrivateStateProvider } from "../../frontend/src/services/midnightInMemoryPrivateStateProvider.ts"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const REPO_ROOT = Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); +const BATCHER_URL = Deno.env.get("BATCHER_URL") ?? "http://localhost:3336"; +const PROOF_URL = Deno.env.get("PROOF_URL") ?? "http://127.0.0.1:6300"; +const INDEXER_HTTP_URL = + Deno.env.get("INDEXER_HTTP_URL") ?? "http://127.0.0.1:8088/api/v3/graphql"; +const INDEXER_WS_URL = + Deno.env.get("INDEXER_WS_URL") ?? "ws://127.0.0.1:8088/api/v3/graphql/ws"; + +const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); +const ZK_CONFIG_PATH = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract/src/managed", +); + +const DELEGATED_SENTINEL = "GoFish: delegated to midnight_balancing batcher"; + +function loadContractAddress(): string { + const parsed = JSON.parse(Deno.readTextFileSync(CONTRACT_ADDRESS_FILE)); + return parsed.contractAddress.replace(/^0x/, ""); +} + +// --------------------------------------------------------------------------- +// Batcher POST — midnight_balancing (unsigned, no target field) +// --------------------------------------------------------------------------- + +function detectTxStage(serializedTx: string): "unproven" | "unbound" | "finalized" { + const prefixBytes = new Uint8Array( + serializedTx.slice(0, 600).padEnd(600, "0").match(/.{2}/g)!.map(b => parseInt(b, 16)), + ); + const header = new TextDecoder().decode(prefixBytes); + const m = header.match(/midnight:(?:transaction|intent)\[v\d+\]\(signature\[v\d+\],([^,]+),([^)]+)\):/); + if (!m) throw new Error(`detectTxStage: cannot parse header: ${header.slice(0, 80)}`); + if (m[1]!.includes("proof-preimage")) return "unproven"; + if (m[2]!.includes("embedded-fr")) return "unbound"; + if (m[2]!.includes("pedersen-schnorr")) return "finalized"; + throw new Error(`detectTxStage: unknown stage markers: ${m[1]} / ${m[2]}`); +} + +/** + * Read the `staticDeckInitialized` boolean from the on-chain contract state + * by running VM query ops at path [1n, 0n]. Mirrors + * frontend/src/services/GoFishContractService.ts `queryIsStaticDeckInitialized`. + * Returns `true` once init_deck has landed, `false` before, `null` on read error. + */ +async function queryStaticDeckInitialized( + publicDataProvider: any, + contractAddress: string, +): Promise { + try { + const contractState = await publicDataProvider.queryContractState(contractAddress); + if (!contractState) return null; + + const keyType = new CompactTypeUnsignedInteger(255n, 1); + const key1 = { value: keyType.toValue(1n), alignment: keyType.alignment() }; + const key0 = { value: keyType.toValue(0n), alignment: keyType.alignment() }; + + const results = contractState.query( + [ + { dup: { n: 0 } }, + { + idx: { + cached: false, + pushPath: false, + path: [ + { tag: "value" as const, value: key1 }, + { tag: "value" as const, value: key0 }, + ], + }, + }, + { popeq: { cached: false, result: null } }, + ], + CostModel.initialCostModel(), + ); + + const gatherResults = (results as any)[1]; + const first = Array.isArray(gatherResults) ? gatherResults[0] : null; + if (!first || first.tag !== "read") return null; + return CompactTypeBoolean.fromValue(first.content.value); + } catch (err) { + console.warn(` queryStaticDeckInitialized error:`, (err as Error).message); + return null; + } +} + +async function postMidnightTx( + serializedTx: string, + circuitId: string, +): Promise { + const txStage = detectTxStage(serializedTx); + console.log(` [batcher] ${circuitId}: txStage=${txStage}, len=${serializedTx.length / 2}B`); + + const body = { + data: { + // target omitted — batcher's default would be effectstreaml2 which is + // wrong, so we DO specify target here. The midnight_balancing adapter + // provides its own signature verification (or none), so we don't need + // to sign. + target: "midnight_balancing", + addressType: 0, + address: "go_fish_player", + input: JSON.stringify({ tx: serializedTx, txStage, circuitId }), + timestamp: Date.now(), + }, + confirmationLevel: "no-wait", + }; + + const res = await fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const text = await res.text(); + console.log(` [batcher] ${circuitId} response: ${res.status} ${text.slice(0, 200)}`); + if (!res.ok) { + throw new Error(`batcher rejected ${circuitId}: ${text}`); + } +} + +// --------------------------------------------------------------------------- +// Delegating provider — intercepts balanceTx, POSTs, throws sentinel +// --------------------------------------------------------------------------- + +function makeDelegatingProvider() { + const provider: any = { + getCoinPublicKey() { + // Return a dummy 32-byte zero coin public key. The midnight_balancing + // adapter handles balancing with the batcher's own wallet, so this + // value doesn't affect the output. + return "00".repeat(32); + }, + getEncryptionPublicKey() { + return "00".repeat(32); + }, + async balanceTx(tx: any) { + if (typeof provider.__delegatedBalanceHook === "function") { + await provider.__delegatedBalanceHook(tx); + throw new Error(DELEGATED_SENTINEL); + } + throw new Error("balanceTx called without hook"); + }, + submitTx(_tx: any) { + throw new Error("submitTx should never be called — balanceTx delegates"); + }, + __delegatedBalanceHook: undefined as undefined | ((tx: any) => Promise), + }; + return provider; +} + +function isDelegationError(err: unknown): boolean { + if (!(err instanceof Error)) return false; + let e: Error | undefined = err; + while (e) { + if (e.message.includes(DELEGATED_SENTINEL)) return true; + e = e.cause instanceof Error ? e.cause : undefined; + } + return false; +} + +async function callDelegated( + provider: any, + circuitId: string, + invoke: () => Promise, +): Promise { + let delegated = false; + provider.__delegatedBalanceHook = async (tx: any) => { + const serializedTx = toHex((tx as any).serialize()); + await postMidnightTx(serializedTx, circuitId); + delegated = true; + }; + try { + await invoke(); + } catch (err) { + if (isDelegationError(err) || delegated) return; + throw err; + } finally { + delete provider.__delegatedBalanceHook; + } +} + +// --------------------------------------------------------------------------- +// The test +// --------------------------------------------------------------------------- + +Deno.test({ + name: "circuit: init_deck via real prover + batcher", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + // Case-sensitive: must be lowercase "undeployed" — the Midnight node + // compares IDs literally and rejects txs proven with the wrong casing. + setNetworkId("undeployed"); + + const contractAddress = loadContractAddress(); + console.log(` contractAddress = ${contractAddress}`); + + const zkConfigProvider = new NodeZkConfigProvider(ZK_CONFIG_PATH); + console.log(` zk config path = ${ZK_CONFIG_PATH}`); + + const proofProvider = httpClientProofProvider(PROOF_URL, zkConfigProvider); + console.log(` proof server = ${PROOF_URL}`); + + const publicDataProvider = indexerPublicDataProvider(INDEXER_HTTP_URL, INDEXER_WS_URL); + + const privateStateProvider = createInMemoryPrivateStateProvider("smoke-init-deck"); + + const walletProvider = makeDelegatingProvider(); + + const compiledContract = CompiledContract.make("go-fish", GoFishContract as any) + .pipe( + CompiledContract.withWitnesses(witnesses as any), + CompiledContract.withCompiledFileAssets(ZK_CONFIG_PATH), + ); + + console.log(" joining deployed contract..."); + const contract: any = await findDeployedContract( + { + privateStateProvider, + zkConfigProvider, + proofProvider, + publicDataProvider, + walletProvider, + midnightProvider: walletProvider, + } as any, + { + contractAddress: contractAddress as any, + compiledContract: compiledContract as any, + privateStateId: "smoke-init-deck" as any, + initialPrivateState: {}, + } as any, + ); + console.log(" ✓ contract joined"); + + // `init_deck()` is a one-shot circuit: it wraps `deck_init_static_deck()` + // which sets the `staticDeckInitialized` flag globally (not per-game). + // Once true, it stays true forever — calling it again would fail the + // internal assert. So the smoke test is idempotent: + // - If already true: skip submission (plumbing still verified above + // via findDeployedContract, queryContractState, VM ops query) + // - If false: invoke and wait for the flip + // The full e2e test will follow the same pattern: read the flag once, + // call init_deck only if needed, then proceed to per-game setup + // (applyMask with a fresh random gameId creates the per-game state + // automatically inside the contract). + const initializedBefore = await queryStaticDeckInitialized( + publicDataProvider, + contractAddress, + ); + console.log(` staticDeckInitialized before: ${initializedBefore}`); + + if (initializedBefore === true) { + console.log(" ✓ already initialized (one-shot already done) — smoke skips submission"); + return; + } + + // Invoke the circuit end-to-end: + // 1. WASM simulation of init_deck + // 2. HTTP prover at :6300 generates the ZK proof + // 3. balanceTx hook POSTs the UnboundTransaction to the batcher + // 4. Batcher's midnight_balancing adapter balances + submits on-chain + console.log(" invoking contract.callTx.init_deck() ..."); + const t0 = Date.now(); + await callDelegated(walletProvider, "init_deck", () => contract.callTx.init_deck()); + console.log(` ✓ init_deck submitted to batcher in ${Math.round((Date.now() - t0) / 1000)}s`); + + // Poll until staticDeckInitialized flips false → true. Real semantic read + // (VM query ops), not an opaque ptr comparison. + console.log(" polling indexer for staticDeckInitialized==true (120s timeout)..."); + const deadline = Date.now() + 120_000; + while (Date.now() < deadline) { + await new Promise(r => setTimeout(r, 2_000)); + const flag = await queryStaticDeckInitialized(publicDataProvider, contractAddress); + if (flag === true) { + console.log(` ✓ staticDeckInitialized: false → true`); + return; + } + console.log(` [poll] flag=${flag} (${Math.round((deadline - Date.now()) / 1000)}s left)`); + } + throw new Error("Timed out waiting for staticDeckInitialized to flip to true"); + }, +}); diff --git a/e2e/smoke/ledger-subscription.test.ts b/e2e/smoke/ledger-subscription.test.ts new file mode 100644 index 0000000..dbc2df1 --- /dev/null +++ b/e2e/smoke/ledger-subscription.test.ts @@ -0,0 +1,95 @@ +/** + * Step 3 smoke test — live WS subscription to the Midnight indexer. + * + * Verifies we can: + * 1. Connect to the Midnight indexer via indexerPublicDataProvider + * 2. Subscribe to contractStateObservable and receive at least one emission + * 3. Also query the state once via queryContractState for comparison + * + * PREREQS — the following must be running: + * - Midnight indexer (default http://127.0.0.1:8088) + * - Midnight node + * - Go Fish contract deployed at the address in go-fish-contract.undeployed.json + * + * Start with: deno task -f @go-fish/node midnight:setup + * + * Run: deno task --filter @go-fish/e2e smoke:ledger + * + * Note on ledger decoding: the compiled ledger() function for go-fish returns + * {} because the contract doesn't declare typed ledger blocks — state lives in + * module ledgers (go_fish_*). Reading specific fields therefore requires either + * (a) raw VM query ops: contractState.query([...ops], CostModel) + * (b) locally instantiating Contract and calling impureCircuits.getGamePhase + * (and friends) with a QueryContext hydrated from indexer state. + * + * This smoke test only proves the pipe is open — field decoding comes in Step 9. + */ + +import { assertExists } from "jsr:@std/assert"; +import { resolve } from "jsr:@std/path"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; +import { firstValueFrom, take, timeout } from "rxjs"; + +// Match packages/shared/contracts/midnight/midnight-env.ts — /api/v1 triggers +// an infinite-redirect bug in this indexer build. +const INDEXER_HTTP_URL = + Deno.env.get("INDEXER_HTTP_URL") ?? "http://127.0.0.1:8088/api/v3/graphql"; +const INDEXER_WS_URL = + Deno.env.get("INDEXER_WS_URL") ?? "ws://127.0.0.1:8088/api/v3/graphql/ws"; + +const REPO_ROOT = Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); +const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); + +function loadContractAddress(): string { + const raw = Deno.readTextFileSync(CONTRACT_ADDRESS_FILE); + const parsed = JSON.parse(raw); + return parsed.contractAddress.replace(/^0x/, ""); +} + +Deno.test({ + name: "indexer: queryContractState returns current state", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + setNetworkId("undeployed"); + const provider = indexerPublicDataProvider(INDEXER_HTTP_URL, INDEXER_WS_URL); + const addr = loadContractAddress(); + console.log(` contract address = ${addr}`); + + const state = await provider.queryContractState(addr as any); + assertExists(state, "queryContractState returned null"); + console.log(` state exists: ${typeof state}, keys=${Object.keys(state).join(",")}`); + if ("data" in state) { + console.log(` state.data type: ${typeof state.data}`); + } + }, +}); + +Deno.test({ + name: "indexer: contractStateObservable emits at least once", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + setNetworkId("undeployed"); + const provider = indexerPublicDataProvider(INDEXER_HTTP_URL, INDEXER_WS_URL); + const addr = loadContractAddress(); + + const observable = provider.contractStateObservable(addr as any, { type: "latest" }); + console.log(` subscribed; awaiting first emission (10s timeout)...`); + + const firstState = await firstValueFrom( + observable.pipe( + take(1), + timeout({ first: 10_000 }), + ), + ); + + assertExists(firstState, "observable did not emit"); + console.log(` received emission: ${typeof firstState}`); + console.log(` emission keys: ${Object.keys(firstState as any).join(",")}`); + }, +}); diff --git a/e2e/smoke/midnight-imports.test.ts b/e2e/smoke/midnight-imports.test.ts new file mode 100644 index 0000000..9e11c99 --- /dev/null +++ b/e2e/smoke/midnight-imports.test.ts @@ -0,0 +1,65 @@ +/** + * Step 1 smoke test — Deno compat for Midnight JS libs + rxjs. + * + * Verifies every package the full e2e test needs can be imported and its + * main export is callable / constructible under Deno. No external services + * required. + * + * Run: deno task --filter @go-fish/e2e smoke:imports + */ + +import { assertExists } from "jsr:@std/assert"; + +import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts"; +import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; +import { setNetworkId, getNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; +import type { NetworkId } from "@midnight-ntwrk/midnight-js-network-id"; +import { filter, firstValueFrom, map, shareReplay, timeout, Subject } from "rxjs"; + +Deno.test("Midnight JS libs import cleanly under Deno", () => { + assertExists(findDeployedContract, "findDeployedContract"); + assertExists(httpClientProofProvider, "httpClientProofProvider"); + assertExists(indexerPublicDataProvider, "indexerPublicDataProvider"); + assertExists(NodeZkConfigProvider, "NodeZkConfigProvider"); + assertExists(setNetworkId, "setNetworkId"); + assertExists(getNetworkId, "getNetworkId"); + const _id: NetworkId = "undeployed"; // type-only use +}); + +Deno.test("rxjs pipes work", async () => { + const s = new Subject(); + const out$ = s.pipe( + map(n => n * 2), + filter(n => n > 4), + shareReplay(1), + timeout({ first: 5_000 }), + ); + queueMicrotask(() => { s.next(1); s.next(2); s.next(3); }); + const first = await firstValueFrom(out$); + if (first !== 6) throw new Error(`expected 6, got ${first}`); +}); + +Deno.test("NodeZkConfigProvider constructs with a path", () => { + const provider = new NodeZkConfigProvider( + "/tmp/does-not-exist-yet", + ); + assertExists(provider, "NodeZkConfigProvider instance"); +}); + +Deno.test("httpClientProofProvider constructs with a URL", () => { + const fakeZkConfig = new NodeZkConfigProvider("/tmp/placeholder"); + const provider = httpClientProofProvider("http://127.0.0.1:6300", fakeZkConfig); + assertExists(provider, "httpClientProofProvider instance"); +}); + +Deno.test("indexerPublicDataProvider constructs with URLs", () => { + const provider = indexerPublicDataProvider( + "http://127.0.0.1:8088/api/v1/graphql", + "ws://127.0.0.1:8088/api/v1/graphql/ws", + ); + assertExists(provider, "indexerPublicDataProvider instance"); + assertExists(provider.queryContractState, "queryContractState method"); + assertExists(provider.contractStateObservable, "contractStateObservable method"); +}); diff --git a/e2e/smoke/one-turn.test.ts b/e2e/smoke/one-turn.test.ts new file mode 100644 index 0000000..05415ff --- /dev/null +++ b/e2e/smoke/one-turn.test.ts @@ -0,0 +1,162 @@ +/** + * Step 9 smoke test — one full turn after setup. + * + * Flow: + * 1. Full setup (applyMask×2 + dealCards×2) → phase=TurnStart, hands=4/4 + * 2. Read P1's hand via doesPlayerHaveSpecificCard loop (21 reads) + * 3. Read P2's hand (debug-only — logs both, consistent with plan §1) + * 4. Pick the first rank in P1's hand, askForCard(gameId, 1, rank, now) + * 5. Wait for phase==WaitForResponse, currentTurn==2 + * 6. respondToAsk(gameId, 2, now) + * 7. Branch on result phase: + * TurnStart → P2 had cards, transferred + * WaitForDrawCheck → Go Fish, call afterGoFish(gameId, 1, now), + * wait for phase to return to TurnStart + * 8. Read both players' final hands and log the delta + * + * PREREQS: same as setup test. + * + * Run: deno task --filter @go-fish/e2e smoke:one-turn + */ + +import { assertEquals } from "jsr:@std/assert"; +import { + callDelegated, + cardName, + clearAllSecrets, + createSmokeSession, + handRanks, + isMissing, + nowSeconds, + PHASE, + readHand, + runFullSetup, + waitFor, +} from "./_helpers.ts"; + +Deno.test({ + name: "turn: askForCard → respondToAsk → (optional) afterGoFish", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + const session = await createSmokeSession("smoke-one-turn"); + const { contract, walletProvider, read } = session; + + const setup = await runFullSetup(session); + const { gameId, gameIdHex } = setup; + + try { + // ─── Read starting hands ─────────────────────────────────────── + console.log("\n── reading hands ──"); + const p1HandBefore = await readHand(session, gameId, 1); + const p2HandBefore = await readHand(session, gameId, 2); + console.log(` P1 hand: ${p1HandBefore.map(cardName).join(" ")}`); + console.log(` P2 hand: ${p2HandBefore.map(cardName).join(" ")}`); + + assertEquals(p1HandBefore.length, 4, "P1 should have 4 cards"); + assertEquals(p2HandBefore.length, 4, "P2 should have 4 cards"); + + const currentTurnBefore = Number(await read("getCurrentTurn", gameId)); + console.log(` currentTurn before ask: ${currentTurnBefore}`); + assertEquals(currentTurnBefore, 1, "first turn should be P1"); + + // ─── P1 asks for the first rank in their hand ────────────────── + const askRank = handRanks(p1HandBefore)[0]!; + const rankLabel = ["A", "2", "3", "4", "5", "6", "7"][askRank]; + const p2HasRank = p2HandBefore.some(c => c % 7 === askRank); + console.log(`\n── P1 asks P2 for ${rankLabel} (P2 actually has it? ${p2HasRank}) ──`); + + const askTime = nowSeconds(); + await callDelegated( + walletProvider, + "askForCard", + () => contract.callTx.askForCard(gameId, 1n, BigInt(askRank), askTime), + { playerId: 1 }, + ); + + // Phase should flip to WaitForResponse. currentTurn STAYS with the + // asker (P1) throughout the ask→respond cycle — the opponent is + // identified inside respondToAsk as "not the last asking player", + // not by being currentTurn. So we only check the phase here. + await waitFor( + "phase == WaitForResponse", + 120_000, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) === PHASE.WaitForResponse, + ); + const turnAfterAsk = Number(await read("getCurrentTurn", gameId)); + console.log(` currentTurn after ask: ${turnAfterAsk} (unchanged from asker)`); + assertEquals(turnAfterAsk, 1); + + // ─── P2 responds ─────────────────────────────────────────────── + console.log("\n── P2 responds to ask ──"); + const respondTime = nowSeconds(); + await callDelegated( + walletProvider, + "respondToAsk", + () => contract.callTx.respondToAsk(gameId, 2n, respondTime), + { playerId: 2 }, + ); + + // After respondToAsk, the contract's phase will be either: + // - TurnStart (P2 had the rank → cards transferred) + // - WaitForDrawCheck (Go Fish, deck non-empty → asker drew a card) + // - (if deck empty and Go Fish → switchTurn, phase becomes TurnStart) + console.log(" waiting for phase to leave WaitForResponse..."); + const postRespondPhase = await waitFor( + "phase != WaitForResponse", + 120_000, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) !== PHASE.WaitForResponse, + ); + const phaseAfterRespond = Number(postRespondPhase); + console.log(` phase after respond: ${phaseAfterRespond}`); + + if (phaseAfterRespond === PHASE.WaitForDrawCheck) { + // Go Fish! P1 drew a card from the deck. Must call afterGoFish to + // verify the drawn card and transition to TurnStart (or switchTurn). + console.log("\n── Go Fish: P1 calls afterGoFish ──"); + const drawTime = nowSeconds(); + await callDelegated( + walletProvider, + "afterGoFish", + () => contract.callTx.afterGoFish(gameId, 1n, drawTime), + { playerId: 1 }, + ); + await waitFor( + "phase == TurnStart after afterGoFish", + 120_000, + () => read("getGamePhase", gameId), + v => !isMissing(v) && Number(v) === PHASE.TurnStart, + ); + } else if (phaseAfterRespond === PHASE.TurnStart) { + console.log(" ✓ P2 had cards — direct transfer, no draw needed"); + } else { + throw new Error( + `unexpected phase after respondToAsk: ${phaseAfterRespond}`, + ); + } + + // ─── Read final hands ────────────────────────────────────────── + console.log("\n── final hands ──"); + const p1HandAfter = await readHand(session, gameId, 1); + const p2HandAfter = await readHand(session, gameId, 2); + console.log(` P1 hand: ${p1HandAfter.map(cardName).join(" ")} (${p1HandAfter.length})`); + console.log(` P2 hand: ${p2HandAfter.map(cardName).join(" ")} (${p2HandAfter.length})`); + + // Sanity: total cards visible to each player should add up the same + // way. If P2 had the rank, cards moved from P2 to P1 (P1 grew by N, + // P2 shrank by N). If Go Fish, P1 grew by 1 (drew from deck), P2 + // stayed the same. + const p1Delta = p1HandAfter.length - p1HandBefore.length; + const p2Delta = p2HandAfter.length - p2HandBefore.length; + console.log(` Δ P1=${p1Delta >= 0 ? "+" : ""}${p1Delta}, P2=${p2Delta >= 0 ? "+" : ""}${p2Delta}`); + + const finalPhase = Number(await read("getGamePhase", gameId)); + console.log(`\n ✓ turn complete, phase=${finalPhase} (TurnStart)`); + assertEquals(finalPhase, PHASE.TurnStart); + } finally { + await clearAllSecrets(gameIdHex); + } + }, +}); diff --git a/e2e/smoke/setup-sequence.test.ts b/e2e/smoke/setup-sequence.test.ts new file mode 100644 index 0000000..27ec08e --- /dev/null +++ b/e2e/smoke/setup-sequence.test.ts @@ -0,0 +1,449 @@ +/** + * Step 8 smoke test — full per-game setup sequence: + * applyMask(1) → applyMask(2) → dealCards(1) → dealCards(2) + * + * End state: phase == TurnStart (1), both players have 4-card hands, the + * 21-card deck has 13 remaining (21 - 8 dealt). + * + * Uses ONE contract instance + ONE delegating provider for both players. + * Both players' secrets are set in the shared witness module upfront (the + * contract's dealCards / respondToAsk circuits consult BOTH players' + * secrets during proving — this is a contract-level design choice, not + * something we can fix in the test layer). + * + * State reads use `localContract.impureCircuits` against a QueryContext + * hydrated from fresh indexer state — no VM-ops path tables, just typed + * circuit results. + * + * PREREQS: full stack running, staticDeckInitialized == true, all three + * smokes (ledger, lobby, init-deck, apply-mask) passing. + * + * Run: deno task --filter @go-fish/e2e smoke:setup + */ + +import { assertEquals } from "jsr:@std/assert"; +import { resolve } from "jsr:@std/path"; +import { + toHex, + QueryContext, + CostModel, + sampleContractAddress, + createConstructorContext, +} from "@midnight-ntwrk/compact-runtime"; +import { CompiledContract } from "@midnight-ntwrk/compact-js"; +import { findDeployedContract } from "@midnight-ntwrk/midnight-js-contracts"; +import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider"; +import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider"; +import { NodeZkConfigProvider } from "@midnight-ntwrk/midnight-js-node-zk-config-provider"; +import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id"; + +import { Contract as GoFishContract } from "@go-fish/midnight-contract/contract"; +import { + witnesses, + setPlayerSecrets, + clearPlayerSecrets, +} from "@go-fish/midnight-contract"; +import { createInMemoryPrivateStateProvider } from "../../frontend/src/services/midnightInMemoryPrivateStateProvider.ts"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const REPO_ROOT = Deno.env.get("GO_FISH_REPO_ROOT") ?? Deno.cwd().replace(/\/e2e\/?$/, ""); +const BATCHER_URL = Deno.env.get("BATCHER_URL") ?? "http://localhost:3336"; +const PROOF_URL = Deno.env.get("PROOF_URL") ?? "http://127.0.0.1:6300"; +const INDEXER_HTTP_URL = + Deno.env.get("INDEXER_HTTP_URL") ?? "http://127.0.0.1:8088/api/v3/graphql"; +const INDEXER_WS_URL = + Deno.env.get("INDEXER_WS_URL") ?? "ws://127.0.0.1:8088/api/v3/graphql/ws"; + +const CONTRACT_ADDRESS_FILE = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract.undeployed.json", +); +const ZK_CONFIG_PATH = resolve( + REPO_ROOT, + "packages/shared/contracts/midnight/go-fish-contract/src/managed", +); + +const DELEGATED_SENTINEL = "GoFish: delegated to midnight_balancing batcher"; +const JUBJUB_R = 0x0e7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7n; + +// GamePhase enum values (from go_fish_GamePhase in GoFish.compact) +const PHASE_SETUP = 0; +const PHASE_TURN_START = 1; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function loadContractAddress(): string { + return JSON.parse(Deno.readTextFileSync(CONTRACT_ADDRESS_FILE)).contractAddress.replace(/^0x/, ""); +} + +function generatePlayerSecret(): bigint { + while (true) { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + const n = BigInt("0x" + Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("")); + if (n >= 1n && n < JUBJUB_R) return n; + } +} + +function generateShuffleSeed(): Uint8Array { + const seed = new Uint8Array(32); + crypto.getRandomValues(seed); + return seed; +} + +function randomGameId(): Uint8Array { + const id = new Uint8Array(32); + crypto.getRandomValues(id); + return id; +} + +function gameIdToHex(gameId: Uint8Array): string { + return "0x" + Array.from(gameId).map(b => b.toString(16).padStart(2, "0")).join(""); +} + +// --------------------------------------------------------------------------- +// Batcher POST — midnight_balancing +// --------------------------------------------------------------------------- + +function detectTxStage(serializedTx: string): "unproven" | "unbound" | "finalized" { + const prefixBytes = new Uint8Array( + serializedTx.slice(0, 600).padEnd(600, "0").match(/.{2}/g)!.map(b => parseInt(b, 16)), + ); + const header = new TextDecoder().decode(prefixBytes); + const m = header.match(/midnight:(?:transaction|intent)\[v\d+\]\(signature\[v\d+\],([^,]+),([^)]+)\):/); + if (!m) throw new Error(`detectTxStage: cannot parse: ${header.slice(0, 80)}`); + if (m[1]!.includes("proof-preimage")) return "unproven"; + if (m[2]!.includes("embedded-fr")) return "unbound"; + if (m[2]!.includes("pedersen-schnorr")) return "finalized"; + throw new Error(`detectTxStage: unknown: ${m[1]} / ${m[2]}`); +} + +async function postMidnightTx( + serializedTx: string, + circuitId: string, + meta: { playerId?: number } = {}, +): Promise { + const txStage = detectTxStage(serializedTx); + console.log(` [batcher] ${circuitId}: txStage=${txStage}, len=${serializedTx.length / 2}B`); + + const res = await fetch(`${BATCHER_URL}/send-input`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + data: { + target: "midnight_balancing", + addressType: 0, + address: "go_fish_player", + input: JSON.stringify({ tx: serializedTx, txStage, circuitId, ...meta }), + timestamp: Date.now(), + }, + confirmationLevel: "no-wait", + }), + }); + const text = await res.text(); + console.log(` [batcher] ${circuitId} → ${res.status} ${text.slice(0, 120)}`); + if (!res.ok) throw new Error(`batcher rejected ${circuitId}: ${text}`); +} + +// --------------------------------------------------------------------------- +// Delegating provider +// --------------------------------------------------------------------------- + +function makeDelegatingProvider() { + const provider: any = { + getCoinPublicKey: () => "00".repeat(32), + getEncryptionPublicKey: () => "00".repeat(32), + async balanceTx(tx: any) { + if (typeof provider.__delegatedBalanceHook === "function") { + await provider.__delegatedBalanceHook(tx); + throw new Error(DELEGATED_SENTINEL); + } + throw new Error("balanceTx called without hook"); + }, + submitTx(_tx: any) { + throw new Error("submitTx should never be called"); + }, + __delegatedBalanceHook: undefined as undefined | ((tx: any) => Promise), + }; + return provider; +} + +function isDelegationError(err: unknown): boolean { + if (!(err instanceof Error)) return false; + let e: Error | undefined = err; + while (e) { + if (e.message.includes(DELEGATED_SENTINEL)) return true; + e = e.cause instanceof Error ? e.cause : undefined; + } + return false; +} + +async function callDelegated( + provider: any, + circuitId: string, + invoke: () => Promise, + meta: { playerId?: number } = {}, +): Promise { + let delegated = false; + provider.__delegatedBalanceHook = async (tx: any) => { + const serializedTx = toHex((tx as any).serialize()); + await postMidnightTx(serializedTx, circuitId, meta); + delegated = true; + }; + try { + await invoke(); + } catch (err) { + if (isDelegationError(err) || delegated) return; + throw err; + } finally { + delete provider.__delegatedBalanceHook; + } +} + +// --------------------------------------------------------------------------- +// Semantic reads via local Contract + hydrated QueryContext +// --------------------------------------------------------------------------- + +function buildLocalContract() { + const localContract: any = new GoFishContract(witnesses as any); + const init = localContract.initialState(createConstructorContext({}, "0".repeat(64))); + return { localContract, initialPrivateState: init.currentPrivateState, initialZswap: init.currentZswapLocalState }; +} + +function makeReadCtx( + initialPrivateState: any, + initialZswap: any, + contractStateData: any, +): any { + return { + currentPrivateState: initialPrivateState, + currentZswapLocalState: initialZswap, + currentQueryContext: new QueryContext(contractStateData, sampleContractAddress()), + costModel: CostModel.initialCostModel(), + }; +} + +async function readCircuit( + publicDataProvider: any, + contractAddress: string, + localContract: any, + initialPrivateState: any, + initialZswap: any, + circuitName: string, + ...args: any[] +): Promise { + const cs = await publicDataProvider.queryContractState(contractAddress); + if (!cs) throw new Error("queryContractState returned null"); + const ctx = makeReadCtx(initialPrivateState, initialZswap, cs.data); + try { + const r = localContract.impureCircuits[circuitName](ctx, ...args); + return r.result as T; + } catch (err) { + const msg = (err as Error).message ?? ""; + // Fresh gameId: map lookups throw "expected a cell, received null" or + // "not present". Callers interpret this as "not yet set". + if (msg.includes("expected a cell, received null") || msg.includes("not present")) { + return { __missing: true }; + } + throw err; + } +} + +async function waitFor( + label: string, + timeoutMs: number, + fetch: () => Promise, + predicate: (v: T) => boolean, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const v = await fetch(); + if (predicate(v)) { + console.log(` ✓ ${label}: matched (${JSON.stringify(v)})`); + return v; + } + const remaining = Math.round((deadline - Date.now()) / 1000); + console.log(` [poll] ${label} pending (${JSON.stringify(v)}, ${remaining}s left)`); + await new Promise(r => setTimeout(r, 2_000)); + } + throw new Error(`Timeout: ${label}`); +} + +// --------------------------------------------------------------------------- +// The test +// --------------------------------------------------------------------------- + +Deno.test({ + name: "setup: applyMask×2 + dealCards×2 → phase=TurnStart, hands=4/4", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + setNetworkId("undeployed"); + + const contractAddress = loadContractAddress(); + const gameId = randomGameId(); + const gameIdHex = gameIdToHex(gameId); + const p1Secret = generatePlayerSecret(); + const p1Seed = generateShuffleSeed(); + const p2Secret = generatePlayerSecret(); + const p2Seed = generateShuffleSeed(); + + console.log(` contractAddress = ${contractAddress}`); + console.log(` gameId = ${gameIdHex.slice(0, 22)}...`); + console.log(` p1 secret = ${p1Secret.toString(16).slice(0, 12)}..., p2 secret = ${p2Secret.toString(16).slice(0, 12)}...`); + + const zkConfigProvider = new NodeZkConfigProvider(ZK_CONFIG_PATH); + const proofProvider = httpClientProofProvider(PROOF_URL, zkConfigProvider); + const publicDataProvider = indexerPublicDataProvider(INDEXER_HTTP_URL, INDEXER_WS_URL); + const privateStateProvider = createInMemoryPrivateStateProvider("smoke-setup"); + const walletProvider = makeDelegatingProvider(); + + const compiledContract = CompiledContract.make("go-fish", GoFishContract as any) + .pipe( + CompiledContract.withWitnesses(witnesses as any), + CompiledContract.withCompiledFileAssets(ZK_CONFIG_PATH), + ); + + console.log(" joining deployed contract..."); + const contract: any = await findDeployedContract( + { + privateStateProvider, + zkConfigProvider, + proofProvider, + publicDataProvider, + walletProvider, + midnightProvider: walletProvider, + } as any, + { + contractAddress: contractAddress as any, + compiledContract: compiledContract as any, + privateStateId: "smoke-setup" as any, + initialPrivateState: {}, + } as any, + ); + console.log(" ✓ contract joined"); + + // Local contract instance for read-only circuit evaluation against + // indexer state. Witnesses are shared with the deployed contract so + // doesPlayerHaveSpecificCard / etc. will use whatever secrets are + // currently set via setPlayerSecrets. + const { localContract, initialPrivateState, initialZswap } = buildLocalContract(); + + const read = async (name: string, ...args: any[]) => + readCircuit( + publicDataProvider, + contractAddress, + localContract, + initialPrivateState, + initialZswap, + name, + ...args, + ); + + // Load BOTH players' secrets into the shared witness module. The + // contract's dealCards / respondToAsk circuits consult both during + // proving (see witnesses.ts `getSecretKey`). They'll be cleared in + // the finally block. + setPlayerSecrets(gameIdHex, 1, p1Secret, p1Seed); + setPlayerSecrets(gameIdHex, 2, p2Secret, p2Seed); + + try { + // ─── applyMask(1) ────────────────────────────────────────────── + console.log("\n── applyMask(gameId, 1n) ──"); + const t0 = Date.now(); + await callDelegated( + walletProvider, + "applyMask:1", + () => contract.callTx.applyMask(gameId, 1n), + { playerId: 1 }, + ); + console.log(` ✓ proven + queued in ${Math.round((Date.now() - t0) / 1000)}s`); + + await waitFor( + "hasMaskApplied(1)", + 120_000, + () => read("hasMaskApplied", gameId, 1n), + v => v === true, + ); + + // ─── applyMask(2) ────────────────────────────────────────────── + console.log("\n── applyMask(gameId, 2n) ──"); + const t1 = Date.now(); + await callDelegated( + walletProvider, + "applyMask:2", + () => contract.callTx.applyMask(gameId, 2n), + { playerId: 2 }, + ); + console.log(` ✓ proven + queued in ${Math.round((Date.now() - t1) / 1000)}s`); + + await waitFor( + "hasMaskApplied(2)", + 120_000, + () => read("hasMaskApplied", gameId, 2n), + v => v === true, + ); + + // ─── dealCards(1) ────────────────────────────────────────────── + console.log("\n── dealCards(gameId, 1n) ──"); + const t2 = Date.now(); + await callDelegated( + walletProvider, + "dealCards:1", + () => contract.callTx.dealCards(gameId, 1n), + { playerId: 1 }, + ); + console.log(` ✓ proven + queued in ${Math.round((Date.now() - t2) / 1000)}s`); + + await waitFor( + "hasDealt(1)", + 180_000, + () => read("hasDealt", gameId, 1n), + v => v === true, + ); + + // ─── dealCards(2) ────────────────────────────────────────────── + console.log("\n── dealCards(gameId, 2n) ──"); + const t3 = Date.now(); + await callDelegated( + walletProvider, + "dealCards:2", + () => contract.callTx.dealCards(gameId, 2n), + { playerId: 2 }, + ); + console.log(` ✓ proven + queued in ${Math.round((Date.now() - t3) / 1000)}s`); + + // Phase should transition to TurnStart after second dealCards. + // getGamePhase's return is the Compact enum compiled to JS number, + // not bigint — just compare numerically. + await waitFor( + "phase == TurnStart", + 180_000, + () => read("getGamePhase", gameId), + v => Number(v) === PHASE_TURN_START, + ); + + // Final assertions + const handSizes = await read<[bigint | number, bigint | number]>("getHandSizes", gameId); + const h1 = Number((handSizes as any)[0]); + const h2 = Number((handSizes as any)[1]); + console.log(`\n final hand sizes: P1=${h1}, P2=${h2}`); + assertEquals(h1, 4, "P1 should have 4 cards"); + assertEquals(h2, 4, "P2 should have 4 cards"); + + const finalPhase = Number(await read("getGamePhase", gameId)); + console.log(` final phase: ${finalPhase}`); + assertEquals(finalPhase, PHASE_TURN_START); + + console.log(`\n ✓ setup complete — game ${gameIdHex.slice(0, 18)}... ready for play`); + } finally { + clearPlayerSecrets(gameIdHex, 1); + clearPlayerSecrets(gameIdHex, 2); + } + }, +}); diff --git a/e2e/smoke/test-witnesses.ts b/e2e/smoke/test-witnesses.ts new file mode 100644 index 0000000..597c997 --- /dev/null +++ b/e2e/smoke/test-witnesses.ts @@ -0,0 +1,270 @@ +/** + * Test-owned witness implementation for the Go Fish contract. + * + * Replaces the default `@go-fish/midnight-contract` witnesses module for + * the e2e test path, fixing BACKEND_ISSUES #1 at its root. + * + * The default module ships with a silent fallback: if `player_secret_key` + * or `shuffle_seed` is consulted for an unset gameIdHex/playerId pair, it + * returns a `Math.random()`-derived static key. Downstream `getSecretFromPlayerId` + * passes its ec_mul round-trip check (the verification doesn't constrain + * the secret's *identity*, only that secret × inverse = 1), so the wrong + * secret silently propagates and produces wrong masked card values. That + * causes `doesPlayerHaveSpecificCard` and `countCardsOfRank_withSecrets` + * to return mismatched results from a different witness call. + * + * This module: + * - Holds explicit per-game secrets in a `WitnessState` instance scoped + * per test session (no module-level global mutable state). + * - Throws immediately with a descriptive error if the contract asks for + * a secret/seed the test hasn't set. Failures are loud and traceable. + * - Reimplements every utility witness (modular inverse, deterministic + * shuffle, field-bit split, print helpers) from scratch so the test + * has zero dependency on the default witness module's state. + * + * The same `WitnessState` instance is wired into BOTH: + * - The deployed contract used for callTx submissions (via + * `CompiledContract.withWitnesses(makeTestWitnesses(state))`) + * - The local Contract instance used for read-only impureCircuit calls + * (via `new GoFishContract(makeTestWitnesses(state))`) + * so they share the same witness state and cannot diverge. + */ + +import type { WitnessContext } from "@midnight-ntwrk/compact-runtime"; + +/** Jubjub prime-order subgroup order — secrets must be in [1, r). */ +const JUBJUB_R = + 0x0e7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7n; + +export interface SecretEntry { + secret: bigint; + seed: Uint8Array; +} + +/** + * Per-session secret store. Holds the test's player secrets and shuffle + * seeds. Reset on every test run; no global state. + */ +export class WitnessState { + private map = new Map(); + private lastShuffleSeed: Uint8Array | null = null; + + /** Set both secret and shuffle seed for one player in one game. */ + set(gameIdHex: string, playerId: 1 | 2, secret: bigint, seed: Uint8Array): void { + this.map.set(`${gameIdHex}-${playerId}`, { secret, seed }); + } + + /** Drop one player's entry. */ + clear(gameIdHex: string, playerId: 1 | 2): void { + this.map.delete(`${gameIdHex}-${playerId}`); + } + + /** Drop both players' entries for a game. */ + clearAll(gameIdHex: string): void { + this.clear(gameIdHex, 1); + this.clear(gameIdHex, 2); + } + + /** Wipe everything. Useful between independent test cases. */ + reset(): void { + this.map.clear(); + this.lastShuffleSeed = null; + } + + getSecret(gameIdHex: string, playerId: number): bigint { + const entry = this.map.get(`${gameIdHex}-${playerId}`); + if (!entry) { + throw new Error( + `testWitness: missing secret for game=${gameIdHex.slice(0, 18)}… player=${playerId}`, + ); + } + return entry.secret; + } + + getSeed(gameIdHex: string, playerId: number): Uint8Array { + const entry = this.map.get(`${gameIdHex}-${playerId}`); + if (!entry) { + throw new Error( + `testWitness: missing seed for game=${gameIdHex.slice(0, 18)}… player=${playerId}`, + ); + } + return entry.seed; + } + + /** shuffle_seed records the seed it returned so get_sorted_deck_witness + * can derive deterministic weights from it inside the same circuit call. */ + setLastShuffleSeed(seed: Uint8Array): void { + this.lastShuffleSeed = seed; + } + takeLastShuffleSeed(): Uint8Array | null { + return this.lastShuffleSeed; + } +} + +// --------------------------------------------------------------------------- +// Pure utility implementations (no shared state) +// --------------------------------------------------------------------------- + +function gameIdToHex(gameId: Uint8Array): string { + return "0x" + Array.from(gameId).map(b => b.toString(16).padStart(2, "0")).join(""); +} + +/** Extended Euclidean modular inverse over n. Throws if not invertible. */ +function modInverse(a: bigint, n: bigint): bigint { + let t = 0n; + let newT = 1n; + let r = n; + let newR = a; + while (newR !== 0n) { + const q = r / newR; + [t, newT] = [newT, t - q * newT]; + [r, newR] = [newR, r - q * newR]; + } + if (r > 1n) throw new Error("testWitness: scalar not invertible"); + if (t < 0n) t += n; + return t; +} + +/** + * xorshift32 seeded from the first 4 bytes of `seed`. Bit-for-bit + * compatible with the default witness module's `deterministicWeights`, + * so a deck shuffled by one is reproducible by the other given the + * same seed. + */ +function deterministicWeights(seed: Uint8Array, count: number): number[] { + let state = + ((seed[0]! | 0) | + ((seed[1]! | 0) << 8) | + ((seed[2]! | 0) << 16) | + ((seed[3]! | 0) << 24)) >>> 0; + if (state === 0) state = 0xdeadbeef; + const out: number[] = []; + for (let i = 0; i < count; i++) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + state = state >>> 0; + out.push(state % 1_000_000); + } + return out; +} + +function splitFieldBits(fieldValue: bigint): [bigint, bigint] { + const TWO_64 = 1n << 64n; + return [fieldValue / TWO_64, fieldValue % TWO_64]; +} + +function noopPrint(ctx: WitnessContext, _b: unknown): [any, boolean] { + return [ctx.privateState, true]; +} + +// --------------------------------------------------------------------------- +// Witness factory +// --------------------------------------------------------------------------- + +/** + * Build a witnesses object backed by `state`. Pass to BOTH the deployed + * contract (CompiledContract.withWitnesses) and any local Contract instance + * used for read-only circuit evaluation, so they share state. + * + * Function names and shapes match the default `@go-fish/midnight-contract` + * witnesses module exactly — drop-in replacement. + */ +export function makeTestWitnesses(state: WitnessState) { + return { + print_field: noopPrint, + print_bytes_32: noopPrint, + print_vector_2_field: noopPrint, + print_curve_point: noopPrint, + print_uint_64: noopPrint, + + split_field_bits: ( + ctx: WitnessContext, + v: bigint, + ): [any, [bigint, bigint]] => [ctx.privateState, splitFieldBits(v)], + + getFieldInverse: ( + ctx: WitnessContext, + x: bigint, + ): [any, bigint] => { + if (x === 0n) throw new Error("testWitness: cannot invert zero"); + if (x >= JUBJUB_R) { + throw new Error(`testWitness: scalar ${x.toString(16)} >= Jubjub r`); + } + return [ctx.privateState, modInverse(x, JUBJUB_R)]; + }, + + /** + * Declared but never called from any .compact file (grep -rn + * "shuffle_seed(" shows only the declaration at Deck.compact:37). Kept + * here for completeness of the witness shape — if any future contract + * change starts calling it, behavior is identical to player_secret_key: + * it captures the seed for the next get_sorted_deck_witness call. + */ + shuffle_seed: ( + ctx: WitnessContext, + gameId: Uint8Array, + playerIndex: bigint, + ): [any, Uint8Array] => { + const seed = state.getSeed(gameIdToHex(gameId), Number(playerIndex)); + state.setLastShuffleSeed(seed); + return [ctx.privateState, seed]; + }, + + player_secret_key: ( + ctx: WitnessContext, + gameId: Uint8Array, + playerIndex: bigint, + ): [any, bigint] => { + const gameIdHex = gameIdToHex(gameId); + const playerIdNum = Number(playerIndex); + const secret = state.getSecret(gameIdHex, playerIdNum); + if (secret === 0n) { + throw new Error("testWitness: zero secret"); + } + if (secret >= JUBJUB_R) { + throw new Error(`testWitness: secret >= Jubjub r`); + } + // Capture this player's shuffle seed for the *next* call to + // `get_sorted_deck_witness`. This makes the deck shuffle + // deterministic PER (game, player): the same player in the same + // game always gets the same shuffled deck, because the seed is + // fixed when the user constructed this WitnessState entry. + // + // `shuffle_deck` (Deck.compact:230, the only caller of + // get_sorted_deck_witness in any .compact file) calls + // player_secret_key *immediately before* get_sorted_deck_witness in + // the same circuit, so the seed captured here is always the right + // one for the upcoming shuffle. + state.setLastShuffleSeed(state.getSeed(gameIdHex, playerIdNum)); + return [ctx.privateState, secret]; + }, + + get_sorted_deck_witness: ( + ctx: WitnessContext, + input: { x: bigint; y: bigint }[], + ): [any, { x: bigint; y: bigint }[]] => { + // The contract verifies (via grand product on jubjubX) that the + // returned vector is a permutation of `input`. The witness chooses + // the ordering — that IS the deck shuffle. + // + // Determinism guarantee: `player_secret_key` runs first inside + // `shuffle_deck` and writes the player's stored shuffle seed to + // `lastShuffleSeed`. We consume it here. Same (gameId, playerId) + // → same seed → same xorshift weights → same permutation. + // Multiple `shuffle_deck` calls for the same (game, player) — e.g. + // applyMask retried in a re-prove — produce identical results. + const lastSeed = state.takeLastShuffleSeed(); + if (!lastSeed) { + throw new Error( + "testWitness: get_sorted_deck_witness called without a captured " + + "shuffle seed — player_secret_key must run first inside shuffle_deck", + ); + } + const weights = deterministicWeights(lastSeed, input.length); + const indexed = input.map((p, i) => ({ x: p.x, y: p.y, w: weights[i]! })); + indexed.sort((a, b) => a.w - b.w); + return [ctx.privateState, indexed.map(p => ({ x: p.x, y: p.y }))]; + }, + }; +} From 712bfe3ce0fffbf0cb8fe11022dec7a6a3beb8b8 Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 23:18:33 -0400 Subject: [PATCH 05/78] =?UTF-8?q?Fix=20end-game=20stall:=20early-win=20at?= =?UTF-8?q?=20=E2=89=A54=20books=20+=20empty-hand=20ask=20+=20addScore=20d?= =?UTF-8?q?ouble-count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three contract changes resolve BACKEND_ISSUES #3 (game stalls in TurnStart with empty hand) and #4 (winner ledger never written): 1. GoFish.compact:addScore — replace the 7-book cap with an early-win at score ≥ 4 (majority of 7). Also fixes a read-after-write double-count bug: Compact's queryLedgerState returns the post-write value on re-read, so the old `lookup → insert → lookup → +1` pattern was triggering game-over at 3 books instead of 4. Now computes post-increment scores from the pre-read locally. 2. game.compact:askForCard — relax rule 5 when the asker's hand is empty. Routes empty-hand turns through respondToAsk's existing go-fish branch (no new circuit needed). Matches real Go Fish rules. 3. game.compact — add top-level getWinner wrapper so the managed contract exposes the V4 winner ledger reader. Client: e2e helpers drop the empty-hand bail, game-round test adds hard assertions for finalPhase==GameOver and winner∈{1,2}. Validated: 164/164 contract tests pass, 27/27 node tests pass, ec_mul guard detection clean, 3 consecutive sim runs all end at exactly 4 books with correct winner. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e/END_GAME_FIX_PLAN.md | 203 ++++++++++++++ e2e/END_GAME_FIX_TESTING.md | 247 ++++++++++++++++++ e2e/game-round.test.ts | 21 +- e2e/smoke/_helpers.ts | 22 +- .../go-fish-contract/src/GoFish.compact | 49 ++-- .../go-fish-contract/src/game.compact | 19 +- 6 files changed, 522 insertions(+), 39 deletions(-) create mode 100644 e2e/END_GAME_FIX_PLAN.md create mode 100644 e2e/END_GAME_FIX_TESTING.md diff --git a/e2e/END_GAME_FIX_PLAN.md b/e2e/END_GAME_FIX_PLAN.md new file mode 100644 index 0000000..2cbf9bf --- /dev/null +++ b/e2e/END_GAME_FIX_PLAN.md @@ -0,0 +1,203 @@ +# End-Game Stall Fix — Plan + +**Scope:** Resolve `BACKEND_ISSUES.md` #3 (game stalls in `TurnStart` +with an empty hand) and #4 (`winner` ledger never written when the +game stalls). #4 is a symptom of #3. + +**Status:** Design — not implemented. + +--- + +## 1. What we observed + +Three e2e runs, all hitting the same exit path in `_helpers.ts:918` +(`endReason: "empty hand"`). `winner` was never set in any run. + +| Run | Turns | Scores | Hands | Books | +|-----|-------|--------|-------|-------| +| 1 | 18 | 6 – 0 | 0, 2 | 6 / 7 | +| 2 | 15 | 0 – 5 | 3, 0 | 5 / 7 | +| 3 | 13 | 2 – 2 | 4, 0 | 4 / 7 | + +## 2. Root cause + +Two gaps combine into a dead end: + +1. **`askForCard` rejects you with 0 cards.** Rule 5 + (`game.compact:315`) requires `currentCount > 0` for the asked + rank, so any player whose hand reaches 0 can't call `askForCard` + for *anything*. +2. **There is no standalone "go fish" circuit.** The deck draw lives + inside `respondToAsk`'s go-fish branch (`game.compact:393-407`), + which is only reachable *after* a successful `askForCard`. Grep + confirms no exported `goFish` / `drawCard` path. + +Combine: empty hand → can't ask → can't reach the draw → stuck. Real +Go Fish has a "draw from deck when empty" exception; the contract +doesn't. + +Separately, `addScore` (`GoFish.compact:152`) only transitions to +`GameOver` at 7 total books — so runs 1 and 2 (decided at book 4) ran +past their mathematical end and stalled on an empty hand. + +## 3. Fix — two Compact diffs + +### 3.1 Early-win at ≥ 4 books — `GoFish.compact:addScore` + +**Replaces** the existing `books + 1 == 7` end-game block (lines +151-167). By pigeonhole, `totalBooks == 7` implies `max(s1,s2) ≥ 4`, +so the new check fires strictly earlier — keeping the old cap is +dead code. No tie branch: by induction, when this check fires, the +other player is strictly `< 4`. + +```compact +// Replaces the full body of addScore. Reads pre-increment values +// ONCE, computes locally, writes ONCE. Never re-reads the ledger +// after an insert (Compact's queryLedgerState returns the post-write +// value, which caused a double-count in the original code). +const prevS1 = scoreP1.lookup(gid) as Uint<8>; +const prevS2 = scoreP2.lookup(gid) as Uint<8>; +const newS1: Uint<8> = (disclosed_playerId == 1 as Uint<64>) + ? (prevS1 + 1 as Uint<8>) as Uint<8> : prevS1; +const newS2: Uint<8> = (disclosed_playerId == 2 as Uint<64>) + ? (prevS2 + 1 as Uint<8>) as Uint<8> : prevS2; + +if (disclosed_playerId == 1 as Uint<64>) { + scoreP1.insert(gid, newS1); +} else if (disclosed_playerId == 2 as Uint<64>) { + scoreP2.insert(gid, newS2); +} +const books = totalBooks.lookup(gid) as Uint<8>; +totalBooks.insert(gid, (books + 1 as Uint<8>) as Uint<8>); + +if (newS1 >= 4 as Uint<8>) { + phase.insert(gid, GamePhase.GameOver); + winner.insert(gid, 1 as Uint<64>); +} else if (newS2 >= 4 as Uint<8>) { + phase.insert(gid, GamePhase.GameOver); + winner.insert(gid, 2 as Uint<64>); +} +``` + +**Compact read-after-write pitfall:** `queryLedgerState` mutates the +circuit context in place, so `lookup` after `insert` on the same +ledger returns the post-write value. The original code used `lookup → +insert → lookup → +1` which double-counted (confirmed: game was +ending at 3 books instead of 4). Audit found no other same-ledger +read-after-write patterns in `GoFish.compact`, `game.compact`, +`Hand.compact`, or `Deck.compact`. + +**ec_mul safety:** scalar reads and phase/winner writes only. Safe. + +### 3.2 Empty-hand exception in rule 5 — `game.compact:askForCard` + +Let a player with an empty hand ask for *any* rank. This routes the +turn through `respondToAsk`'s existing go-fish branch — no new +circuit, no new signing convention. Matches the real Go Fish rule +("draw from deck when empty"). + +```compact +// game.compact, inside askForCard. REPLACE line 315: +// assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); +// with: + +// RULE 5: Must hold ≥ 1 card of the asked rank — EXCEPT when the hand is +// empty. An empty-hand ask is the real-Go-Fish "draw from deck" move and +// flows through respondToAsk's existing go-fish branch. +const askerHandSize = hand_getHandSize(gid, askingPlayerId); +if (askerHandSize > 0 as Uint<64>) { + assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); +} +``` + +**ec_mul safety:** `countCardsOfRank_withSecrets` (which contains the +ec_mul) still runs unconditionally above this block. The new `if` +only wraps the `assert`, which has no ec_mul. Safe. + +**Why rule 5 still exists for non-empty hands:** a non-empty player +asking for a rank they don't hold would leak nothing useful and waste +a turn — keep the assert for correctness on normal moves. + +## 4. Client changes — `e2e/smoke/_helpers.ts` + +1. **Drop the empty-hand bail** (`_helpers.ts:918-929`). With rule 5 + relaxed, an empty-handed `askForCard` is legal — let it flow + through `playOneTurn` normally. +2. **Pick a rank when the hand is empty.** `handRanks(handBefore)[0]` + (`_helpers.ts:932`) throws on an empty array. Replace with + `handRanks(handBefore)[0] ?? 0` (or any constant 0-6) — the + contract no longer cares whether the asker holds the rank. +3. **Promote the final-state assertion** in `e2e/game-round.test.ts`: + assert `result.finalPhase === PHASE.GameOver` and + `result.winner !== null && result.winner !== 0`. With the early-win + rule, ties are unreachable (§3.1 induction argument). + +No other helper changes. `checkAndEndGame` stays in the contract as a +safety net but the client never needs to call it — every finished +game exits via early-win. + +## 5. Managed contract regeneration + +`packages/shared/contracts/midnight/go-fish-contract/src/managed/**` +is auto-generated and currently stale (missing V4's `winner` / +`getWinner`). Run `deno task build:midnight` twice: once **before** +the edits (clean baseline), and once **after** (verify the `addScore` +diff and the relaxed assert appear in the generated output). + +Then run the ec_mul guard detection script from +`ZKIR-EC-MUL-GUARD-BUG.md`. Expected: 0 violations. + +## 6. Validation + +1. **Contract test suite** — `deno task --filter + @go-fish/midnight-contract test`. Existing 164 tests should still + pass. +2. **New contract test — early-win at 4.** Seed a game to 3 – 0, + score P1's 4th book, assert `phase == GameOver && winner == 1 && + totalBooks == 4`. +3. **New contract test — empty-hand ask.** Seed P1 with 0 cards and a + non-empty deck, call `askForCard(gid, 1, 0, now)`, then + `respondToAsk(gid, 2, now)`. Assert phase transitions correctly + through the go-fish branch and P1's hand size ends ≥ 1. +4. **`deno task test`** — state-machine tests; should be unaffected. +5. **`deno task test:e2e`** — run `game-round.test.ts` at least 3 + times. Expected: `finalPhase == GameOver`, `winner ∈ {1, 2}`, + `divergences == 0`, `exitReason` showing early-win. + +## 7. Rollout + +1. Regenerate managed contract on current sources (baseline commit). +2. Apply §3.1 diff to `GoFish.compact`. +3. Apply §3.2 diff to `game.compact`. +4. Regenerate managed contract (second commit — makes the diff + obvious). +5. Apply §4 client changes. +6. Run validation (§6). Close `BACKEND_ISSUES.md` #3 and #4. + +**Risk:** low. Both diffs are localized edits inside existing +circuits; no new circuits, no new signing conventions, no new ZKIR +surfaces. + +## 8. Open questions + +1. **Does `askForCard`'s timestamp / `assertCallerIsPlayer` path + depend on any side-channel that assumes `currentCount > 0`?** A + quick grep shows no — the check is self-contained. Verify + during implementation. +2. **Should `autoScoreBooks` also run for the *non-asking* player + after each turn?** Tangential to this fix, but with relaxed rule + 5 the non-active player may accumulate 3 of a rank via transfer + and not get scored until their own next turn. Not a correctness + issue (they'll still score eventually), but worth a follow-up. + +## 9. Summary + +- **Contract fix A** — `GoFish.compact:addScore`: replace the 7-book + cap with an early-win at score ≥ 4. Writes `winner`. +- **Contract fix B** — `game.compact:askForCard`: skip rule 5 when + the asker's hand is empty. Routes empty-hand turns through + `respondToAsk`'s existing go-fish branch — no new circuit. +- **Client fix** — `_helpers.ts`: drop the empty-hand bail and + tolerate `handRanks([])` when picking a rank. +- Issue #4 (`winner` never set) resolves automatically once every + game ends via early-win. diff --git a/e2e/END_GAME_FIX_TESTING.md b/e2e/END_GAME_FIX_TESTING.md new file mode 100644 index 0000000..629f207 --- /dev/null +++ b/e2e/END_GAME_FIX_TESTING.md @@ -0,0 +1,247 @@ +# End-Game Fix — Testing Handoff + +**Context:** Two Compact diffs + a managed-contract regen resolved +`BACKEND_ISSUES.md` #3 (game stalls in `TurnStart` with empty hand) +and #4 (`winner` ledger never written). Full design rationale lives +in `e2e/END_GAME_FIX_PLAN.md`. The implementation is complete and all +162 pre-existing contract tests still pass; your job is to add +coverage for the new behavior and run e2e validation. + +--- + +## What changed in the contract + +1. **Early-win at ≥ 4 books** — `GoFish.compact:addScore` now + transitions to `GameOver` and writes `winner` the moment either + score reaches 4. Replaces the old `books + 1 == 7` cap. By + pigeonhole, book 7 is now unreachable; every game ends at + totalBooks ∈ {4, 5, 6}. + - `winner == 0` (tie) is **unreachable** via the early-win path by + induction — the other player is always strictly `< 4` when the + check fires. Only the legacy `endGame` fallback can ever write + `winner == 0`. + +2. **Empty-hand asks are legal** — `game.compact:askForCard` rule 5 + (`assert(currentCount > 0, ...)`) is now wrapped in + `if (hand_getHandSize(gid, disclosed_playerId) > 0 as Uint<64>)`. + With an empty hand, the asker may request *any* rank; the flow + continues through `respondToAsk`'s existing three branches: + - **Transfer:** opponent had cards of rank → move them to the + asker's hand → phase → `TurnStart`. + - **Go fish:** opponent had none, deck non-empty → draw 1 for + asker → phase → `WaitForDrawCheck` → `afterGoFish`. + - **Switch:** opponent had none, deck empty → `switchTurn` to + opponent. + +3. **`getWinner` exposed at top level** — new wrapper in + `game.compact` delegating to `go_fish_getWinner`. Managed contract + now has 31 circuits (was 30). Before this, `getWinner` was only in + the `GoFish` module and didn't appear in + `managed/contract/index.d.ts`, which is why the e2e test was + always reading it as `__missing`. + +4. **Nothing else changed** — `respondToAsk`, `afterGoFish`, + `checkAndScoreBook`, `switchTurn`, `claimTimeoutWin`, + `checkAndEndGame` are all untouched. + +## What changed in the e2e harness + +- `e2e/smoke/_helpers.ts:playOneTurn` — the empty-hand bail at the + top of the function is gone. When `handBefore.length === 0` the + helper now picks rank 0 and calls `askForCard` normally, relying + on the contract's new rule-5 exception. Logs a distinct message + for the empty-hand path. +- `e2e/game-round.test.ts` — added two hard assertions at the end of + the test body: + - `result.finalPhase === PHASE.GameOver` (throws on regression) + - `result.winner === 1 || result.winner === 2` + (throws on `null`, `0`, or anything else) + +## Contract tests to add + +Add to `packages/shared/contracts/midnight/example.test.ts` (same +file as the existing 162 tests). Tag each with a new `N`-series ID +or similar. + +1. **Early-win at exactly 4.** Seed scoreP1 = 3, P2 = 0 via repeated + `checkAndScoreBook` calls. Score P1's 4th book. Assert: + - `getGamePhase(gid) == GamePhase.GameOver` (6) + - `getWinner(gid) == 1` + - `totalBooks == 4` + - Follow-up `askForCard`, `switchTurn`, `checkAndScoreBook` + calls all fail with the existing "game over" guards. + +2. **Early-win on the trailing side.** Mirror of (1): P2 scores the + 4th book first. Assert `winner == 2`. + +3. **Early-win at 4-2 and 4-3.** Mid-game states where the winner's + score reaches 4 while the opponent holds 2 or 3. Verify the rule + fires regardless of opponent score. + +4. **Guard against off-by-one.** Seed a 3 – X state and verify the + game is *not* over — the rule must not fire below 4. + +5. **Empty-hand ask — transfer branch.** Seed asker's hand to 0, + opponent holds ≥ 1 card of rank R. Call + `askForCard(gid, asker, R, now)` then + `respondToAsk(gid, opponent, now)`. Assert: + - Both calls succeed (no rule-5 assertion failure). + - Asker's hand size increases by exactly the count of R the + opponent held. + - Phase transitions to `TurnStart`. + +6. **Empty-hand ask — go-fish branch.** Asker hand = 0, opponent has + 0 of the asked rank, deck ≥ 1 card. Assert: + - Phase transitions to `WaitForDrawCheck` after `respondToAsk`. + - `afterGoFish` succeeds and transitions out of + `WaitForDrawCheck`. + - Asker's hand size == 1 at the end. + +7. **Empty-hand ask — switch-turn branch.** Asker hand = 0, + opponent has 0, deck empty. Assert: + - `currentTurn` switches to opponent. + - Phase is `TurnStart`. + - Asker's hand is still 0. + +8. **Rule 5 still enforced for non-empty hands.** Asker has exactly + 1 card (rank X). Call `askForCard` for rank Y ≠ X. Assert it + fails with `"Cannot ask for a rank you don't have in your hand"`. + +9. **`getWinner` pre-game-over.** Immediately after `init_game` or + `applyMask`, call `getWinner(gid)`. Assert `== 0` (no winner + recorded). + +10. **`getWinner` post-game-over.** Full game → early-win. Assert + `getWinner` returns `1` or `2` matching the `winner` ledger + entry and consistent with the final score comparison. + +11. **No off-by-one on early-win (regression for read-after-write + double-count bug).** Seed scoreP1 = 3, totalBooks = 3, scoreP2 = + 0. Call `checkAndScoreBook` to score P1's 4th book. Re-read + `getScores` — must be `[4, 0]`, NOT `[3, 0]` (which would mean + game-over fired before the 4th write via the old double-count). + Assert `getGamePhase == GameOver`, `getWinner == 1`, + `totalBooks == 4`. This is the regression test for the Compact + `queryLedgerState` read-after-write pitfall. + +## e2e validation + +Run the full stack and execute `game-round.test.ts` ≥ 3 times to +sample across shuffle nondeterminism. + +**Green run looks like:** + +``` +exitReason: phase → GameOver +finalPhase: 6 (GameOver=6) +winner: 1 (or 2; never null and never 0) +divergences: 0 +``` + +**Red flags (regressions):** + +- `exitReason: "empty hand"` — rule 5 still blocking. Verify + `disclosed_playerId` is being passed to `hand_getHandSize`. +- `exitReason: "MAX_TURNS (20) reached"` — the loop isn't + progressing. Likely the empty-hand path returned without + advancing state. Check the logs for repeated `(empty)` hands. +- `winner: null` — `getWinner` isn't exposed at the top level (or + the managed contract wasn't regenerated). Run + `deno task build:midnight` and check `index.d.ts` for the + `getWinner` entry. +- `winner: 0` — the early-win path produced a tie, which should be + mathematically impossible. Inspect `finalS1` / `finalS2` + computation in `addScore`. +- The helper's `sum >= 7` break in `runFullGame` (`_helpers.ts:1131`) + firing — also should be unreachable. If it fires, the contract's + majority rule isn't ending games. + +## Commands + +```bash +# Contract simulation suite (expect 162/162) +cd packages/shared/contracts/midnight && deno task test + +# Node API suite (expect 27/27) +deno task test + +# Rebuild managed contract from .compact sources +deno task build:midnight + +# ec_mul guard bug detection (expect zero output) +cd packages/shared/contracts/midnight/go-fish-contract && \ +for zkir in src/managed/zkir/*.zkir; do + ec_muls=$(grep -c "ec_mul" "$zkir") + guarded=$(grep -c 'public_input { guard: [^n]' "$zkir") + if [ "$ec_muls" -gt 0 ] && [ "$guarded" -gt 0 ]; then + echo "UNSAFE: $(basename "$zkir")" + fi +done + +# Full e2e (requires Hardhat :8545, Paima :9996, batcher :3336, +# Midnight node+indexer :8088, proof server :6300, deployed contract) +deno task test:e2e +``` + +## Gotchas + +1. **Managed contract is regenerated.** `managed/**` diffs are + large and auto-generated. Don't hand-edit; `deno task + build:midnight` to reproduce. + +2. **Witness-disclosure rule.** The first build failed because + `hand_getHandSize(gid, askingPlayerId)` leaked the witness. Fix + was to pass `disclosed_playerId` instead. If you add new Hand + circuit calls with variable player IDs, use the disclosed form. + +3. **Existing test `N5 game ends at 7 books`** now reports "game + over with 5 books (ended via addScore)" and passes. The test's + assertion was flexible enough to accept the new behavior. Leave + it alone — or rename to `game ends at early-win threshold`. + +4. **`BACKEND_ISSUES.md` #1 is NOT fixed.** The default witness + module still has the `Math.random()` fallback bug. The e2e test + sidesteps it via `e2e/smoke/test-witnesses.ts`. Don't regress to + the default witnesses. + +5. **Frontend is out of scope.** No changes to `frontend/`. A + follow-up ticket should track: (a) reading `getWinner` on the + GameOver screen, (b) wiring the empty-hand UI to the relaxed + rule-5 ask path. Not your problem for this PR. + +6. **`checkAndEndGame` is now dead code in the happy path.** It's + still present and still works, but the client never needs to + call it — every game ends via early-win. Keep it as a safety + net for the pathological 3-3 edge case (which only matters if + the 4+ rule is ever disabled). + +7. **Compact read-after-write pitfall.** `queryLedgerState` mutates + the circuit context in place — a `lookup` after an `insert` on + the same ledger returns the post-write value, not a snapshot. + The original `addScore` had `lookup → insert → lookup → +1` + which double-counted. If you add new contract logic that reads + a ledger after writing to it in the same circuit, compute the + value locally instead. Audit confirmed no other same-ledger + read-after-write patterns exist in the current codebase. + +## Files of interest + +- `e2e/END_GAME_FIX_PLAN.md` — design rationale, coverage table, + edge cases. +- `e2e/BACKEND_ISSUES.md` — issues #3 and #4 can move to "Resolved" + once you've confirmed the e2e runs are green. +- `packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact` + — the `addScore` diff. +- `packages/shared/contracts/midnight/go-fish-contract/src/game.compact` + — the `askForCard` rule-5 relaxation + the new top-level + `getWinner` wrapper. +- `packages/shared/contracts/midnight/go-fish-contract/src/managed/contract/index.d.ts` + — verify `getWinner` appears in `ImpureCircuits`, + `ProvableCircuits`, and `Circuits`. + +## Done means + +- [ ] 11 new contract tests added and passing (list above). +- [ ] E2E run 3×, all green on the new assertions. +- [ ] `BACKEND_ISSUES.md` #3 and #4 moved to "Resolved". +- [ ] ec_mul guard script reports zero violations. diff --git a/e2e/game-round.test.ts b/e2e/game-round.test.ts index f3d0835..a46b409 100644 --- a/e2e/game-round.test.ts +++ b/e2e/game-round.test.ts @@ -57,8 +57,11 @@ Deno.test({ const setup = await runFullSetup(session, { gameId }); try { - // 3. Turn loop until GameOver / sum==7 / MAX_TURNS (20). - const result = await runFullGame(session, gameId, 20); + // 3. Turn loop until GameOver / sum==7 / MAX_TURNS. + // 30 turns gives headroom for shuffles where books form late — + // with the early-win threshold correctly at 4 (not the old + // double-increment 3), some games need 22–25 turns. + const result = await runFullGame(session, gameId, 30); console.log(`\n══════════════════════════════════`); console.log(` E2E result`); @@ -71,6 +74,20 @@ Deno.test({ console.log(` divergences: ${result.divergenceCount}`); console.log(` finalPhase: ${result.finalPhase} (GameOver=${PHASE.GameOver})`); console.log(` winner: ${result.winner ?? "(not set)"}`); + + // With the early-win fix (addScore ≥ 4) and the rule-5 relaxation in + // askForCard, every finished game must exit in GameOver with a real + // winner. Ties are unreachable via the early-win path. + if (result.finalPhase !== PHASE.GameOver) { + throw new Error( + `expected finalPhase == GameOver (${PHASE.GameOver}), got ${result.finalPhase}. exitReason: ${result.exitReason}`, + ); + } + if (result.winner !== 1 && result.winner !== 2) { + throw new Error( + `expected winner in {1, 2}, got ${result.winner ?? "null"}. scores: ${result.finalScores[0]}-${result.finalScores[1]}`, + ); + } } finally { clearAllSecrets(session, setup.gameIdHex); } diff --git a/e2e/smoke/_helpers.ts b/e2e/smoke/_helpers.ts index b2a1d8d..35e8237 100644 --- a/e2e/smoke/_helpers.ts +++ b/e2e/smoke/_helpers.ts @@ -915,23 +915,17 @@ export async function playOneTurn( console.log(` P${playerId} hand: ${handBefore.map(cardName).join(" ") || "(empty)"} [${handBefore.length}]`); console.log(` P${opponent} hand: ${oppHandBefore.map(cardName).join(" ") || "(empty)"} [${oppHandBefore.length}]`); + // Empty hand is legal after the rule-5 relaxation — the contract routes + // an empty-hand ask through respondToAsk's go-fish branch, giving the + // player a card from the deck. Pick rank 0 when we have nothing to ask + // about (the contract no longer requires the asker to hold the rank). + const askRank = handBefore.length === 0 ? 0 : handRanks(handBefore)[0]!; if (handBefore.length === 0) { - return { - ...empty, - playerId, - opponent, - handBefore, - oppHandBefore, - handAfter: handBefore, - oppHandAfter: oppHandBefore, - endReason: `P${playerId} has empty hand`, - }; + console.log(` → P${playerId} is empty, asking for ${RANKS[askRank]} to trigger go-fish draw`); + } else { + console.log(` → P${playerId} asks P${opponent} for ${RANKS[askRank]}`); } - // Pick the first rank in our hand (simple heuristic; fine for testing) - const askRank = handRanks(handBefore)[0]!; - console.log(` → P${playerId} asks P${opponent} for ${RANKS[askRank]}`); - // Ask await callDelegated( walletProvider, diff --git a/packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact b/packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact index 383747d..f53b55e 100644 --- a/packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact +++ b/packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact @@ -137,33 +137,40 @@ export circuit addScore(gameId: Bytes<32>, playerId: Uint<64>): [] { const gid = disclose(gameId); const disclosed_playerId = disclose(playerId); assert(gameExists.member(gid), "Game does not exist"); - + + // Read pre-increment scores ONCE, compute post-increment values + // locally, then write. Do NOT re-read scoreP1/scoreP2 after the + // insert — Compact's queryLedgerState mutates the circuit context + // in place, so a re-read returns the POST-write value, and a + // naive `s + 1` would double-count. Using newS1/newS2 directly + // avoids any dependency on ledger read-after-write semantics. + const prevS1 = scoreP1.lookup(gid) as Uint<8>; + const prevS2 = scoreP2.lookup(gid) as Uint<8>; + const newS1: Uint<8> = (disclosed_playerId == 1 as Uint<64>) + ? (prevS1 + 1 as Uint<8>) as Uint<8> + : prevS1; + const newS2: Uint<8> = (disclosed_playerId == 2 as Uint<64>) + ? (prevS2 + 1 as Uint<8>) as Uint<8> + : prevS2; + if (disclosed_playerId == 1 as Uint<64>) { - const current = scoreP1.lookup(gid) as Uint<8>; - scoreP1.insert(gid, (current + 1 as Uint<8>) as Uint<8>); + scoreP1.insert(gid, newS1); } else if (disclosed_playerId == 2 as Uint<64>) { - const current = scoreP2.lookup(gid) as Uint<8>; - scoreP2.insert(gid, (current + 1 as Uint<8>) as Uint<8>); + scoreP2.insert(gid, newS2); } const books = totalBooks.lookup(gid) as Uint<8>; totalBooks.insert(gid, (books + 1 as Uint<8>) as Uint<8>); - - // CHANGED: Game ends at 7 books (7 ranks available) - if (books + 1 as Uint<8> == 7 as Uint<8>) { + + // Early-win at majority threshold (≥ 4 books of 7). Supersedes the + // old "books + 1 == 7" cap — by pigeonhole, 7 books implies max + // score ≥ 4, so this check fires strictly earlier. No tie branch: + // by induction, when this fires the other player is strictly < 4. + if (newS1 >= 4 as Uint<8>) { + phase.insert(gid, GamePhase.GameOver); + winner.insert(gid, 1 as Uint<64>); + } else if (newS2 >= 4 as Uint<8>) { phase.insert(gid, GamePhase.GameOver); - // V4: Record winner — player with more books wins - const s1 = scoreP1.lookup(gid) as Uint<8>; - const s2 = scoreP2.lookup(gid) as Uint<8>; - // After incrementing, one of them is the current player's new score - const finalS1: Uint<8> = (disclosed_playerId == 1 as Uint<64>) ? (s1 + 1 as Uint<8>) as Uint<8> : s1; - const finalS2: Uint<8> = (disclosed_playerId == 2 as Uint<64>) ? (s2 + 1 as Uint<8>) as Uint<8> : s2; - if (finalS1 > finalS2) { - winner.insert(gid, 1 as Uint<64>); - } else if (finalS2 > finalS1) { - winner.insert(gid, 2 as Uint<64>); - } else { - winner.insert(gid, 0 as Uint<64>); // tie - } + winner.insert(gid, 2 as Uint<64>); } return []; } diff --git a/packages/shared/contracts/midnight/go-fish-contract/src/game.compact b/packages/shared/contracts/midnight/go-fish-contract/src/game.compact index 2b129e0..4c46623 100644 --- a/packages/shared/contracts/midnight/go-fish-contract/src/game.compact +++ b/packages/shared/contracts/midnight/go-fish-contract/src/game.compact @@ -311,8 +311,14 @@ export circuit askForCard(gameId: Bytes<32>, askingPlayerId: Uint<64>, targetRan const [secret2, invScalar2] = deck_getSecretFromPlayerId(gid, 2); const currentCount = countCardsOfRank_withSecrets(gid, askingPlayerId, targetRank, secret1, secret2); - // RULE 5: Asking player must have at least one card of the requested rank - assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); + // RULE 5: Asking player must have ≥ 1 card of the requested rank — + // EXCEPT when the hand is empty. An empty-hand ask is the real-Go-Fish + // "draw from deck" move and flows through respondToAsk's existing + // go-fish branch (no standalone draw circuit needed). + const askerHandSize = hand_getHandSize(gid, disclosed_playerId); + if (askerHandSize > 0 as Uint<64>) { + assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); + } // Store the asked rank and asking player for the response phase go_fish_setLastAskedRank(gid, targetRank); @@ -721,6 +727,15 @@ export circuit isGameOver(gameId: Bytes<32>): Boolean { return go_fish_isGameOver(gid); } +// Top-level wrapper so the managed contract exposes getWinner. +// Returns 0 if no winner has been recorded yet (game still in progress +// or in a pre-V4 state), 1 for P1, 2 for P2. +export circuit getWinner(gameId: Bytes<32>): Uint<64> { + const gid = disclose(gameId); + assert(go_fish_doesGameExist(gid), "Game does not exist"); + return go_fish_getWinner(gid); +} + export circuit isDeckEmpty(gameId: Bytes<32>): Boolean { const gid = disclose(gameId); assert(go_fish_doesGameExist(gid), "Game does not exist"); From 1555553740802131065cf005acb17bd7b2dbe51c Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 23:22:16 -0400 Subject: [PATCH 06/78] Updates --- .gitignore | 1 + e2e/END_GAME_FIX_PLAN.md | 203 -------------- e2e/END_GAME_FIX_TESTING.md | 247 ------------------ packages/client/node/.gitignore | 1 + .../deployments/chain-31337/journal.jsonl | 2 +- .../shared/contracts/midnight/TEST_PLAN.md | 185 ------------- patch.sh | 144 ++++++++++ 7 files changed, 147 insertions(+), 636 deletions(-) delete mode 100644 e2e/END_GAME_FIX_PLAN.md delete mode 100644 e2e/END_GAME_FIX_TESTING.md create mode 100644 packages/client/node/.gitignore delete mode 100644 packages/shared/contracts/midnight/TEST_PLAN.md create mode 100755 patch.sh diff --git a/.gitignore b/.gitignore index 95e1de2..5106e71 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ logs/ # Midnight database packages/shared/contracts/midnight/midnight-level-db-deploy/* +packages/client/node/.gitignore diff --git a/e2e/END_GAME_FIX_PLAN.md b/e2e/END_GAME_FIX_PLAN.md deleted file mode 100644 index 2cbf9bf..0000000 --- a/e2e/END_GAME_FIX_PLAN.md +++ /dev/null @@ -1,203 +0,0 @@ -# End-Game Stall Fix — Plan - -**Scope:** Resolve `BACKEND_ISSUES.md` #3 (game stalls in `TurnStart` -with an empty hand) and #4 (`winner` ledger never written when the -game stalls). #4 is a symptom of #3. - -**Status:** Design — not implemented. - ---- - -## 1. What we observed - -Three e2e runs, all hitting the same exit path in `_helpers.ts:918` -(`endReason: "empty hand"`). `winner` was never set in any run. - -| Run | Turns | Scores | Hands | Books | -|-----|-------|--------|-------|-------| -| 1 | 18 | 6 – 0 | 0, 2 | 6 / 7 | -| 2 | 15 | 0 – 5 | 3, 0 | 5 / 7 | -| 3 | 13 | 2 – 2 | 4, 0 | 4 / 7 | - -## 2. Root cause - -Two gaps combine into a dead end: - -1. **`askForCard` rejects you with 0 cards.** Rule 5 - (`game.compact:315`) requires `currentCount > 0` for the asked - rank, so any player whose hand reaches 0 can't call `askForCard` - for *anything*. -2. **There is no standalone "go fish" circuit.** The deck draw lives - inside `respondToAsk`'s go-fish branch (`game.compact:393-407`), - which is only reachable *after* a successful `askForCard`. Grep - confirms no exported `goFish` / `drawCard` path. - -Combine: empty hand → can't ask → can't reach the draw → stuck. Real -Go Fish has a "draw from deck when empty" exception; the contract -doesn't. - -Separately, `addScore` (`GoFish.compact:152`) only transitions to -`GameOver` at 7 total books — so runs 1 and 2 (decided at book 4) ran -past their mathematical end and stalled on an empty hand. - -## 3. Fix — two Compact diffs - -### 3.1 Early-win at ≥ 4 books — `GoFish.compact:addScore` - -**Replaces** the existing `books + 1 == 7` end-game block (lines -151-167). By pigeonhole, `totalBooks == 7` implies `max(s1,s2) ≥ 4`, -so the new check fires strictly earlier — keeping the old cap is -dead code. No tie branch: by induction, when this check fires, the -other player is strictly `< 4`. - -```compact -// Replaces the full body of addScore. Reads pre-increment values -// ONCE, computes locally, writes ONCE. Never re-reads the ledger -// after an insert (Compact's queryLedgerState returns the post-write -// value, which caused a double-count in the original code). -const prevS1 = scoreP1.lookup(gid) as Uint<8>; -const prevS2 = scoreP2.lookup(gid) as Uint<8>; -const newS1: Uint<8> = (disclosed_playerId == 1 as Uint<64>) - ? (prevS1 + 1 as Uint<8>) as Uint<8> : prevS1; -const newS2: Uint<8> = (disclosed_playerId == 2 as Uint<64>) - ? (prevS2 + 1 as Uint<8>) as Uint<8> : prevS2; - -if (disclosed_playerId == 1 as Uint<64>) { - scoreP1.insert(gid, newS1); -} else if (disclosed_playerId == 2 as Uint<64>) { - scoreP2.insert(gid, newS2); -} -const books = totalBooks.lookup(gid) as Uint<8>; -totalBooks.insert(gid, (books + 1 as Uint<8>) as Uint<8>); - -if (newS1 >= 4 as Uint<8>) { - phase.insert(gid, GamePhase.GameOver); - winner.insert(gid, 1 as Uint<64>); -} else if (newS2 >= 4 as Uint<8>) { - phase.insert(gid, GamePhase.GameOver); - winner.insert(gid, 2 as Uint<64>); -} -``` - -**Compact read-after-write pitfall:** `queryLedgerState` mutates the -circuit context in place, so `lookup` after `insert` on the same -ledger returns the post-write value. The original code used `lookup → -insert → lookup → +1` which double-counted (confirmed: game was -ending at 3 books instead of 4). Audit found no other same-ledger -read-after-write patterns in `GoFish.compact`, `game.compact`, -`Hand.compact`, or `Deck.compact`. - -**ec_mul safety:** scalar reads and phase/winner writes only. Safe. - -### 3.2 Empty-hand exception in rule 5 — `game.compact:askForCard` - -Let a player with an empty hand ask for *any* rank. This routes the -turn through `respondToAsk`'s existing go-fish branch — no new -circuit, no new signing convention. Matches the real Go Fish rule -("draw from deck when empty"). - -```compact -// game.compact, inside askForCard. REPLACE line 315: -// assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); -// with: - -// RULE 5: Must hold ≥ 1 card of the asked rank — EXCEPT when the hand is -// empty. An empty-hand ask is the real-Go-Fish "draw from deck" move and -// flows through respondToAsk's existing go-fish branch. -const askerHandSize = hand_getHandSize(gid, askingPlayerId); -if (askerHandSize > 0 as Uint<64>) { - assert(currentCount > 0 as Uint<8>, "Cannot ask for a rank you don't have in your hand"); -} -``` - -**ec_mul safety:** `countCardsOfRank_withSecrets` (which contains the -ec_mul) still runs unconditionally above this block. The new `if` -only wraps the `assert`, which has no ec_mul. Safe. - -**Why rule 5 still exists for non-empty hands:** a non-empty player -asking for a rank they don't hold would leak nothing useful and waste -a turn — keep the assert for correctness on normal moves. - -## 4. Client changes — `e2e/smoke/_helpers.ts` - -1. **Drop the empty-hand bail** (`_helpers.ts:918-929`). With rule 5 - relaxed, an empty-handed `askForCard` is legal — let it flow - through `playOneTurn` normally. -2. **Pick a rank when the hand is empty.** `handRanks(handBefore)[0]` - (`_helpers.ts:932`) throws on an empty array. Replace with - `handRanks(handBefore)[0] ?? 0` (or any constant 0-6) — the - contract no longer cares whether the asker holds the rank. -3. **Promote the final-state assertion** in `e2e/game-round.test.ts`: - assert `result.finalPhase === PHASE.GameOver` and - `result.winner !== null && result.winner !== 0`. With the early-win - rule, ties are unreachable (§3.1 induction argument). - -No other helper changes. `checkAndEndGame` stays in the contract as a -safety net but the client never needs to call it — every finished -game exits via early-win. - -## 5. Managed contract regeneration - -`packages/shared/contracts/midnight/go-fish-contract/src/managed/**` -is auto-generated and currently stale (missing V4's `winner` / -`getWinner`). Run `deno task build:midnight` twice: once **before** -the edits (clean baseline), and once **after** (verify the `addScore` -diff and the relaxed assert appear in the generated output). - -Then run the ec_mul guard detection script from -`ZKIR-EC-MUL-GUARD-BUG.md`. Expected: 0 violations. - -## 6. Validation - -1. **Contract test suite** — `deno task --filter - @go-fish/midnight-contract test`. Existing 164 tests should still - pass. -2. **New contract test — early-win at 4.** Seed a game to 3 – 0, - score P1's 4th book, assert `phase == GameOver && winner == 1 && - totalBooks == 4`. -3. **New contract test — empty-hand ask.** Seed P1 with 0 cards and a - non-empty deck, call `askForCard(gid, 1, 0, now)`, then - `respondToAsk(gid, 2, now)`. Assert phase transitions correctly - through the go-fish branch and P1's hand size ends ≥ 1. -4. **`deno task test`** — state-machine tests; should be unaffected. -5. **`deno task test:e2e`** — run `game-round.test.ts` at least 3 - times. Expected: `finalPhase == GameOver`, `winner ∈ {1, 2}`, - `divergences == 0`, `exitReason` showing early-win. - -## 7. Rollout - -1. Regenerate managed contract on current sources (baseline commit). -2. Apply §3.1 diff to `GoFish.compact`. -3. Apply §3.2 diff to `game.compact`. -4. Regenerate managed contract (second commit — makes the diff - obvious). -5. Apply §4 client changes. -6. Run validation (§6). Close `BACKEND_ISSUES.md` #3 and #4. - -**Risk:** low. Both diffs are localized edits inside existing -circuits; no new circuits, no new signing conventions, no new ZKIR -surfaces. - -## 8. Open questions - -1. **Does `askForCard`'s timestamp / `assertCallerIsPlayer` path - depend on any side-channel that assumes `currentCount > 0`?** A - quick grep shows no — the check is self-contained. Verify - during implementation. -2. **Should `autoScoreBooks` also run for the *non-asking* player - after each turn?** Tangential to this fix, but with relaxed rule - 5 the non-active player may accumulate 3 of a rank via transfer - and not get scored until their own next turn. Not a correctness - issue (they'll still score eventually), but worth a follow-up. - -## 9. Summary - -- **Contract fix A** — `GoFish.compact:addScore`: replace the 7-book - cap with an early-win at score ≥ 4. Writes `winner`. -- **Contract fix B** — `game.compact:askForCard`: skip rule 5 when - the asker's hand is empty. Routes empty-hand turns through - `respondToAsk`'s existing go-fish branch — no new circuit. -- **Client fix** — `_helpers.ts`: drop the empty-hand bail and - tolerate `handRanks([])` when picking a rank. -- Issue #4 (`winner` never set) resolves automatically once every - game ends via early-win. diff --git a/e2e/END_GAME_FIX_TESTING.md b/e2e/END_GAME_FIX_TESTING.md deleted file mode 100644 index 629f207..0000000 --- a/e2e/END_GAME_FIX_TESTING.md +++ /dev/null @@ -1,247 +0,0 @@ -# End-Game Fix — Testing Handoff - -**Context:** Two Compact diffs + a managed-contract regen resolved -`BACKEND_ISSUES.md` #3 (game stalls in `TurnStart` with empty hand) -and #4 (`winner` ledger never written). Full design rationale lives -in `e2e/END_GAME_FIX_PLAN.md`. The implementation is complete and all -162 pre-existing contract tests still pass; your job is to add -coverage for the new behavior and run e2e validation. - ---- - -## What changed in the contract - -1. **Early-win at ≥ 4 books** — `GoFish.compact:addScore` now - transitions to `GameOver` and writes `winner` the moment either - score reaches 4. Replaces the old `books + 1 == 7` cap. By - pigeonhole, book 7 is now unreachable; every game ends at - totalBooks ∈ {4, 5, 6}. - - `winner == 0` (tie) is **unreachable** via the early-win path by - induction — the other player is always strictly `< 4` when the - check fires. Only the legacy `endGame` fallback can ever write - `winner == 0`. - -2. **Empty-hand asks are legal** — `game.compact:askForCard` rule 5 - (`assert(currentCount > 0, ...)`) is now wrapped in - `if (hand_getHandSize(gid, disclosed_playerId) > 0 as Uint<64>)`. - With an empty hand, the asker may request *any* rank; the flow - continues through `respondToAsk`'s existing three branches: - - **Transfer:** opponent had cards of rank → move them to the - asker's hand → phase → `TurnStart`. - - **Go fish:** opponent had none, deck non-empty → draw 1 for - asker → phase → `WaitForDrawCheck` → `afterGoFish`. - - **Switch:** opponent had none, deck empty → `switchTurn` to - opponent. - -3. **`getWinner` exposed at top level** — new wrapper in - `game.compact` delegating to `go_fish_getWinner`. Managed contract - now has 31 circuits (was 30). Before this, `getWinner` was only in - the `GoFish` module and didn't appear in - `managed/contract/index.d.ts`, which is why the e2e test was - always reading it as `__missing`. - -4. **Nothing else changed** — `respondToAsk`, `afterGoFish`, - `checkAndScoreBook`, `switchTurn`, `claimTimeoutWin`, - `checkAndEndGame` are all untouched. - -## What changed in the e2e harness - -- `e2e/smoke/_helpers.ts:playOneTurn` — the empty-hand bail at the - top of the function is gone. When `handBefore.length === 0` the - helper now picks rank 0 and calls `askForCard` normally, relying - on the contract's new rule-5 exception. Logs a distinct message - for the empty-hand path. -- `e2e/game-round.test.ts` — added two hard assertions at the end of - the test body: - - `result.finalPhase === PHASE.GameOver` (throws on regression) - - `result.winner === 1 || result.winner === 2` - (throws on `null`, `0`, or anything else) - -## Contract tests to add - -Add to `packages/shared/contracts/midnight/example.test.ts` (same -file as the existing 162 tests). Tag each with a new `N`-series ID -or similar. - -1. **Early-win at exactly 4.** Seed scoreP1 = 3, P2 = 0 via repeated - `checkAndScoreBook` calls. Score P1's 4th book. Assert: - - `getGamePhase(gid) == GamePhase.GameOver` (6) - - `getWinner(gid) == 1` - - `totalBooks == 4` - - Follow-up `askForCard`, `switchTurn`, `checkAndScoreBook` - calls all fail with the existing "game over" guards. - -2. **Early-win on the trailing side.** Mirror of (1): P2 scores the - 4th book first. Assert `winner == 2`. - -3. **Early-win at 4-2 and 4-3.** Mid-game states where the winner's - score reaches 4 while the opponent holds 2 or 3. Verify the rule - fires regardless of opponent score. - -4. **Guard against off-by-one.** Seed a 3 – X state and verify the - game is *not* over — the rule must not fire below 4. - -5. **Empty-hand ask — transfer branch.** Seed asker's hand to 0, - opponent holds ≥ 1 card of rank R. Call - `askForCard(gid, asker, R, now)` then - `respondToAsk(gid, opponent, now)`. Assert: - - Both calls succeed (no rule-5 assertion failure). - - Asker's hand size increases by exactly the count of R the - opponent held. - - Phase transitions to `TurnStart`. - -6. **Empty-hand ask — go-fish branch.** Asker hand = 0, opponent has - 0 of the asked rank, deck ≥ 1 card. Assert: - - Phase transitions to `WaitForDrawCheck` after `respondToAsk`. - - `afterGoFish` succeeds and transitions out of - `WaitForDrawCheck`. - - Asker's hand size == 1 at the end. - -7. **Empty-hand ask — switch-turn branch.** Asker hand = 0, - opponent has 0, deck empty. Assert: - - `currentTurn` switches to opponent. - - Phase is `TurnStart`. - - Asker's hand is still 0. - -8. **Rule 5 still enforced for non-empty hands.** Asker has exactly - 1 card (rank X). Call `askForCard` for rank Y ≠ X. Assert it - fails with `"Cannot ask for a rank you don't have in your hand"`. - -9. **`getWinner` pre-game-over.** Immediately after `init_game` or - `applyMask`, call `getWinner(gid)`. Assert `== 0` (no winner - recorded). - -10. **`getWinner` post-game-over.** Full game → early-win. Assert - `getWinner` returns `1` or `2` matching the `winner` ledger - entry and consistent with the final score comparison. - -11. **No off-by-one on early-win (regression for read-after-write - double-count bug).** Seed scoreP1 = 3, totalBooks = 3, scoreP2 = - 0. Call `checkAndScoreBook` to score P1's 4th book. Re-read - `getScores` — must be `[4, 0]`, NOT `[3, 0]` (which would mean - game-over fired before the 4th write via the old double-count). - Assert `getGamePhase == GameOver`, `getWinner == 1`, - `totalBooks == 4`. This is the regression test for the Compact - `queryLedgerState` read-after-write pitfall. - -## e2e validation - -Run the full stack and execute `game-round.test.ts` ≥ 3 times to -sample across shuffle nondeterminism. - -**Green run looks like:** - -``` -exitReason: phase → GameOver -finalPhase: 6 (GameOver=6) -winner: 1 (or 2; never null and never 0) -divergences: 0 -``` - -**Red flags (regressions):** - -- `exitReason: "empty hand"` — rule 5 still blocking. Verify - `disclosed_playerId` is being passed to `hand_getHandSize`. -- `exitReason: "MAX_TURNS (20) reached"` — the loop isn't - progressing. Likely the empty-hand path returned without - advancing state. Check the logs for repeated `(empty)` hands. -- `winner: null` — `getWinner` isn't exposed at the top level (or - the managed contract wasn't regenerated). Run - `deno task build:midnight` and check `index.d.ts` for the - `getWinner` entry. -- `winner: 0` — the early-win path produced a tie, which should be - mathematically impossible. Inspect `finalS1` / `finalS2` - computation in `addScore`. -- The helper's `sum >= 7` break in `runFullGame` (`_helpers.ts:1131`) - firing — also should be unreachable. If it fires, the contract's - majority rule isn't ending games. - -## Commands - -```bash -# Contract simulation suite (expect 162/162) -cd packages/shared/contracts/midnight && deno task test - -# Node API suite (expect 27/27) -deno task test - -# Rebuild managed contract from .compact sources -deno task build:midnight - -# ec_mul guard bug detection (expect zero output) -cd packages/shared/contracts/midnight/go-fish-contract && \ -for zkir in src/managed/zkir/*.zkir; do - ec_muls=$(grep -c "ec_mul" "$zkir") - guarded=$(grep -c 'public_input { guard: [^n]' "$zkir") - if [ "$ec_muls" -gt 0 ] && [ "$guarded" -gt 0 ]; then - echo "UNSAFE: $(basename "$zkir")" - fi -done - -# Full e2e (requires Hardhat :8545, Paima :9996, batcher :3336, -# Midnight node+indexer :8088, proof server :6300, deployed contract) -deno task test:e2e -``` - -## Gotchas - -1. **Managed contract is regenerated.** `managed/**` diffs are - large and auto-generated. Don't hand-edit; `deno task - build:midnight` to reproduce. - -2. **Witness-disclosure rule.** The first build failed because - `hand_getHandSize(gid, askingPlayerId)` leaked the witness. Fix - was to pass `disclosed_playerId` instead. If you add new Hand - circuit calls with variable player IDs, use the disclosed form. - -3. **Existing test `N5 game ends at 7 books`** now reports "game - over with 5 books (ended via addScore)" and passes. The test's - assertion was flexible enough to accept the new behavior. Leave - it alone — or rename to `game ends at early-win threshold`. - -4. **`BACKEND_ISSUES.md` #1 is NOT fixed.** The default witness - module still has the `Math.random()` fallback bug. The e2e test - sidesteps it via `e2e/smoke/test-witnesses.ts`. Don't regress to - the default witnesses. - -5. **Frontend is out of scope.** No changes to `frontend/`. A - follow-up ticket should track: (a) reading `getWinner` on the - GameOver screen, (b) wiring the empty-hand UI to the relaxed - rule-5 ask path. Not your problem for this PR. - -6. **`checkAndEndGame` is now dead code in the happy path.** It's - still present and still works, but the client never needs to - call it — every game ends via early-win. Keep it as a safety - net for the pathological 3-3 edge case (which only matters if - the 4+ rule is ever disabled). - -7. **Compact read-after-write pitfall.** `queryLedgerState` mutates - the circuit context in place — a `lookup` after an `insert` on - the same ledger returns the post-write value, not a snapshot. - The original `addScore` had `lookup → insert → lookup → +1` - which double-counted. If you add new contract logic that reads - a ledger after writing to it in the same circuit, compute the - value locally instead. Audit confirmed no other same-ledger - read-after-write patterns exist in the current codebase. - -## Files of interest - -- `e2e/END_GAME_FIX_PLAN.md` — design rationale, coverage table, - edge cases. -- `e2e/BACKEND_ISSUES.md` — issues #3 and #4 can move to "Resolved" - once you've confirmed the e2e runs are green. -- `packages/shared/contracts/midnight/go-fish-contract/src/GoFish.compact` - — the `addScore` diff. -- `packages/shared/contracts/midnight/go-fish-contract/src/game.compact` - — the `askForCard` rule-5 relaxation + the new top-level - `getWinner` wrapper. -- `packages/shared/contracts/midnight/go-fish-contract/src/managed/contract/index.d.ts` - — verify `getWinner` appears in `ImpureCircuits`, - `ProvableCircuits`, and `Circuits`. - -## Done means - -- [ ] 11 new contract tests added and passing (list above). -- [ ] E2E run 3×, all green on the new assertions. -- [ ] `BACKEND_ISSUES.md` #3 and #4 moved to "Resolved". -- [ ] ec_mul guard script reports zero violations. diff --git a/packages/client/node/.gitignore b/packages/client/node/.gitignore new file mode 100644 index 0000000..adbb97d --- /dev/null +++ b/packages/client/node/.gitignore @@ -0,0 +1 @@ +data/ \ No newline at end of file diff --git a/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl b/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl index 40cb038..8c6d8b2 100644 --- a/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl +++ b/packages/shared/contracts/evm/ignition/deployments/chain-31337/journal.jsonl @@ -4,5 +4,5 @@ {"futureId":"GoFishModule#PaimaL2Contract","networkInteraction":{"data":"0x608060405234801561000f575f5ffd5b50604051610b6c380380610b6c83398181016040528101906100319190610113565b8181815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060018190555050505050610151565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100af82610086565b9050919050565b6100bf816100a5565b81146100c9575f5ffd5b50565b5f815190506100da816100b6565b92915050565b5f819050919050565b6100f2816100e0565b81146100fc575f5ffd5b50565b5f8151905061010d816100e9565b92915050565b5f5f6040838503121561012957610128610082565b5b5f610136858286016100cc565b9250506020610147858286016100ff565b9150509250929050565b610a0e8061015e5f395ff3fe608060405260043610610054575f3560e01c806313af40351461005857806324600fc3146100805780633804df031461009657806369fe0e2d146100b25780638da5cb5b146100da578063ddca3f4314610104575b5f5ffd5b348015610063575f5ffd5b5061007e6004803603810190610079919061058b565b61012e565b005b34801561008b575f5ffd5b506100946101fe565b005b6100b060048036038101906100ab9190610617565b6102e1565b005b3480156100bd575f5ffd5b506100d860048036038101906100d39190610695565b61037c565b005b3480156100e5575f5ffd5b506100ee610414565b6040516100fb91906106cf565b60405180910390f35b34801561010f575f5ffd5b50610118610438565b60405161012591906106f7565b60405180910390f35b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146101bc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101b39061076a565b60405180910390fd5b805f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461028c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610283906107d2565b60405180910390fd5b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505f4790506102dd818373ffffffffffffffffffffffffffffffffffffffff1661043e90919063ffffffff16565b5050565b600154341015610326576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161031d90610860565b60405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff167fffa7cf79b6173c04d5ec2b41bce25acc6e48f9cf86349011288bab7da23fc517838334604051610370939291906108d8565b60405180910390a25050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461040a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161040190610952565b60405180910390fd5b8060018190555050565b5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60015481565b804710156104855747816040517fcf47918100000000000000000000000000000000000000000000000000000000815260040161047c929190610970565b60405180910390fd5b5f8273ffffffffffffffffffffffffffffffffffffffff16826040516104aa906109c4565b5f6040518083038185875af1925050503d805f81146104e4576040519150601f19603f3d011682016040523d82523d5f602084013e6104e9565b606091505b5050905080610524576040517fd6bda27500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61055a82610531565b9050919050565b61056a81610550565b8114610574575f5ffd5b50565b5f8135905061058581610561565b92915050565b5f602082840312156105a05761059f610529565b5b5f6105ad84828501610577565b91505092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126105d7576105d66105b6565b5b8235905067ffffffffffffffff8111156105f4576105f36105ba565b5b6020830191508360018202830111156106105761060f6105be565b5b9250929050565b5f5f6020838503121561062d5761062c610529565b5b5f83013567ffffffffffffffff81111561064a5761064961052d565b5b610656858286016105c2565b92509250509250929050565b5f819050919050565b61067481610662565b811461067e575f5ffd5b50565b5f8135905061068f8161066b565b92915050565b5f602082840312156106aa576106a9610529565b5b5f6106b784828501610681565b91505092915050565b6106c981610550565b82525050565b5f6020820190506106e25f8301846106c0565b92915050565b6106f181610662565b82525050565b5f60208201905061070a5f8301846106e8565b92915050565b5f82825260208201905092915050565b7f4f6e6c79206f776e65722063616e206368616e6765206f776e657200000000005f82015250565b5f610754601b83610710565b915061075f82610720565b602082019050919050565b5f6020820190508181035f83015261078181610748565b9050919050565b7f4f6e6c79206f776e65722063616e2077697468647261772066756e64730000005f82015250565b5f6107bc601d83610710565b91506107c782610788565b602082019050919050565b5f6020820190508181035f8301526107e9816107b0565b9050919050565b7f53756666696369656e742066756e647320726571756972656420746f207375625f8201527f6d69742067616d6520696e707574000000000000000000000000000000000000602082015250565b5f61084a602e83610710565b9150610855826107f0565b604082019050919050565b5f6020820190508181035f8301526108778161083e565b9050919050565b5f82825260208201905092915050565b828183375f83830152505050565b5f601f19601f8301169050919050565b5f6108b7838561087e565b93506108c483858461088e565b6108cd8361089c565b840190509392505050565b5f6040820190508181035f8301526108f18185876108ac565b905061090060208301846106e8565b949350505050565b7f4f6e6c79206f776e65722063616e206368616e676520666565000000000000005f82015250565b5f61093c601983610710565b915061094782610908565b602082019050919050565b5f6020820190508181035f83015261096981610930565b9050919050565b5f6040820190506109835f8301856106e8565b61099060208301846106e8565b9392505050565b5f81905092915050565b50565b5f6109af5f83610997565b91506109ba826109a1565b5f82019050919050565b5f6109ce826109a4565b915081905091905056fea26469706673582212204996e7c4d7c04b30cc59e3ac0a05e259eaa1e10fffdba7a316a1f8233c93dddb64736f6c634300081b0033000000000000000000000000effe522d441d971ddc7153439a7d10235ae6301f0000000000000000000000000000000000000000000000000000000000000000","id":1,"type":"ONCHAIN_INTERACTION","value":{"_kind":"bigint","value":"0"}},"type":"NETWORK_INTERACTION_REQUEST"} {"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":0,"type":"TRANSACTION_PREPARE_SEND"} {"futureId":"GoFishModule#PaimaL2Contract","networkInteractionId":1,"nonce":0,"transaction":{"fees":{"maxFeePerGas":{"_kind":"bigint","value":"2339843750"},"maxPriorityFeePerGas":{"_kind":"bigint","value":"1000000000"}},"hash":"0x5d635d19fe69ab100184e6c723616d9f529925e18b3d0bcae481a650d07ab7dd"},"type":"TRANSACTION_SEND"} -{"futureId":"GoFishModule#PaimaL2Contract","hash":"0x5d635d19fe69ab100184e6c723616d9f529925e18b3d0bcae481a650d07ab7dd","networkInteractionId":1,"receipt":{"blockHash":"0x63c89b70c533230f66b52735158f52c8768f44d4c65c91d214a52aabc5bf9419","blockNumber":4,"contractAddress":"0x5FbDB2315678afecb367f032d93F642f64180aa3","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} +{"futureId":"GoFishModule#PaimaL2Contract","hash":"0x5d635d19fe69ab100184e6c723616d9f529925e18b3d0bcae481a650d07ab7dd","networkInteractionId":1,"receipt":{"blockHash":"0xe2b16d2b42f162cb6268624eeab081bbe1a1ae24b799c9a9ba2768410a1147bb","blockNumber":4,"contractAddress":"0x5FbDB2315678afecb367f032d93F642f64180aa3","logs":[],"status":"SUCCESS"},"type":"TRANSACTION_CONFIRM"} {"futureId":"GoFishModule#PaimaL2Contract","result":{"address":"0x5FbDB2315678afecb367f032d93F642f64180aa3","type":"SUCCESS"},"type":"DEPLOYMENT_EXECUTION_STATE_COMPLETE"} \ No newline at end of file diff --git a/packages/shared/contracts/midnight/TEST_PLAN.md b/packages/shared/contracts/midnight/TEST_PLAN.md deleted file mode 100644 index 4b10d1a..0000000 --- a/packages/shared/contracts/midnight/TEST_PLAN.md +++ /dev/null @@ -1,185 +0,0 @@ -# Go Fish Contract — Test Plan - -## Current State - -The existing test suite (`example.test.ts`) has **65 happy-path tests** covering: -- Setup flow (init_deck, applyMask, dealCards) -- State queries (getGamePhase, getCurrentTurn, getScores, getHandSizes, etc.) -- Card discovery and uniqueness -- Single-turn game flow (askForCard → respondToAsk → goFish → afterGoFish) -- Book scoring (checkAndScoreBook) -- Multi-game isolation (2 concurrent games with independent state) -- Full game simulation to completion - -**What is NOT tested:** Every `assert(...)` guard in the contract. The current suite only tests the success path — it never attempts an illegal action and verifies the contract rejects it. - ---- - -## Missing Tests - -Each test below targets a specific `assert` in `game.compact` or its modules. Tests are grouped by circuit. The "Assert message" column shows the exact error the test should expect. - -### A. Setup Phase — `applyMask` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| A1 | Invalid player ID | `applyMask(gid, 0)` | `"Invalid player index"` | -| A2 | Invalid player ID (3) | `applyMask(gid, 3)` | `"Invalid player index"` | -| A3 | Duplicate mask P1 | P1 calls `applyMask` twice | `"Player has already applied their mask"` | -| A4 | Duplicate mask P2 | P2 calls `applyMask` twice | `"Player has already applied their mask"` | -| A5 | applyMask after setup phase | Complete setup, then call `applyMask` on same gameId | `"Can only apply mask during setup"` | - -### B. Setup Phase — `dealCards` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| B1 | Deal on non-existent game | `dealCards(randomId, 1)` | `"Game does not exist"` | -| B2 | Deal before masks applied | Create game (P1 mask only), then `dealCards(gid, 1)` | `"Player 2 must apply mask before dealing"` | -| B3 | P2 deals first | Both masks applied, P2 calls `dealCards` before P1 | `"First player to deal must use player ID 1"` | -| B4 | P1 deals twice | P1 calls `dealCards` twice | `"Player has already dealt cards"` | -| B5 | Deal after setup complete | Full setup, then call `dealCards` again | `"Can only deal cards during setup"` | -| B6 | Deal with invalid player ID | `dealCards(gid, 0)` | `"Invalid player index"` | - -### C. Asking for Cards — `askForCard` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| C1 | Ask on non-existent game | `askForCard(randomId, ...)` | `"Game does not exist"` | -| C2 | Ask during Setup phase | Call before dealing completes | `"Can only ask for cards at turn start"` | -| C3 | Wrong player asks | P2 asks when it's P1's turn | `"Not your turn - only current player can ask for cards"` | -| C4 | Ask for rank not in hand | P1 asks for a rank they don't hold | `"Cannot ask for a rank you don't have in your hand"` | -| C5 | Ask for invalid rank (7) | `askForCard(gid, 1, 7, now)` | `"Invalid card rank"` | -| C6 | Ask for invalid rank (255) | `askForCard(gid, 1, 255, now)` | `"Invalid card rank"` | -| C7 | Ask during WaitForResponse | Ask again while waiting for response | `"Can only ask for cards at turn start"` | - -### D. Responding to Ask — `respondToAsk` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| D1 | Respond on non-existent game | `respondToAsk(randomId, ...)` | `"Game does not exist"` | -| D2 | Respond during wrong phase | Call during TurnStart | `"Not waiting for a response"` | -| D3 | Asking player responds to self | P1 asks, P1 responds | `"Asking player cannot respond to their own request"` | -| D4 | Invalid responder ID | `respondToAsk(gid, 3, now)` | `"Invalid player index"` | -| D5 | Respond when opponent has cards | Verify returns `[true, N]` and phase → TurnStart | *(positive, verify transfer)* | -| D6 | Respond when opponent has no cards | Verify returns `[false, 0]` and phase → WaitForDraw | *(positive, verify Go Fish)* | - -### E. Drawing Cards — `goFish` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| E1 | goFish on non-existent game | `goFish(randomId, ...)` | `"Game does not exist"` | -| E2 | goFish during wrong phase | Call during TurnStart (without prior ask) | `"Not in draw phase"` | -| E3 | Wrong player draws | Opponent calls goFish | `"Not your turn - only current player can draw"` | -| E4 | goFish with empty deck | Exhaust deck, then goFish | `"Cannot draw - deck is empty"` | - -### F. After Go Fish — `afterGoFish` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| F1 | afterGoFish on non-existent game | `afterGoFish(randomId, ...)` | `"Game does not exist"` | -| F2 | afterGoFish during wrong phase | Call during TurnStart | `"Not waiting for draw check"` | -| F3 | Wrong player calls afterGoFish | Opponent calls it | `"Only current player can call afterGoFish"` | -| F4 | Cheating: claim drew requested card | P1 draws rank X, claims drew rank Y | `"Cheating: You did not draw the requested card"` | -| F5 | Drew requested card → keep turn | `afterGoFish(gid, pid, true, now)` when true | phase → TurnStart, same player's turn | -| F6 | Didn't draw requested → switch turn | `afterGoFish(gid, pid, false, now)` | turn switches to opponent | - -### G. Turn Management — `switchTurn` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| G1 | switchTurn on non-existent game | `switchTurn(randomId, 1)` | `"Game does not exist"` | -| G2 | switchTurn during wrong phase | Call during WaitForResponse | `"Can only switch turn during TurnStart phase"` | -| G3 | Non-current player switches | P2 switches when it's P1's turn | `"Only current player can switch turns"` | - -### H. Book Scoring — `checkAndScoreBook` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| H1 | Score on non-existent game | `checkAndScoreBook(randomId, ...)` | `"Game does not exist"` | -| H2 | Invalid player ID | `checkAndScoreBook(gid, 0, 0)` | `"Invalid player index"` | -| H3 | Invalid rank (7) | `checkAndScoreBook(gid, 1, 7)` | `"Invalid card rank"` | -| H4 | Score during Setup | Call before game starts | `"Cannot score books during setup"` | -| H5 | Score during GameOver | Call after game ended | `"Game is already over"` | -| H6 | Score with < 3 cards of rank | Player has 1-2 of rank | returns `false`, no state change | -| H7 | Score with exactly 3 cards | Player has all 3 suits | returns `true`, hand size -3, score +1 | -| H8 | Game ends at 7 total books | Score the 7th book | phase → GameOver | - -### I. Game End — `checkAndEndGame` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| I1 | checkAndEndGame non-existent | `checkAndEndGame(randomId)` | `"Game does not exist"` | -| I2 | Deck not empty | Call mid-game | returns `false` | -| I3 | Deck empty, both have cards | Exhaust deck but keep hands | returns `false` | -| I4 | Deck empty, one hand empty | Exhaust deck + empty one hand | returns `true`, phase → GameOver | - -### J. Timeout — `claimTimeoutWin` - -| # | Test | Action | Assert message | -|---|------|--------|----------------| -| J1 | Timeout non-existent game | `claimTimeoutWin(randomId, 1)` | `"Game does not exist"` | -| J2 | Timeout during Setup | Call before game starts | `"Game has not started yet"` | -| J3 | Timeout during GameOver | Call after game ended | `"Game is already over"` | -| J4 | Active player claims timeout | Current-turn player claims | `"Active player cannot claim timeout against themselves"` | -| J5 | Invalid player ID | `claimTimeoutWin(gid, 3)` | `"Invalid player index"` | -| J6 | Timeout not yet elapsed | Claim immediately after a move | `"Timeout period has not elapsed yet"` | - -> **Note:** J6 and a valid-timeout test (J7) require `blockTimeGte`/`blockTimeLt` to be -> controllable from the test harness. If the simulator doesn't support advancing block time, -> these tests should be skipped with a comment explaining why. - -### K. Card Integrity — Cross-cutting - -| # | Test | What it verifies | -|---|------|-----------------| -| K1 | No duplicate cards after dealing | Union of both hands has 8 unique cards out of 21 | -| K2 | No duplicate cards after transfers | After respondToAsk with transfers, all cards still unique | -| K3 | Hand sizes match card count | `getHandSizes` matches `doesPlayerHaveSpecificCard` scan count | -| K4 | Deck + hands = 21 | `(21 - topCardIndex) + P1 hand + P2 hand = 21` at all times | -| K5 | Card point decryption round-trips | `get_card_from_point(partial_decryption(gid, card, pid))` returns original index | - -### L. Multi-Game Isolation (extend existing) - -| # | Test | What it verifies | -|---|------|-----------------| -| L1 | Score in game A doesn't affect game B | Score a book in A, verify B scores unchanged | -| L2 | Timeout in game A doesn't affect game B | End A via timeout, B still in TurnStart | -| L3 | Different phase per game | A in WaitForDraw, B in TurnStart — both operate independently | - ---- - -## Implementation Notes - -- **Negative tests pattern:** Wrap the circuit call in `try/catch`. On catch, verify the error message contains the expected assert string. If no error is thrown, fail the test. - ```typescript - try { - provableCircuits.askForCard(sim.circuitContext, gameId, BigInt(2), BigInt(rank), BigInt(Date.now())); - sim.circuitContext = ...; // update if needed - recordTest('wrong player asks', false, 'Expected assert but call succeeded'); - } catch (e) { - const msg = (e as Error).message; - if (msg.includes('Not your turn')) { - recordTest('wrong player asks', true, null, msg); - } else { - recordTest('wrong player asks', false, e); - } - } - ``` - -- **Test grouping:** Add new tests after the existing 65, grouped by circuit (A-L). Each group should start from a fresh `sim.reset()` + `init_deck` + full setup, so one failing test doesn't cascade. - -- **Block time tests (J-series):** The `blockTimeLt` and `blockTimeGte` asserts in the contract compare against the circuit context's block timestamp. If the compact-runtime simulator doesn't expose a way to set block time, these tests must be documented as untestable in the simulator and deferred to on-chain integration tests. - -- **Cheating test (F4):** Requires careful orchestration — ask for rank X, draw a card, determine its actual rank, then call `afterGoFish(drewRequestedCard=true)` when the actual rank differs. The contract checks `currentCount == prevCount + 1` which will fail. - -## Priority Order - -1. **A, B** — Setup guards (fast to write, catches config errors) -2. **C, D** — Ask/respond guards (core game loop protection) -3. **F4** — Cheating detection (security-critical) -4. **H6-H8** — Book scoring edge cases (game completion logic) -5. **E, F, G** — Remaining phase/turn guards -6. **K** — Card integrity invariants -7. **I** — Game end conditions -8. **J** — Timeout (may need harness changes) -9. **L** — Multi-game isolation extensions diff --git a/patch.sh b/patch.sh new file mode 100755 index 0000000..931d64d --- /dev/null +++ b/patch.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# Apply patches +echo "🔧 Applying patches..." + +# Function to comment out a line at specific line number +comment_line() { + local file="$1" + local line_num="$2" + local comment_text="$3" + + if [[ -f "$file" ]]; then + # Use sed to comment out the line (add // at the beginning) + sed -i.bak "${line_num}s|^|// |" "$file" + echo "✅ Commented line $line_num in $file" + else + echo "⚠️ Warning: File $file not found" + fi +} + +# Function to replace content in a file +replace_in_file() { + local file="$1" + local old_content="$2" + local new_content="$3" + + if [[ -f "$file" ]]; then + # Create backup first + cp "$file" "$file.bak" + + # Use perl for more reliable string replacement + # Only escape the search pattern, not the replacement text + perl -i -pe "s/Q$old_contentE/$new_content/g" "$file" + echo "✅ Replaced content in $file" + else + echo "⚠️ Warning: File $file not found" + fi +} + +# Function to replace content using a temp file approach for complex strings +replace_complex_content() { + local file="$1" + local old_content="$2" + local new_content="$3" + + if [[ -f "$file" ]]; then + # Create backup first + cp "$file" "$file.bak" + + # Write old and new content to temp files + local temp_old=$(mktemp) + local temp_new=$(mktemp) + printf '%s' "$old_content" > "$temp_old" + printf '%s' "$new_content" > "$temp_new" + + # Use python for reliable string replacement + python3 -c " +import sys +with open('$file', 'r') as f: + content = f.read() +with open('$temp_old', 'r') as f: + old = f.read() +with open('$temp_new', 'r') as f: + new = f.read() +content = content.replace(old, new) +with open('$file', 'w') as f: + f.write(content) +" + + # Clean up temp files + rm "$temp_old" "$temp_new" + echo "✅ Replaced complex content in $file" + else + echo "⚠️ Warning: File $file not found" + fi +} + +# Apply Common Hardhat Patches +shopt -s nullglob # Expands to nothing if no match is found + +echo "Applying common Hardhat patches for versions 3.0.0-3.0.9..." + +# Patch hardhat compiler.js +for dir in ./node_modules/.deno/hardhat@3.[0-1]*.[0-9]*/ ; do + file_to_patch="${dir}node_modules/hardhat/dist/src/internal/builtin-plugins/solidity/build-system/compiler/compiler.js" + echo "Commenting out await stdoutFileHandle.close() in ${file_to_patch}..." + comment_line "$file_to_patch" 48 "await stdoutFileHandle.close();" +done + +# Patch hardhat-utils fs.js +for dir in ./node_modules/.deno/@nomicfoundation+hardhat-utils@3.[0-1]*.[0-9]*/ ; do + file_to_patch="${dir}node_modules/@nomicfoundation/hardhat-utils/dist/src/fs.js" + echo "Commenting out first await fileHandle?.close() in ${file_to_patch}..." + comment_line "$file_to_patch" 209 "await fileHandle?.close();" + echo "Commenting out second await fileHandle?.close() in ${file_to_patch}..." + comment_line "$file_to_patch" 275 "await fileHandle?.close();" +done + +shopt -u nullglob # Revert to default + +echo "✅ All patches applied successfully" + +# Apply Specific Patches +echo "Replacing fetch-blob streams.cjs content..." +replace_complex_content "./node_modules/.deno/fetch-blob@3.2.0/node_modules/fetch-blob/streams.cjs" " // `node:stream/web` got introduced in v16.5.0 as experimental + // and it's preferred over the polyfilled version. So we also + // suppress the warning that gets emitted by NodeJS for using it. + try { + const process = require('node:process') + const { emitWarning } = process + try { + process.emitWarning = () => {} + Object.assign(globalThis, require('node:stream/web')) + process.emitWarning = emitWarning + } catch (error) { + process.emitWarning = emitWarning + throw error + } + } catch (error) { + // fallback to polyfill implementation + Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js')) + }" " Object.assign(globalThis, require('web-streams-polyfill/dist/ponyfill.es2018.js'))" + +echo "Replacing fetch-blob from.js imports..." +replace_complex_content "./node_modules/.deno/fetch-blob@3.2.0/node_modules/fetch-blob/from.js" "import { statSync, createReadStream, promises as fs } from 'node:fs' +import { basename } from 'node:path' +import DOMException from 'node-domexception' + +import File from './file.js' +import Blob from './index.js' + +const { stat } = fs" "import { statSync, createReadStream } from 'node:fs' +import { basename } from 'node:path' +import DOMException from 'node-domexception' + +import File from './file.js' +import Blob from './index.js' + +import { promises as stat } from 'node:fs' +" + +echo "✅ All patches applied successfully" + + \ No newline at end of file From 68ec4e70df4459ad04509bc6af0fd3df9d672c73 Mon Sep 17 00:00:00 2001 From: Eddie Date: Wed, 15 Apr 2026 23:58:47 -0400 Subject: [PATCH 07/78] Align frontend with e2e reference: fix circuits, wire book scoring, harden witnesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 (EVM lobby alignment): - Remove maxPlayers from GoFishGameState type and in-memory state (DB column dropped per EVM_UPDATE.md; game is always 2 players) Phase 1 (critical fixes): - Fix callAfterGoFish: remove stale drewRequestedCard param (4→3 args). Contract v3 decrypts the drawn card internally (game.compact:451-510). Updated across all 6 files in the call chain (GoFishContractService, MidnightService, MidnightOnChainService, midnightBridge, GameScreen, GameScene). - Delete callGoFish / goFish: no goFish circuit exists in the compiled contract. respondToAsk handles the draw internally. Turn flow is now: askForCard → respondToAsk → (if WaitForDrawCheck) afterGoFish. - Allow empty-hand asks: contract rule-5 relaxation permits asking for any rank when hand is empty. GameScreen now shows an "Ask for a card" button instead of the old passive "Drawing will happen automatically" message that stalled the game. Phase 2 (book scoring — without this, games never end): - Add callCheckAndScoreBook to GoFishContractService (batcher delegation) - Add onChainCheckAndScoreBook to MidnightOnChainService - Add checkAndScoreBook routing to MidnightService - Add autoScoreBooks() to GameScreen: after respondToAsk and afterGoFish, scans the player's hand for ranks with ≥3 cards and submits checkAndScoreBook for each. The contract's addScore checks ≥4 books and sets phase=GameOver + winner. Phase 3 (winner display): - Update renderGameOverPanel to use ≥4 threshold for winner determination (matches the contract's early-win rule). Added TODO for reading getWinner from the contract once direct indexer access is available. Phase 5 (witness hardening): - Remove silent Math.random() fallback from witnesses.ts getSecretKey and getShuffleSeed. Both now throw immediately on a miss with a descriptive error naming the game+player that's missing. Delete the static `keys` object that powered the fallback (20-bit random values that produced wrong ec_mul results). See BACKEND_ISSUES.md #1. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/midnightBridge.ts | 55 +---- frontend/src/screens/GameScreen.ts | 199 ++++++++++-------- .../src/services/GoFishContractService.ts | 36 ++-- frontend/src/services/GoFishGameService.ts | 1 - .../src/services/MidnightOnChainService.ts | 71 ++++--- frontend/src/services/MidnightService.ts | 45 ++-- frontend/src/three/scenes/GameScene.ts | 44 ++-- .../go-fish-contract/src/witnesses.ts | 71 +++---- .../shared/data-types/src/go-fish-types.ts | 1 - 9 files changed, 261 insertions(+), 262 deletions(-) diff --git a/frontend/src/midnightBridge.ts b/frontend/src/midnightBridge.ts index b3f5f4f..737e756 100644 --- a/frontend/src/midnightBridge.ts +++ b/frontend/src/midnightBridge.ts @@ -371,74 +371,35 @@ export async function respondToAsk( } } -/** - * Draw a card from the deck - */ -export async function goFish( - lobbyId: string, - playerId: 1 | 2 -): Promise<{ success: boolean; card?: { x: bigint; y: bigint }; errorMessage?: string }> { - try { - if (!isMidnightConnected() || !contract || !circuitContext) { - return { success: false, errorMessage: 'Midnight contract not initialized' }; - } - - // Set game context for witness functions - setGameContext(lobbyId, playerId); - - const gameId = lobbyIdToGameId(lobbyId); - - console.log(`[MidnightBridge] goFish(gameId: ${lobbyId}, playerId: ${playerId})`); - - // Call contract circuit - returns CurvePoint (semi-masked card) - const result = contract.provableCircuits.goFish( - circuitContext, - gameId, - BigInt(playerId) - ); - - // Update circuit context with result - circuitContext = result.context; - - const card = result.result; - - console.log(`[MidnightBridge] goFish succeeded: drew card`); - return { success: true, card }; - } catch (error) { - console.error('[MidnightBridge] goFish failed:', error); - return { - success: false, - errorMessage: error instanceof Error ? error.message : 'Unknown error', - }; - } -} +// goFish() deleted — no goFish circuit exists in the compiled contract. +// respondToAsk handles the draw internally; the turn flow is: +// askForCard → respondToAsk → (if WaitForDrawCheck) afterGoFish. /** - * Complete the go fish action (check if drew requested card) + * Complete the go fish action — contract v3 decrypts the drawn card + * internally and determines drewRequestedCard (game.compact:451-510). */ export async function afterGoFish( lobbyId: string, playerId: 1 | 2, - drewRequestedCard: boolean ): Promise<{ success: boolean; errorMessage?: string }> { try { if (!isMidnightConnected() || !contract || !circuitContext) { return { success: false, errorMessage: 'Midnight contract not initialized' }; } - // Set game context for witness functions setGameContext(lobbyId, playerId); const gameId = lobbyIdToGameId(lobbyId); + const now = BigInt(Math.floor(Date.now() / 1000)); - console.log(`[MidnightBridge] afterGoFish(gameId: ${lobbyId}, playerId: ${playerId}, drew: ${drewRequestedCard})`); + console.log(`[MidnightBridge] afterGoFish(gameId: ${lobbyId}, playerId: ${playerId})`); - // Call contract circuit const result = contract.provableCircuits.afterGoFish( circuitContext, gameId, BigInt(playerId), - drewRequestedCard + now ); // Update circuit context with result diff --git a/frontend/src/screens/GameScreen.ts b/frontend/src/screens/GameScreen.ts index 7491c3a..2d34d4f 100644 --- a/frontend/src/screens/GameScreen.ts +++ b/frontend/src/screens/GameScreen.ts @@ -880,9 +880,15 @@ export class GameScreen { private _renderActionPanel(game: GoFishGameState, currentPlayer: GoFishPlayer): string { if (!currentPlayer || currentPlayer.hand.length === 0) { + // Empty-hand asks are legal (contract rule-5 relaxation). The asker + // can request any rank; respondToAsk handles transfer/draw/switchTurn. return `
-

You have no cards. Drawing will happen automatically...

+

Ask for a Card

+

You have no cards — ask your opponent for any rank.

+
`; } @@ -1158,12 +1164,68 @@ export class GameScreen { `; } + /** + * After any hand-changing action, check if the player has 3 of a rank + * and submit checkAndScoreBook for each. The contract removes the 3 + * cards and increments the score. At ≥4 books, it sets GameOver. + * + * Mirrors _helpers.ts:autoScoreBooks from the e2e reference. + */ + private async autoScoreBooks(): Promise { + if (!this.gameState) return; + const playerId = this.gameState.playerId as 1 | 2; + + try { + const hand = await MidnightService.getPlayerHand(this.lobbyId, playerId); + if (!hand || hand.length < 3) return; + + // Group cards by rank and find any with 3 (a complete book) + const rankCounts = new Map(); + for (const card of hand) { + rankCounts.set(card.rank, (rankCounts.get(card.rank) ?? 0) + 1); + } + + const RANK_NAMES = ['A', '2', '3', '4', '5', '6', '7']; + for (const [rank, count] of rankCounts) { + if (count >= 3) { + console.log(`[GameScreen] autoScoreBooks: P${playerId} has ${count} of ${RANK_NAMES[rank]} — scoring book`); + try { + const result = await MidnightService.checkAndScoreBook(this.lobbyId, playerId, rank); + if (result.success) { + this.showNotification({ + type: 'book', + rank: RANK_NAMES[rank] as any, + }); + console.log(`[GameScreen] ★ Book of ${RANK_NAMES[rank]} scored!`); + } else { + console.warn(`[GameScreen] checkAndScoreBook(${RANK_NAMES[rank]}) failed: ${result.errorMessage}`); + } + } catch (err) { + // BACKEND_ISSUES #1: local hand read may disagree with the + // contract's internal rank counting. Log and continue — the + // on-chain state is authoritative. + console.warn(`[GameScreen] checkAndScoreBook(${RANK_NAMES[rank]}) threw:`, err); + } + } + } + } catch (err) { + console.warn('[GameScreen] autoScoreBooks hand read failed:', err); + } + } + private renderGameOverPanel(): string { if (!this.gameState) return ''; const myScore = this.gameState.scores[this.gameState.playerId - 1]; const opponentScore = this.gameState.scores[this.gameState.playerId === 1 ? 1 : 0]; - const isWinner = myScore > opponentScore; + // Early-win: the contract sets GameOver when a player reaches ≥4 books. + // Winner is the player with ≥4 (ties are unreachable via the early-win + // path). The on-chain `getWinner(gameId)` circuit returns 1 or 2; here + // we derive the same from scores since the backend doesn't yet expose + // the getWinner field in its /game_state response. + // TODO: read getWinner from the contract via the indexer once the + // frontend has direct indexer access (Phase 7 / E1 in FRONTEND_UPDATE.md). + const isWinner = myScore >= 4; const isTie = myScore === opponentScore; return ` @@ -1172,6 +1234,7 @@ export class GameScreen {

Your Books: ${myScore}

Opponent's Books: ${opponentScore}

+

Total: ${myScore + opponentScore}/7 books

+ + ZK Go-Fish @@ -1157,6 +1164,10 @@ + + +