diff --git a/.gitignore b/.gitignore index 01087ad..5e05952 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !.prettierrc !.prettierignore !.github +!.gitkeep node_modules/ build/ @@ -16,10 +17,11 @@ cache/ *.ptau *.tsbuildinfo -# Only keep the anvil dataset definition -/infra/amp/datasets/* -!/infra/amp/datasets/anvil__*.json +# Build output +anvil.json +battleship.json # Postgres data volume /infra/postgres/data/ -/infra/amp/data/ +/infra/amp/data/* +/infra/amp/datasets/* diff --git a/amp.config.ts b/amp.config.ts index 8f1d1f3..dd1f016 100644 --- a/amp.config.ts +++ b/amp.config.ts @@ -4,14 +4,9 @@ import { abi } from "./app/src/lib/abi.ts" export default defineDataset(() => ({ name: "battleship", - version: "0.1.0", - network: "mainnet", + network: "anvil", dependencies: { - anvil: { - owner: "graphprotocol", - name: "anvil", - version: "0.1.0", - }, + anvil: "_/anvil@0.0.1", }, tables: eventTables(abi), })) diff --git a/app/src/components/CreateGame.tsx b/app/src/components/CreateGame.tsx index 1eda861..929317f 100644 --- a/app/src/components/CreateGame.tsx +++ b/app/src/components/CreateGame.tsx @@ -72,7 +72,7 @@ const createGameAtom = runtime.fn( to: "/game/$id", params: { id: `${event.args.gameId}` }, }) - ).pipe(Effect.delay("1 second")) + ) }), { reactivityKeys: ["openGames"], diff --git a/app/src/components/GameBoard.tsx b/app/src/components/GameBoard.tsx index fa6525a..f74cef4 100644 --- a/app/src/components/GameBoard.tsx +++ b/app/src/components/GameBoard.tsx @@ -1,10 +1,10 @@ -import { Atom, Result, useAtom, useAtomValue } from "@effect-atom/atom-react" +import { Atom, Result, useAtom } from "@effect-atom/atom-react" import type { FnContext } from "@effect-atom/atom/Atom" import { PerlinNoise } from "@paper-design/shaders-react" -import { Effect, Schema } from "effect" +import { Cause, Effect, Schema } from "effect" import { type ComponentProps, type ReactNode, useState } from "react" import { Params, SoundEffect } from "sfxr.js" -import type { Address, Client } from "viem" +import { type Address, type Client } from "viem" import { simulateContract, writeContract } from "viem/actions" import { useAccount, useConnectorClient } from "wagmi" import BattleshipHorizontal from "../assets/ships/battleship-horizontal.png" @@ -17,6 +17,7 @@ import DestroyerHorizontal from "../assets/ships/destroyer-horizontal.png" import DestroyerVertical from "../assets/ships/destroyer-vertical.png" import SubmarineHorizontal from "../assets/ships/submarine-horizontal.png" import SubmarineVertical from "../assets/ships/submarine-vertical.png" +import { useBoardViewDetailsQuery } from "../hooks/useBoardViewDetailsQuery" import { type BoardConfig, createImpactProof, @@ -30,9 +31,9 @@ import { Orientation, Ship, } from "../lib/battleship" -import type { ShotFired } from "../lib/queries" -import { BoardViewArg, boardViewAtom, shotsFiredAtom } from "../lib/queries" +import { shotsFiredAtom } from "../lib/queries" import { runtime } from "../lib/runtime" +import { BoardViewArg, type ShotFired } from "../lib/schema" import { battleship, client } from "../lib/viem" import { ErrorCause } from "./ErrorCause" @@ -239,32 +240,24 @@ export const GameBoard: React.FC = ({ } export const HiddenBoard = ({ gameId, player }: { gameId: bigint; player: Address }) => { - const view = useAtomValue(boardViewAtom( - new BoardViewArg({ - game: gameId, - player, - }), - )) + const { isError, isLoading, shots } = useBoardViewDetailsQuery(new BoardViewArg({ game: gameId, player })) - return Result.match(view, { - onSuccess: (success) => , - onFailure: (error) => } />, - onInitial: () => , - }) + if (isLoading) { + return + } else if (isError) { + return } /> + } + + return } export const OpponentBoard = ( - { attackable, gameId, player }: { gameId: bigint; player: Address; attackable: boolean }, + { attackable, gameId, player }: Readonly<{ gameId: bigint; player: Address; attackable: boolean }>, ) => { const [result, attack] = useAtom(attackAtom(gameId)) const account = useAccount() const wallet = useConnectorClient({ account: account.address }) - const view = useAtomValue(boardViewAtom( - new BoardViewArg({ - game: gameId, - player, - }), - )) + const { isError, isLoading, shots } = useBoardViewDetailsQuery(new BoardViewArg({ game: gameId, player })) const [isAttacking, setIsAttacking] = useState(false) const overlay = !attackable @@ -280,62 +273,59 @@ export const OpponentBoard = ( ) : null - return Result.match(view, { - onSuccess: (success) => ( - <> - { - setIsAttacking(true) - const params = new Params() - params.fromB58( - "7BMHBGQPRLZrdnfJGmid89mBHUU23CwsNTZPqzxY8w1MKoepUEjBra5hUCGtEuSty3CHQjbmKJoRfk3KftdQA2o9hnMqb8dMJ4rwAt2n9QXAmNRNRhFx16FBV", - ) - const soundEffect = new SoundEffect(params) - const audioData = soundEffect.generate() - const audio = audioData.getAudio?.() - audio?.play() - attack({ x, y, wallet: wallet.data! }) - setTimeout(() => { - setIsAttacking(false) - }, 1000) - } : - undefined} - overlay={Result.match(result, { - onFailure: (error) => , - onSuccess: () => overlay, - onInitial: () => overlay, - })} - className="data-[is-attacking]:parent-boards:animate-shake" - /> - - ), - onFailure: (error) => } />, - onInitial: () => , - }) + if (isLoading) { + return + } else if (isError) { + return } /> + } + + return ( + { + setIsAttacking(true) + const params = new Params() + params.fromB58( + "7BMHBGQPRLZrdnfJGmid89mBHUU23CwsNTZPqzxY8w1MKoepUEjBra5hUCGtEuSty3CHQjbmKJoRfk3KftdQA2o9hnMqb8dMJ4rwAt2n9QXAmNRNRhFx16FBV", + ) + const soundEffect = new SoundEffect(params) + const audioData = soundEffect.generate() + const audio = audioData.getAudio?.() + audio?.play() + attack({ x, y, wallet: wallet.data! }) + setTimeout(() => { + setIsAttacking(false) + }, 1000) + } : + undefined} + overlay={Result.match(result, { + onFailure: (error) => , + onSuccess: () => overlay, + onInitial: () => overlay, + })} + className="data-[is-attacking]:parent-boards:animate-shake" + /> + ) } -export const PlayerBoard = ({ gameId, player }: { gameId: bigint; player: Address }) => { +export const PlayerBoard = ({ gameId, player }: Readonly<{ gameId: bigint; player: Address }>) => { const board = generateBoardFromAddress(player) - const view = useAtomValue(boardViewAtom( - new BoardViewArg({ - game: gameId, - player, - }), - )) + const { isError, isLoading, shots } = useBoardViewDetailsQuery(new BoardViewArg({ game: gameId, player })) - return Result.all({ board, view }).pipe(Result.match({ - onSuccess: (success) => ( - - ), - onFailure: (error) => } />, - onInitial: () => , - })) + if (isLoading) { + return + } else if (isError) { + return } /> + } + + return ( + + ) } class AttackError extends Schema.TaggedError()("AttackError", { diff --git a/app/src/components/GameList.tsx b/app/src/components/GameList.tsx index 7969d69..54f31fc 100644 --- a/app/src/components/GameList.tsx +++ b/app/src/components/GameList.tsx @@ -1,12 +1,16 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" import { CrownIcon } from "@graphprotocol/gds-react/icons" import { cn } from "@graphprotocol/gds-react/utils" +import { eq, useLiveQuery } from "@tanstack/react-db" import { Link } from "@tanstack/react-router" +import { Cause } from "effect" import type { ComponentProps } from "react" +import { useMemo } from "react" import { formatEther, isAddressEqual } from "viem" import { useAccount, useConnectorClient } from "wagmi" -import type { GameListItem } from "../lib/queries" -import { gamesListAtom } from "../lib/queries" +import { gameCreatedCollection } from "../lib/collections/gameCreated" +import { gameEndedCollection } from "../lib/collections/gameEnded" +import { gameStartedCollection } from "../lib/collections/gameStarted" +import { GameListItem } from "../lib/schema" import { CreateGame } from "./CreateGame" import { ErrorCause } from "./ErrorCause" import { GameID } from "./GameID" @@ -19,54 +23,91 @@ export const GameList = () => { const wallet = useConnectorClient({ account: account.address }) const address = wallet.data?.account.address - return ( - Result.match(useAtomValue(gamesListAtom), { - onInitial: () => , - onFailure: (error) => , - onSuccess: (success) => { - const [myGames, otherGames] = success.value.reduce(([my, other], game) => { - if ( - address && - (isAddressEqual(game.firstPlayer, address) || - (game.secondPlayer && isAddressEqual(game.secondPlayer, address))) - ) { - my.push(game) - } else { - other.push(game) - } - return [my, other] - }, [[], []] as [Array, Array]) + const { data: games, isError, isLoading } = useLiveQuery((q) => + q.from({ gc: gameCreatedCollection }) + .leftJoin({ gs: gameStartedCollection }, ({ gc, gs }) => eq(gs.gameId, gc.gameId)) + .leftJoin({ ge: gameEndedCollection }, ({ gc, ge }) => eq(gc.gameId, ge.gameId)) + .orderBy(({ gc }) => gc.gameId) + ) + + const [myGames, otherGames] = useMemo<[Array, Array]>(() => { + if (address == null) { + return [ + [], + games.map((game) => + GameListItem.make({ + gameId: game.gc.gameId, + stakeAmount: game.gc.stakeAmount, + firstPlayer: game.gc.firstPlayer, + secondPlayer: game.gs?.secondPlayer, + startingPlayer: game.gs?.startingPlayer, + winner: game.ge?.winner, + }) + ), + ] as const + } + const mine = games.filter((game) => + isAddressEqual(game.gc.firstPlayer, address) || + (game.gs?.secondPlayer != null && isAddressEqual(game.gs.secondPlayer, address)) + ).map((game) => + GameListItem.make({ + gameId: game.gc.gameId, + stakeAmount: game.gc.stakeAmount, + firstPlayer: game.gc.firstPlayer, + secondPlayer: game.gs?.secondPlayer, + startingPlayer: game.gs?.startingPlayer, + winner: game.ge?.winner, + }) + ) + const others = games.filter((game) => + !isAddressEqual(game.gc.firstPlayer, address) || + (game.gs?.secondPlayer != null && !isAddressEqual(game.gs.secondPlayer, address)) + ).map((game) => + GameListItem.make({ + gameId: game.gc.gameId, + stakeAmount: game.gc.stakeAmount, + firstPlayer: game.gc.firstPlayer, + secondPlayer: game.gs?.secondPlayer, + startingPlayer: game.gs?.startingPlayer, + winner: game.ge?.winner, + }) + ) + return [mine, others] as const + }, [address, games]) - return ( -
- {myGames.length > 0 ? - ( -
-
-

My Games

- -
- -
- ) : - null} -
-
-

