From f1dbe3cd775b22569362651b61f91492e650db8d Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 12 Jan 2025 19:52:04 -0600 Subject: [PATCH 01/21] Split wss & consumer services, more touchups to ui, full worlds home screen finish --- backend/Dockerfile.websocket | 22 ++ backend/Dockerfile.websocket.prod | 22 ++ backend/cmd/consumer/consumer.go | 1 - backend/cmd/web-sockets/web-sockets.go | 64 ++++ backend/config/backend.go | 4 + backend/routes/indexer/nft.go | 6 +- backend/routes/indexer/pixel.go | 16 +- backend/routes/indexer/worlds.go | 29 +- backend/routes/utils/responses.go | 16 +- backend/routes/websocket.go | 31 ++ backend/routes/worlds.go | 26 +- configs/backend.config.json | 2 + configs/canvas.config.json | 2 +- configs/docker-backend.config.json | 2 + configs/prod-backend.config.json | 2 + docker-compose.yml | 9 + frontend/src/App.js | 71 +++-- frontend/src/canvas/Canvas.js | 30 +- frontend/src/canvas/CanvasContainer.css | 15 + frontend/src/canvas/CanvasContainer.js | 279 ++++++++++++++++-- frontend/src/configs/backend.config.json | 1 + frontend/src/configs/canvas.config.json | 2 +- frontend/src/footer/PixelSelector.js | 4 + frontend/src/tabs/stencils/Stencils.js | 10 - frontend/src/tabs/worlds/WorldItem.js | 3 +- frontend/src/tabs/worlds/Worlds.js | 75 +++-- .../src/tabs/worlds/WorldsCreationPanel.js | 118 ++++++-- frontend/src/utils/Consts.js | 2 +- .../websockets/websocket-configmap.yaml | 8 + .../websockets/websocket-deployment.yaml | 24 ++ .../websockets/websocket-service.yaml | 12 + 31 files changed, 773 insertions(+), 135 deletions(-) create mode 100644 backend/Dockerfile.websocket create mode 100644 backend/Dockerfile.websocket.prod create mode 100644 backend/cmd/web-sockets/web-sockets.go create mode 100644 infra/art-peace-infra/templates/websockets/websocket-configmap.yaml create mode 100644 infra/art-peace-infra/templates/websockets/websocket-deployment.yaml create mode 100644 infra/art-peace-infra/templates/websockets/websocket-service.yaml diff --git a/backend/Dockerfile.websocket b/backend/Dockerfile.websocket new file mode 100644 index 00000000..06e2577d --- /dev/null +++ b/backend/Dockerfile.websocket @@ -0,0 +1,22 @@ +FROM golang:1.22.2-alpine + +RUN apk add --no-cache bash curl git jq + +# Copy over the configs +WORKDIR /configs +COPY ./configs/ . +COPY ./configs/docker-database.config.json ./database.config.json +COPY ./configs/docker-backend.config.json ./backend.config.json + +# Copy over the app +WORKDIR /app +COPY ./backend/go.mod ./backend/go.sum ./ +RUN go mod download +COPY ./backend . + +# Build the app & run it +RUN go build -o web-sockets ./cmd/web-sockets/web-sockets.go + +EXPOSE 8082 + +CMD ["./web-sockets"] diff --git a/backend/Dockerfile.websocket.prod b/backend/Dockerfile.websocket.prod new file mode 100644 index 00000000..0cd0409f --- /dev/null +++ b/backend/Dockerfile.websocket.prod @@ -0,0 +1,22 @@ +FROM --platform=linux/amd64 golang:1.22.2-alpine + +RUN apk add --no-cache bash curl git jq + +# Copy over the configs +WORKDIR /configs +COPY ./configs/ . +COPY ./configs/prod-database.config.json ./database.config.json +COPY ./configs/prod-backend.config.json ./backend.config.json + +# Copy over the app +WORKDIR /app +COPY ./backend/go.mod ./backend/go.sum ./ +RUN go mod download +COPY ./backend . + +# Build the app & run it +RUN go build -o web-sockets ./cmd/web-sockets/web-sockets.go + +EXPOSE 8082 + +CMD ["./web-sockets"] diff --git a/backend/cmd/consumer/consumer.go b/backend/cmd/consumer/consumer.go index 5f87e2eb..622744c0 100644 --- a/backend/cmd/consumer/consumer.go +++ b/backend/cmd/consumer/consumer.go @@ -59,7 +59,6 @@ func main() { routes.InitBaseRoutes() indexer.InitIndexerRoutes() - routes.InitWebsocketRoutes() routes.InitNFTStaticRoutes() routes.InitWorldsStaticRoutes() indexer.StartMessageProcessor() diff --git a/backend/cmd/web-sockets/web-sockets.go b/backend/cmd/web-sockets/web-sockets.go new file mode 100644 index 00000000..dc9f330f --- /dev/null +++ b/backend/cmd/web-sockets/web-sockets.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + + "github.com/keep-starknet-strange/art-peace/backend/config" + "github.com/keep-starknet-strange/art-peace/backend/core" + "github.com/keep-starknet-strange/art-peace/backend/routes" +) + +func isFlagSet(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} + +func main() { + roundsConfigFilename := flag.String("rounds-config", config.DefaultRoundsConfigPath, "Rounds config file") + canvasConfigFilename := flag.String("canvas-config", config.DefaultCanvasConfigPath, "Canvas config file") + databaseConfigFilename := flag.String("database-config", config.DefaultDatabaseConfigPath, "Database config file") + backendConfigFilename := flag.String("backend-config", config.DefaultBackendConfigPath, "Backend config file") + production := flag.Bool("production", false, "Production mode") + + flag.Parse() + + roundsConfig, err := config.LoadRoundsConfig(*roundsConfigFilename) + if err != nil { + panic(err) + } + + canvasConfig, err := config.LoadCanvasConfig(*canvasConfigFilename) + if err != nil { + panic(err) + } + + databaseConfig, err := config.LoadDatabaseConfig(*databaseConfigFilename) + if err != nil { + panic(err) + } + + backendConfig, err := config.LoadBackendConfig(*backendConfigFilename) + if err != nil { + panic(err) + } + + if isFlagSet("production") { + backendConfig.Production = *production + } + + databases := core.NewDatabases(databaseConfig) + defer databases.Close() + + core.ArtPeaceBackend = core.NewBackend(databases, roundsConfig, canvasConfig, backendConfig, false) + + routes.InitBaseRoutes() + routes.InitWebsocketRoutes() + routes.StartWebsocketServer() + + core.ArtPeaceBackend.Start(core.ArtPeaceBackend.BackendConfig.WsPort) +} diff --git a/backend/config/backend.go b/backend/config/backend.go index e4e92fdf..bc797df8 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -47,6 +47,8 @@ type BackendConfig struct { Host string `json:"host"` Port int `json:"port"` ConsumerPort int `json:"consumer_port"` + WsHost string `json:"ws_host"` + WsPort int `json:"ws_port"` Scripts BackendScriptsConfig `json:"scripts"` Production bool `json:"production"` WebSocket WebSocketConfig `json:"websocket"` @@ -57,6 +59,8 @@ var DefaultBackendConfig = BackendConfig{ Host: "localhost", Port: 8080, ConsumerPort: 8081, + WsHost: "localhost", + WsPort: 8082, Scripts: BackendScriptsConfig{ PlacePixelDevnet: "../scripts/place_pixel.sh", PlaceExtraPixelsDevnet: "../scripts/place_extra_pixels.sh", diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index b70e5371..5e9d95b2 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -250,12 +250,12 @@ func processNFTMintedEvent(event IndexerEvent) { return } - message := map[string]interface{}{ - "token_id": tokenId, + message := map[string]string { + "token_id": strconv.FormatUint(tokenId, 10), "minter": minter, "messageType": "nftMinted", } - routeutils.SendWebSocketMessage(message) + routeutils.SendMessageToWSS(message) // TODO: Response? } diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 5aa7a8b6..8f72570f 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -61,12 +61,12 @@ func processPixelPlacedEvent(event IndexerEvent) { } // Send message to all connected clients - var message = map[string]interface{}{ - "position": position, - "color": color, + var message = map[string]string { + "position": strconv.FormatInt(position, 10), + "color": strconv.FormatInt(color, 10), "messageType": "colorPixel", } - routeutils.SendWebSocketMessage(message) + routeutils.SendMessageToWSS(message) } func revertPixelPlacedEvent(event IndexerEvent) { @@ -105,12 +105,12 @@ func revertPixelPlacedEvent(event IndexerEvent) { } // Send message to all connected clients - var message = map[string]interface{}{ - "position": position, - "color": oldColor, + var message = map[string]string { + "position": strconv.FormatInt(position, 10), + "color": strconv.Itoa(*oldColor), "messageType": "colorPixel", } - routeutils.SendWebSocketMessage(message) + routeutils.SendMessageToWSS(message) } func processBasicPixelPlacedEvent(event IndexerEvent) { diff --git a/backend/routes/indexer/worlds.go b/backend/routes/indexer/worlds.go index 66b0d429..277e25c3 100644 --- a/backend/routes/indexer/worlds.go +++ b/backend/routes/indexer/worlds.go @@ -184,22 +184,11 @@ func processCanvasCreatedEvent(event IndexerEvent) { } // After world creation - var message = map[string]interface{}{ + var message = map[string]string { "messageType": "newWorld", - "worldId": canvasId, - "world": map[string]interface{}{ - "worldId": canvasId, - "host": host, - "name": name, - "uniqueName": uniqueName, - "width": width, - "height": height, - "timeBetweenPixels": timeBetweenPixels, - "startTime": startTime, - "endTime": endTime, - }, - } - routeutils.SendWebSocketMessage(message) + "worldId": strconv.Itoa(int(canvasId)), + } + routeutils.SendMessageToWSS(message) } func revertCanvasCreatedEvent(event IndexerEvent) { @@ -500,13 +489,13 @@ func processCanvasPixelPlacedEvent(event IndexerEvent) { } } - var message = map[string]interface{}{ - "worldId": canvasId, - "position": pos, - "color": colorVal, + var message = map[string]string { + "worldId": strconv.Itoa(int(canvasId)), + "position": strconv.Itoa(int(pos)), + "color": strconv.Itoa(int(colorVal)), "messageType": "colorWorldPixel", } - routeutils.SendWebSocketMessage(message) + routeutils.SendMessageToWSS(message) } func revertCanvasPixelPlacedEvent(event IndexerEvent) { diff --git a/backend/routes/utils/responses.go b/backend/routes/utils/responses.go index 35e1837d..cd6f4171 100644 --- a/backend/routes/utils/responses.go +++ b/backend/routes/utils/responses.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "strings" "github.com/gorilla/websocket" @@ -59,7 +60,7 @@ func WriteDataJson(w http.ResponseWriter, data string) { w.Write(BasicDataJson(data)) } -func SendWebSocketMessage(message map[string]interface{}) { +func SendWebSocketMessage(message map[string]string) { messageBytes, err := json.Marshal(message) if err != nil { fmt.Println("Failed to marshal websocket message") @@ -80,3 +81,16 @@ func SendWebSocketMessage(message map[string]interface{}) { } core.ArtPeaceBackend.WSConnectionsLock.Unlock() } + +func SendMessageToWSS(message map[string]string) { + websocketHost := core.ArtPeaceBackend.BackendConfig.WsHost + ":" + strconv.Itoa(core.ArtPeaceBackend.BackendConfig.WsPort) + "/ws-msg" + messageBytes, err := json.Marshal(message) + if err != nil { + fmt.Println("Failed to marshal websocket message") + return + } + _, err = http.Post("http://" + websocketHost, "application/json", strings.NewReader(string(messageBytes))) + if err != nil { + fmt.Println("Failed to send message to websocket server", err) + } +} diff --git a/backend/routes/websocket.go b/backend/routes/websocket.go index b65e14dc..0bf21bc4 100644 --- a/backend/routes/websocket.go +++ b/backend/routes/websocket.go @@ -7,10 +7,41 @@ import ( "github.com/gorilla/websocket" "github.com/keep-starknet-strange/art-peace/backend/core" + routeutils "github.com/keep-starknet-strange/art-peace/backend/routes/utils" ) +var WsMsgQueue chan map[string]string + func InitWebsocketRoutes() { + WsMsgQueue = make(chan map[string]string, 10000) http.HandleFunc("/ws", wsEndpoint) + http.HandleFunc("/ws-msg", wsMsgEndpoint) +} + +func wsMsgEndpoint(w http.ResponseWriter, r *http.Request) { + // TODO: Only allow consumer to send messages + msg, err := routeutils.ReadJsonBody[map[string]string](r) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid request body") + return + } + + WsMsgQueue <- *msg + routeutils.WriteResultJson(w, "WS message added to queue") +} + +func wsWriter() { + for { + msg := <-WsMsgQueue + routeutils.SendWebSocketMessage(msg) + } +} + +func StartWebsocketServer() { + go wsWriter() + go wsWriter() + go wsWriter() + go wsWriter() } func wsReader(conn *websocket.Conn) { diff --git a/backend/routes/worlds.go b/backend/routes/worlds.go index cb7f64ed..6fbd16bd 100644 --- a/backend/routes/worlds.go +++ b/backend/routes/worlds.go @@ -99,7 +99,31 @@ func getWorld(w http.ResponseWriter, r *http.Request) { return } - world, err := core.PostgresQueryOneJson[WorldData]("SELECT * FROM worlds WHERE world_id = $1", worldId) + address := r.URL.Query().Get("address") + if address == "" { + address = "0" + } + + query := ` + SELECT + worlds.*, + COALESCE(worldfavorites.favorite_count, 0) AS favorites, + COALESCE((SELECT true FROM worldfavorites WHERE user_address = $1 AND worldfavorites.world_id = worlds.world_id), false) as favorited + FROM + worlds + LEFT JOIN ( + SELECT + world_id, + COUNT(*) AS favorite_count + FROM + worldfavorites + GROUP BY + world_id + ) worldfavorites ON worlds.world_id = worldfavorites.world_id + WHERE + worlds.world_id = $2` + + world, err := core.PostgresQueryOneJson[WorldData](query, address, worldId) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to retrieve World") return diff --git a/configs/backend.config.json b/configs/backend.config.json index c3956b9e..19124671 100644 --- a/configs/backend.config.json +++ b/configs/backend.config.json @@ -2,6 +2,8 @@ "host": "localhost", "port": 8080, "consumer_port": 8081, + "ws_host": "localhost", + "ws_port": 8082, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", diff --git a/configs/canvas.config.json b/configs/canvas.config.json index e5af818e..5e24e7f8 100644 --- a/configs/canvas.config.json +++ b/configs/canvas.config.json @@ -1,6 +1,6 @@ { "canvas": { - "width": 518, + "width": 528, "height": 396 }, "colors": [ diff --git a/configs/docker-backend.config.json b/configs/docker-backend.config.json index 08ed6218..56610b9d 100644 --- a/configs/docker-backend.config.json +++ b/configs/docker-backend.config.json @@ -2,6 +2,8 @@ "host": "localhost", "port": 8080, "consumer_port": 8081, + "ws_host": "art-peace-websockets-1", + "ws_port": 8082, "scripts": { "place_pixel_devnet": "/scripts/place_pixel.sh", "place_extra_pixels_devnet": "/scripts/place_extra_pixels.sh", diff --git a/configs/prod-backend.config.json b/configs/prod-backend.config.json index 02058ea1..af771404 100644 --- a/configs/prod-backend.config.json +++ b/configs/prod-backend.config.json @@ -2,6 +2,8 @@ "host": "backend.art-peace-sepolia.svc.cluster.local", "port": 8080, "consumer_port": 8081, + "ws_host": "websockets.art-peace-sepolia.svc.cluster.local", + "ws_port": 8082, "scripts": { "place_pixel_devnet": "/scripts/place_pixel.sh", "place_extra_pixels_devnet": "/scripts/place_extra_pixels.sh", diff --git a/docker-compose.yml b/docker-compose.yml index b9ce1bda..baa18134 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,6 +54,15 @@ services: volumes: - nfts:/app/nfts - worlds:/app/worlds + websockets: + build: + dockerfile: backend/Dockerfile.websocket + context: . + ports: + - 8082:8082 + links: + - consumer + restart: always devnet: image: shardlabs/starknet-devnet-rs:0.0.3 command: diff --git a/frontend/src/App.js b/frontend/src/App.js index 63cf0216..9d6c6682 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -43,30 +43,42 @@ function App() { const [surroundingWorlds, setSurroundingWorlds] = useState([]); // Page management + const [isHome, setIsHome] = useState(true); useEffect(() => { const getWorldId = async () => { let currentWorldId = 0; if (location.pathname.startsWith('/worlds/')) { - let worldSlug = location.pathname.split('/worlds/')[1]; - let response = await fetchWrapper( - `get-world-id?worldName=${worldSlug}` - ); + setIsHome(false); + try { + let worldSlug = location.pathname.split('/worlds/')[1]; + let response = await fetchWrapper( + `get-world-id?worldName=${worldSlug}` + ); - if (response.data === undefined || response.data === null) { + if (response.data === undefined || response.data === null) { + setActiveWorld(null); + setOpenedWorldId(0); + } else { + setActiveWorld(response.data); + setOpenedWorldId(response.data); + currentWorldId = response.data; + } + } catch (error) { setActiveWorld(null); setOpenedWorldId(0); - } else { - setActiveWorld(response.data); - setOpenedWorldId(response.data); - currentWorldId = response.data; } } else { - const response = await fetchWrapper('get-world?worldId=0'); - if (response.data) { - setActiveWorld(response.data); + setIsHome(true); + try { + const response = await fetchWrapper('get-world?worldId=0'); + if (response.data) { + setActiveWorld(response.data); + } + setOpenedWorldId(0); + } catch (error) { + console.error(error); } - setOpenedWorldId(0); } // Always fetch surrounding worlds @@ -85,6 +97,8 @@ function App() { paddedWorlds = paddedWorlds.slice(0, 12); } + // Randomize order of worlds + paddedWorlds.sort(() => Math.random() - 0.5); setSurroundingWorlds(paddedWorlds); } else { setSurroundingWorlds(Array(12).fill(null)); // Fill with 12 null values if no worlds found @@ -94,6 +108,11 @@ function App() { getWorldId(); }, [location.pathname]); + useEffect(() => { + setOverlayTemplate(null); + setTemplateOverlayMode(false); + }, [openedWorldId]); + // Window management usePreventZoom(); /* @@ -973,14 +992,21 @@ function App() { useEffect(() => { // TODO: Done twice ( here and src/tabs/worlds/Worlds.js ) const getWorld = async () => { - const getWorldPath = `get-world?worldId=${openedWorldId}`; - const response = await fetchWrapper(getWorldPath); - if (!response.data) { - return; + try { + const getWorldPath = `get-world?worldId=${openedWorldId}`; + const response = await fetchWrapper(getWorldPath); + if (!response.data) { + return; + } + setActiveWorld(response.data); + setWidth(response.data.width); + setHeight(response.data.height); + } catch (error) { + console.error(error); + setActiveWorld(null); + setWidth(canvasConfig.canvas.width); + setHeight(canvasConfig.canvas.height); } - setActiveWorld(response.data); - setWidth(response.data.width); - setHeight(response.data.height); }; if (openedWorldId === null) { setActiveWorld(null); @@ -1268,6 +1294,8 @@ function App() { /> {modal && } {(!isMobile || activeTab === tabs[0]) && (
{ + /* const draw = useCallback( (ctx, imageData) => { ctx.putImageData(imageData, 0, 0); @@ -14,10 +15,26 @@ const Canvas = (props) => { useEffect(() => { const fetchCanvas = async () => { try { + let canvasColors = props.colors; if (props.colors.length === 0) { - return; + // Try to fetch colors from the backend + let canvasColorsEndpoint = + backendUrl + + (props.openedWorldId === null + ? '/get-colors' + : `/get-worlds-colors?worldId=${props.openedWorldId}`); + let response = await fetch(canvasColorsEndpoint); + let canvasColorsData = await response.json(); + canvasColors = canvasColorsData.data; + if (canvasColors.length === 0) { + console.error('No colors found'); + return; + } } const canvas = props.canvasRef.current; + if (!canvas) { + return; + } const context = canvas.getContext('2d'); context.imageSmoothingEnabled = false; @@ -50,7 +67,7 @@ const Canvas = (props) => { } let imageDataArray = []; for (let i = 0; i < dataArray.length; i++) { - const color = '#' + props.colors[dataArray[i]] + 'FF'; + const color = '#' + canvasColors[dataArray[i]] + 'FF'; const [r, g, b, a] = color.match(/\w\w/g).map((x) => parseInt(x, 16)); imageDataArray.push(r, g, b, a); } @@ -68,6 +85,7 @@ const Canvas = (props) => { fetchCanvas(); }, [props.width, props.height, props.colors, props.openedWorldId]); + */ return ( { width={props.width} height={props.height} style={props.style} - className='Canvas' + className={`Canvas ${props.className}`} onClick={props.pixelClicked} /> ); diff --git a/frontend/src/canvas/CanvasContainer.css b/frontend/src/canvas/CanvasContainer.css index b80e2f1e..6f3be3e7 100644 --- a/frontend/src/canvas/CanvasContainer.css +++ b/frontend/src/canvas/CanvasContainer.css @@ -70,3 +70,18 @@ left: 50%; transform: translate(-50%, -50%); } + +.Canvas__surrounding { + transition: transform 0.3s ease; + transform: scale(1); +} + +.Canvas__surrounding:hover { + transform: scale(1.02); + box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.2); +} + +.Canvas__surrounding:active { + transform: scale(1); + box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.1); +} diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index 9c6a8e4c..179a05cd 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -7,7 +7,8 @@ import TemplateCreationOverlay from './TemplateCreationOverlay.js'; import StencilCreationOverlay from './StencilCreationOverlay.js'; import NFTSelector from './NFTSelector.js'; import { fetchWrapper } from '../services/apiService.js'; -import { devnetMode } from '../utils/Consts.js'; +import { devnetMode, backendUrl } from '../utils/Consts.js'; +import canvasConfig from '../configs/canvas.config.json'; const CanvasContainer = (props) => { // TODO: Handle window resize @@ -73,6 +74,7 @@ const CanvasContainer = (props) => { const rect = props.canvasRef.current.getBoundingClientRect(); let cursorX = e.clientX - rect.left; let cursorY = e.clientY - rect.top; + console.log(rect, cursorX, cursorY); if (cursorX < 0) { cursorX = 0; } else if (cursorX > rect.width) { @@ -93,8 +95,8 @@ const CanvasContainer = (props) => { } else if (newScale > maxScale) { newScale = maxScale; } - const newWidth = props.width * newScale; - const newHeight = props.height * newScale; + const newWidth = props.width * newScale * artificialZoom; + const newHeight = props.height * newScale * artificialZoom; const oldCursorXRelative = cursorX / rect.width; const oldCursorYRelative = cursorY / rect.height; const newCursorX = oldCursorXRelative * newWidth; @@ -193,12 +195,16 @@ const CanvasContainer = (props) => { }, [canvasScale, canvasX, canvasY, touchInitialDistance]); // Init canvas transform to center of the viewport + const [hasInit, setHasInit] = useState(false); useEffect(() => { + if (hasInit) return; const containerRect = canvasContainerRef.current.getBoundingClientRect(); + console.log(containerRect); const adjustX = ((canvasScale - 1) * props.width) / 2; const adjustY = ((canvasScale - 1) * props.height) / 2; setCanvasX(containerRect.width / 2 - adjustX); setCanvasY(containerRect.height / 2 - adjustY); + setHasInit(true); }, [canvasContainerRef, props.width, props.height]); const colorExtraPixel = (x, y, colorId) => { @@ -390,6 +396,14 @@ const CanvasContainer = (props) => { // TODO: Fix last placed time if error in placing pixel }; + const openWorld = (index, world) => { + // Set current world to surrounding world at index + let newSurroundingWorlds = [...props.surroundingWorlds]; + newSurroundingWorlds[index] = props.activeWorld; + props.setSurroundingWorlds(newSurroundingWorlds); + props.setOpenedWorldId(world.worldId); + }; + useEffect(() => { const hoverColor = (e) => { if (props.selectedColorId === -1 && !props.isEraserMode) { @@ -541,6 +555,197 @@ const CanvasContainer = (props) => { props.isExtraDeleteMode ]); + const baseWorldX = 528; + const baseWorldY = 396; + const getWorldPosition = (index) => { + // TODO: To config + let xGap = 16; + let yGap = 12; + if (index === 0) { + return { + x: -(baseWorldX + xGap) / 2, + y: -(baseWorldY + yGap) / 2 + }; + } else if (index === 1) { + return { + x: 0, + y: -(baseWorldY + yGap) / 2 + }; + } else if (index === 2) { + return { + x: (baseWorldX + xGap) / 2, + y: -(baseWorldY + yGap) / 2 + }; + } else if (index === 3) { + return { + x: baseWorldX + xGap, + y: -(baseWorldY + yGap) / 2 + }; + } else if (index === 4) { + return { + x: -(baseWorldX + xGap) / 2, + y: 0 + }; + } else if (index === 5) { + return { + x: baseWorldX + xGap, + y: 0 + }; + } else if (index === 6) { + return { + x: -(baseWorldX + xGap) / 2, + y: (baseWorldY + yGap) / 2 + }; + } else if (index === 7) { + return { + x: baseWorldX + xGap, + y: (baseWorldY + yGap) / 2 + }; + } else if (index === 8) { + return { + x: -(baseWorldX + xGap) / 2, + y: baseWorldY + yGap + }; + } else if (index === 9) { + return { + x: 0, + y: baseWorldY + yGap + }; + } else if (index === 10) { + return { + x: (baseWorldX + xGap) / 2, + y: baseWorldY + yGap + }; + } else if (index === 11) { + return { + x: baseWorldX + xGap, + y: baseWorldY + yGap + }; + } + // Circle around the center + return { + x: baseWorldX * 2, + y: baseWorldY * 2 + }; + }; + + const [surroundingCanvasRefs, setSurroundingCanvasRefs] = useState([]); + useEffect(() => { + setSurroundingCanvasRefs( + Array.from({ length: props.surroundingWorlds.length }, () => + React.createRef() + ) + ); + }, [props.surroundingWorlds]); + + useEffect(() => { + const fetchCanvas = async ( + width, + height, + colors, + canvasRef, + openedWorldId + ) => { + try { + let canvasColors = colors; + if (colors.length === 0) { + // Try to fetch colors from the backend + let canvasColorsEndpoint = + backendUrl + + (openedWorldId === null + ? '/get-colors' + : `/get-worlds-colors?worldId=${openedWorldId}`); + let response = await fetch(canvasColorsEndpoint); + let canvasColorsData = await response.json(); + canvasColors = canvasColorsData.data; + if (canvasColors.length === 0) { + console.error('No colors found'); + return; + } + } + const canvas = canvasRef.current; + if (!canvas) { + return; + } + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + + let getCanvasEndpoint = + backendUrl + + (openedWorldId === null + ? '/get-canvas' + : `/get-world-canvas?worldId=${openedWorldId}`); + let response = await fetch(getCanvasEndpoint); + let canvasData = await response.arrayBuffer(); + + let colorData = new Uint8Array(canvasData, 0, canvasData.byteLength); + let dataArray = []; + let bitwidth = canvasConfig.colorsBitwidth; + let oneByteBitOffset = 8 - bitwidth; + let twoByteBitOffset = 16 - bitwidth; + let canvasBits = width * height * bitwidth; + for (let bitPos = 0; bitPos < canvasBits; bitPos += bitwidth) { + let bytePos = Math.floor(bitPos / 8); + let bitOffset = bitPos % 8; + if (bitOffset <= oneByteBitOffset) { + let byte = colorData[bytePos]; + let value = (byte >> (oneByteBitOffset - bitOffset)) & 0b11111; + dataArray.push(value); + } else { + let byte = (colorData[bytePos] << 8) | colorData[bytePos + 1]; + let value = (byte >> (twoByteBitOffset - bitOffset)) & 0b11111; + dataArray.push(value); + } + } + let imageDataArray = []; + for (let i = 0; i < dataArray.length; i++) { + const color = '#' + canvasColors[dataArray[i]] + 'FF'; + const [r, g, b, a] = color.match(/\w\w/g).map((x) => parseInt(x, 16)); + imageDataArray.push(r, g, b, a); + } + const uint8ClampedArray = new Uint8ClampedArray(imageDataArray); + const imageData = new ImageData(uint8ClampedArray, width, height); + context.putImageData(imageData, 0, 0); + } catch (error) { + console.error(error); + } + }; + + if (props.openedWorldId !== null) { + fetchCanvas( + props.width, + props.height, + props.colors, + props.canvasRef, + props.openedWorldId + ); + } + for (let i = 0; i < surroundingCanvasRefs.length; i++) { + if (surroundingCanvasRefs[i].current) { + let canvasConfig = props.surroundingWorlds[i]; + fetchCanvas( + canvasConfig.width, + canvasConfig.height, + [], + surroundingCanvasRefs[i], + canvasConfig.worldId + ); + } + } + }, [surroundingCanvasRefs, props.openedWorldId, props.activeWorld]); + + const [artificialZoom, setArtificialZoom] = useState(1); + useEffect(() => { + if ( + props.openedWorldId === 0 || + (props.activeWorld && props.activeWorld.worldId === 0) + ) { + setArtificialZoom(1); + } else { + setArtificialZoom(2.0625); + } + }, [props.isHome, props.worldsMode, props.activeWorld, props.openedWorldId]); + return (
{
- {props.openedWorldId !== null && props.activeWorld !== null && ( -

- {props.activeWorld.name} -

- )} + {props.isHome && + props.worldsMode && + props.surroundingWorlds && + props.surroundingWorlds.length > 0 && + props.surroundingWorlds.map((world, index) => { + if (world === null) return null; + const position = getWorldPosition(index); + if (!position) return null; + let surroundingWorldScaler = 1; + if (world.worldId === 0) { + surroundingWorldScaler = 1 / 2.0625; + } + return ( + openWorld(index, world)} + /> + ); + })} + {!props.isHome && + props.openedWorldId !== null && + props.activeWorld !== null && ( +

+ {props.activeWorld.name} +

+ )} {props.pixelSelectedMode && (
{ width={props.width} height={props.height} style={{ - width: props.width * canvasScale, - height: props.height * canvasScale + width: props.width * canvasScale * artificialZoom, + height: props.height * canvasScale * artificialZoom }} colors={props.colors} pixelClicked={pixelClicked} @@ -661,6 +901,7 @@ const CanvasContainer = (props) => { setTemplateOverlayMode={props.setTemplateOverlayMode} setOverlayTemplate={props.setOverlayTemplate} colors={props.colors} + openedWorldId={props.openedWorldId} /> )} {props.templateCreationMode && ( diff --git a/frontend/src/configs/backend.config.json b/frontend/src/configs/backend.config.json index 9636245b..cb76ee85 100644 --- a/frontend/src/configs/backend.config.json +++ b/frontend/src/configs/backend.config.json @@ -2,6 +2,7 @@ "host": "api.art-peace.net", "port": 8080, "consumer_port": 8081, + "ws_port": 8082, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", diff --git a/frontend/src/configs/canvas.config.json b/frontend/src/configs/canvas.config.json index e5af818e..5e24e7f8 100644 --- a/frontend/src/configs/canvas.config.json +++ b/frontend/src/configs/canvas.config.json @@ -1,6 +1,6 @@ { "canvas": { - "width": 518, + "width": 528, "height": 396 }, "colors": [ diff --git a/frontend/src/footer/PixelSelector.js b/frontend/src/footer/PixelSelector.js index 5f58e8f4..dae85b0b 100644 --- a/frontend/src/footer/PixelSelector.js +++ b/frontend/src/footer/PixelSelector.js @@ -170,6 +170,10 @@ const PixelSelector = (props) => { setEnded(false); }; + useEffect(() => { + cancelSelector(); + }, [props.openedWorldId]); + return (
{(props.selectorMode || ended) && ( diff --git a/frontend/src/tabs/stencils/Stencils.js b/frontend/src/tabs/stencils/Stencils.js index 43cae3bf..661ea69a 100644 --- a/frontend/src/tabs/stencils/Stencils.js +++ b/frontend/src/tabs/stencils/Stencils.js @@ -188,16 +188,6 @@ const StencilsExpandedSection = (props) => { stateValue={props.allStencilsPagination} />
- {props.queryAddress !== '0' && ( -
-

- Create Stencil -

-
- )}
); }; diff --git a/frontend/src/tabs/worlds/WorldItem.js b/frontend/src/tabs/worlds/WorldItem.js index 356e8401..5cbbedb7 100644 --- a/frontend/src/tabs/worlds/WorldItem.js +++ b/frontend/src/tabs/worlds/WorldItem.js @@ -136,7 +136,7 @@ const WorldItem = (props) => { }, [props.favorites, props.favorited]); function handleShare() { - const worldLink = `${window.location.origin}/worlds/${props.name}`; + const worldLink = `${window.location.origin}/worlds/${props.uniqueName}`; const twitterShareUrl = `https://x.com/intent/post?text=${encodeURIComponent('Gm. Join our forces! Draw on our art/peace World! @art_peace_sn 🗺️')}&url=${encodeURIComponent(worldLink)}`; window.open(twitterShareUrl, '_blank'); } @@ -179,6 +179,7 @@ const WorldItem = (props) => { const selectWorld = () => { props.setActiveWorldId(props.worldId); + window.location.href = `/worlds/${props.uniqueName}`; }; const [showInfo, setShowInfo] = React.useState(false); diff --git a/frontend/src/tabs/worlds/Worlds.js b/frontend/src/tabs/worlds/Worlds.js index c0c41d52..5afa6839 100644 --- a/frontend/src/tabs/worlds/Worlds.js +++ b/frontend/src/tabs/worlds/Worlds.js @@ -40,7 +40,7 @@ const WorldsMainSection = (props) => { Please login to view your Worlds

)} - {props.activeWorld && !props.activeWorld.favorited && ( + {props.activeWorld && ( { queryAddress={props.queryAddress} updateFavorites={props.updateFavorites} canvasFactoryContract={props.canvasFactoryContract} + uniqueName={props.activeWorld.uniqueName} /> )} {props.favoriteWorlds.map((world, index) => { @@ -87,6 +88,7 @@ const WorldsMainSection = (props) => { queryAddress={props.queryAddress} updateFavorites={props.updateFavorites} canvasFactoryContract={props.canvasFactoryContract} + uniqueName={world.uniqueName} /> ); })} @@ -164,6 +166,7 @@ const WorldsExpandedSection = (props) => { queryAddress={props.queryAddress} updateFavorites={props.updateFavorites} canvasFactoryContract={props.canvasFactoryContract} + uniqueName={world.uniqueName} /> ); })} @@ -174,19 +177,6 @@ const WorldsExpandedSection = (props) => { stateValue={props.allWorldsPagination} />
- {props.queryAddress !== '0' && ( -
-

{ - props.setWorldsCreationMode(true); - props.setActiveTab('Canvas'); - }} - > - Create World -

-
- )}
); }; @@ -213,11 +203,24 @@ const Worlds = (props) => { }); if (result.data) { + let newFavoriteWorlds = []; if (myWorldsPagination.page === 1) { - setFavoriteWorlds(result.data); + newFavoriteWorlds = result.data; } else { - setFavoriteWorlds([...favoriteWorlds, ...result.data]); + newFavoriteWorlds = [...favoriteWorlds, ...result.data]; } + // Remove duplicates + newFavoriteWorlds = newFavoriteWorlds.filter( + (world, index, self) => + index === self.findIndex((t) => t.worldId === world.worldId) + ); + // Remove active world from favorite worlds + if (props.openedWorldId !== null) { + newFavoriteWorlds = newFavoriteWorlds.filter( + (world) => world.worldId !== props.openedWorldId + ); + } + setFavoriteWorlds(newFavoriteWorlds); } } catch (error) { console.log('Error fetching Worlds', error); @@ -227,7 +230,8 @@ const Worlds = (props) => { }, [ props.queryAddress, myWorldsPagination.page, - myWorldsPagination.pageLength + myWorldsPagination.pageLength, + props.openedWorldId ]); const [expanded, setExpanded] = useState(false); @@ -302,15 +306,20 @@ const Worlds = (props) => { const [activeWorld, setActiveWorld] = useState(null); useEffect(() => { const getWorld = async () => { - const getWorldPath = `get-world?worldId=${activeWorldId}`; - const response = await fetchWrapper(getWorldPath); - if (!response.data) { - return; + try { + const getWorldPath = `get-world?worldId=${activeWorldId}&address=${props.queryAddress}`; + const response = await fetchWrapper(getWorldPath); + if (!response.data) { + return; + } + setActiveWorld(response.data); + // Route path to "/worlds/:worldId" when activeWorldId changes + // let path = `/worlds/${response.data.uniqueName}`; + // window.history.pushState({}, '', path); + } catch (error) { + console.log('Error fetching World', error); + setActiveWorld(null); } - setActiveWorld(response.data); - // Route path to "/worlds/:worldId" when activeWorldId changes - let path = `/worlds/${response.data.uniqueName}`; - window.history.pushState({}, '', path); }; if (activeWorldId === null) { return; @@ -325,6 +334,17 @@ const Worlds = (props) => { } return world; }); + // Filter out duplicates + newFavoriteWorlds = newFavoriteWorlds.filter( + (world, index, self) => + index === self.findIndex((t) => t.worldId === world.worldId) + ); + // Filter out active world + if (activeWorldId !== null) { + newFavoriteWorlds = newFavoriteWorlds.filter( + (world) => world.worldId !== activeWorldId + ); + } let newAllWorldss = allWorlds.map((world) => { if (world.worldId === worldId) { @@ -332,6 +352,11 @@ const Worlds = (props) => { } return world; }); + // Filter out duplicates + newAllWorldss = newAllWorldss.filter( + (world, index, self) => + index === self.findIndex((t) => t.worldId === world.worldId) + ); setFavoriteWorlds(newFavoriteWorlds); setAllWorlds(newAllWorldss); diff --git a/frontend/src/tabs/worlds/WorldsCreationPanel.js b/frontend/src/tabs/worlds/WorldsCreationPanel.js index b6757523..3b1d16cc 100644 --- a/frontend/src/tabs/worlds/WorldsCreationPanel.js +++ b/frontend/src/tabs/worlds/WorldsCreationPanel.js @@ -132,6 +132,7 @@ const WorldsCreationPanel = (props) => { const [worldWidth, setWorldWidth] = useState(128); const [worldHeight, setWorldHeight] = useState(128); const [timer, setTimer] = useState(10); + const [newBaseColor, setNewBaseColor] = useState(''); const [newColor, setNewColor] = useState(''); const [palette, setPalette] = useState(defaultPalette); const [start, setStart] = useState(new Date().getTime()); @@ -364,14 +365,10 @@ const WorldsCreationPanel = (props) => { type='number' placeholder='Width...' value={worldWidth} + min='16' + max='1024' onChange={(e) => { - if (e.target.value < minWorldSize) { - setWorldWidth(minWorldSize); - } else if (e.target.value > maxWorldSize) { - setWorldWidth(maxWorldSize); - } else { - setWorldWidth(e.target.value); - } + setWorldWidth(e.target.value); }} />

@@ -392,14 +389,10 @@ const WorldsCreationPanel = (props) => { type='number' placeholder='Height...' value={worldHeight} + min='16' + max='1024' onChange={(e) => { - if (e.target.value < minWorldSize) { - setWorldHeight(minWorldSize); - } else if (e.target.value > maxWorldSize) { - setWorldHeight(maxWorldSize); - } else { - setWorldHeight(e.target.value); - } + setWorldHeight(e.target.value); }} />

@@ -426,7 +419,10 @@ const WorldsCreationPanel = (props) => { type='number' placeholder='Timer (seconds)...' value={timer} - onChange={(e) => setTimer(Math.round(e.target.value))} + min='1' + onChange={(e) => { + setTimer(e.target.value); + }} />

Seconds between pixels

@@ -435,6 +431,94 @@ const WorldsCreationPanel = (props) => {

Palette

+
+

Base Color

+ {palette.length > 0 && ( +
{ + let newPalette = [...palette]; + newPalette.splice(0, 1); + setPalette(newPalette); + }} + style={{ + backgroundColor: `#${palette[0]}` + }} + > +

+ X +

+
+ )} +
+ { + setNewBaseColor(e.target.value); + }} + /> +
{ + // TODO: Allow host to add colors later? + // TODO: Color wheel? + // TODO: Pressing enter should add the color too + // TODO: Display color when done typing + // Check if the color is valid + let formattedColor = newBaseColor.replace('#', ''); + // To uppercase + formattedColor = formattedColor.toUpperCase(); + if (formattedColor.length !== 6) { + setValidationMessage( + 'Invalid color: must be #FFFFFF format' + ); + return; + } + if (!/^[0-9A-F]{6}$/i.test(formattedColor)) { + setValidationMessage( + 'Invalid color: must be #FFFFFF format' + ); + return; + } + // Check if the color is already in the palette + if (palette.includes(formattedColor)) { + setValidationMessage('Color already in palette'); + return; + } + // Remove the base color if it exists + setPalette([formattedColor, ...palette]); + setNewBaseColor(''); + setValidationMessage(''); + }} + > +

+ + +

+
+
+
{ flexWrap: 'wrap' }} > - {palette.map((color, index) => ( + {palette.slice(1).map((color, index) => (
{ let newPalette = [...palette]; - newPalette.splice(index, 1); + newPalette.splice(index + 1, 1); setPalette(newPalette); }} style={{ diff --git a/frontend/src/utils/Consts.js b/frontend/src/utils/Consts.js index fa2e4239..78ebbe12 100644 --- a/frontend/src/utils/Consts.js +++ b/frontend/src/utils/Consts.js @@ -7,7 +7,7 @@ export const backendUrl = backendConfig.production export const wsUrl = backendConfig.production ? 'wss://' + backendConfig.host + '/ws' - : 'ws://' + backendConfig.host + ':' + backendConfig.consumer_port + '/ws'; + : 'ws://' + backendConfig.host + ':' + backendConfig.ws_port + '/ws'; export const nftUrl = backendConfig.production ? 'https://' + backendConfig.host diff --git a/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml b/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml new file mode 100644 index 00000000..457754dc --- /dev/null +++ b/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.labels.websocket.name }}-secret + labels: + app: {{ .Values.labels.websocket.name }} +data: + ART_PEACE_CONTRACT_ADDRESS: {{ .Values.contracts.artPeace }} diff --git a/infra/art-peace-infra/templates/websockets/websocket-deployment.yaml b/infra/art-peace-infra/templates/websockets/websocket-deployment.yaml new file mode 100644 index 00000000..65988d69 --- /dev/null +++ b/infra/art-peace-infra/templates/websockets/websocket-deployment.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.labels.websocket.name }} +spec: + replicas: {{ .Values.deployments.websocket.replicas }} + selector: + matchLabels: + app: {{ .Values.labels.websocket.name }} + template: + metadata: + labels: + app: {{ .Values.labels.websocket.name }} + spec: + containers: + - name: {{ .Values.labels.websocket.name }} + image: {{ .Values.deployments.websocket.image }}:{{ .Chart.AppVersion }}-{{ .Values.deployments.sha }} + command: ["./web-sockets"] + imagePullPolicy: Always + ports: + - containerPort: {{ .Values.ports.websocket }} + envFrom: + - configMapRef: + name: {{ .Values.labels.websocket.name }}-secret diff --git a/infra/art-peace-infra/templates/websockets/websocket-service.yaml b/infra/art-peace-infra/templates/websockets/websocket-service.yaml new file mode 100644 index 00000000..65ffef67 --- /dev/null +++ b/infra/art-peace-infra/templates/websockets/websocket-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.labels.websocket.name }} + labels: + app: {{ .Values.labels.websocket.name }} +spec: + type: NodePort + ports: + - port: {{ .Values.ports.websocket }} + selector: + app: {{ .Values.labels.websocket.name }} From bad6cb131151ee4efccf4ca6e236b6fb584a3cc8 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 12 Jan 2025 19:54:20 -0600 Subject: [PATCH 02/21] gofmt --- backend/cmd/web-sockets/web-sockets.go | 4 +-- backend/config/backend.go | 8 +++--- backend/routes/indexer/nft.go | 2 +- backend/routes/indexer/pixel.go | 4 +-- backend/routes/indexer/worlds.go | 6 ++--- backend/routes/utils/responses.go | 20 +++++++------- backend/routes/websocket.go | 36 +++++++++++++------------- backend/routes/worlds.go | 10 +++---- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/backend/cmd/web-sockets/web-sockets.go b/backend/cmd/web-sockets/web-sockets.go index dc9f330f..d4e6ae7a 100644 --- a/backend/cmd/web-sockets/web-sockets.go +++ b/backend/cmd/web-sockets/web-sockets.go @@ -57,8 +57,8 @@ func main() { core.ArtPeaceBackend = core.NewBackend(databases, roundsConfig, canvasConfig, backendConfig, false) routes.InitBaseRoutes() - routes.InitWebsocketRoutes() - routes.StartWebsocketServer() + routes.InitWebsocketRoutes() + routes.StartWebsocketServer() core.ArtPeaceBackend.Start(core.ArtPeaceBackend.BackendConfig.WsPort) } diff --git a/backend/config/backend.go b/backend/config/backend.go index bc797df8..ac09f750 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -47,8 +47,8 @@ type BackendConfig struct { Host string `json:"host"` Port int `json:"port"` ConsumerPort int `json:"consumer_port"` - WsHost string `json:"ws_host"` - WsPort int `json:"ws_port"` + WsHost string `json:"ws_host"` + WsPort int `json:"ws_port"` Scripts BackendScriptsConfig `json:"scripts"` Production bool `json:"production"` WebSocket WebSocketConfig `json:"websocket"` @@ -59,8 +59,8 @@ var DefaultBackendConfig = BackendConfig{ Host: "localhost", Port: 8080, ConsumerPort: 8081, - WsHost: "localhost", - WsPort: 8082, + WsHost: "localhost", + WsPort: 8082, Scripts: BackendScriptsConfig{ PlacePixelDevnet: "../scripts/place_pixel.sh", PlaceExtraPixelsDevnet: "../scripts/place_extra_pixels.sh", diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 5e9d95b2..7cfe7196 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -250,7 +250,7 @@ func processNFTMintedEvent(event IndexerEvent) { return } - message := map[string]string { + message := map[string]string{ "token_id": strconv.FormatUint(tokenId, 10), "minter": minter, "messageType": "nftMinted", diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 8f72570f..5bfa6e01 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -61,7 +61,7 @@ func processPixelPlacedEvent(event IndexerEvent) { } // Send message to all connected clients - var message = map[string]string { + var message = map[string]string{ "position": strconv.FormatInt(position, 10), "color": strconv.FormatInt(color, 10), "messageType": "colorPixel", @@ -105,7 +105,7 @@ func revertPixelPlacedEvent(event IndexerEvent) { } // Send message to all connected clients - var message = map[string]string { + var message = map[string]string{ "position": strconv.FormatInt(position, 10), "color": strconv.Itoa(*oldColor), "messageType": "colorPixel", diff --git a/backend/routes/indexer/worlds.go b/backend/routes/indexer/worlds.go index 277e25c3..e37c9ca0 100644 --- a/backend/routes/indexer/worlds.go +++ b/backend/routes/indexer/worlds.go @@ -184,9 +184,9 @@ func processCanvasCreatedEvent(event IndexerEvent) { } // After world creation - var message = map[string]string { + var message = map[string]string{ "messageType": "newWorld", - "worldId": strconv.Itoa(int(canvasId)), + "worldId": strconv.Itoa(int(canvasId)), } routeutils.SendMessageToWSS(message) } @@ -489,7 +489,7 @@ func processCanvasPixelPlacedEvent(event IndexerEvent) { } } - var message = map[string]string { + var message = map[string]string{ "worldId": strconv.Itoa(int(canvasId)), "position": strconv.Itoa(int(pos)), "color": strconv.Itoa(int(colorVal)), diff --git a/backend/routes/utils/responses.go b/backend/routes/utils/responses.go index cd6f4171..bf582b5c 100644 --- a/backend/routes/utils/responses.go +++ b/backend/routes/utils/responses.go @@ -83,14 +83,14 @@ func SendWebSocketMessage(message map[string]string) { } func SendMessageToWSS(message map[string]string) { - websocketHost := core.ArtPeaceBackend.BackendConfig.WsHost + ":" + strconv.Itoa(core.ArtPeaceBackend.BackendConfig.WsPort) + "/ws-msg" - messageBytes, err := json.Marshal(message) - if err != nil { - fmt.Println("Failed to marshal websocket message") - return - } - _, err = http.Post("http://" + websocketHost, "application/json", strings.NewReader(string(messageBytes))) - if err != nil { - fmt.Println("Failed to send message to websocket server", err) - } + websocketHost := core.ArtPeaceBackend.BackendConfig.WsHost + ":" + strconv.Itoa(core.ArtPeaceBackend.BackendConfig.WsPort) + "/ws-msg" + messageBytes, err := json.Marshal(message) + if err != nil { + fmt.Println("Failed to marshal websocket message") + return + } + _, err = http.Post("http://"+websocketHost, "application/json", strings.NewReader(string(messageBytes))) + if err != nil { + fmt.Println("Failed to send message to websocket server", err) + } } diff --git a/backend/routes/websocket.go b/backend/routes/websocket.go index 0bf21bc4..e6d2aa20 100644 --- a/backend/routes/websocket.go +++ b/backend/routes/websocket.go @@ -13,35 +13,35 @@ import ( var WsMsgQueue chan map[string]string func InitWebsocketRoutes() { - WsMsgQueue = make(chan map[string]string, 10000) + WsMsgQueue = make(chan map[string]string, 10000) http.HandleFunc("/ws", wsEndpoint) - http.HandleFunc("/ws-msg", wsMsgEndpoint) + http.HandleFunc("/ws-msg", wsMsgEndpoint) } func wsMsgEndpoint(w http.ResponseWriter, r *http.Request) { - // TODO: Only allow consumer to send messages - msg, err := routeutils.ReadJsonBody[map[string]string](r) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid request body") - return - } + // TODO: Only allow consumer to send messages + msg, err := routeutils.ReadJsonBody[map[string]string](r) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid request body") + return + } - WsMsgQueue <- *msg - routeutils.WriteResultJson(w, "WS message added to queue") + WsMsgQueue <- *msg + routeutils.WriteResultJson(w, "WS message added to queue") } func wsWriter() { - for { - msg := <-WsMsgQueue - routeutils.SendWebSocketMessage(msg) - } + for { + msg := <-WsMsgQueue + routeutils.SendWebSocketMessage(msg) + } } func StartWebsocketServer() { - go wsWriter() - go wsWriter() - go wsWriter() - go wsWriter() + go wsWriter() + go wsWriter() + go wsWriter() + go wsWriter() } func wsReader(conn *websocket.Conn) { diff --git a/backend/routes/worlds.go b/backend/routes/worlds.go index 6fbd16bd..aac4631f 100644 --- a/backend/routes/worlds.go +++ b/backend/routes/worlds.go @@ -99,12 +99,12 @@ func getWorld(w http.ResponseWriter, r *http.Request) { return } - address := r.URL.Query().Get("address") - if address == "" { - address = "0" - } + address := r.URL.Query().Get("address") + if address == "" { + address = "0" + } - query := ` + query := ` SELECT worlds.*, COALESCE(worldfavorites.favorite_count, 0) AS favorites, From ec86549bf9543ec8957572351e25d180851f14d9 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 05:32:47 -0600 Subject: [PATCH 03/21] Websocket deployment --- Makefile | 2 ++ backend/Dockerfile.websocket.prod | 2 +- frontend/src/canvas/CanvasContainer.js | 3 ++- infra/art-peace-infra/values.yaml | 6 ++++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 86c9be7d..56992a38 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,8 @@ docker-build: docker build . -f backend/Dockerfile.prod -t "brandonjroberts/art-peace-backend:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Building consumer..." docker build . -f backend/Dockerfile.consumer.prod -t "brandonjroberts/art-peace-consumer:$(APP_VERSION)-$(COMMIT_SHA)" + @echo "Building websocket..." + docker build . -f backend/Dockerfile.websocket.prod -t "brandonjroberts/art-peace-websocket:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Building indexer..." docker build . -f indexer/Dockerfile.prod -t "brandonjroberts/art-peace-indexer:$(APP_VERSION)-$(COMMIT_SHA)" diff --git a/backend/Dockerfile.websocket.prod b/backend/Dockerfile.websocket.prod index 0cd0409f..0dbdb4db 100644 --- a/backend/Dockerfile.websocket.prod +++ b/backend/Dockerfile.websocket.prod @@ -17,6 +17,6 @@ COPY ./backend . # Build the app & run it RUN go build -o web-sockets ./cmd/web-sockets/web-sockets.go -EXPOSE 8082 +EXPOSE 8083 CMD ["./web-sockets"] diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index 179a05cd..abe34fe6 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -854,7 +854,8 @@ const CanvasContainer = (props) => { width: props.width * canvasScale, height: props.height * canvasScale }} - disabled={true} + colors={props.colors} + pixelClicked={pixelClicked} />

Date: Thu, 16 Jan 2025 05:36:42 -0600 Subject: [PATCH 04/21] Websocket ingress --- .../templates/backend/backend-ingress.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/art-peace-infra/templates/backend/backend-ingress.yaml b/infra/art-peace-infra/templates/backend/backend-ingress.yaml index 58bf380c..425798c5 100644 --- a/infra/art-peace-infra/templates/backend/backend-ingress.yaml +++ b/infra/art-peace-infra/templates/backend/backend-ingress.yaml @@ -12,9 +12,9 @@ spec: pathType: Exact backend: service: - name: {{ .Values.labels.consumer.name }} + name: {{ .Values.labels.websocket.name }} port: - number: {{ .Values.ports.consumer }} + number: {{ .Values.ports.websocket }} - path: /nft-images pathType: Prefix backend: @@ -49,9 +49,9 @@ spec: pathType: Exact backend: service: - name: {{ .Values.labels.consumer.name }} + name: {{ .Values.labels.websocket.name }} port: - number: {{ .Values.ports.consumer }} + number: {{ .Values.ports.websocket }} - path: / pathType: Prefix backend: From 00514590129c257c092f2c03e0236d9faaa5d63e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 05:44:04 -0600 Subject: [PATCH 05/21] Non-worlds mode checks --- frontend/src/App.js | 7 +++++++ frontend/src/canvas/CanvasContainer.js | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 9d6c6682..226a7745 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -70,6 +70,10 @@ function App() { } } else { setIsHome(true); + if (!worldsMode) { + setOpenedWorldId(null); + return; + } try { const response = await fetchWrapper('get-world?worldId=0'); if (response.data) { @@ -81,6 +85,9 @@ function App() { } } + if (!worldsMode) { + return; + } // Always fetch surrounding worlds const surroundingResponse = await fetchWrapper('get-home-worlds'); if (surroundingResponse.data) { diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index abe34fe6..4d17cc5a 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -199,7 +199,6 @@ const CanvasContainer = (props) => { useEffect(() => { if (hasInit) return; const containerRect = canvasContainerRef.current.getBoundingClientRect(); - console.log(containerRect); const adjustX = ((canvasScale - 1) * props.width) / 2; const adjustY = ((canvasScale - 1) * props.height) / 2; setCanvasX(containerRect.width / 2 - adjustX); From 8424ef665c2b457ff906759a2f822e9aa912fc1c Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 06:36:21 -0600 Subject: [PATCH 06/21] Non-worlds home screen --- configs/canvas.config.json | 4 +++ frontend/src/canvas/CanvasContainer.js | 35 ++++++++++++++++--------- frontend/src/configs/canvas.config.json | 4 +++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/configs/canvas.config.json b/configs/canvas.config.json index 5e24e7f8..ebfecb91 100644 --- a/configs/canvas.config.json +++ b/configs/canvas.config.json @@ -1,5 +1,9 @@ { "canvas": { + "width": 1024, + "height": 768 + }, + "world": { "width": 528, "height": 396 }, diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index 4d17cc5a..2157bc63 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -74,7 +74,6 @@ const CanvasContainer = (props) => { const rect = props.canvasRef.current.getBoundingClientRect(); let cursorX = e.clientX - rect.left; let cursorY = e.clientY - rect.top; - console.log(rect, cursorX, cursorY); if (cursorX < 0) { cursorX = 0; } else if (cursorX > rect.width) { @@ -710,15 +709,13 @@ const CanvasContainer = (props) => { } }; - if (props.openedWorldId !== null) { - fetchCanvas( - props.width, - props.height, - props.colors, - props.canvasRef, - props.openedWorldId - ); - } + fetchCanvas( + props.width, + props.height, + props.colors, + props.canvasRef, + props.openedWorldId + ); for (let i = 0; i < surroundingCanvasRefs.length; i++) { if (surroundingCanvasRefs[i].current) { let canvasConfig = props.surroundingWorlds[i]; @@ -736,6 +733,7 @@ const CanvasContainer = (props) => { const [artificialZoom, setArtificialZoom] = useState(1); useEffect(() => { if ( + props.openedWorldId === null || props.openedWorldId === 0 || (props.activeWorld && props.activeWorld.worldId === 0) ) { @@ -856,12 +854,24 @@ const CanvasContainer = (props) => { colors={props.colors} pixelClicked={pixelClicked} /> +

art/peace Worlds @@ -871,7 +881,8 @@ const CanvasContainer = (props) => { style={{ top: '90%', left: '50%', - transform: `translate(-50%, -50%) scale(${titleScale})` + transform: `translate(-50%, -50%) scale(${titleScale})`, + color: 'rgba(255, 120, 30, 1)' }} > coming soon... diff --git a/frontend/src/configs/canvas.config.json b/frontend/src/configs/canvas.config.json index 5e24e7f8..a311eccf 100644 --- a/frontend/src/configs/canvas.config.json +++ b/frontend/src/configs/canvas.config.json @@ -1,5 +1,9 @@ { "canvas": { + "width": 512, + "height": 384 + }, + "world": { "width": 528, "height": 396 }, From de22dc7f327ca225b27e48aad9f378101cce215f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 07:18:37 -0600 Subject: [PATCH 07/21] Upgrade chart versions --- infra/art-peace-infra/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/art-peace-infra/Chart.yaml b/infra/art-peace-infra/Chart.yaml index fc7815c3..f5518370 100644 --- a/infra/art-peace-infra/Chart.yaml +++ b/infra/art-peace-infra/Chart.yaml @@ -3,5 +3,5 @@ name: art-peace-infra description: Helm charts for the art/peace infrastructure type: application -version: 0.1.0 -appVersion: "v2.0.0" +version: 0.1.1 +appVersion: "v2.0.1" From a88c309dbf5e149e01fd50b5868e14019ecbb259 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 08:05:54 -0600 Subject: [PATCH 08/21] Websocket svc host fix --- Makefile | 2 ++ configs/prod-backend.config.json | 2 +- indexer/prod-script.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 56992a38..1c91f005 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,8 @@ docker-push: docker push "brandonjroberts/art-peace-backend:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Pushing consumer..." docker push "brandonjroberts/art-peace-consumer:$(APP_VERSION)-$(COMMIT_SHA)" + @echo "Pushing websocket..." + docker push "brandonjroberts/art-peace-websocket:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Pushing indexer..." docker push "brandonjroberts/art-peace-indexer:$(APP_VERSION)-$(COMMIT_SHA)" diff --git a/configs/prod-backend.config.json b/configs/prod-backend.config.json index af771404..7e00c063 100644 --- a/configs/prod-backend.config.json +++ b/configs/prod-backend.config.json @@ -2,7 +2,7 @@ "host": "backend.art-peace-sepolia.svc.cluster.local", "port": 8080, "consumer_port": 8081, - "ws_host": "websockets.art-peace-sepolia.svc.cluster.local", + "ws_host": "websocket.art-peace-sepolia.svc.cluster.local", "ws_port": 8082, "scripts": { "place_pixel_devnet": "/scripts/place_pixel.sh", diff --git a/indexer/prod-script.js b/indexer/prod-script.js index 8f550fea..3ad1f17f 100644 --- a/indexer/prod-script.js +++ b/indexer/prod-script.js @@ -1,6 +1,6 @@ export const config = { streamUrl: Deno.env.get("APIBARA_STREAM_URL"), - startingBlock: 600_000, + startingBlock: 720_000, network: "starknet", finality: "DATA_STATUS_PENDING", filter: { From e0f3ab6107b55bd85f3a4f414057599104f0a531 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 08:32:06 -0600 Subject: [PATCH 09/21] Websocket svc port fix --- backend/Dockerfile.websocket | 2 +- backend/config/backend.go | 2 +- configs/backend.config.json | 2 +- configs/docker-backend.config.json | 2 +- configs/prod-backend.config.json | 2 +- docker-compose.yml | 2 +- frontend/src/configs/backend.config.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/Dockerfile.websocket b/backend/Dockerfile.websocket index 06e2577d..7fb00043 100644 --- a/backend/Dockerfile.websocket +++ b/backend/Dockerfile.websocket @@ -17,6 +17,6 @@ COPY ./backend . # Build the app & run it RUN go build -o web-sockets ./cmd/web-sockets/web-sockets.go -EXPOSE 8082 +EXPOSE 8083 CMD ["./web-sockets"] diff --git a/backend/config/backend.go b/backend/config/backend.go index ac09f750..7a858016 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -60,7 +60,7 @@ var DefaultBackendConfig = BackendConfig{ Port: 8080, ConsumerPort: 8081, WsHost: "localhost", - WsPort: 8082, + WsPort: 8083, Scripts: BackendScriptsConfig{ PlacePixelDevnet: "../scripts/place_pixel.sh", PlaceExtraPixelsDevnet: "../scripts/place_extra_pixels.sh", diff --git a/configs/backend.config.json b/configs/backend.config.json index 19124671..aef484d0 100644 --- a/configs/backend.config.json +++ b/configs/backend.config.json @@ -3,7 +3,7 @@ "port": 8080, "consumer_port": 8081, "ws_host": "localhost", - "ws_port": 8082, + "ws_port": 8083, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", diff --git a/configs/docker-backend.config.json b/configs/docker-backend.config.json index 56610b9d..4ce67945 100644 --- a/configs/docker-backend.config.json +++ b/configs/docker-backend.config.json @@ -3,7 +3,7 @@ "port": 8080, "consumer_port": 8081, "ws_host": "art-peace-websockets-1", - "ws_port": 8082, + "ws_port": 8083, "scripts": { "place_pixel_devnet": "/scripts/place_pixel.sh", "place_extra_pixels_devnet": "/scripts/place_extra_pixels.sh", diff --git a/configs/prod-backend.config.json b/configs/prod-backend.config.json index 7e00c063..e22b2f8d 100644 --- a/configs/prod-backend.config.json +++ b/configs/prod-backend.config.json @@ -3,7 +3,7 @@ "port": 8080, "consumer_port": 8081, "ws_host": "websocket.art-peace-sepolia.svc.cluster.local", - "ws_port": 8082, + "ws_port": 8083, "scripts": { "place_pixel_devnet": "/scripts/place_pixel.sh", "place_extra_pixels_devnet": "/scripts/place_extra_pixels.sh", diff --git a/docker-compose.yml b/docker-compose.yml index baa18134..dd3ef4b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: dockerfile: backend/Dockerfile.websocket context: . ports: - - 8082:8082 + - 8083:8083 links: - consumer restart: always diff --git a/frontend/src/configs/backend.config.json b/frontend/src/configs/backend.config.json index cb76ee85..3506cc51 100644 --- a/frontend/src/configs/backend.config.json +++ b/frontend/src/configs/backend.config.json @@ -2,7 +2,7 @@ "host": "api.art-peace.net", "port": 8080, "consumer_port": 8081, - "ws_port": 8082, + "ws_port": 8083, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", From d41beaea3f8bec1f2df2c3dd7b68154dd6ad17ae Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 09:07:25 -0600 Subject: [PATCH 10/21] round number on indexer --- backend/routes/indexer/nft.go | 4 +++- backend/routes/indexer/pixel.go | 9 +++++++-- backend/routes/pixel.go | 9 +++++++-- backend/video/video.go | 4 +++- .../templates/consumer/consumer-configmap.yaml | 1 + infra/art-peace-infra/values.yaml | 1 + 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 7cfe7196..0403919a 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -90,7 +90,9 @@ func processNFTMintedEvent(event IndexerEvent) { // Load image from redis ctx := context.Background() - canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result() + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, canvasKey).Result() if err != nil { PrintIndexerError("processNFTMintedEvent", "Error getting canvas from redis", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 5bfa6e01..51ebedf2 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -2,6 +2,7 @@ package indexer import ( "context" + "fmt" "strconv" "github.com/keep-starknet-strange/art-peace/backend/core" @@ -46,7 +47,9 @@ func processPixelPlacedEvent(event IndexerEvent) { pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth ctx := context.Background() - err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, color).Err() if err != nil { PrintIndexerError("processPixelPlacedEvent", "Error setting pixel in redis", address, posHex, dayIdxHex, colorHex) return @@ -98,7 +101,9 @@ func revertPixelPlacedEvent(event IndexerEvent) { pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth ctx := context.Background() - err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, oldColor).Err() + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, oldColor).Err() if err != nil { PrintIndexerError("revertPixelPlacedEvent", "Error resetting pixel in redis", address, posHex) return diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 223e42fb..6b2bcd06 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -2,6 +2,7 @@ package routes import ( "context" + "fmt" "net/http" "os" "os/exec" @@ -39,7 +40,9 @@ func getPixel(w http.ResponseWriter, r *http.Request) { pos := uint(position) * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth ctx := context.Background() - val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result() + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "GET", bitfieldType, pos).Result() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Error getting pixel") return @@ -215,7 +218,9 @@ func placePixelRedis(w http.ResponseWriter, r *http.Request) { pos := position * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth ctx := context.Background() - err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err() + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, color).Err() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Error setting pixel on redis") return diff --git a/backend/video/video.go b/backend/video/video.go index d98ded8f..b18da185 100644 --- a/backend/video/video.go +++ b/backend/video/video.go @@ -42,11 +42,13 @@ func GenerateImageFromCanvas(orderId int) { } generatedImage := image.NewRGBA(image.Rect(0, 0, canvasWidth, canvasHeight)) bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) + roundNumber := core.ArtPeaceBackend.CanvasConfig.Round + canvasKey := fmt.Sprintf("canvas-%d", roundNumber) for y := 0; y < canvasHeight; y++ { for x := 0; x < canvasWidth; x++ { position := y*canvasWidth + x pos := position * int(colorWidth) - val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result() + val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "GET", bitfieldType, pos).Result() if err != nil { fmt.Println("Failed to get bitfield value. Error: ", err) return diff --git a/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml b/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml index ca138855..6dd73bcd 100644 --- a/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml +++ b/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml @@ -8,3 +8,4 @@ data: ART_PEACE_CONTRACT_ADDRESS: {{ .Values.contracts.artPeace }} USERNAME_STORE_CONTRACT_ADDRESS: {{ .Values.contracts.usernameStore }} POSTGRES_PASSWORD: {{ .Values.postgres.password }} + ROUND_NUMBER: {{ .Values.contracts.roundNumber }} diff --git a/infra/art-peace-infra/values.yaml b/infra/art-peace-infra/values.yaml index f9ff4169..84a8b77e 100644 --- a/infra/art-peace-infra/values.yaml +++ b/infra/art-peace-infra/values.yaml @@ -90,3 +90,4 @@ contracts: nft: 0x042dbc0bbdb0faaad99d0b116d0105f9e213ac0d2faf75c878f49d69b544befb host: "05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa" end: "1727769600" + roundNumber: "2" From c811d6a2aa2cf443098422151c582eb84f2a01ed Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 09:15:46 -0600 Subject: [PATCH 11/21] round number on indexer 2 --- backend/config/canvas.go | 4 ++-- backend/routes/canvas.go | 3 +-- backend/routes/indexer/nft.go | 1 - configs/canvas.config.json | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/config/canvas.go b/backend/config/canvas.go index ee18f1bd..4b07b184 100644 --- a/backend/config/canvas.go +++ b/backend/config/canvas.go @@ -15,7 +15,7 @@ type CanvasConfig struct { Colors []string `json:"colors"` VotableColors []string `json:"votableColors"` ColorsBitWidth uint `json:"colorsBitwidth"` - Round uint `json:"round"` + Round string `json:"round"` } var DefaultCanvasConfig = &CanvasConfig{ @@ -43,7 +43,7 @@ var DefaultCanvasConfig = &CanvasConfig{ "#00DDDD", }, ColorsBitWidth: 5, - Round: 2, + Round: "2", } var DefaultCanvasConfigPath = "../configs/canvas.config.json" diff --git a/backend/routes/canvas.go b/backend/routes/canvas.go index 41e22cfa..a3294d1d 100644 --- a/backend/routes/canvas.go +++ b/backend/routes/canvas.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "strconv" "github.com/keep-starknet-strange/art-peace/backend/core" routeutils "github.com/keep-starknet-strange/art-peace/backend/routes/utils" @@ -53,7 +52,7 @@ func getCanvas(w http.ResponseWriter, r *http.Request) { // Get round number from query params, default to config round roundNumber := r.URL.Query().Get("round") if roundNumber == "" { - roundNumber = strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.Round)) + roundNumber = core.ArtPeaceBackend.CanvasConfig.Round } canvasKey := fmt.Sprintf("canvas-%s", roundNumber) diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 0403919a..d3626fd8 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -164,7 +164,6 @@ func processNFTMintedEvent(event IndexerEvent) { } // TODO: Check if file exists - roundNumber := os.Getenv("ROUND_NUMBER") if roundNumber == "" { PrintIndexerError("processNFTMintedEvent", "Error getting round number from environment", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return diff --git a/configs/canvas.config.json b/configs/canvas.config.json index ebfecb91..e79b4e42 100644 --- a/configs/canvas.config.json +++ b/configs/canvas.config.json @@ -52,5 +52,5 @@ "D4D7D9" ], "colorsBitwidth": 5, - "round": 2 + "round": "2" } From 9ec017d30d35736bd61f60478a75e0dd4de0f9e7 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 16 Jan 2025 10:15:55 -0600 Subject: [PATCH 12/21] error with string to decimal formatting --- backend/routes/canvas.go | 6 +++--- backend/routes/indexer/nft.go | 2 +- backend/routes/indexer/pixel.go | 4 ++-- backend/routes/pixel.go | 4 ++-- backend/video/video.go | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/routes/canvas.go b/backend/routes/canvas.go index a3294d1d..acc0b11a 100644 --- a/backend/routes/canvas.go +++ b/backend/routes/canvas.go @@ -21,7 +21,7 @@ func initCanvas(w http.ResponseWriter, r *http.Request) { } roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) if core.ArtPeaceBackend.Databases.Redis.Exists(context.Background(), canvasKey).Val() == 0 { totalBitSize := core.ArtPeaceBackend.CanvasConfig.Canvas.Width * core.ArtPeaceBackend.CanvasConfig.Canvas.Height * core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth @@ -40,9 +40,9 @@ func initCanvas(w http.ResponseWriter, r *http.Request) { return } - routeutils.WriteResultJson(w, fmt.Sprintf("Canvas for round %d initialized", roundNumber)) + routeutils.WriteResultJson(w, fmt.Sprintf("Canvas for round %s initialized", roundNumber)) } else { - routeutils.WriteErrorJson(w, http.StatusConflict, fmt.Sprintf("Canvas for round %d already initialized", roundNumber)) + routeutils.WriteErrorJson(w, http.StatusConflict, fmt.Sprintf("Canvas for round %s already initialized", roundNumber)) } } diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index d3626fd8..ad76ad82 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -91,7 +91,7 @@ func processNFTMintedEvent(event IndexerEvent) { // Load image from redis ctx := context.Background() roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, canvasKey).Result() if err != nil { PrintIndexerError("processNFTMintedEvent", "Error getting canvas from redis", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 51ebedf2..018f5f37 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -48,7 +48,7 @@ func processPixelPlacedEvent(event IndexerEvent) { ctx := context.Background() roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, color).Err() if err != nil { PrintIndexerError("processPixelPlacedEvent", "Error setting pixel in redis", address, posHex, dayIdxHex, colorHex) @@ -102,7 +102,7 @@ func revertPixelPlacedEvent(event IndexerEvent) { ctx := context.Background() roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, oldColor).Err() if err != nil { PrintIndexerError("revertPixelPlacedEvent", "Error resetting pixel in redis", address, posHex) diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 6b2bcd06..d4f10d4a 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -41,7 +41,7 @@ func getPixel(w http.ResponseWriter, r *http.Request) { ctx := context.Background() roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) val, err := core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "GET", bitfieldType, pos).Result() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Error getting pixel") @@ -219,7 +219,7 @@ func placePixelRedis(w http.ResponseWriter, r *http.Request) { ctx := context.Background() roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) err = core.ArtPeaceBackend.Databases.Redis.BitField(ctx, canvasKey, "SET", bitfieldType, pos, color).Err() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Error setting pixel on redis") diff --git a/backend/video/video.go b/backend/video/video.go index b18da185..bac2dc83 100644 --- a/backend/video/video.go +++ b/backend/video/video.go @@ -43,7 +43,7 @@ func GenerateImageFromCanvas(orderId int) { generatedImage := image.NewRGBA(image.Rect(0, 0, canvasWidth, canvasHeight)) bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) roundNumber := core.ArtPeaceBackend.CanvasConfig.Round - canvasKey := fmt.Sprintf("canvas-%d", roundNumber) + canvasKey := fmt.Sprintf("canvas-%s", roundNumber) for y := 0; y < canvasHeight; y++ { for x := 0; x < canvasWidth; x++ { position := y*canvasWidth + x From 6b701dd11489caf9e24111c1ae6d003935e57528 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 18:23:28 -0600 Subject: [PATCH 13/21] Setup deployment for worlds mode sepolia --- Makefile | 9 +- configs/sepolia-contracts.config.json | 3 +- frontend/src/App.js | 10 +- indexer/Dockerfile.worlds.prod | 6 + indexer/prod-worlds-script.js | 158 ++++++++++++++++++ infra/art-peace-infra/Chart.yaml | 4 +- .../templates/backend/backend-configmap.yaml | 1 + .../templates/backend/worlds-pvc.yaml | 13 ++ .../consumer/consumer-configmap.yaml | 1 + .../consumer/consumer-deployment.yaml | 6 +- .../templates/indexer/indexer-configmap.yaml | 1 + .../templates/indexer/indexer-deployment.yaml | 4 + .../websockets/websocket-configmap.yaml | 1 + infra/art-peace-infra/values.yaml | 9 + infra/instructions/worlds-deploy-steps.txt | 50 ++++++ onchain/src/multi_canvas.cairo | 30 +++- tests/integration/sepolia/deploy-multi.sh | 103 ++++++++++++ 17 files changed, 394 insertions(+), 15 deletions(-) create mode 100644 indexer/Dockerfile.worlds.prod create mode 100644 indexer/prod-worlds-script.js create mode 100644 infra/art-peace-infra/templates/backend/worlds-pvc.yaml create mode 100644 infra/instructions/worlds-deploy-steps.txt create mode 100755 tests/integration/sepolia/deploy-multi.sh diff --git a/Makefile b/Makefile index 1c91f005..2a850b94 100644 --- a/Makefile +++ b/Makefile @@ -31,8 +31,10 @@ docker-build: docker build . -f backend/Dockerfile.consumer.prod -t "brandonjroberts/art-peace-consumer:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Building websocket..." docker build . -f backend/Dockerfile.websocket.prod -t "brandonjroberts/art-peace-websocket:$(APP_VERSION)-$(COMMIT_SHA)" - @echo "Building indexer..." + @echo "Building indexer main..." docker build . -f indexer/Dockerfile.prod -t "brandonjroberts/art-peace-indexer:$(APP_VERSION)-$(COMMIT_SHA)" + @echo "Building indexer worlds..." + docker build . -f indexer/Dockerfile.worlds.prod -t "brandonjroberts/art-peace-worlds-indexer:$(APP_VERSION)-$(COMMIT_SHA)" docker-push: $(eval APP_VERSION := $(shell cat infra/art-peace-infra/Chart.yaml | yq eval '.appVersion' -)) @@ -44,8 +46,10 @@ docker-push: docker push "brandonjroberts/art-peace-consumer:$(APP_VERSION)-$(COMMIT_SHA)" @echo "Pushing websocket..." docker push "brandonjroberts/art-peace-websocket:$(APP_VERSION)-$(COMMIT_SHA)" - @echo "Pushing indexer..." + @echo "Pushing indexer main..." docker push "brandonjroberts/art-peace-indexer:$(APP_VERSION)-$(COMMIT_SHA)" + @echo "Pushing indexer worlds..." + docker push "brandonjroberts/art-peace-worlds-indexer:$(APP_VERSION)-$(COMMIT_SHA)" helm-uninstall: @echo "Uninstalling helm chart..." @@ -75,3 +79,4 @@ update-frontend-contracts: cat onchain/target/dev/art_peace_ArtPeace.contract_class.json| jq -r '.abi' > frontend/src/contracts/art_peace.abi.json cat onchain/target/dev/art_peace_CanvasNFT.contract_class.json| jq -r '.abi' > frontend/src/contracts/canvas_nft.abi.json cat onchain/target/dev/art_peace_UsernameStore.contract_class.json| jq -r '.abi' > frontend/src/contracts/username_store.abi.json + cat onchain/target/dev/art_peace_MultiCanvas.contract_class.json | jq -r '.abi' > frontend/src/contracts/multi_canvas.abi.json diff --git a/configs/sepolia-contracts.config.json b/configs/sepolia-contracts.config.json index 95587753..44a1fdeb 100644 --- a/configs/sepolia-contracts.config.json +++ b/configs/sepolia-contracts.config.json @@ -2,6 +2,5 @@ "usernameStore": "0x051cf8214d781f5ee503cb668505e58f0374bfd602145560ba1b897eb98f2e1a", "artPeace": "0x078f4e772300472a68a19f2b1aedbcb7cf2acd6f67a2236372310a528c7eaa67", "canvasNFT": "0x03f169f36006f9007b94a7046944d0c351a7a301aae692c995fec41bb38693e8", - - "pixelQuest": "0x0715004f8805f938b1d42b0bce12a9b1dc49ec55bb2c401ea9c4695461960576" + "worlds": "0x03ce937f91fa0c88a4023f582c729935a5366385091166a763e53281e45ac410" } diff --git a/frontend/src/App.js b/frontend/src/App.js index 226a7745..cf8a07f0 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -37,7 +37,8 @@ import ModalPanel from './ui/ModalPanel.js'; import Hamburger from './resources/icons/Hamburger.png'; function App() { - const worldsMode = devnetMode; + const [worldsMode, setWorldsMode] = useState(devnetMode); + const [homeCounter, setHomeCounter] = useState(0); const [openedWorldId, setOpenedWorldId] = useState(worldsMode ? 0 : null); const [activeWorld, setActiveWorld] = useState(null); const [surroundingWorlds, setSurroundingWorlds] = useState([]); @@ -113,7 +114,7 @@ function App() { }; getWorldId(); - }, [location.pathname]); + }, [location.pathname, worldsMode]); useEffect(() => { setOverlayTemplate(null); @@ -1371,6 +1372,11 @@ function App() {
{ + setHomeCounter(homeCounter + 1); + if (homeCounter > 2) { + setWorldsMode(true); + setOpenedWorldId(0); + } setActiveTab(tabs[0]); window.location.pathname = '/'; }} diff --git a/indexer/Dockerfile.worlds.prod b/indexer/Dockerfile.worlds.prod new file mode 100644 index 00000000..50a8a0d0 --- /dev/null +++ b/indexer/Dockerfile.worlds.prod @@ -0,0 +1,6 @@ +FROM quay.io/apibara/sink-webhook:0.6.0 as sink-webhook + +WORKDIR /indexer +COPY ./indexer/prod-worlds-script.js . + +CMD ["run", "prod-worlds-script.js", "--allow-env-from-env", "CONSUMER_TARGET_URL,APIBARA_STREAM_URL,PERSIST_TO_REDIS,INDEXER_ID,CANVAS_FACTORY_CONTRACT_ADDRESS", "--allow-net", "--sink-id", "canvas-factory-sink-id"] diff --git a/indexer/prod-worlds-script.js b/indexer/prod-worlds-script.js new file mode 100644 index 00000000..2ec227ba --- /dev/null +++ b/indexer/prod-worlds-script.js @@ -0,0 +1,158 @@ +export const config = { + streamUrl: Deno.env.get("APIBARA_STREAM_URL"), + startingBlock: 455000, + network: "starknet", + finality: "DATA_STATUS_PENDING", + filter: { + events: [ + { + // Canvas Created Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x0003fddf2e955d6c8fbd5ec6e98da32f7e9ebe7731b86b4ef7de342b165222e0" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Host Changed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x00569981649f1a25a7a012ccf216e9c0f807068f8ba4689ee58c2d55df22cc45" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Time Between Pixels Changed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x02e1eccce24e49cc4ab3df0795f173bbe667dd4fddbc52c8af731b4e2ad78cf5" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Color Added Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x03e856f8abfe58c8841f552ce76651ebff20c1550d167b3a18b049b7552fe8a2" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Pixel Placed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x02adf9f56e1f4e16a3e116f34424bd26cb5fc45363498015b4c007835318f7bb" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Basic Pixel Placed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x03066baa9c37a42082799e6bc6426ff7d4dc8a635ed9dfc444d0d3c51e605a6b" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Extra Pixels Placed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x01e42e4d6ca5843bfd4e86e344db6c418b295c23bed38831a7ec9b4a83148830" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Host Awarded User Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x01bf6ede8c6c232cee1830a5227fd638383f5af669701289d113492b1d41fda5" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Favorited Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x032105bd4f21a32bc92e45a49b30eab9355f7f89619d87e9801628e3acc5b502" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Canvas Unfavorited Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x014ee6480f95acb4b7286d3a7f95b6033299e66e502cfb4b207ccf088b5f601d" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Stencil Added Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x03384fcf8ff5c539c31feec6626511aa15ae53dba7459fd3a3c67af615ef6b5d" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Stencil Removed Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x023c933ed3ee3f94b5b82f8e2e570c8354e6f5036c3a079092ceeed15979e7fa" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Stencil Favorited Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x007cb4ae927fb597834e194e2c950a2d813461c72f372f78d0610ea246f53017" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Stencil Unfavorited Event + fromAddress: Deno.env.get("CANVAS_FACTORY_CONTRACT_ADDRESS"), + keys: [ + "0x00a5477c7df6522316b652e56317e69e52429ab43a6772fb6f6c2a574f7e196f" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + ] + }, + sinkType: "webhook", + sinkOptions: { + targetUrl: Deno.env.get("CONSUMER_TARGET_URL") + } +}; + +export default function transform(block) { + return block; +} diff --git a/infra/art-peace-infra/Chart.yaml b/infra/art-peace-infra/Chart.yaml index f5518370..e6dbd168 100644 --- a/infra/art-peace-infra/Chart.yaml +++ b/infra/art-peace-infra/Chart.yaml @@ -3,5 +3,5 @@ name: art-peace-infra description: Helm charts for the art/peace infrastructure type: application -version: 0.1.1 -appVersion: "v2.0.1" +version: 0.1.2 +appVersion: "v2.1.0" diff --git a/infra/art-peace-infra/templates/backend/backend-configmap.yaml b/infra/art-peace-infra/templates/backend/backend-configmap.yaml index c470c23e..d0dba95c 100644 --- a/infra/art-peace-infra/templates/backend/backend-configmap.yaml +++ b/infra/art-peace-infra/templates/backend/backend-configmap.yaml @@ -10,3 +10,4 @@ data: POSTGRES_PASSWORD: {{ .Values.postgres.password }} ART_PEACE_HOST: {{ .Values.contracts.host }} ART_PEACE_END_TIME: {{ .Values.contracts.end }} + CANVAS_FACTORY_CONTRACT_ADDRESS: {{ .Values.contracts.canvasFactory }} diff --git a/infra/art-peace-infra/templates/backend/worlds-pvc.yaml b/infra/art-peace-infra/templates/backend/worlds-pvc.yaml new file mode 100644 index 00000000..e52c1bbc --- /dev/null +++ b/infra/art-peace-infra/templates/backend/worlds-pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.volumes.worlds.claim }} + labels: + app: worlds +spec: + storageClassName: {{ .Values.volumes.worlds.class }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.volumes.worlds.storage }} diff --git a/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml b/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml index 6dd73bcd..8bcee76b 100644 --- a/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml +++ b/infra/art-peace-infra/templates/consumer/consumer-configmap.yaml @@ -7,5 +7,6 @@ metadata: data: ART_PEACE_CONTRACT_ADDRESS: {{ .Values.contracts.artPeace }} USERNAME_STORE_CONTRACT_ADDRESS: {{ .Values.contracts.usernameStore }} + CANVAS_FACTORY_CONTRACT_ADDRESS: {{ .Values.contracts.canvasFactory }} POSTGRES_PASSWORD: {{ .Values.postgres.password }} ROUND_NUMBER: {{ .Values.contracts.roundNumber }} diff --git a/infra/art-peace-infra/templates/consumer/consumer-deployment.yaml b/infra/art-peace-infra/templates/consumer/consumer-deployment.yaml index 1e16680c..681ab0a7 100644 --- a/infra/art-peace-infra/templates/consumer/consumer-deployment.yaml +++ b/infra/art-peace-infra/templates/consumer/consumer-deployment.yaml @@ -25,7 +25,11 @@ spec: volumeMounts: - mountPath: /app/nfts name: {{ .Values.volumes.nfts.name }} + - mountPath: /app/worlds + name: {{ .Values.volumes.worlds.name }} volumes: - name: {{ .Values.volumes.nfts.name }} persistentVolumeClaim: - claimName: {{ .Values.volumes.nfts.claim }} + - name: {{ .Values.volumes.worlds.name }} + persistentVolumeClaim: + claimName: {{ .Values.volumes.worlds.claim }} diff --git a/infra/art-peace-infra/templates/indexer/indexer-configmap.yaml b/infra/art-peace-infra/templates/indexer/indexer-configmap.yaml index 3dedfde4..0f024111 100644 --- a/infra/art-peace-infra/templates/indexer/indexer-configmap.yaml +++ b/infra/art-peace-infra/templates/indexer/indexer-configmap.yaml @@ -8,6 +8,7 @@ data: ART_PEACE_CONTRACT_ADDRESS: {{ .Values.contracts.artPeace }} USERNAME_STORE_ADDRESS: {{ .Values.contracts.usernameStore }} NFT_CONTRACT_ADDRESS: {{ .Values.contracts.nft }} + CANVAS_FACTORY_CONTRACT_ADDRESS: {{ .Values.contracts.canvasFactory }} CONSUMER_TARGET_URL: http://{{ .Values.labels.consumer.name }}.art-peace-sepolia.svc.cluster.local:{{ .Values.ports.consumer }}/consume-indexer-msg APIBARA_STREAM_URL: {{ .Values.apibara.streamUrl }} AUTH_TOKEN: {{ .Values.apibara.authToken }} diff --git a/infra/art-peace-infra/templates/indexer/indexer-deployment.yaml b/infra/art-peace-infra/templates/indexer/indexer-deployment.yaml index 91e98340..92de9a40 100644 --- a/infra/art-peace-infra/templates/indexer/indexer-deployment.yaml +++ b/infra/art-peace-infra/templates/indexer/indexer-deployment.yaml @@ -17,7 +17,11 @@ spec: kubernetes.io/arch: arm64 containers: - name: {{ .Values.labels.indexer.name }} + {{- if .Values.contracts.worldsMode}} + image: {{ .Values.deployments.indexerWorlds.image }}:{{ .Chart.AppVersion }}-{{ .Values.deployments.sha }} + {{- else }} image: {{ .Values.deployments.indexer.image }}:{{ .Chart.AppVersion }}-{{ .Values.deployments.sha }} + {{- end }} imagePullPolicy: Always envFrom: - configMapRef: diff --git a/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml b/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml index 457754dc..bb280318 100644 --- a/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml +++ b/infra/art-peace-infra/templates/websockets/websocket-configmap.yaml @@ -6,3 +6,4 @@ metadata: app: {{ .Values.labels.websocket.name }} data: ART_PEACE_CONTRACT_ADDRESS: {{ .Values.contracts.artPeace }} + CANVAS_FACTORY_CONTRACT_ADDRESS: {{ .Values.contracts.canvasFactory }} diff --git a/infra/art-peace-infra/values.yaml b/infra/art-peace-infra/values.yaml index 84a8b77e..0a98b1c8 100644 --- a/infra/art-peace-infra/values.yaml +++ b/infra/art-peace-infra/values.yaml @@ -36,6 +36,11 @@ volumes: claim: nft-volume-claim class: standard-rwo storage: 50Gi + worlds: + name: worlds-data + claim: worlds-volume-claim + class: standard-rwo + storage: 50Gi factions: name: faction-data claim: faction-volume-claim @@ -72,6 +77,8 @@ deployments: indexer: replicas: 1 image: brandonjroberts/art-peace-indexer + indexerWorlds: + image: brandonjroberts/art-peace-worlds-indexer postgres: db: art-peace-db @@ -91,3 +98,5 @@ contracts: host: "05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa" end: "1727769600" roundNumber: "2" + canvasFactory: 0x03ce937f91fa0c88a4023f582c729935a5366385091166a763e53281e45ac410 + worldsMode: true diff --git a/infra/instructions/worlds-deploy-steps.txt b/infra/instructions/worlds-deploy-steps.txt new file mode 100644 index 00000000..372169e0 --- /dev/null +++ b/infra/instructions/worlds-deploy-steps.txt @@ -0,0 +1,50 @@ +setup .env w/ STARKNET_KEYSTORE=$HOME/.starkli-sepolia/starkli-keystore.json + STARKNET_ACCOUNT=$HOME/.starkli-sepolia/starkli-account.json + +source .env + +build contracts : scarb build +deploy worlds to sepolia + ./tests/integration/sepolia/deploy-multi.sh + save address + +setup indexer starting block + +copy abis to frontend + make update-frontend-contracts + +build prod docker images w/ new version & push to docker hub + Update docker version in infra/art-peace-infra/Chart.yaml if needed + Update contracts in values.yaml & worldsMode + commit and merge changes + make docker-build + make docker-push + +apply changes to cloud + cloud console + clone / pull latest main + git clone https://github.com/keep-starknet-strange/art-peace.git + git pull origin main + cd art-peace + If full reset + make helm-uninstall + POSTGRES_PASSWORD=test AUTH_TOKEN=dna_abc make helm-install + make init-infra-prod + else + POSTGRES_PASSWORD=test AUTH_TOKEN=dna_abc make helm-upgrade + +change frontend contract addresses in vercel + https://vercel.com/keep-starknet-strange/art-peace/settings + + + kubectl cp ../postgres/init.sql pod:/home + kubectl exec -it pod/pod-name bash + psql -U art-peace-user -d art-peace-db -f /home/init.sql + kubectl delete pvc redis-volume-claim + kubectl delete deployment.apps/redis + kubectl delete deployment.apps/indexer + POSTGRES_PASSWORD=test AUTH_TOKEN=dna_abc make helm-upgrade + kubectl exec -it pod/admin-backend-xxx bash + reset : + kubectl delete pvc nft-volume-claim redis-volume-claim + kubectl delete deployment.apps/backend deployment.apps/admin-backend deployment.apps/consumer deployment.apps/indexer deployment.apps/redis diff --git a/onchain/src/multi_canvas.cairo b/onchain/src/multi_canvas.cairo index 676b12af..35e1fe89 100644 --- a/onchain/src/multi_canvas.cairo +++ b/onchain/src/multi_canvas.cairo @@ -15,6 +15,8 @@ pub trait IMultiCanvas { fn get_last_placed_time(self: @TContractState, canvas_id: u32, user: ContractAddress) -> u64; fn get_time_between_pixels(self: @TContractState, canvas_id: u32) -> u64; fn set_time_between_pixels(ref self: TContractState, canvas_id: u32, time_between_pixels: u64); + fn enable_awards(ref self: TContractState); + fn disable_awards(ref self: TContractState); fn award_user(ref self: TContractState, canvas_id: u32, user: ContractAddress, amount: u32); fn get_color_count(self: @TContractState, canvas_id: u32) -> u8; fn get_colors(self: @TContractState, canvas_id: u32) -> Span; @@ -114,7 +116,8 @@ pub mod MultiCanvas { // Map: (canvas_id, stencil_id) -> stencil metadata stencils: LegacyMap::<(u32, u32), StencilMetadata>, // Maps: (canvas_id, stencil_id, user addr) -> if favorited - stencil_favorites: LegacyMap::<(u32, u32, ContractAddress), bool> + stencil_favorites: LegacyMap::<(u32, u32, ContractAddress), bool>, + awards_enabled: bool, } #[event] @@ -385,13 +388,28 @@ pub mod MultiCanvas { ); } + fn enable_awards(ref self: ContractState) { + let caller = get_caller_address(); + assert(caller == self.game_master.read(), 'Only game master can enable'); + self.awards_enabled.write(true); + } + + fn disable_awards(ref self: ContractState) { + let caller = get_caller_address(); + assert(caller == self.game_master.read(), 'Only game master can disable'); + self.awards_enabled.write(false); + } + fn award_user( ref self: ContractState, canvas_id: u32, user: ContractAddress, amount: u32 - ) { // TODO - // let caller = get_caller_address(); - // assert(caller == self.hosts.read(canvas_id), 'Only host can award users'); - // self.extra_pixels.write((canvas_id, user), self.extra_pixels.read((canvas_id, user)) + amount); - // self.emit(CanvasHostAwardedUser { canvas_id, user, amount }); + ) { + if !self.awards_enabled.read() { + return; + } + let caller = get_caller_address(); + assert(caller == self.hosts.read(canvas_id) || caller == self.game_master.read(), 'Only hosts can award'); + self.extra_pixels.write((canvas_id, user), self.extra_pixels.read((canvas_id, user)) + amount); + self.emit(CanvasHostAwardedUser { canvas_id, user, amount }); } fn get_color_count(self: @ContractState, canvas_id: u32) -> u8 { diff --git a/tests/integration/sepolia/deploy-multi.sh b/tests/integration/sepolia/deploy-multi.sh new file mode 100755 index 00000000..30be0c43 --- /dev/null +++ b/tests/integration/sepolia/deploy-multi.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +PROJECT_ROOT=$SCRIPT_DIR/../../.. + +# Load env variable from `.env` only if they're not already set +if [ -z "$STARKNET_KEYSTORE" ] || [ -z "$STARKNET_ACCOUNT" ]; then + source $PROJECT_ROOT/.env +fi + +# Check if required env variables are set, if not exit +if [ -z "$STARKNET_KEYSTORE" ]; then + echo "Error: STARKNET_KEYSTORE is not set." + exit 1 +elif [ -z "$STARKNET_ACCOUNT" ]; then + echo "Error: STARKNET_ACCOUNT is not set." + exit 1 +fi + +# TODO: Host & ... +HOST=0x0000 +display_help() { + echo "Usage: $0 [option...]" + echo + echo " -H, --host ADDR Host address for ArtPeace contract" + echo " (required)" + echo + echo " -h, --help display help" + + echo + echo "Example: $0 --host 0x0" +} + +# Transform long options to short ones +for arg in "$@"; do + shift + case "$arg" in + "--host") set -- "$@" "-H" ;; + "--help") set -- "$@" "-h" ;; + --*) unrecognized_options+=("$arg") ;; + *) set -- "$@" "$arg" + esac +done + +# Check if unknown options are passed, if so exit +if [ ! -z "${unrecognized_options[@]}" ]; then + echo "Error: invalid option(s) passed ${unrecognized_options[*]}" 1>&2 + exit 1 +fi + +# Parse command line arguments +while getopts ":hH:" opt; do + case ${opt} in + H ) + HOST=$OPTARG + ;; + h ) + display_help + exit 0 + ;; + \? ) + echo "Invalid Option: -$OPTARG" 1>&2 + display_help + exit 1 + ;; + : ) + echo "Invalid Option: -$OPTARG requires an argument" 1>&2 + display_help + exit 1 + ;; + esac +done + +# Check if required options are set, if not exit +if [ -z "$HOST" ]; then + echo "Error: --host is required." + exit 1 +fi + +ONCHAIN_DIR=$PROJECT_ROOT/onchain +ART_PEACE_SIERRA_FILE=$ONCHAIN_DIR/target/dev/art_peace_MultiCanvas.contract_class.json + +# Build the contract +echo "Building the contract..." +cd $ONCHAIN_DIR && scarb build + +# Declaring the contract +echo "Declaring the contract..." +echo "starkli declare --network sepolia --keystore $STARKNET_KEYSTORE --account $STARKNET_ACCOUNT --watch $ART_PEACE_SIERRA_FILE" +ART_PEACE_DECLARE_OUTPUT=$(starkli declare --network sepolia --keystore $STARKNET_KEYSTORE --account $STARKNET_ACCOUNT --watch $ART_PEACE_SIERRA_FILE 2>&1) +ART_PEACE_CONTRACT_CLASSHASH=$(echo $ART_PEACE_DECLARE_OUTPUT | tail -n 1 | awk '{print $NF}') +echo "Contract class hash: $ART_PEACE_CONTRACT_CLASSHASH" + +# Deploying the contract +CANVAS_CONFIG=$PROJECT_ROOT/configs/canvas.config.json + +ACCOUNT_ADDRESS=$(cat $STARKNET_ACCOUNT | jq -r '.deployment.address') + +CALLDATA=$(echo -n $ACCOUNT_ADDRESS) + +echo "Deploying the contract..." +echo "starkli deploy --network sepolia --keystore $STARKNET_KEYSTORE --account $STARKNET_ACCOUNT --watch $ART_PEACE_CONTRACT_CLASSHASH $CALLDATA" +starkli deploy --network sepolia --keystore $STARKNET_KEYSTORE --account $STARKNET_ACCOUNT --watch $ART_PEACE_CONTRACT_CLASSHASH $CALLDATA From b448e66942c79437e8b9a8502fc308053bca6249 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 18:25:41 -0600 Subject: [PATCH 14/21] Scarb fmt --- onchain/src/multi_canvas.cairo | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/onchain/src/multi_canvas.cairo b/onchain/src/multi_canvas.cairo index 35e1fe89..58368ed8 100644 --- a/onchain/src/multi_canvas.cairo +++ b/onchain/src/multi_canvas.cairo @@ -400,15 +400,18 @@ pub mod MultiCanvas { self.awards_enabled.write(false); } - fn award_user( - ref self: ContractState, canvas_id: u32, user: ContractAddress, amount: u32 - ) { + fn award_user(ref self: ContractState, canvas_id: u32, user: ContractAddress, amount: u32) { if !self.awards_enabled.read() { return; } let caller = get_caller_address(); - assert(caller == self.hosts.read(canvas_id) || caller == self.game_master.read(), 'Only hosts can award'); - self.extra_pixels.write((canvas_id, user), self.extra_pixels.read((canvas_id, user)) + amount); + assert( + caller == self.hosts.read(canvas_id) || caller == self.game_master.read(), + 'Only hosts can award' + ); + self + .extra_pixels + .write((canvas_id, user), self.extra_pixels.read((canvas_id, user)) + amount); self.emit(CanvasHostAwardedUser { canvas_id, user, amount }); } From b1476224446bb484a546e2d91bb14c51f00e0640 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 18:37:58 -0600 Subject: [PATCH 15/21] Patch frontend enabler --- frontend/src/App.js | 9 ++++++--- infra/instructions/worlds-deploy-steps.txt | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index cf8a07f0..ec19f0ea 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1372,13 +1372,16 @@ function App() {
{ - setHomeCounter(homeCounter + 1); - if (homeCounter > 2) { + let newCounter = homeCounter + 1; + setHomeCounter(newCounter); + console.log('Home counter:', newCounter); + if (homeCounter === 2) { setWorldsMode(true); setOpenedWorldId(0); + console.log('Worlds mode activated'); } setActiveTab(tabs[0]); - window.location.pathname = '/'; + //TODO: window.location.pathname = '/'; }} > Date: Fri, 17 Jan 2025 20:24:30 -0600 Subject: [PATCH 16/21] tabs and contract call fix --- frontend/src/App.js | 26 +- frontend/src/contracts/art_peace.abi.json | 281 ++++++++++++------ frontend/src/contracts/canvas_nft.abi.json | 4 + frontend/src/contracts/multi_canvas.abi.json | 256 ++++++++++++++++ .../src/tabs/worlds/WorldsCreationPanel.js | 9 +- 5 files changed, 471 insertions(+), 105 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index ec19f0ea..84c3fc7f 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -123,22 +123,35 @@ function App() { // Window management usePreventZoom(); + const defaultWorldsTabs = ['Canvas', 'Worlds', 'Stencils', 'Account']; + const defaultTabs = []; + // TODO: Add features back /* - const tabs = [ + [ 'Canvas', 'Factions', 'NFTs', 'Quests', 'Vote', - 'Worlds', 'Account' ]; - // : ['Canvas', 'Factions', 'NFTs', 'Quests', 'Vote', 'Account']; */ - // TODO: Add features back - const tabs = devnetMode ? ['Canvas', 'Worlds', 'Stencils', 'Account'] : []; - const [activeTab, setActiveTab] = useState(tabs[0]); + const [tabs, setTabs] = useState( + worldsMode ? defaultWorldsTabs : defaultTabs + ); + const [activeTab, setActiveTab] = useState( + worldsMode ? defaultWorldsTabs[0] : defaultTabs[0] + ); useLockScroll(activeTab === 'Canvas'); + useEffect(() => { + if (worldsMode) { + setTabs(defaultWorldsTabs); + setActiveTab(defaultWorldsTabs[0]); + } else { + setTabs(defaultTabs); + setActiveTab(defaultTabs[0]); + } + }, [worldsMode]); const isDesktopOrLaptop = useMediaQuery({ query: '(min-width: 1224px)' @@ -226,6 +239,7 @@ function App() { ); const multiCanvasContract = new Contract( multi_canvas_abi, + // '0x03ce937f91fa0c88a4023f582c729935a5366385091166a763e53281e45ac410', process.env.REACT_APP_CANVAS_FACTORY_CONTRACT_ADDRESS, account ); diff --git a/frontend/src/contracts/art_peace.abi.json b/frontend/src/contracts/art_peace.abi.json index c837aeab..36cdf14e 100644 --- a/frontend/src/contracts/art_peace.abi.json +++ b/frontend/src/contracts/art_peace.abi.json @@ -80,6 +80,20 @@ } ] }, + { + "type": "struct", + "name": "core::integer::u256", + "members": [ + { + "name": "low", + "type": "core::integer::u128" + }, + { + "name": "high", + "type": "core::integer::u128" + } + ] + }, { "type": "struct", "name": "art_peace::templates::interfaces::FactionTemplateMetadata", @@ -813,6 +827,26 @@ ], "state_mutability": "view" }, + { + "type": "function", + "name": "already_liked_nft", + "inputs": [ + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "nft_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, { "type": "function", "name": "add_faction_template", @@ -1131,23 +1165,44 @@ }, { "type": "impl", - "name": "TemplateStoreComponentImpl", - "interface_name": "art_peace::templates::interfaces::ITemplateStore" + "name": "ArtPeaceCanvasNFTLikeAndUnlike", + "interface_name": "art_peace::nfts::interfaces::ICanvasNFTLikeAndUnlike" }, { - "type": "struct", - "name": "core::integer::u256", - "members": [ + "type": "interface", + "name": "art_peace::nfts::interfaces::ICanvasNFTLikeAndUnlike", + "items": [ { - "name": "low", - "type": "core::integer::u128" + "type": "function", + "name": "like_nft", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" }, { - "name": "high", - "type": "core::integer::u128" + "type": "function", + "name": "unlike_nft", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, + { + "type": "impl", + "name": "TemplateStoreComponentImpl", + "interface_name": "art_peace::templates::interfaces::ITemplateStore" + }, { "type": "struct", "name": "art_peace::templates::interfaces::TemplateMetadata", @@ -1489,65 +1544,6 @@ } ] }, - { - "type": "event", - "name": "art_peace::art_peace::ArtPeace::DailyQuestClaimed", - "kind": "struct", - "members": [ - { - "name": "day_index", - "type": "core::integer::u32", - "kind": "key" - }, - { - "name": "quest_id", - "type": "core::integer::u32", - "kind": "key" - }, - { - "name": "user", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "key" - }, - { - "name": "reward", - "type": "core::integer::u32", - "kind": "data" - }, - { - "name": "calldata", - "type": "core::array::Span::", - "kind": "data" - } - ] - }, - { - "type": "event", - "name": "art_peace::art_peace::ArtPeace::MainQuestClaimed", - "kind": "struct", - "members": [ - { - "name": "quest_id", - "type": "core::integer::u32", - "kind": "key" - }, - { - "name": "user", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "key" - }, - { - "name": "reward", - "type": "core::integer::u32", - "kind": "data" - }, - { - "name": "calldata", - "type": "core::array::Span::", - "kind": "data" - } - ] - }, { "type": "event", "name": "art_peace::art_peace::ArtPeace::VoteColor", @@ -1769,63 +1765,166 @@ }, { "type": "event", - "name": "art_peace::art_peace::ArtPeace::HostAwardedUser", + "name": "art_peace::templates::component::TemplateStoreComponent::TemplateAdded", "kind": "struct", "members": [ + { + "name": "id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "metadata", + "type": "art_peace::templates::interfaces::TemplateMetadata", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::templates::component::TemplateStoreComponent::TemplateCompleted", + "kind": "struct", + "members": [ + { + "name": "id", + "type": "core::integer::u32", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::templates::component::TemplateStoreComponent::Event", + "kind": "enum", + "variants": [ + { + "name": "TemplateAdded", + "type": "art_peace::templates::component::TemplateStoreComponent::TemplateAdded", + "kind": "nested" + }, + { + "name": "TemplateCompleted", + "type": "art_peace::templates::component::TemplateStoreComponent::TemplateCompleted", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::DailyQuestClaimed", + "kind": "struct", + "members": [ + { + "name": "day_index", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "quest_id", + "type": "core::integer::u32", + "kind": "key" + }, { "name": "user", "type": "core::starknet::contract_address::ContractAddress", "kind": "key" }, { - "name": "amount", + "name": "reward", "type": "core::integer::u32", "kind": "data" + }, + { + "name": "calldata", + "type": "core::array::Span::", + "kind": "data" } ] }, { "type": "event", - "name": "art_peace::templates::component::TemplateStoreComponent::TemplateAdded", + "name": "art_peace::art_peace::ArtPeace::MainQuestClaimed", "kind": "struct", "members": [ { - "name": "id", + "name": "quest_id", "type": "core::integer::u32", "kind": "key" }, { - "name": "metadata", - "type": "art_peace::templates::interfaces::TemplateMetadata", + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "reward", + "type": "core::integer::u32", + "kind": "data" + }, + { + "name": "calldata", + "type": "core::array::Span::", "kind": "data" } ] }, { "type": "event", - "name": "art_peace::templates::component::TemplateStoreComponent::TemplateCompleted", + "name": "art_peace::art_peace::ArtPeace::HostAwardedUser", "kind": "struct", "members": [ { - "name": "id", + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "amount", "type": "core::integer::u32", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::LikeNftAwarded", + "kind": "struct", + "members": [ + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", "kind": "key" + }, + { + "name": "amount", + "type": "core::integer::u32", + "kind": "data" } ] }, { "type": "event", - "name": "art_peace::templates::component::TemplateStoreComponent::Event", + "name": "art_peace::art_peace::ArtPeace::ExtraPixelsAwarded", "kind": "enum", "variants": [ { - "name": "TemplateAdded", - "type": "art_peace::templates::component::TemplateStoreComponent::TemplateAdded", + "name": "DailyQuest", + "type": "art_peace::art_peace::ArtPeace::DailyQuestClaimed", "kind": "nested" }, { - "name": "TemplateCompleted", - "type": "art_peace::templates::component::TemplateStoreComponent::TemplateCompleted", + "name": "MainQuest", + "type": "art_peace::art_peace::ArtPeace::MainQuestClaimed", + "kind": "nested" + }, + { + "name": "HostAwardedUser", + "type": "art_peace::art_peace::ArtPeace::HostAwardedUser", + "kind": "nested" + }, + { + "name": "LikeNft", + "type": "art_peace::art_peace::ArtPeace::LikeNftAwarded", "kind": "nested" } ] @@ -1875,16 +1974,6 @@ "type": "art_peace::art_peace::ArtPeace::ExtraPixelsPlaced", "kind": "nested" }, - { - "name": "DailyQuestClaimed", - "type": "art_peace::art_peace::ArtPeace::DailyQuestClaimed", - "kind": "nested" - }, - { - "name": "MainQuestClaimed", - "type": "art_peace::art_peace::ArtPeace::MainQuestClaimed", - "kind": "nested" - }, { "name": "VoteColor", "type": "art_peace::art_peace::ArtPeace::VoteColor", @@ -1945,15 +2034,15 @@ "type": "art_peace::art_peace::ArtPeace::ChainFactionTemplateRemoved", "kind": "nested" }, - { - "name": "HostAwardedUser", - "type": "art_peace::art_peace::ArtPeace::HostAwardedUser", - "kind": "nested" - }, { "name": "TemplateEvent", "type": "art_peace::templates::component::TemplateStoreComponent::Event", "kind": "flat" + }, + { + "name": "ExtraPixelsAwardedEvent", + "type": "art_peace::art_peace::ArtPeace::ExtraPixelsAwarded", + "kind": "flat" } ] } diff --git a/frontend/src/contracts/canvas_nft.abi.json b/frontend/src/contracts/canvas_nft.abi.json index 19442e29..506c3a14 100644 --- a/frontend/src/contracts/canvas_nft.abi.json +++ b/frontend/src/contracts/canvas_nft.abi.json @@ -673,6 +673,10 @@ { "name": "symbol", "type": "core::byte_array::ByteArray" + }, + { + "name": "round_number", + "type": "core::felt252" } ] }, diff --git a/frontend/src/contracts/multi_canvas.abi.json b/frontend/src/contracts/multi_canvas.abi.json index 5c97f85a..467f65ff 100644 --- a/frontend/src/contracts/multi_canvas.abi.json +++ b/frontend/src/contracts/multi_canvas.abi.json @@ -12,6 +12,10 @@ "name": "name", "type": "core::felt252" }, + { + "name": "unique_name", + "type": "core::felt252" + }, { "name": "width", "type": "core::integer::u128" @@ -52,6 +56,10 @@ "name": "name", "type": "core::felt252" }, + { + "name": "unique_name", + "type": "core::felt252" + }, { "name": "width", "type": "core::integer::u128" @@ -78,6 +86,28 @@ } ] }, + { + "type": "struct", + "name": "art_peace::multi_canvas::MultiCanvas::StencilMetadata", + "members": [ + { + "name": "hash", + "type": "core::felt252" + }, + { + "name": "width", + "type": "core::integer::u128" + }, + { + "name": "height", + "type": "core::integer::u128" + }, + { + "name": "position", + "type": "core::integer::u128" + } + ] + }, { "type": "interface", "name": "art_peace::multi_canvas::IMultiCanvas", @@ -280,6 +310,20 @@ "outputs": [], "state_mutability": "external" }, + { + "type": "function", + "name": "enable_awards", + "inputs": [], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "disable_awards", + "inputs": [], + "outputs": [], + "state_mutability": "external" + }, { "type": "function", "name": "award_user", @@ -483,6 +527,110 @@ ], "outputs": [], "state_mutability": "external" + }, + { + "type": "function", + "name": "get_stencil_count", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_stencil", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + }, + { + "name": "stencil_id", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "art_peace::multi_canvas::MultiCanvas::StencilMetadata" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "add_stencil", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + }, + { + "name": "stencil", + "type": "art_peace::multi_canvas::MultiCanvas::StencilMetadata" + } + ], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "remove_stencil", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + }, + { + "name": "stencil_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "favorite_stencil", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + }, + { + "name": "stencil_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "unfavorite_stencil", + "inputs": [ + { + "name": "canvas_id", + "type": "core::integer::u32" + }, + { + "name": "stencil_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, @@ -706,6 +854,94 @@ } ] }, + { + "type": "event", + "name": "art_peace::multi_canvas::MultiCanvas::StencilAdded", + "kind": "struct", + "members": [ + { + "name": "canvas_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil", + "type": "art_peace::multi_canvas::MultiCanvas::StencilMetadata", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::multi_canvas::MultiCanvas::StencilRemoved", + "kind": "struct", + "members": [ + { + "name": "canvas_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil", + "type": "art_peace::multi_canvas::MultiCanvas::StencilMetadata", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::multi_canvas::MultiCanvas::StencilFavorited", + "kind": "struct", + "members": [ + { + "name": "canvas_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::multi_canvas::MultiCanvas::StencilUnfavorited", + "kind": "struct", + "members": [ + { + "name": "canvas_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "stencil_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, { "type": "event", "name": "art_peace::multi_canvas::MultiCanvas::Event", @@ -760,6 +996,26 @@ "name": "CanvasUnfavorited", "type": "art_peace::multi_canvas::MultiCanvas::CanvasUnfavorited", "kind": "nested" + }, + { + "name": "StencilAdded", + "type": "art_peace::multi_canvas::MultiCanvas::StencilAdded", + "kind": "nested" + }, + { + "name": "StencilRemoved", + "type": "art_peace::multi_canvas::MultiCanvas::StencilRemoved", + "kind": "nested" + }, + { + "name": "StencilFavorited", + "type": "art_peace::multi_canvas::MultiCanvas::StencilFavorited", + "kind": "nested" + }, + { + "name": "StencilUnfavorited", + "type": "art_peace::multi_canvas::MultiCanvas::StencilUnfavorited", + "kind": "nested" } ] } diff --git a/frontend/src/tabs/worlds/WorldsCreationPanel.js b/frontend/src/tabs/worlds/WorldsCreationPanel.js index 3b1d16cc..4d0d76d8 100644 --- a/frontend/src/tabs/worlds/WorldsCreationPanel.js +++ b/frontend/src/tabs/worlds/WorldsCreationPanel.js @@ -30,6 +30,7 @@ const WorldsCreationPanel = (props) => { const createWorldCall = async ( name, + slug, width, height, timer, @@ -42,13 +43,15 @@ const WorldsCreationPanel = (props) => { return; // TODO: Validate ... const host = props.account.address; + let formattedPalette = palette.map((color) => '0x' + color); let createWorldParams = { host: host, name: toHex(name), + unique_name: toHex(slug), width: width, height: height, time_between_pixels: timer, - color_palette: palette, + color_palette: formattedPalette, start_time: start, end_time: end }; @@ -61,12 +64,12 @@ const WorldsCreationPanel = (props) => { const { suggestedMaxFee } = await props.estimateInvokeFee({ contractAddress: props.canvasFactoryContract.address, entrypoint: 'create_canvas', - calldata: createWorldsCalldata + calldata: createWorldsCalldata.calldata }); /* global BigInt */ const maxFee = (suggestedMaxFee * BigInt(15)) / BigInt(10); const result = await props.canvasFactoryContract.create_canvas( - createWorldParams.calldata, + createWorldsCalldata.calldata, { maxFee } From c8a25cfdc324bdd368e2321944073eb73978a171 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 21:19:10 -0600 Subject: [PATCH 17/21] Format time for contract call --- frontend/src/tabs/worlds/WorldsCreationPanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/tabs/worlds/WorldsCreationPanel.js b/frontend/src/tabs/worlds/WorldsCreationPanel.js index 4d0d76d8..faa246e7 100644 --- a/frontend/src/tabs/worlds/WorldsCreationPanel.js +++ b/frontend/src/tabs/worlds/WorldsCreationPanel.js @@ -52,8 +52,8 @@ const WorldsCreationPanel = (props) => { height: height, time_between_pixels: timer, color_palette: formattedPalette, - start_time: start, - end_time: end + start_time: start / 1000, + end_time: end / 1000 }; const createWorldsCalldata = props.canvasFactoryContract.populate( 'create_canvas', From c34299da00e214eaf64c3805262f1d6930434f1b Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 21:42:13 -0600 Subject: [PATCH 18/21] Place pixel call --- frontend/src/App.js | 60 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 84c3fc7f..7d4f374d 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -720,6 +720,46 @@ function App() { setExtraPixelsData([...extraPixelsData, ...pixels]); }; + /* global BigInt */ + const placeWorldPixelCall = async (worldId, positions, colors, now) => { + if (devnetMode) return; + if (!address || !multiCanvasContract || !account) return; + if (positions.length !== colors.length) { + console.error('Positions and colors length mismatch'); + return; + } + if (positions.length === 0) { + console.error('No pixels to place'); + return; + } + if (positions.length > 1) { + console.error('Only one pixel placement supported'); + return; + } + const placeWorldPixelCallData = multiCanvasContract.populate( + 'place_pixel', + { + canvas_id: worldId, + pos: positions[0], + color: colors[0], + now: now + } + ); + const { suggestedMaxFee } = await estimateInvokeFee({ + contractAddress: multiCanvasContract.address, + entrypoint: 'place_pixel', + calldata: placeWorldPixelCallData.calldata + }); + const maxFee = (suggestedMaxFee * BigInt(15)) / BigInt(10); + const result = await multiCanvasContract.place_pixel( + placeWorldPixelCallData.calldata, + { + maxFee + } + ); + console.log(result); + }; + const extraPixelPlaceCall = async (positions, colors, now) => { if (devnetMode) return; if (!address || !artPeaceContract || !account) return; @@ -737,7 +777,6 @@ function App() { entrypoint: 'place_extra_pixels', calldata: placeExtraPixelsCallData.calldata }); - /* global BigInt */ const maxFee = (suggestedMaxFee * BigInt(15)) / BigInt(10); const result = await artPeaceContract.place_extra_pixels( placeExtraPixelsCallData.calldata, @@ -806,11 +845,20 @@ function App() { const submit = async () => { let timestamp = Math.floor(Date.now() / 1000); if (!devnetMode) { - await extraPixelPlaceCall( - extraPixelsData.map((pixel) => pixel.x + pixel.y * width), - extraPixelsData.map((pixel) => pixel.colorId), - timestamp - ); + if (worldsMode) { + await placeWorldPixelCall( + openedWorldId, + extraPixelsData.map((pixel) => pixel.x + pixel.y * width), + extraPixelsData.map((pixel) => pixel.colorId), + timestamp + ); + } else { + await extraPixelPlaceCall( + extraPixelsData.map((pixel) => pixel.x + pixel.y * width), + extraPixelsData.map((pixel) => pixel.colorId), + timestamp + ); + } } else { if (worldsMode) { const firstPixel = extraPixelsData[0]; From f554cb97868e320b9d4b64f18dfbb10b25107111 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 17 Jan 2025 21:47:15 -0600 Subject: [PATCH 19/21] Place single pixel call --- frontend/src/App.js | 1 + frontend/src/canvas/CanvasContainer.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 7d4f374d..489bb8ea 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1369,6 +1369,7 @@ function App() { colorPixel={colorPixel} worldsMode={worldsMode} openedWorldId={openedWorldId} + placeWorldPixelCall={placeWorldPixelCall} activeWorld={activeWorld} width={width} height={height} diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index 2157bc63..7c4daf99 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -353,7 +353,16 @@ const CanvasContainer = (props) => { if (!devnetMode) { props.setSelectedColorId(-1); props.colorPixel(position, colorId); - await placePixelCall(position, colorId, timestamp); + if (props.worldsMode) { + await props.placeWorldPixelCall( + props.openedWorldId, + [position], + [colorId], + timestamp + ); + } else { + await placePixelCall(position, colorId, timestamp); + } props.clearPixelSelection(); props.setLastPlacedTime(timestamp * 1000); return; From cc45a61d6e967f8c6a95df0ba419ab8ed26e20bf Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sat, 18 Jan 2025 08:31:50 -0600 Subject: [PATCH 20/21] Add stencil calldata --- frontend/src/tabs/stencils/StencilCreationPanel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/tabs/stencils/StencilCreationPanel.js b/frontend/src/tabs/stencils/StencilCreationPanel.js index 997d9136..60e458c8 100644 --- a/frontend/src/tabs/stencils/StencilCreationPanel.js +++ b/frontend/src/tabs/stencils/StencilCreationPanel.js @@ -18,9 +18,8 @@ const StencilCreationPanel = (props) => { if (!props.address || !props.canvasFactoryContract || !props.account) return; // TODO: Validate the position, width, and height - console.log('Adding Stencil:', position, width, height); + console.log('Adding Stencil:', hash, position, width, height); let addStencilParams = { - world_id: worldId, hash: hash, width: width, height: height, @@ -29,7 +28,8 @@ const StencilCreationPanel = (props) => { const addStencilCalldata = props.canvasFactoryContract.populate( 'add_stencil', { - stencil_metadata: addStencilParams + canvas_id: worldId, + stencil: addStencilParams } ); const { suggestedMaxFee } = await props.estimateInvokeFee({ From aea66b74a9e5470058a0a08b1adeba7fc96c96ee Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sat, 18 Jan 2025 08:58:12 -0600 Subject: [PATCH 21/21] Favorite calls --- frontend/src/tabs/stencils/StencilItem.js | 2 ++ frontend/src/tabs/worlds/WorldItem.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/tabs/stencils/StencilItem.js b/frontend/src/tabs/stencils/StencilItem.js index 15619775..5c209947 100644 --- a/frontend/src/tabs/stencils/StencilItem.js +++ b/frontend/src/tabs/stencils/StencilItem.js @@ -44,6 +44,7 @@ const StencilItem = (props) => { const favoriteCallData = props.canvasFactoryContract.populate( 'favorite_stencil', { + canvas_id: props.openedWorldId, stencil_id: stencilId } ); @@ -70,6 +71,7 @@ const StencilItem = (props) => { const unfavoriteCallData = props.canvasFactoryContract.populate( 'unfavorite_stencil', { + canvas_id: props.openedWorldId, stencil_id: stencilId } ); diff --git a/frontend/src/tabs/worlds/WorldItem.js b/frontend/src/tabs/worlds/WorldItem.js index 5cbbedb7..df2175c5 100644 --- a/frontend/src/tabs/worlds/WorldItem.js +++ b/frontend/src/tabs/worlds/WorldItem.js @@ -43,7 +43,7 @@ const WorldItem = (props) => { const favoriteCallData = props.canvasFactoryContract.populate( 'favorite_canvas', { - world_id: worldId + canvas_id: worldId } ); const { suggestedMaxFee } = await props.estimateInvokeFee({ @@ -69,7 +69,7 @@ const WorldItem = (props) => { const unfavoriteCallData = props.canvasFactoryContract.populate( 'unfavorite_canvas', { - world_id: worldId + canvas_id: worldId } ); const { suggestedMaxFee } = await props.estimateInvokeFee({