diff --git a/.gitignore b/.gitignore index ed9fc436..d39871ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules docs -.DS_Store \ No newline at end of file +.DS_Store +# Local Netlify folder +.netlify diff --git a/.gitmodules b/.gitmodules index 346e7c80..7f956c78 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "dojo-starter"] path = worlds/dojo-starter - url = https://github.com/dojoengine/dojo-starter \ No newline at end of file + url = https://github.com/dojoengine/dojo-starter +[submodule "onchain-dash"] + path = worlds/onchain-dash + url = https://github.com/MartianGreed/onchain-dash +[submodule "worlds/onchain-dash"] + path = worlds/onchain-dash + url = https://github.com/MartianGreed/onchain-dash diff --git a/examples/example-vite-kitchen-sink/.env.dist b/examples/example-vite-kitchen-sink/.env.dist new file mode 100644 index 00000000..76793b2c --- /dev/null +++ b/examples/example-vite-kitchen-sink/.env.dist @@ -0,0 +1,6 @@ +VITE_RPC_URL="http://localhost:5050" +VITE_RPC_API_KEY="" +VITE_CONTROLLER_URL="https://x.cartridge.gg/mainnet" +VITE_CONTROLLER_RPC="https://x.cartridge.gg/mainnet" +VITE_TORII_URL="http://localhost:8080" +VITE_RELAY_URL="/ip4/127.0.0.1/tcp/9090" \ No newline at end of file diff --git a/examples/example-vite-kitchen-sink/.eslintrc.json b/examples/example-vite-kitchen-sink/.eslintrc.json new file mode 100644 index 00000000..37224185 --- /dev/null +++ b/examples/example-vite-kitchen-sink/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/examples/example-vite-kitchen-sink/.gitignore b/examples/example-vite-kitchen-sink/.gitignore new file mode 100644 index 00000000..5bc6e635 --- /dev/null +++ b/examples/example-vite-kitchen-sink/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage +/dist/ + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +.env diff --git a/examples/example-vite-kitchen-sink/README.md b/examples/example-vite-kitchen-sink/README.md new file mode 100644 index 00000000..9dcc547c --- /dev/null +++ b/examples/example-vite-kitchen-sink/README.md @@ -0,0 +1,49 @@ +# OnChainDash Vite Kitchen Sink + +This project aims at showcasing dojo's capabilities outside of gaming. + +## Getting Started + +First, install dependencies: + +```bash +pnpm install +``` + +In one terminal window, start katana (the sequencer). If you want to use sepolia / mainnet contracts, you can just use a classic rpc (e.g. `https://rpc.netermind.io/(mainnet|sepolia)-juno`). If this is the case, you can skip the next command. + +```bash +katana --disable-fee --allowed-origins "*" +``` + +In another terminal window, start torii server + +```bash +# with katana +torii --world 0x6dd367f5e11f11e0502cb2c4db7ae9bb6d8b5a4a431750bed7bec88b218e12 --allowed-origins "*" +# with mainnet|sepolia +torii --world 0x6dd367f5e11f11e0502cb2c4db7ae9bb6d8b5a4a431750bed7bec88b218e12 --allowed-origins "*" --rpc "https://rpc.nethermind.io/(mainnet|sepolia)-juno?apikey={apikey}" -s 204922 +``` + +Then, start the development server: + +```bash +pnpm run dev +``` + +Open [http://localhost:5173](http://localhost:5173) with your browser to see the result. + +## Local Contracts deployment + +In order to make those commands work, you need to have torii & katana running. + +```bash +cd src/onchain +sozo build +sozo migrate apply +``` + +### Notes + +- you main want to update `actions` contract address in `src/components/caller-counter.tsx` & `src/components/global-counter.tsx` which is hardcoded in those files. +- if you want to have braavos & argent wallet working, you need to deploy classes and deploy your wallet manually. \ No newline at end of file diff --git a/examples/example-vite-kitchen-sink/components.json b/examples/example-vite-kitchen-sink/components.json new file mode 100644 index 00000000..4c43999a --- /dev/null +++ b/examples/example-vite-kitchen-sink/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/examples/example-vite-kitchen-sink/index.html b/examples/example-vite-kitchen-sink/index.html new file mode 100644 index 00000000..3e8f7a98 --- /dev/null +++ b/examples/example-vite-kitchen-sink/index.html @@ -0,0 +1,13 @@ + + + + + + + Dojo Onchain Dash + + +
+ + + diff --git a/examples/example-vite-kitchen-sink/netlify.toml b/examples/example-vite-kitchen-sink/netlify.toml new file mode 100644 index 00000000..59264545 --- /dev/null +++ b/examples/example-vite-kitchen-sink/netlify.toml @@ -0,0 +1,21 @@ +# example netlify.toml +[build] + command = "pnpm run build" + functions = "netlify/functions" + publish = "examples/example-vite-kitchen-sink/dist" + + ## Uncomment to use this redirect for Single Page Applications like create-react-app. + ## Not needed for static site generators. + #[[redirects]] + # from = "/*" + # to = "/index.html" + # status = 200 + + ## (optional) Settings for Netlify Dev + ## https://github.com/netlify/cli/blob/main/docs/netlify-dev.md#project-detection + #[dev] + # command = "yarn start" # Command to start your dev server + # port = 3000 # Port that the dev server will be listening on + # publish = "dist" # Folder with the static content for _redirect file + + ## more info on configuring this file: https://ntl.fyi/file-based-build-config diff --git a/examples/example-vite-kitchen-sink/package.json b/examples/example-vite-kitchen-sink/package.json new file mode 100644 index 00000000..4350dde2 --- /dev/null +++ b/examples/example-vite-kitchen-sink/package.json @@ -0,0 +1,59 @@ +{ + "name": "sink", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@cartridge/connector": "^0.3.46", + "@dojoengine/core": "workspace:*", + "@dojoengine/sdk": "workspace:*", + "@dojoengine/torii-wasm": "workspace:*", + "@dojoengine/torii-client": "workspace:*", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "@starknet-react/chains": "^3.0.0", + "@starknet-react/core": "2.9.0", + "@t3-oss/env-core": "^0.11.1", + "@t3-oss/env-nextjs": "^0.11.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "dotenv": "^16.4.5", + "jiti": "^1.21.6", + "lucide-react": "^0.441.0", + "next": "14.2.12", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "starknet": "6.11.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.16.10", + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.2", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.12", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "typescript": "^5.6.2", + "vite": "^5.4.8", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0", + "vite-preset-react": "^2.3.0" + } +} diff --git a/examples/example-vite-kitchen-sink/postcss.config.mjs b/examples/example-vite-kitchen-sink/postcss.config.mjs new file mode 100644 index 00000000..1a69fd2a --- /dev/null +++ b/examples/example-vite-kitchen-sink/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/examples/example-vite-kitchen-sink/public/dojo-logo.svg b/examples/example-vite-kitchen-sink/public/dojo-logo.svg new file mode 100644 index 00000000..4aa739e8 --- /dev/null +++ b/examples/example-vite-kitchen-sink/public/dojo-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/example-vite-kitchen-sink/src/app/favicon.ico b/examples/example-vite-kitchen-sink/src/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/examples/example-vite-kitchen-sink/src/app/favicon.ico differ diff --git a/examples/example-vite-kitchen-sink/src/app/fonts/GeistMonoVF.woff b/examples/example-vite-kitchen-sink/src/app/fonts/GeistMonoVF.woff new file mode 100644 index 00000000..f2ae185c Binary files /dev/null and b/examples/example-vite-kitchen-sink/src/app/fonts/GeistMonoVF.woff differ diff --git a/examples/example-vite-kitchen-sink/src/app/fonts/GeistVF.woff b/examples/example-vite-kitchen-sink/src/app/fonts/GeistVF.woff new file mode 100644 index 00000000..1b62daac Binary files /dev/null and b/examples/example-vite-kitchen-sink/src/app/fonts/GeistVF.woff differ diff --git a/examples/example-vite-kitchen-sink/src/app/globals.css b/examples/example-vite-kitchen-sink/src/app/globals.css new file mode 100644 index 00000000..071a4c63 --- /dev/null +++ b/examples/example-vite-kitchen-sink/src/app/globals.css @@ -0,0 +1,111 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + color: hsl(var(--foreground)); + background: hsl(var(--background)); +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --font-body: var(--font-geist-sans); + --font-heading: var(--font-geist-mono); + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } + .dojo { +--background: 20 14.3% 4.1%; + --foreground: 0 0% 95%; + --card: 24 9.8% 10%; + --card-foreground: 0 0% 95%; + --popover: 0 0% 9%; + --popover-foreground: 0 0% 95%; + --primary: 346.8 77.2% 49.8%; + --primary-foreground: 355.7 100% 97.3%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 15%; + --muted-foreground: 240 5% 64.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 346.8 77.2% 49.8%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-family: var(--font-geist-sans); + } + h1, h2, h3, h4, h5, h6, h7 { + font-family: var(--font-geist-sans); + } +} diff --git a/examples/example-vite-kitchen-sink/src/app/layout.tsx b/examples/example-vite-kitchen-sink/src/app/layout.tsx new file mode 100644 index 00000000..4183e713 --- /dev/null +++ b/examples/example-vite-kitchen-sink/src/app/layout.tsx @@ -0,0 +1,22 @@ +import { TooltipProvider } from "@/components/ui/tooltip"; + +import Sidebar from "@/components/sidebar"; +import Header from "@/components/header"; +import StarknetProvider from "@/components/starknet-provider"; + + +export default function RootLayout({ children }: React.PropsWithChildren<{}>) { + return ( + + +
+ +
+
+ {children} +
+
+
+
+ ); +} diff --git a/examples/example-vite-kitchen-sink/src/app/page.tsx b/examples/example-vite-kitchen-sink/src/app/page.tsx new file mode 100644 index 00000000..dcff572e --- /dev/null +++ b/examples/example-vite-kitchen-sink/src/app/page.tsx @@ -0,0 +1,39 @@ +import { useDojoDb } from "@/dojo/provider" +import { useEffect, useState } from "react" +import { OnchainDashSchemaType } from "@/dojo/models" +import { SDK } from "@dojoengine/sdk" +import { Subscription } from "@dojoengine/torii-client" +import GlobalCounter from "@/components/global-counter" +import CallerCounter from "@/components/caller-counter" +import Chat from "@/components/chat" + +export default function Home() { + return ( +
+
+
+
+ + Settings + +
+
+
+ + +
+ + Stats + +
+ Some stats about whats happening +
+
+
+
+ +
+ ); +} diff --git a/examples/example-vite-kitchen-sink/src/components/caller-counter.tsx b/examples/example-vite-kitchen-sink/src/components/caller-counter.tsx new file mode 100644 index 00000000..0243729d --- /dev/null +++ b/examples/example-vite-kitchen-sink/src/components/caller-counter.tsx @@ -0,0 +1,105 @@ +import { useCallback, useEffect, useState } from "react"; +import { Button } from "./ui/button"; +import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useDojoDb } from "@/dojo/provider"; +import { ensureStarkFelt } from "@/lib/utils"; +import { SDK } from "@dojoengine/sdk"; +import { OnchainDashSchemaType } from "@/dojo/models"; +import { Subscription } from "@dojoengine/torii-wasm"; +import { dojoConfig } from "@/dojo.config"; + +export default function CallerCounter() { + const [count, setCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [sub, setSub] = useState(null); + const { address } = useAccount(); + const { write: incrementCallerCounter } = useContractWrite({ + calls: [{ + contractAddress: dojoConfig.manifest.contracts[0].address, + entrypoint: "increment_caller_counter", + calldata: [] + }] + }); + + const handleCallerClick = useCallback(async () => { + incrementCallerCounter(); + setIsLoading(true); + }, [incrementCallerCounter, setIsLoading]); + + const db = useDojoDb(); + useEffect(() => { + async function getEntity(db: SDK, address: string) { + const entity = await db.getEntities({ + onchain_dash: { + CallerCounter: { $: { where: { caller: { $eq: ensureStarkFelt(address) } } } } + } + }, () => { }); + const counter = entity.pop(); + if (!counter) { + return 0; + } + const count = counter.models.onchain_dash?.CallerCounter?.counter + if (undefined === count) { + return 0; + } + + return parseInt(count.toString(), 16); + } + if (address && db) { + getEntity(db, address).then(setCount).catch(console.error) + } + }, [address, db]) + + useEffect(() => { + async function subscribeToEntityUpdates(db: SDK, address: string) { + const sub = await db.subscribeEntityQuery({ + // @ts-expect-error $eq is working there + onchain_dash: { CallerCounter: { $: { where: { caller: { $eq: ensureStarkFelt(address) } } } } } + }, ({ data, error }) => { + if (data) { + const entity = data.pop(); + if (!entity) { + return; + } + if (entity.models.onchain_dash?.CallerCounter?.counter === undefined) { + return + } + const count = entity.models.onchain_dash?.CallerCounter?.counter; + if (undefined === count) { + return 0; + } + + setIsLoading(false); + setCount(parseInt(count.toString(), 16)); + return; + } + if (error) { + throw error; + } + }); + setSub(sub); + } + if (address && db && sub === null) { + subscribeToEntityUpdates(db, address).then(() => { }).catch(console.error) + } + return () => { + if (sub) { + sub.free(); + } + }; + }, [address, db, sub]); + return ( +
+ + Per wallet counter + +
+ Count : {count} +
+
+ +
+
+ + ); +} diff --git a/examples/example-vite-kitchen-sink/src/components/chat.tsx b/examples/example-vite-kitchen-sink/src/components/chat.tsx new file mode 100644 index 00000000..fdc7f1f7 --- /dev/null +++ b/examples/example-vite-kitchen-sink/src/components/chat.tsx @@ -0,0 +1,142 @@ +import { CornerDownLeft } from "lucide-react"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { Label } from "./ui/label"; +import { Textarea } from "./ui/textarea"; +import { useCallback, useEffect, useRef, useState, KeyboardEvent } from "react"; +import { useForm } from "react-hook-form"; +import { useDojoDb } from "@/dojo/provider"; +import { useAccount } from "@starknet-react/core"; +import { toValidAscii } from "@/lib/utils"; +import { SDK } from "@dojoengine/sdk"; +import { Message, OnchainDashSchemaType } from "@/dojo/models"; +import { Subscription } from "@dojoengine/torii-wasm"; +import { shortAddress } from "@/lib/utils"; + +interface MessageItem { + content: string; + identity: string; + timestamp: number; +} + +interface FormValues { + message: string; +} + +export default function Chat() { + const { register, handleSubmit, reset } = useForm(); + const { account } = useAccount(); + const [messages, setMessages] = useState([]); + const [sub, setSub] = useState(null); + const formRef = useRef(null); + + const db = useDojoDb(); + const publish = useCallback(async (data: FormValues) => { + if (!account || !db) return; + + const asciiMessage = toValidAscii(data.message); + const msg = db.generateTypedData('onchain_dash-Message', { identity: account?.address, content: asciiMessage, timestamp: Date.now() }) + try { + const signature = await account.signMessage(msg); + + try { + await db.client.publishMessage(JSON.stringify(msg), signature as string[]); + reset(); + } catch (error) { + console.error("failed to publish message:", error); + } + } catch (error) { + console.error("failed to sign message:", error); + } + }, [db, account, reset]); + + useEffect(() => { + async function getEntity(db: SDK) { + const entity = await db.getEntities({ + onchain_dash: { Message: { $: {} } } + }, () => { }); + + // @ts-expect-error a & b are not undefined as they are filtered out with `filer(Boolean)` + return entity.map(e => e.models.onchain_dash.Message).filter(Boolean).sort((a: Message, b: Message): number => parseInt(a.timestamp.toString(), 16) < parseInt(b.timestamp.toString(), 16) ? -1 : 1); + } + if (db && messages.length === 0 && sub === null) { + // @ts-expect-error ts is getting drunk there + getEntity(db).then(setMessages).catch(console.error) + } + }, [db, messages, sub]) + + useEffect(() => { + async function subscribeToEntityUpdates(db: SDK) { + const sub = await db.subscribeEntityQuery({ + onchain_dash: { Message: { $: {} } } + }, ({ data }) => { + if (data) { + const entity = data.pop(); + if (!entity) { + return; + } + const msg = entity.models.onchain_dash.Message; + if (msg === undefined) { + return; + } + setMessages(prevMessages => [...prevMessages, msg]); + } + }); + setSub(sub); + } + if (db && sub === null) { + subscribeToEntityUpdates(db).then().catch(console.error) + } + }, [db, sub, setMessages]); + + const handleKeyPress = useCallback((e: KeyboardEvent) => { + if (e.key !== 'Enter') { + return; + } + if (e.shiftKey && e.key === 'Enter') { + e.shiftKey = false; + return; + } + e.preventDefault(); + formRef.current?.requestSubmit(); + return; + }, []) + + return ( +
publish(data))} onKeyPress={handleKeyPress}> + + Output + +
+ {messages.map((msg, index) => ( +
+
+ {shortAddress(msg.identity)} + {msg.content} +
+
+ ))} +
+
+ +