{myGames.length > 0 ? "Other" : null} Games

- {myGames.length === 0 ? : null} -
- {otherGames.length > 0 ? - : - ( -
- No {myGames.length > 0 ? "other" : null} games available. Create one! -
- )} + if (isLoading) { + return + } else if (isError) { + return + } + + return ( +
+ {myGames.length > 0 ? + ( +
+
+

My Games

+
+
- ) - }, - }) + ) : + null} +
+
+

{myGames.length > 0 ? "Other" : null} Games

+ {myGames.length === 0 ? : null} +
+ {otherGames.length > 0 ? + : + ( +
+ No {myGames.length > 0 ? "other" : null} games available. Create one! +
+ )} +
+
) } diff --git a/app/src/hooks/useBoardViewDetailsQuery.tsx b/app/src/hooks/useBoardViewDetailsQuery.tsx new file mode 100644 index 0000000..159d8b1 --- /dev/null +++ b/app/src/hooks/useBoardViewDetailsQuery.tsx @@ -0,0 +1,50 @@ +"use client" + +import { Schema } from "effect" +import { useMemo } from "react" +import { isAddressEqual } from "viem" +import type { BoardViewArg } from "../lib/schema" +import { IncomingShotsArg, PackedCoordinates, ShotFired } from "../lib/schema" +import { useIncomingShotsQuery } from "./useIncomingShotsQuery" + +export function useBoardViewDetailsQuery(arg: BoardViewArg) { + const { data, ...rest } = useIncomingShotsQuery( + new IncomingShotsArg({ gameId: arg.game, defender: arg.player }), + ) + + const shots = useMemo>(() => { + if (data.length === 0) { + return [] + } + // each record would link back to same game_started and game_created records + const gameStarted = data[0].gs + const gameCreated = data[0].gc + + // Return early if second player hasn't joined yet + if (gameStarted.secondPlayer == null) { + return [] + } + + // Validate that arg.player is one of the two players in the game + if (arg.player !== gameCreated.firstPlayer && arg.player !== gameStarted.secondPlayer) { + return [] + } + + // Determine the attacker (opposite of defender) + const attacker = arg.player === gameCreated.firstPlayer + ? gameStarted.secondPlayer + : gameCreated.firstPlayer + + return data.filter((shot) => isAddressEqual(shot.sf.attacker, attacker)).map((shot) => + ShotFired.make({ + gameId: arg.game, + attacker: shot.sf.attacker, + coordinates: Schema.decodeUnknownSync(PackedCoordinates)(shot.sf.coordinates), + ship: shot.ir?.ship, + impact: shot.ir?.impact, + }) + ) + }, [data, arg.player, arg.game]) + + return { shots, ...rest } +} diff --git a/app/src/hooks/useGameDetailsQuery.tsx b/app/src/hooks/useGameDetailsQuery.tsx new file mode 100644 index 0000000..4c66942 --- /dev/null +++ b/app/src/hooks/useGameDetailsQuery.tsx @@ -0,0 +1,31 @@ +"use client" + +import { eq, useLiveQuery } from "@tanstack/react-db" +import { gameCreatedCollection } from "../lib/collections/gameCreated" +import { gameEndedCollection } from "../lib/collections/gameEnded" +import { gameStartedCollection } from "../lib/collections/gameStarted" +import { shotFiredCollection } from "../lib/collections/shotFired" + +export function useGameDetailsQuery(gameId: bigint) { + return useLiveQuery((q) => { + const lastShotFired = q.from({ sf: shotFiredCollection }) + .where(({ sf }) => eq(sf.gameId, gameId)) + .orderBy(({ sf }) => sf.blockNum, "desc") + .limit(1) + return q.from({ gc: gameCreatedCollection }) + .where(({ gc }) => eq(gc.gameId, gameId)) + .leftJoin({ gs: gameStartedCollection }, ({ gc, gs }) => eq(gs.gameId, gc.gameId)) + .leftJoin({ ge: gameEndedCollection }, ({ gc, ge }) => eq(ge.gameId, gc.gameId)) + .leftJoin({ sf: lastShotFired }, ({ gc, sf }) => eq(sf.gameId, gc.gameId)) + .select(({ gc, ge, gs, sf }) => ({ + firstPlayer: gc.firstPlayer, + secondPlayer: gs?.secondPlayer, + stakeAmount: gc.stakeAmount, + startingPlayer: gs?.startingPlayer, + winner: ge?.winner, + lastAttacker: sf?.attacker, + })) + .orderBy(({ gc }) => gc.gameId) + .limit(1) + }) +} diff --git a/app/src/hooks/useIncomingShotsQuery.tsx b/app/src/hooks/useIncomingShotsQuery.tsx new file mode 100644 index 0000000..9e7b8b3 --- /dev/null +++ b/app/src/hooks/useIncomingShotsQuery.tsx @@ -0,0 +1,34 @@ +"use client" + +import { eq, useLiveQuery } from "@tanstack/react-db" +import { gameCreatedCollection } from "../lib/collections/gameCreated" +import { gameStartedCollection } from "../lib/collections/gameStarted" +import { impactReportedCollection } from "../lib/collections/impactReported" +import { shotFiredCollection } from "../lib/collections/shotFired" +import type { IncomingShotsArg } from "../lib/schema" + +export function useIncomingShotsQuery({ gameId }: Readonly) { + return useLiveQuery((q) => + q.from({ sf: shotFiredCollection }) + .innerJoin({ gc: gameCreatedCollection }, ({ gc, sf }) => eq(sf.gameId, gc.gameId)) + .innerJoin({ gs: gameStartedCollection }, ({ gs, sf }) => eq(sf.gameId, gs.gameId)) + .leftJoin( + { ir: impactReportedCollection }, + ({ ir, sf }) => eq(sf.coordinates, ir.coordinates), + ) + .fn.where((row) => { + const { ir, sf } = row + + if (sf.gameId !== gameId) return false + + // Only match impact reports for the same game + if (ir != null && ir.gameId !== sf.gameId) return false + + // Only match impact reports where attacker != defender + if (ir != null && sf.attacker === ir.defender) return false + + return true + }) + .orderBy(({ sf }) => sf.blockNum) + ) +} diff --git a/app/src/lib/collections/gameCreated.ts b/app/src/lib/collections/gameCreated.ts new file mode 100644 index 0000000..88c816f --- /dev/null +++ b/app/src/lib/collections/gameCreated.ts @@ -0,0 +1,35 @@ +import { electricCollectionOptions } from "@tanstack/electric-db-collection" +import { createCollection } from "@tanstack/react-db" +import { Schema } from "effect" +import { AddressFromString } from "../schema" + +export const GameCreated = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + stakeAmount: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("stake_amount")), + firstPlayer: AddressFromString.pipe(Schema.propertySignature, Schema.fromKey("first_player")), + blockHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_hash")), + txHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("tx_hash")), + address: AddressFromString, + blockNum: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_num")), + timestamp: Schema.String, +}) +export type GameCreated = typeof GameCreated.Type + +export const gameCreatedCollection = createCollection( + electricCollectionOptions({ + id: "game_created", + shapeOptions: { + url: `${window.location.origin}/electric`, + params: { + table: "game_created", + }, + transformer(message) { + return Schema.decodeUnknownSync(GameCreated)(message) + }, + }, + schema: Schema.standardSchemaV1(GameCreated), + getKey(item) { + return String(item.gameId) + }, + }), +) diff --git a/app/src/lib/collections/gameEnded.ts b/app/src/lib/collections/gameEnded.ts new file mode 100644 index 0000000..3302906 --- /dev/null +++ b/app/src/lib/collections/gameEnded.ts @@ -0,0 +1,34 @@ +import { electricCollectionOptions } from "@tanstack/electric-db-collection" +import { createCollection } from "@tanstack/react-db" +import { Schema } from "effect" +import { AddressFromString } from "../schema" + +export const GameEnded = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + blockHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_hash")), + txHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("tx_hash")), + address: AddressFromString, + blockNum: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_num")), + timestamp: Schema.String, + winner: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("winner")), +}) +export type GameEnded = typeof GameEnded.Type + +export const gameEndedCollection = createCollection( + electricCollectionOptions({ + id: "game_ended", + shapeOptions: { + url: `${window.location.origin}/electric`, + params: { + table: "game_ended", + }, + transformer(message) { + return Schema.decodeUnknownSync(GameEnded)(message) + }, + }, + schema: Schema.standardSchemaV1(GameEnded), + getKey(item) { + return String(item.gameId) + }, + }), +) diff --git a/app/src/lib/collections/gameStarted.ts b/app/src/lib/collections/gameStarted.ts new file mode 100644 index 0000000..0696012 --- /dev/null +++ b/app/src/lib/collections/gameStarted.ts @@ -0,0 +1,35 @@ +import { electricCollectionOptions } from "@tanstack/electric-db-collection" +import { createCollection } from "@tanstack/react-db" +import { Schema } from "effect" +import { AddressFromString } from "../schema" + +export const GameStarted = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + blockHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_hash")), + txHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("tx_hash")), + address: AddressFromString, + blockNum: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_num")), + timestamp: Schema.String, + secondPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("second_player")), + startingPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("starting_player")), +}) +export type GameStarted = typeof GameStarted.Type + +export const gameStartedCollection = createCollection( + electricCollectionOptions({ + id: "game_started", + shapeOptions: { + url: `${window.location.origin}/electric`, + params: { + table: "game_started", + }, + transformer(message) { + return Schema.decodeUnknownSync(GameStarted)(message) + }, + }, + schema: Schema.standardSchemaV1(GameStarted), + getKey(item) { + return String(item.gameId) + }, + }), +) diff --git a/app/src/lib/collections/impactReported.ts b/app/src/lib/collections/impactReported.ts new file mode 100644 index 0000000..0fc8210 --- /dev/null +++ b/app/src/lib/collections/impactReported.ts @@ -0,0 +1,125 @@ +import { electricCollectionOptions } from "@tanstack/electric-db-collection" +import { createCollection } from "@tanstack/react-db" +import { Schema } from "effect" +import { AddressFromString } from "../schema" + +export const ImpactFromNumber = Schema.transform( + Schema.Int, + Schema.Literal("MISS", "HIT", "SUNK"), + { + strict: true, + decode(num) { + switch (num) { + case 0: { + return "MISS" + } + case 1: { + return "HIT" + } + case 2: { + return "SUNK" + } + default: { + throw new Error("invalid impact value") + } + } + }, + encode(impact) { + switch (impact) { + case "SUNK": { + return 2 + } + case "HIT": { + return 1 + } + default: { + return 0 + } + } + }, + }, +) +export const ShipFromNumber = Schema.transform( + Schema.Int, + Schema.Literal("CARRIER", "BATTLESHIP", "CRUISER", "SUBMARINE", "DESTROYER", "UNKNOWN"), + { + strict: true, + decode(num) { + switch (num) { + case 1: { + return "CARRIER" + } + case 2: { + return "BATTLESHIP" + } + case 3: { + return "CRUISER" + } + case 4: { + return "SUBMARINE" + } + case 5: { + return "DESTROYER" + } + default: { + return "UNKNOWN" + } + } + }, + encode(impact) { + switch (impact) { + case "CARRIER": { + return 1 + } + case "BATTLESHIP": { + return 2 + } + case "CRUISER": { + return 3 + } + case "SUBMARINE": { + return 4 + } + case "DESTROYER": { + return 5 + } + default: { + return 0 + } + } + }, + }, +) + +export const ImpactReported = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + blockHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_hash")), + txHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("tx_hash")), + address: AddressFromString, + blockNum: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_num")), + timestamp: Schema.String, + defender: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("defender")), + coordinates: Schema.Int, + impact: Schema.Int.pipe(Schema.optionalWith({ nullable: true })), + ship: Schema.Int.pipe(Schema.optionalWith({ nullable: true })), +}) +export type ImpactReported = typeof ImpactReported.Type + +export const impactReportedCollection = createCollection( + electricCollectionOptions({ + id: "impact_reported", + shapeOptions: { + url: `${window.location.origin}/electric`, + params: { + table: "impact_reported", + }, + transformer(message) { + return Schema.decodeUnknownSync(ImpactReported)(message) + }, + }, + schema: Schema.standardSchemaV1(ImpactReported), + getKey(item) { + return item.txHash + }, + }), +) diff --git a/app/src/lib/collections/shotFired.ts b/app/src/lib/collections/shotFired.ts new file mode 100644 index 0000000..1412ded --- /dev/null +++ b/app/src/lib/collections/shotFired.ts @@ -0,0 +1,35 @@ +import { electricCollectionOptions } from "@tanstack/electric-db-collection" +import { createCollection } from "@tanstack/react-db" +import { Schema } from "effect" +import { AddressFromString } from "../schema" + +export const ShotFired = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + blockHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_hash")), + txHash: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("tx_hash")), + address: AddressFromString, + blockNum: Schema.String.pipe(Schema.propertySignature, Schema.fromKey("block_num")), + timestamp: Schema.String, + attacker: AddressFromString, + coordinates: Schema.Int, +}) +export type ShotFired = typeof ShotFired.Type + +export const shotFiredCollection = createCollection( + electricCollectionOptions({ + id: "shot_fired", + shapeOptions: { + url: `${window.location.origin}/electric`, + params: { + table: "shot_fired", + }, + transformer(message) { + return Schema.decodeUnknownSync(ShotFired)(message) + }, + }, + schema: Schema.standardSchemaV1(ShotFired), + getKey(item) { + return String(item.txHash) + }, + }), +) diff --git a/app/src/lib/queries.ts b/app/src/lib/queries.ts index ecd9841..7c8922b 100644 --- a/app/src/lib/queries.ts +++ b/app/src/lib/queries.ts @@ -1,9 +1,9 @@ import { Arrow, ArrowFlight } from "@edgeandnode/amp" import { Atom } from "@effect-atom/atom-react" import { Table } from "apache-arrow" -import { Array, Chunk, Effect, Schema, Stream } from "effect" +import { Chunk, Effect, Schema, Stream } from "effect" import type { Address } from "viem" -import { getAddress, isAddressEqual } from "viem" +import { getAddress } from "viem" import { packCoordinates, unpackCoordinates } from "./battleship" import { runtime } from "./runtime" @@ -23,7 +23,7 @@ export const query = Effect.fn(function*(schema: Schema.Schema return yield* Schema.decodeUnknown(Schema.Array(schema))(result) }) -const AddressFromSelf: Schema.Schema
= Schema.String.pipe( +export const AddressFromSelf: Schema.Schema
= Schema.String.pipe( Schema.pattern(/^0x[0-9a-fA-F]{40}$/), Schema.transform(Schema.String, { decode: (address) => getAddress(address as Address), @@ -32,21 +32,12 @@ const AddressFromSelf: Schema.Schema
= Schema.String.pipe( }), ).pipe(Schema.asSchema) as any -const AddressFromString = Schema.String.pipe(Schema.transform(AddressFromSelf, { - decode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address}` as Address), - encode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address}` as Address), +export const AddressFromString = Schema.String.pipe(Schema.transform(AddressFromSelf, { + decode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address.replace(/^\\x/, "")}` as Address), + encode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address.replace(/^\\x/, "")}` as Address), strict: true, })).pipe(Schema.asSchema) -export class GameListItem extends Schema.Class("GameListItem")({ - gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), - stakeAmount: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("stake_amount")), - firstPlayer: AddressFromString.pipe(Schema.propertySignature, Schema.fromKey("first_player")), - secondPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("second_player")), - startingPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("starting_player")), - winner: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("winner")), -}) {} - export class Coordinates extends Schema.Class("Coordinates")({ x: Schema.Int.pipe(Schema.between(0, 9)), y: Schema.Int.pipe(Schema.between(0, 9)), @@ -80,35 +71,8 @@ export class ShotFired extends Schema.Class("ShotFired")({ impact: Schema.Number.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("impact")), }) {} -export class ImpactReported extends Schema.Class("ImpactReported")({ - gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), - defender: AddressFromString.pipe(Schema.propertySignature, Schema.fromKey("defender")), - coordinates: PackedCoordinates.pipe(Schema.propertySignature, Schema.fromKey("coordinates")), - impact: Schema.Number.pipe(Schema.propertySignature, Schema.fromKey("impact")), - ship: Schema.Number.pipe(Schema.propertySignature, Schema.fromKey("ship")), -}) {} - -const gamesListQuery = ` - SELECT gc.*, gs.*, ge.* FROM battleship.game_created gc - LEFT JOIN battleship.game_started gs - ON gc.game_id = gs.game_id - LEFT JOIN battleship.game_ended ge - ON gc.game_id = ge.game_id - ORDER BY gs.block_num DESC -` - -export const queryGamesList = () => query(GameListItem, gamesListQuery) -export const gamesListAtom = runtime.atom(queryGamesList()).pipe(Atom.withReactivity(["gamesList"])) - -const lastShotFiredQuery = (game: bigint) => ` - SELECT * FROM battleship.shot_fired - WHERE game_id = ${game} - ORDER BY block_num DESC - LIMIT 1 -` - const shotsFiredQuery = (game: bigint) => ` - SELECT sf.*, ir.* FROM battleship.shot_fired sf + SELECT sf.*, ir.ship, ir.impact FROM battleship.shot_fired sf LEFT JOIN battleship.impact_reported ir ON sf.game_id = ir.game_id AND sf.coordinates = ir.coordinates @@ -121,76 +85,3 @@ export const shotsFired = (game: bigint) => query(ShotFired, shotsFiredQuery(gam export const shotsFiredAtom = Atom.family((id: bigint) => runtime.atom(shotsFired(id)).pipe(Atom.withReactivity(["shotsFired"])) ) - -export class IncomingShotsArg extends Schema.TaggedClass()("IncomingShotsArg", { - gameId: Schema.BigInt, - defender: AddressFromString, -}) {} - -export const incomingShotsAtom = Atom.family((arg: IncomingShotsArg) => - runtime.atom(Effect.fn(function*(ctx) { - const game = yield* ctx.result(gameDetailsAtom(arg.gameId)) - if (game.secondPlayer === undefined) { - return [] - } - - if (arg.defender !== game.firstPlayer && arg.defender !== game.secondPlayer) { - return [] - } - - const shots = yield* ctx.result(shotsFiredAtom(arg.gameId)) - const attacker = arg.defender === game.firstPlayer ? game.secondPlayer : game.firstPlayer - const matching = shots.filter((shot) => isAddressEqual(shot.attacker, attacker)) - return matching - })).pipe(Atom.withReactivity(["incomingShots"])) -) - -export class BoardViewArg extends Schema.TaggedClass()("BoardViewArg", { - game: Schema.BigInt, - player: AddressFromString, -}) {} - -export const boardViewAtom = Atom.family((arg: BoardViewArg) => - runtime.atom(Effect.fn(function*(ctx) { - const shots = yield* ctx.result(incomingShotsAtom( - new IncomingShotsArg({ - gameId: arg.game, - defender: arg.player, - }), - )) - return { shots } - })).pipe(Atom.withReactivity(["boardView"])) -) - -const gameDetailsQuery = (id: bigint) => ` - SELECT gc.*, gs.*, sf.attacker as last_attacker, sf.coordinates as last_coordinates FROM battleship.game_created gc - LEFT JOIN battleship.game_started gs - ON gc.game_id = gs.game_id - LEFT JOIN (${lastShotFiredQuery(id)}) sf - ON sf.game_id = gc.game_id - WHERE gc.game_id = ${id} - LIMIT 1 -` - -export class GameNotFound extends Schema.TaggedError()("GameNotFound", { - id: Schema.BigInt, -}) {} - -export const queryGameDetails = (id: bigint) => query(GameDetails, gameDetailsQuery(id)) -export const gameDetailsAtom = Atom.family((id: bigint) => - runtime.atom(Effect.gen(function*() { - const details = yield* queryGameDetails(id) - return yield* Array.head(details).pipe(Effect.orElse(() => new GameNotFound({ id }))) - })).pipe(Atom.withReactivity(["gameDetails"])) -) - -export const impactsQuery = (game: bigint) => ` - SELECT * FROM battleship.impact_reported - WHERE game_id = ${game} - ORDER BY block_num ASC -` - -export const queryImpacts = (game: bigint) => query(ImpactReported, impactsQuery(game)) -export const impactsReportedAtom = Atom.family((id: bigint) => - runtime.atom(queryImpacts(id)).pipe(Atom.withReactivity(["impactsReported"])) -) diff --git a/app/src/lib/schema.ts b/app/src/lib/schema.ts new file mode 100644 index 0000000..977cd72 --- /dev/null +++ b/app/src/lib/schema.ts @@ -0,0 +1,61 @@ +import { Schema } from "effect" +import type { Address } from "viem" +import { getAddress } from "viem" +import { packCoordinates, unpackCoordinates } from "./battleship" + +export const AddressFromSelf: Schema.Schema
= Schema.String.pipe( + Schema.pattern(/^0x[0-9a-fA-F]{40}$/), + Schema.transform(Schema.String, { + decode: (address) => getAddress(address as Address), + encode: (address) => getAddress(address as Address), + strict: true, + }), +).pipe(Schema.asSchema) as any + +export const AddressFromString = Schema.String.pipe(Schema.transform(AddressFromSelf, { + decode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address.replace(/^\\x/, "")}` as Address), + encode: (address) => getAddress(address.startsWith("0x") ? address : `0x${address.replace(/^\\x/, "")}` as Address), + strict: true, +})).pipe(Schema.asSchema) +export type AddressFromString = typeof AddressFromString.Type + +export class IncomingShotsArg extends Schema.TaggedClass()("IncomingShotsArg", { + gameId: Schema.BigInt, + defender: AddressFromString, +}) {} +export class BoardViewArg extends Schema.TaggedClass()("BoardViewArg", { + game: Schema.BigInt, + player: AddressFromString, +}) {} + +export class Coordinates extends Schema.Class("Coordinates")({ + x: Schema.Int.pipe(Schema.between(0, 9)), + y: Schema.Int.pipe(Schema.between(0, 9)), +}) {} + +export const PackedCoordinates = Schema.transform(Schema.Number, Coordinates, { + decode: (coordinates) => { + const [x, y] = unpackCoordinates(coordinates) + return new Coordinates({ x, y }) + }, + encode: (coordinates) => packCoordinates(coordinates.x, coordinates.y), + strict: true, +}) + +export class ShotFired extends Schema.Class("ShotFired")({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + coordinates: PackedCoordinates.pipe(Schema.propertySignature, Schema.fromKey("coordinates")), + attacker: AddressFromString.pipe(Schema.propertySignature, Schema.fromKey("attacker")), + ship: Schema.Number.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("ship")), + impact: Schema.Number.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("impact")), +}) {} + +export const GameListItem = Schema.Struct({ + gameId: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("game_id")), + stakeAmount: Schema.BigInt.pipe(Schema.propertySignature, Schema.fromKey("stake_amount")), + firstPlayer: AddressFromString.pipe(Schema.propertySignature, Schema.fromKey("first_player")), + secondPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("second_player")), + startingPlayer: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("starting_player")), + winner: AddressFromString.pipe(Schema.optionalWith({ nullable: true }), Schema.fromKey("winner")), +}) +export type GameListItem = typeof GameListItem.Type diff --git a/app/src/routes/game.$id.tsx b/app/src/routes/game.$id.tsx index 0db0867..bee1f70 100644 --- a/app/src/routes/game.$id.tsx +++ b/app/src/routes/game.$id.tsx @@ -1,4 +1,3 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react" import { ClockIcon, ClockUserIcon, @@ -9,6 +8,7 @@ import { SkullIcon, } from "@graphprotocol/gds-react/icons" import { createFileRoute } from "@tanstack/react-router" +import { Cause } from "effect" import { formatEther, isAddressEqual } from "viem" import { useAccount, useConnectorClient } from "wagmi" import { ErrorCause } from "../components/ErrorCause" @@ -17,216 +17,220 @@ import { GameID } from "../components/GameID" import { JoinGame } from "../components/JoinGame" import { Loading } from "../components/Loading" import { Player } from "../components/Player" -import { gameDetailsAtom } from "../lib/queries" +import { useGameDetailsQuery } from "../hooks/useGameDetailsQuery" const Game = () => { const params = Route.useParams() - const details = useAtomValue(gameDetailsAtom(BigInt(params.id))) + const gameId = BigInt(params.id) + const account = useAccount() const wallet = useConnectorClient({ account: account.address }) - return Result.match(details, { - onInitial: (_) => ( + const { data: details, isError, isLoading } = useGameDetailsQuery(gameId) + + if (isLoading) { + return (
- ), - onFailure: (error) => , - onSuccess: (success) => { - const address = wallet.data?.account.address - const { - firstPlayer, - gameId, - lastAttacker, - secondPlayer, - stakeAmount, - startingPlayer, - winner, - } = success.value + ) + } else if (isError) { + return + } else if (!isLoading && details.length === 0) { + return + } - const isStarted = Boolean(secondPlayer && startingPlayer) - const isFinished = Boolean(winner) - const isFirstPlayer = Boolean(address && isAddressEqual(firstPlayer, address)) - const isSecondPlayer = Boolean(address && secondPlayer && isAddressEqual(secondPlayer, address)) - const isPlayer = isFirstPlayer || isSecondPlayer - const playerTurn = isStarted && !isFinished ? - (((lastAttacker === undefined && isAddressEqual(startingPlayer!, firstPlayer)) || - (lastAttacker !== undefined && !isAddressEqual(lastAttacker, firstPlayer))) ? - "first" : - "second") : - undefined - const isPlayerTurn = (playerTurn === "first" && isFirstPlayer) || (playerTurn === "second" && isSecondPlayer) - const winnerPlayer = winner ? (isAddressEqual(winner, firstPlayer) ? "first" : "second") : undefined - const isWinner = (winnerPlayer === "first" && isFirstPlayer) || (winnerPlayer === "second" && isSecondPlayer) + const address = wallet.data?.account.address + const { + firstPlayer, + lastAttacker, + secondPlayer, + stakeAmount, + startingPlayer, + winner, + } = details[0] - const playerBoard = isPlayer - ? - : (isStarted - ? - : ) + const isStarted = Boolean(secondPlayer && startingPlayer) + const isFinished = Boolean(winner) + const isFirstPlayer = Boolean(address && isAddressEqual(firstPlayer, address)) + const isSecondPlayer = Boolean(address && secondPlayer && isAddressEqual(secondPlayer, address)) + const isPlayer = isFirstPlayer || isSecondPlayer + const playerTurn = isStarted && !isFinished ? + (((lastAttacker === undefined && isAddressEqual(startingPlayer!, firstPlayer)) || + (lastAttacker !== undefined && !isAddressEqual(lastAttacker, firstPlayer))) ? + "first" : + "second") : + undefined + const isPlayerTurn = (playerTurn === "first" && isFirstPlayer) || (playerTurn === "second" && isSecondPlayer) + const winnerPlayer = winner ? (isAddressEqual(winner, firstPlayer) ? "first" : "second") : undefined + const isWinner = (winnerPlayer === "first" && isFirstPlayer) || (winnerPlayer === "second" && isSecondPlayer) - const opponentBoard = isStarted ? - (isPlayer - ? ( - - ) - : ) - : + const playerBoard = isPlayer + ? + : (isStarted + ? + : ) + + const opponentBoard = isStarted ? + (isPlayer + ? ( + + ) + : ) + : - const winnerIcon = - const turnIcon = + const winnerIcon = + const turnIcon = - const status = !isStarted + const status = !isStarted + ? { + id: "not-started" as const, + shortText: "Not started", + longText: "Waiting for someone to join", + icon: , + } + : isFinished ? + isPlayer ? + (isWinner ? { - id: "not-started" as const, - shortText: "Not started", - longText: "Waiting for someone to join", - icon: , - } - : isFinished ? - isPlayer ? - (isWinner - ? { - id: "won" as const, - shortText: "You won", - longText: "Congratulations, you won!", - icon: , - } - : { - id: "lost" as const, - shortText: "You lost", - longText: "Sorry, you lost", - icon: , - }) : - { id: "finished" as const, shortText: "Finished", longText: "Game has ended", icon: } : - isPlayer ? - (isPlayerTurn - ? { - id: "your-turn" as const, - shortText: "Your turn", - longText: "It’s your turn", - icon: turnIcon, - } - : { - id: "their-turn" as const, - shortText: "Their turn", - longText: "It’s their turn", - icon: , - }) : - { - id: "in-progress" as const, - shortText: "In progress", - longText: "Game in progress", - icon: , + id: "won" as const, + shortText: "You won", + longText: "Congratulations, you won!", + icon: , } + : { + id: "lost" as const, + shortText: "You lost", + longText: "Sorry, you lost", + icon: , + }) : + { id: "finished" as const, shortText: "Finished", longText: "Game has ended", icon: } : + isPlayer ? + (isPlayerTurn + ? { + id: "your-turn" as const, + shortText: "Your turn", + longText: "It’s your turn", + icon: turnIcon, + } + : { + id: "their-turn" as const, + shortText: "Their turn", + longText: "It’s their turn", + icon: , + }) : + { + id: "in-progress" as const, + shortText: "In progress", + longText: "Game in progress", + icon: , + } - const joinGameButton = !isPlayer && !isStarted - ? - : null + const joinGameButton = !isPlayer && !isStarted + ? + : null - return ( -
-
-
-
+
+
+
-
-
Game ID
-
- -
-
+ > +
+
Game ID
+
+ +
+
+
+
Creator
+
+ +
+
+ {secondPlayer ? + (
-
Creator
+
Challenger
- {secondPlayer ? - ( -
-
Challenger
-
- -
-
- ) : - null} -
-
Stake Amount
-
{formatEther(stakeAmount)} ETH
-
- {status - ? ( -
-
Status
-
- {status.icon} - {status.shortText} -
-
- ) - : null} -
- {joinGameButton ?
{joinGameButton}
: null} + ) : + null} +
+
Stake Amount
+
{formatEther(stakeAmount)} ETH
+ {status + ? ( +
+
Status
+
+ {status.icon} + {status.shortText} +
+
+ ) + : null} +
+ {joinGameButton ?
{joinGameButton}
: null} +
+
+
+
+
+ {opponentBoard}
-
-
-
- {opponentBoard} -
-
- {playerBoard} -
-
+
+ {playerBoard}
-
- {joinGameButton ?? (status - ? ( -
+
+
+ {joinGameButton ?? (status + ? ( +
-
- {status.icon} - {status.longText} -
-
- ) - : null)} -
-
- ) - }, - }) + > +
+ {status.icon} + {status.longText} +
+
+ ) + : null)} +
+
+ ) } export const Route = createFileRoute("/game/$id")({ diff --git a/docker-compose.yaml b/docker-compose.yaml index 304c476..6f69147 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,20 +10,16 @@ services: POSTGRES_PASSWORD: postgres ports: - 5432:5432 + - 6435:5432 volumes: - ./infra/postgres/postgres.conf:/etc/postgresql/postgresql.conf:ro - ./infra/postgres/init.sh:/docker-entrypoint-initdb.d/init.sh:ro - ./infra/postgres/data:/var/lib/postgresql/data - - electric: - image: electricsql/electric - environment: - DATABASE_URL: postgresql://postgres:postgres@postgres:5432/battleship?sslmode=disable - ELECTRIC_INSECURE: true - ports: - - 3000:3000 - depends_on: - - postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 # DB explorer right in your web browser at # Amp: http://localhost:7402/?pgsql=postgres&username=postgres&db=amp&ns=public @@ -71,7 +67,34 @@ services: - ./infra/amp/providers:/var/lib/amp/providers - ./infra/amp/datasets:/var/lib/amp/datasets - ./infra/amp/data:/var/lib/amp/data + restart: unless-stopped ports: - 1610:1610 # Admin API - 1603:1603 # JSON Lines - 1602:1602 # Arrow Flight + + ampsync: + image: ghcr.io/edgeandnode/ampsync:latest + command: ["sync"] + environment: + DATASET_NAME: "_/battleship" + DATASET_VERSION: "0.0.1" + AMP_FLIGHT_ADDR: http://amp:1602 + AMP_ADMIN_API_ADDR: http://amp:1610 + DATABASE_URL: postgres://postgres:postgres@postgres:5432/battleship?sslmode=disable + restart: unless-stopped + depends_on: + - postgres + - amp + - anvil + + electric: + image: electricsql/electric + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/battleship?sslmode=disable + ELECTRIC_INSECURE: true + ports: + - 3000:3000 + depends_on: + - postgres + # - ampsync diff --git a/infra/amp/datasets/anvil__0_1_0.json b/infra/amp/datasets/anvil__0_1_0.json deleted file mode 100644 index 2abd704..0000000 --- a/infra/amp/datasets/anvil__0_1_0.json +++ /dev/null @@ -1 +0,0 @@ -{"kind":"evm-rpc","network":"anvil","name":"anvil","version":"0.1.0","schema":{"blocks":{"timestamp":{"Timestamp":["Nanosecond","+00:00"]},"mix_hash":{"FixedSizeBinary":32},"base_fee_per_gas":{"Decimal128":[38,0]},"blob_gas_used":"UInt64","miner":{"FixedSizeBinary":20},"block_num":"UInt64","parent_hash":{"FixedSizeBinary":32},"excess_blob_gas":"UInt64","state_root":{"FixedSizeBinary":32},"receipt_root":{"FixedSizeBinary":32},"withdrawals_root":{"FixedSizeBinary":32},"hash":{"FixedSizeBinary":32},"difficulty":{"Decimal128":[38,0]},"ommers_hash":{"FixedSizeBinary":32},"transactions_root":{"FixedSizeBinary":32},"nonce":"UInt64","parent_beacon_root":{"FixedSizeBinary":32},"extra_data":"Binary","gas_used":"UInt64","logs_bloom":"Binary","gas_limit":"UInt64"},"logs":{"topic3":{"FixedSizeBinary":32},"topic2":{"FixedSizeBinary":32},"tx_index":"UInt32","tx_hash":{"FixedSizeBinary":32},"data":"Binary","address":{"FixedSizeBinary":20},"block_hash":{"FixedSizeBinary":32},"block_num":"UInt64","log_index":"UInt32","topic0":{"FixedSizeBinary":32},"topic1":{"FixedSizeBinary":32},"timestamp":{"Timestamp":["Nanosecond","+00:00"]}},"transactions":{"gas_used":"UInt64","from":{"FixedSizeBinary":20},"block_hash":{"FixedSizeBinary":32},"type":"Int32","gas_limit":"UInt64","timestamp":{"Timestamp":["Nanosecond","+00:00"]},"status":"Boolean","s":"Binary","tx_index":"UInt32","gas_price":{"Decimal128":[38,0]},"input":"Binary","max_fee_per_gas":{"Decimal128":[38,0]},"r":"Binary","v":"Binary","max_priority_fee_per_gas":{"Decimal128":[38,0]},"max_fee_per_blob_gas":{"Decimal128":[38,0]},"tx_hash":{"FixedSizeBinary":32},"to":{"FixedSizeBinary":20},"block_num":"UInt64","nonce":"UInt64","value":{"Decimal128":[38,0]}}}} \ No newline at end of file diff --git a/justfile b/justfile index 4cffd55..0b848e6 100644 --- a/justfile +++ b/justfile @@ -110,9 +110,19 @@ down: # Deploy the battleship contract [working-directory: "contracts"] -deploy: +deploy-contract: forge script script/DeployBattleship.s.sol --broadcast --rpc-url http://localhost:8545 --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +# Deploy the amp datasets +deploy-datasets: + ampctl manifest generate --network anvil --kind evm-rpc --out anvil.json + ampctl dataset register _/anvil -t 0.0.1 ./anvil.json + ampctl dataset deploy _/anvil@0.0.1 + + pnpm amp build --output ./battleship.json + ampctl dataset register _/battleship -t 0.0.1 ./battleship.json + ampctl dataset deploy _/battleship@0.0.1 + # Format solidity and typescript code [parallel] fmt: fmt-sol fmt-ts @@ -155,7 +165,7 @@ test-ts *args: # Run all development services in parallel [parallel] -dev: dev-app dev-proxy dev-amp +dev: dev-app dev-proxy # Run the app in development mode [private] @@ -166,8 +176,3 @@ dev-app: [private] dev-proxy: pnpm amp proxy - -# Start the amp development server (watch config changes) -[private] -dev-amp: - pnpm amp dev diff --git a/package.json b/package.json index edcf862..e2a4c5d 100644 --- a/package.json +++ b/package.json @@ -8,18 +8,23 @@ "lint": "eslint", "format": "pnpm lint --fix", "dev": "vite", - "test": "vitest" + "test": "vitest", + "amp": "amp" }, "dependencies": { "@connectrpc/connect-web": "^2.1.0", - "@edgeandnode/amp": "^0.0.18", + "@edgeandnode/amp": "^0.0.28", "@effect-atom/atom": "^0.3.2", "@effect-atom/atom-react": "^0.3.2", "@effect/experimental": "^0.56.0", "@effect/platform": "^0.92.1", + "@electric-sql/client": "^1.0.14", + "@electric-sql/react": "^1.0.14", "@graphprotocol/gds-react": "https://pkg.pr.new/graphprotocol/gds/@graphprotocol/gds-react@31e7393", "@paper-design/shaders-react": "^0.0.57", - "@tanstack/react-query": "^5.89.0", + "@tanstack/electric-db-collection": "^0.1.32", + "@tanstack/react-db": "^0.1.30", + "@tanstack/react-query": "^5.90.2", "@tanstack/react-router": "^1.132.47", "@tanstack/react-router-devtools": "^1.132.51", "abitype": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24460c2..9f6d563 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: specifier: ^2.1.0 version: 2.1.0(@bufbuild/protobuf@2.9.0)(@connectrpc/connect@2.1.0(@bufbuild/protobuf@2.9.0)) '@edgeandnode/amp': - specifier: ^0.0.18 - version: 0.0.18(841ba832ed0c57053c144dec33c7a36f) + specifier: ^0.0.28 + version: 0.0.28(3b926f80525b07f40023dae5d461fa81) '@effect-atom/atom': specifier: ^0.3.2 version: 0.3.2(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) @@ -31,14 +31,26 @@ importers: '@effect/platform': specifier: ^0.92.1 version: 0.92.1(effect@3.18.4) + '@electric-sql/client': + specifier: ^1.0.14 + version: 1.0.14 + '@electric-sql/react': + specifier: ^1.0.14 + version: 1.0.14(react@19.2.0) '@graphprotocol/gds-react': specifier: https://pkg.pr.new/graphprotocol/gds/@graphprotocol/gds-react@31e7393 version: https://pkg.pr.new/graphprotocol/gds/@graphprotocol/gds-react@31e7393(@types/react@19.2.2)(js-cookie@3.0.5)(react-dom@19.2.0(react@19.2.0))(react-is@19.1.1)(react@19.2.0)(tailwindcss@4.1.14) '@paper-design/shaders-react': specifier: ^0.0.57 version: 0.0.57(@types/react@19.2.2)(react@19.2.0) + '@tanstack/electric-db-collection': + specifier: ^0.1.32 + version: 0.1.32(typescript@5.9.3) + '@tanstack/react-db': + specifier: ^0.1.30 + version: 0.1.30(react@19.2.0)(typescript@5.9.3) '@tanstack/react-query': - specifier: ^5.89.0 + specifier: ^5.90.2 version: 5.90.2(react@19.2.0) '@tanstack/react-router': specifier: ^1.132.47 @@ -287,6 +299,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime-corejs3@7.28.4': + resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -330,6 +346,21 @@ packages: '@bufbuild/protobuf@2.9.0': resolution: {integrity: sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==} + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -366,18 +397,19 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@edgeandnode/amp@0.0.18': - resolution: {integrity: sha512-Po8EIw3chHzdYf/jhsYcVZlW8ngNziHBP0yUiuci6wLARkxixlt3CHN9h5kCq+SnWPCsPRyNZFE3SXEJL7kd2Q==} + '@edgeandnode/amp@0.0.28': + resolution: {integrity: sha512-c1fX3Gdzw8cY6hhkPJDdjNny+napW5lYP647g8TBZQMmlRptvNbP/Ikx9FOiaHc6CaMRHsD2zsplmFUTcwk/OA==} hasBin: true peerDependencies: '@bufbuild/protobuf': 2.6.3 '@connectrpc/connect': ^2.1.0 '@connectrpc/connect-node': ^2.1.0 - '@effect/experimental': ^0.55.0 - '@effect/platform': ^0.91.1 - '@effect/platform-node': ^0.97.1 - apache-arrow: ^21.0.0 - effect: ^3.17.14 + '@effect/cluster': ^0.52.5 + '@effect/experimental': ^0.57.0 + '@effect/platform': ^0.93.0 + '@effect/platform-node': ^0.100.0 + apache-arrow: ^21.1.0 + effect: ^3.19.2 '@effect-atom/atom-react@0.3.2': resolution: {integrity: sha512-RZ8ookTqmP4Zz8oUWOZD8F5iZ1WIYlr+RyHJiz+LdmV4vAf9uAItmpkRbW2r0c7Qa6ZQx5GvFCB6dmDf+HD5Wg==} @@ -394,13 +426,13 @@ packages: '@effect/rpc': ^0.71.0 effect: ^3.18.0 - '@effect/cli@0.70.0': - resolution: {integrity: sha512-nnPYeKuDzizHDFmn02zKBC3QtzWEuU63xsmEZODLqS3gxcA1Z8PRxTr9i/M6YoxBwtMm/7eEQodHcflsP2p0GA==} + '@effect/cli@0.72.0': + resolution: {integrity: sha512-shfnMTtfVaFJPn0JaCmgtVmp53dOExwoCYv2hMKidpLMrxJFm28z20VAdxYS+xIPqkYnXLbDXfVGXlQ7mQjZUA==} peerDependencies: - '@effect/platform': ^0.91.0 - '@effect/printer': ^0.45.0 - '@effect/printer-ansi': ^0.45.0 - effect: ^3.17.14 + '@effect/platform': ^0.93.0 + '@effect/printer': ^0.47.0 + '@effect/printer-ansi': ^0.47.0 + effect: ^3.19.0 '@effect/cluster@0.49.6': resolution: {integrity: sha512-wwfrQtfBkSnY5MJlrv9cp/Wyv0TuCkHFU7w7EBjtmqquPwXJ/g93qi37hWqXedbMRoA4ebY2SIH7S2JiUbPjOA==} @@ -491,6 +523,17 @@ packages: '@effect/rpc': ^0.70.2 effect: ^3.17.14 + '@electric-sql/client@1.0.14': + resolution: {integrity: sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q==} + + '@electric-sql/react@1.0.14': + resolution: {integrity: sha512-Cbi1Nacluhq1EC1QoIMnYfKij4UR516R39QvzMS6cMaetFqf1k0ZXpmPNhDiNG7HgleYA/0vYkTBkPa29gNJjA==} + peerDependencies: + react: '>=18.3.1 <20.0.0' + peerDependenciesMeta: + react: + optional: true + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -1006,6 +1049,9 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@microsoft/fetch-event-source@2.0.1': + resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -2014,6 +2060,19 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/db-ivm@0.1.9': + resolution: {integrity: sha512-EfrtTn34SQ6HOw1ZI4O1DKsrCSCzMufIYMpAYlO6kE7/DMZ7M9N8tQdXYyN2ZasL0zR8zjB1AHj4tMqcmPIt7w==} + peerDependencies: + typescript: '>=4.7' + + '@tanstack/db@0.4.8': + resolution: {integrity: sha512-KD4unbzac8Iyb+UB/EvW0WlM8Z/YTwHZWQw+AviGGYvfmgtaz/vzAFGQ2JQNTv0ALQdRsfV/qpYPkOADyqh+AA==} + peerDependencies: + typescript: '>=4.7' + + '@tanstack/electric-db-collection@0.1.32': + resolution: {integrity: sha512-wDB7alwi9MI2IyiWL6XNUvO4yOPsiwcPpnxVMzkDUqXfwT9982wFGTE8U50MbhJg0mnU35KzmRG5sfhATnswSw==} + '@tanstack/history@1.132.31': resolution: {integrity: sha512-UCHM2uS0t/uSszqPEo+SBSSoQVeQ+LlOWAVBl5SA7+AedeAbKafIPjFn8huZCXNLAYb0WKV2+wETr7lDK9uz7g==} engines: {node: '>=12'} @@ -2021,6 +2080,11 @@ packages: '@tanstack/query-core@5.90.2': resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + '@tanstack/react-db@0.1.30': + resolution: {integrity: sha512-4IXj/TC47OXNYFkYPHsLjZWRo4KrrcizKUtpEGykqyd95vdJpRBa3Q620/mA2ZK0D2Y4cIxRQd2QT8cUUK8vlw==} + peerDependencies: + react: '>=16.8.0' + '@tanstack/react-query@5.90.2': resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} peerDependencies: @@ -2676,6 +2740,10 @@ packages: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2725,6 +2793,9 @@ packages: check-types@11.2.3: resolution: {integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==} + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2798,6 +2869,9 @@ packages: cookie-es@2.0.0: resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + core-js-pure@3.46.0: + resolution: {integrity: sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2843,6 +2917,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -2882,10 +2959,22 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -3276,6 +3365,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + fractional-indexing@3.2.0: + resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} + engines: {node: ^14.13.1 || >=16.0.0} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3492,6 +3585,11 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3516,6 +3614,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3572,6 +3675,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3617,6 +3724,9 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-toml@1.0.2: + resolution: {integrity: sha512-/7IQ//bzn2a/5IDazPUNzlW7bsjxS51cxciYZDR+Z+3Le60yzT0YfI8KOWqTtBcZkXXVklhWd2OuGd8ZksB0wQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -3753,6 +3863,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -3815,6 +3928,10 @@ packages: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3943,6 +4060,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} @@ -4245,6 +4366,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4375,6 +4500,9 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sorted-btree@1.8.1: + resolution: {integrity: sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4720,11 +4848,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -4780,6 +4903,14 @@ packages: typescript: optional: true + viem@2.38.6: + resolution: {integrity: sha512-aqO6P52LPXRjdnP6rl5Buab65sYa4cZ6Cpn+k4OLOzVJhGIK8onTVoKMFMT04YjDfyDICa/DZyV9HmvLDgcjkw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4983,10 +5114,17 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xregexp@5.1.2: + resolution: {integrity: sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -5258,6 +5396,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime-corejs3@7.28.4': + dependencies: + core-js-pure: 3.46.0 + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': @@ -5313,7 +5455,7 @@ snapshots: react-dom: 19.2.0(react@19.2.0) reselect: 5.1.1 tabbable: 6.2.0 - use-sync-external-store: 1.5.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: '@types/react': 19.2.2 @@ -5324,12 +5466,29 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) reselect: 5.1.1 - use-sync-external-store: 1.5.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: '@types/react': 19.2.2 '@bufbuild/protobuf@2.9.0': {} + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.2 @@ -5386,20 +5545,25 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@edgeandnode/amp@0.0.18(841ba832ed0c57053c144dec33c7a36f)': + '@edgeandnode/amp@0.0.28(3b926f80525b07f40023dae5d461fa81)': dependencies: '@bufbuild/protobuf': 2.9.0 '@connectrpc/connect': 2.1.0(@bufbuild/protobuf@2.9.0) '@connectrpc/connect-node': 2.1.0(@bufbuild/protobuf@2.9.0)(@connectrpc/connect@2.1.0(@bufbuild/protobuf@2.9.0)) - '@effect/cli': 0.70.0(@effect/platform@0.92.1(effect@3.18.4))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) + '@effect/cli': 0.72.0(@effect/platform@0.92.1(effect@3.18.4))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) + '@effect/cluster': 0.49.6(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.45.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/workflow@0.10.3(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) '@effect/experimental': 0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4) '@effect/platform': 0.92.1(effect@3.18.4) '@effect/platform-node': 0.97.1(@effect/cluster@0.49.6(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.45.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/workflow@0.10.3(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.45.0(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(bufferutil@4.0.9)(effect@3.18.4)(utf-8-validate@5.0.10) abitype: 1.1.1(typescript@5.9.3)(zod@4.1.12) apache-arrow: 21.1.0 + date-fns: 4.1.0 effect: 3.18.4 jiti: 2.6.1 - viem: 2.38.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + js-toml: 1.0.2 + minimatch: 10.1.1 + open: 10.2.0 + viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) transitivePeerDependencies: - '@effect/printer' - '@effect/printer-ansi' @@ -5426,7 +5590,7 @@ snapshots: '@effect/rpc': 0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4) effect: 3.18.4 - '@effect/cli@0.70.0(@effect/platform@0.92.1(effect@3.18.4))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(effect@3.18.4)': + '@effect/cli@0.72.0(@effect/platform@0.92.1(effect@3.18.4))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4))(effect@3.18.4)': dependencies: '@effect/platform': 0.92.1(effect@3.18.4) '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.18.4))(effect@3.18.4) @@ -5528,6 +5692,19 @@ snapshots: '@effect/rpc': 0.70.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4) effect: 3.18.4 + '@electric-sql/client@1.0.14': + dependencies: + '@microsoft/fetch-event-source': 2.0.1 + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.52.4 + + '@electric-sql/react@1.0.14(react@19.2.0)': + dependencies: + '@electric-sql/client': 1.0.14 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + react: 19.2.0 + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -6282,6 +6459,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@microsoft/fetch-event-source@2.0.1': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -6666,7 +6845,7 @@ snapshots: '@swc/helpers': 0.5.17 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.5.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) '@react-aria/link@3.8.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -7216,7 +7395,7 @@ snapshots: dependencies: '@swc/helpers': 0.5.17 react: 19.2.0 - use-sync-external-store: 1.5.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) '@react-stately/toggle@3.9.1(react@19.2.0)': dependencies: @@ -7854,10 +8033,41 @@ snapshots: tailwindcss: 4.1.14 vite: 7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/db-ivm@0.1.9(typescript@5.9.3)': + dependencies: + fractional-indexing: 3.2.0 + sorted-btree: 1.8.1 + typescript: 5.9.3 + + '@tanstack/db@0.4.8(typescript@5.9.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@tanstack/db-ivm': 0.1.9(typescript@5.9.3) + typescript: 5.9.3 + + '@tanstack/electric-db-collection@0.1.32(typescript@5.9.3)': + dependencies: + '@electric-sql/client': 1.0.14 + '@standard-schema/spec': 1.0.0 + '@tanstack/db': 0.4.8(typescript@5.9.3) + '@tanstack/store': 0.7.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + - typescript + '@tanstack/history@1.132.31': {} '@tanstack/query-core@5.90.2': {} + '@tanstack/react-db@0.1.30(react@19.2.0)(typescript@5.9.3)': + dependencies: + '@tanstack/db': 0.4.8(typescript@5.9.3) + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + transitivePeerDependencies: + - typescript + '@tanstack/react-query@5.90.2(react@19.2.0)': dependencies: '@tanstack/query-core': 5.90.2 @@ -9080,6 +9290,10 @@ snapshots: dependencies: node-gyp-build: 4.8.4 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -9130,6 +9344,15 @@ snapshots: check-types@11.2.3: {} + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -9209,6 +9432,8 @@ snapshots: cookie-es@2.0.0: {} + core-js-pure@3.46.0: {} + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -9261,6 +9486,8 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 + date-fns@4.1.0: {} + dayjs@1.11.13: {} debug@4.3.4: @@ -9281,12 +9508,21 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -9825,6 +10061,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + fractional-indexing@3.2.0: {} + fsevents@2.3.3: optional: true @@ -10065,6 +10303,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -10092,6 +10332,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -10144,6 +10388,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.1 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -10177,6 +10425,11 @@ snapshots: js-tokens@9.0.1: {} + js-toml@1.0.2: + dependencies: + chevrotain: 11.0.3 + xregexp: 5.1.2 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -10297,6 +10550,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} lodash.merge@4.6.2: {} @@ -10344,6 +10599,10 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -10458,6 +10717,13 @@ snapshots: dependencies: wrappy: 1.0.2 + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + openapi-fetch@0.13.8: dependencies: openapi-typescript-helpers: 0.0.15 @@ -10866,6 +11132,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11030,6 +11298,8 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sorted-btree@1.8.1: {} + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -11355,10 +11625,6 @@ snapshots: dependencies: react: 19.2.0 - use-sync-external-store@1.5.0(react@19.2.0): - dependencies: - react: 19.2.0 - use-sync-external-store@1.6.0(react@19.2.0): dependencies: react: 19.2.0 @@ -11443,6 +11709,23 @@ snapshots: - utf-8-validate - zod + viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.3)(zod@4.1.12) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.3)(zod@4.1.12) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-node@3.2.4(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -11673,8 +11956,16 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + xmlhttprequest-ssl@2.1.2: {} + xregexp@5.1.2: + dependencies: + '@babel/runtime-corejs3': 7.28.4 + xtend@4.0.2: {} y18n@4.0.3: {} diff --git a/vite.config.ts b/vite.config.ts index ce4db40..94ed402 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,6 +32,11 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/amp/, ""), }, + "/electric": { + target: "http://localhost:3000/v1/shape", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/electric/, ""), + }, }, }, })