From 0cbd56cc51c1f35629d01c0547a411d703de67c9 Mon Sep 17 00:00:00 2001 From: Supreme2580 Date: Wed, 11 Dec 2024 15:06:04 +0100 Subject: [PATCH 1/6] added isStencil --- backend/routes/stencils.go | 55 +++++++++++++++++++ backend/routes/templates.go | 5 +- frontend/src/App.js | 17 ++++++ .../src/tabs/stencils/StencilCreationPanel.js | 14 +++-- frontend/src/tabs/stencils/StencilItem.js | 3 +- 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/backend/routes/stencils.go b/backend/routes/stencils.go index b8c60e92..fab76e5b 100644 --- a/backend/routes/stencils.go +++ b/backend/routes/stencils.go @@ -1,6 +1,8 @@ package routes import ( + "bytes" + "encoding/json" "fmt" "image" "image/color" @@ -32,6 +34,7 @@ func InitStencilsRoutes() { http.HandleFunc("/favorite-stencil-devnet", favoriteStencilDevnet) http.HandleFunc("/unfavorite-stencil-devnet", unfavoriteStencilDevnet) } + http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData) } func InitStencilsStaticRoutes() { @@ -708,3 +711,55 @@ func unfavoriteStencilDevnet(w http.ResponseWriter, r *http.Request) { routeutils.WriteResultJson(w, "Stencil unfavorited in devnet") } + +func getStencilPixelData(w http.ResponseWriter, r *http.Request) { + // Get stencil hash from query params + hash := r.URL.Query().Get("hash") + if hash == "" { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required") + return + } + + // Read the stencil image file + filename := fmt.Sprintf("stencils/stencil-%s.png", hash) + fileBytes, err := os.ReadFile(filename) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusNotFound, "Stencil not found") + return + } + + // Convert image to pixel data + pixelData, err := imageToPixelData(fileBytes, 1) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") + return + } + + // Get image dimensions + img, _, err := image.Decode(bytes.NewReader(fileBytes)) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to decode image") + return + } + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + + // Create response structure + response := struct { + Width int `json:"width"` + Height int `json:"height"` + PixelData []int `json:"pixelData"` + }{ + Width: width, + Height: height, + PixelData: pixelData, + } + + jsonResponse, err := json.Marshal(response) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create response") + return + } + + routeutils.WriteDataJson(w, string(jsonResponse)) +} diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 3d3278f7..998ac43c 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -29,7 +29,7 @@ func InitTemplateRoutes() { http.HandleFunc("/add-template-data", addTemplateData) http.HandleFunc("/get-template-pixel-data", getTemplatePixelData) if !core.ArtPeaceBackend.BackendConfig.Production { - // http.HandleFunc("/add-template-devnet", addTemplateDevnet) + http.HandleFunc("/add-template-devnet", addTemplateDevnet) http.HandleFunc("/add-faction-template-devnet", addFactionTemplateDevnet) http.HandleFunc("/remove-faction-template-devnet", removeFactionTemplateDevnet) http.HandleFunc("/add-chain-faction-template-devnet", addChainFactionTemplateDevnet) @@ -462,6 +462,7 @@ func addTemplateData(w http.ResponseWriter, r *http.Request) { } func getTemplatePixelData(w http.ResponseWriter, r *http.Request) { + // Get template hash from query params hash := r.URL.Query().Get("hash") if hash == "" { routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required") @@ -477,7 +478,7 @@ func getTemplatePixelData(w http.ResponseWriter, r *http.Request) { } // Convert image to pixel data using existing function - pixelData, err := imageToPixelData(fileBytes, 1) + pixelData, err := imageToPixelData(fileBytes, 1) // Scale factor 1 for templates if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") return diff --git a/frontend/src/App.js b/frontend/src/App.js index 0631f69d..6a57de4b 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1111,6 +1111,16 @@ function App() { return []; }; + const getStencilPixelData = async (hash) => { + if (hash !== null) { + const response = await fetchWrapper( + `get-stencil-pixel-data?hash=${hash}` + ); + return response.data; + } + return []; + }; + const getNftPixelData = async (tokenId) => { if (tokenId !== null) { const response = await fetchWrapper( @@ -1139,6 +1149,13 @@ function App() { return; } + // Handle stencil overlay case + if (overlayTemplate.isStencil && overlayTemplate.hash) { + const data = await getStencilPixelData(overlayTemplate.hash); + setTemplatePixels(data); + return; + } + // Handle template overlay case if (overlayTemplate.hash) { const data = await getTemplatePixelData(overlayTemplate.hash); diff --git a/frontend/src/tabs/stencils/StencilCreationPanel.js b/frontend/src/tabs/stencils/StencilCreationPanel.js index a813325d..90921ee4 100644 --- a/frontend/src/tabs/stencils/StencilCreationPanel.js +++ b/frontend/src/tabs/stencils/StencilCreationPanel.js @@ -112,12 +112,16 @@ const StencilCreationPanel = (props) => { }) }); if (addResponse.result) { - // TODO: after tx done, add stencil to backend - // TODO: Double check hash match - // TODO: Update UI optimistically & go to specific faction in factions tab - console.log(addResponse.result); + props.setOverlayTemplate({ + hash: hash, + width: props.stencilImage.width, + height: props.stencilImage.height, + image: props.stencilImage.image, + isStencil: true + }); + props.setTemplateOverlayMode(true); closePanel(); - props.setActiveTab('Stencils'); + props.setActiveTab('Canvas'); } return; } diff --git a/frontend/src/tabs/stencils/StencilItem.js b/frontend/src/tabs/stencils/StencilItem.js index af422245..15619775 100644 --- a/frontend/src/tabs/stencils/StencilItem.js +++ b/frontend/src/tabs/stencils/StencilItem.js @@ -164,7 +164,8 @@ const StencilItem = (props) => { width: props.width, height: props.height, position: props.position, - image: props.image + image: props.image, + isStencil: true }; props.setTemplateOverlayMode(true); props.setOverlayTemplate(template); From 3c965439d6a47d74e8c6df8d627562c2cacb15ec Mon Sep 17 00:00:00 2001 From: Supreme2580 Date: Wed, 11 Dec 2024 18:54:37 +0100 Subject: [PATCH 2/6] removed error --- frontend/src/App.js | 19 ++++++++++--------- frontend/src/tabs/TabPanel.js | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 6a57de4b..b1ef2aee 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -704,17 +704,18 @@ function App() { timestamp ); } else { - let placeExtraPixelsEndpoint = 'place-extra-pixels-devnet'; - const response = await fetchWrapper(placeExtraPixelsEndpoint, { + const formattedData = { + extraPixels: extraPixelsData.map((pixel) => ({ + position: pixel.x + pixel.y * width, + colorId: pixel.colorId + })), + timestamp: timestamp + }; + + const response = await fetchWrapper('place-extra-pixels-devnet', { mode: 'cors', method: 'POST', - body: JSON.stringify({ - extraPixels: extraPixelsData.map((pixel) => ({ - position: pixel.x + pixel.y * canvasConfig.canvas.width, - colorId: pixel.colorId - })), - timestamp: timestamp - }) + body: JSON.stringify(formattedData) }); if (response.result) { console.log(response.result); diff --git a/frontend/src/tabs/TabPanel.js b/frontend/src/tabs/TabPanel.js index e54e55cf..ae7684b2 100644 --- a/frontend/src/tabs/TabPanel.js +++ b/frontend/src/tabs/TabPanel.js @@ -182,6 +182,8 @@ const TabPanel = (props) => { stencilCreationSelected={props.stencilCreationSelected} setStencilCreationSelected={props.setStencilCreationSelected} canvasWidth={props.width} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> From b7ee7a704cf5efe79a5bdc29f32e061c3a8434e2 Mon Sep 17 00:00:00 2001 From: Supreme2580 Date: Thu, 12 Dec 2024 00:34:58 +0100 Subject: [PATCH 3/6] wip --- backend/routes/pixel.go | 114 ++++++++++++++++++++++++++++++++++++---- frontend/src/App.js | 22 +++++++- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 223e42fb..6dce8aeb 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -2,6 +2,8 @@ package routes import ( "context" + "encoding/json" + "fmt" "net/http" "os" "os/exec" @@ -139,11 +141,24 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) { } type ExtraPixelJson struct { - ExtraPixels []map[string]int `json:"extraPixels"` - Timestamp int `json:"timestamp"` + ExtraPixels []struct { + Position int `json:"position"` + ColorId int `json:"colorId"` + } `json:"extraPixels"` + Timestamp int `json:"timestamp"` } func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) { + // Handle CORS + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + // Disable this in production if routeutils.NonProductionMiddleware(w, r) { return @@ -151,28 +166,105 @@ func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) { jsonBody, err := routeutils.ReadJsonBody[ExtraPixelJson](r) if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid JSON request body") + routeutils.WriteErrorJson(w, http.StatusBadRequest, fmt.Sprintf("Invalid JSON request body: %v", err)) + return + } + + // Validate input + if len(jsonBody.ExtraPixels) == 0 { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "No pixels provided") return } shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlaceExtraPixelsDevnet contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - positions := strconv.Itoa(len(jsonBody.ExtraPixels)) - colors := strconv.Itoa(len(jsonBody.ExtraPixels)) + if contract == "" { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Contract address not configured") + return + } + + // Format calldata for the contract + // The contract expects: [pixel_count, pos1, pos2, ..., posN, color1, color2, ..., colorN, timestamp] + var args []string + args = append(args, contract, "place_extra_pixels") + + // Add pixel count + pixelCount := len(jsonBody.ExtraPixels) + args = append(args, strconv.Itoa(pixelCount)) + + // Add positions for _, pixel := range jsonBody.ExtraPixels { - positions += " " + strconv.Itoa(pixel["position"]) - colors += " " + strconv.Itoa(pixel["colorId"]) + args = append(args, strconv.Itoa(pixel.Position)) } - cmd := exec.Command(shellCmd, contract, "place_extra_pixels", positions, colors, strconv.Itoa(jsonBody.Timestamp)) - _, err = cmd.Output() + // Add colors + for _, pixel := range jsonBody.ExtraPixels { + args = append(args, strconv.Itoa(pixel.ColorId)) + } + + // Add timestamp + args = append(args, strconv.Itoa(jsonBody.Timestamp)) + + // Execute the command + cmd := exec.Command(shellCmd, args...) + output, err := cmd.CombinedOutput() if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to place extra pixels on devnet") + routeutils.WriteErrorJson(w, http.StatusInternalServerError, + fmt.Sprintf("Failed to place extra pixels on devnet: %v - Output: %s", err, string(output))) return } - routeutils.WriteResultJson(w, "Extra pixels placed") + // Create response structure + response := struct { + Message string `json:"message"` + Data struct { + PixelsPlaced int `json:"pixelsPlaced"` + Positions []int `json:"positions"` + Colors []int `json:"colors"` + Timestamp int `json:"timestamp"` + } `json:"data"` + }{ + Message: "Extra pixels placed successfully", + Data: struct { + PixelsPlaced int `json:"pixelsPlaced"` + Positions []int `json:"positions"` + Colors []int `json:"colors"` + Timestamp int `json:"timestamp"` + }{ + PixelsPlaced: pixelCount, + Positions: extractPositions(jsonBody.ExtraPixels), + Colors: extractColors(jsonBody.ExtraPixels), + Timestamp: jsonBody.Timestamp, + }, + } + + // Write response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// Helper functions to extract positions and colors +func extractPositions(pixels []struct { + Position int `json:"position"` + ColorId int `json:"colorId"` +}) []int { + positions := make([]int, len(pixels)) + for i, pixel := range pixels { + positions[i] = pixel.Position + } + return positions +} + +func extractColors(pixels []struct { + Position int `json:"position"` + ColorId int `json:"colorId"` +}) []int { + colors := make([]int, len(pixels)) + for i, pixel := range pixels { + colors[i] = pixel.ColorId + } + return colors } func placePixelRedis(w http.ResponseWriter, r *http.Request) { diff --git a/frontend/src/App.js b/frontend/src/App.js index b1ef2aee..b7aa93ae 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -611,7 +611,27 @@ function App() { }; const extraPixelPlaceCall = async (positions, colors, now) => { - if (devnetMode) return; + if (devnetMode) { + try { + const extraPixels = positions.map((pos, idx) => ({ + position: pos, + colorId: colors[idx] + })); + + const response = await fetchWrapper('place-extra-pixels-devnet', { + mode: 'cors', + method: 'POST', + body: JSON.stringify({ + extraPixels: extraPixels, + timestamp: now + }) + }); + return response; + } catch (error) { + console.error('Failed to place extra pixels:', error); + throw error; + } + } if (!address || !artPeaceContract || !account) return; // TODO: Validate inputs const placeExtraPixelsCallData = artPeaceContract.populate( From 26e3656e2f84b4bf0592bb76d76e47ecb1eb2bb2 Mon Sep 17 00:00:00 2001 From: Supreme2580 Date: Thu, 12 Dec 2024 01:58:05 +0100 Subject: [PATCH 4/6] wip --- tests/integration/docker/deploy_worlds.sh | 43 ++++++++++++++----- tests/integration/docker/initialize_worlds.sh | 13 ++++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/tests/integration/docker/deploy_worlds.sh b/tests/integration/docker/deploy_worlds.sh index 921c3d6c..f636c444 100755 --- a/tests/integration/docker/deploy_worlds.sh +++ b/tests/integration/docker/deploy_worlds.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# This script deploys the ArtPeace contract to the StarkNet devnet in docker +# This script deploys both MultiCanvas and ArtPeace contracts to the StarkNet devnet in docker RPC_HOST="devnet" RPC_PORT=5050 @@ -16,9 +16,6 @@ TIMESTAMP=$(date +%s) LOG_DIR=$OUTPUT_DIR/logs/$TIMESTAMP TMP_DIR=$OUTPUT_DIR/tmp/$TIMESTAMP -# TODO: Clean option to remove old logs and state -#rm -rf $OUTPUT_DIR/logs/* -#rm -rf $OUTPUT_DIR/tmp/* mkdir -p $LOG_DIR mkdir -p $TMP_DIR @@ -31,6 +28,8 @@ ACCOUNT_FILE=$TMP_DIR/starknet_accounts.json /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE account add --name $ACCOUNT_NAME --address $ACCOUNT_ADDRESS --private-key $ACCOUNT_PRIVATE_KEY CONTRACT_DIR=$WORK_DIR/onchain + +# First deploy MultiCanvas CANVAS_FACTORY_CLASS_NAME="MultiCanvas" #TODO: Issue if no declare done @@ -40,17 +39,41 @@ echo "Declared class \"$CANVAS_FACTORY_CLASS_NAME\" with hash $CANVAS_FACTORY_CL CALLDATA=$(echo -n $ACCOUNT_ADDRESS) -# Precalculated contract address -# echo "Precalculating contract address..." - -# TODO: calldata passed as parameters echo "Deploying contract \"$CANVAS_FACTORY_CLASS_NAME\"..." -echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $CANVAS_FACTORY_CLASS_HASH --constructor-calldata $CALLDATA" CANVAS_FACTORY_CONTRACT_DEPLOY_RESULT=$(/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $CANVAS_FACTORY_CLASS_HASH --constructor-calldata $CALLDATA | tail -n 1) CANVAS_FACTORY_CONTRACT_ADDRESS=$(echo $CANVAS_FACTORY_CONTRACT_DEPLOY_RESULT | jq -r '.contract_address') echo "Deployed contract \"$CANVAS_FACTORY_CLASS_NAME\" with address $CANVAS_FACTORY_CONTRACT_ADDRESS" +# Then deploy ArtPeace +ART_PEACE_CLASS_NAME="ArtPeace" + +ART_PEACE_CLASS_DECLARE_RESULT=$(cd $CONTRACT_DIR && /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json declare --contract-name $ART_PEACE_CLASS_NAME | tail -n 1) +ART_PEACE_CLASS_HASH=$(echo $ART_PEACE_CLASS_DECLARE_RESULT | jq -r '.class_hash') +echo "Declared class \"$ART_PEACE_CLASS_NAME\" with hash $ART_PEACE_CLASS_HASH" + +CANVAS_CONFIG=$WORK_DIR/configs/canvas.config.json +WIDTH=$(jq -r '.canvas.width' $CANVAS_CONFIG) +HEIGHT=$(jq -r '.canvas.height' $CANVAS_CONFIG) +PLACE_DELAY=30 +COLOR_COUNT=$(jq -r '.colors[]' $CANVAS_CONFIG | wc -l | tr -d ' ') +COLORS=$(jq -r '.colors[]' $CANVAS_CONFIG | sed 's/^/0x/') +VOTABLE_COLOR_COUNT=0 +VOTABLE_COLORS="" +DAILY_NEW_COLORS_COUNT=0 +START_TIME=0 +END_TIME=3000000000 +DAILY_QUESTS_COUNT=0 +DEVNET_MODE=1 + +CALLDATA=$(echo -n $ACCOUNT_ADDRESS $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $VOTABLE_COLOR_COUNT $VOTABLE_COLORS $DAILY_NEW_COLORS_COUNT $START_TIME $END_TIME $DAILY_QUESTS_COUNT $DEVNET_MODE) + +echo "Deploying contract \"$ART_PEACE_CLASS_NAME\"..." +ART_PEACE_CONTRACT_DEPLOY_RESULT=$(/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $ART_PEACE_CLASS_HASH --constructor-calldata $CALLDATA | tail -n 1) +ART_PEACE_CONTRACT_ADDRESS=$(echo $ART_PEACE_CONTRACT_DEPLOY_RESULT | jq -r '.contract_address') +echo "Deployed contract \"$ART_PEACE_CLASS_NAME\" with address $ART_PEACE_CONTRACT_ADDRESS" -# TODO: Remove these lines? +# Write both addresses to configs.env echo "CANVAS_FACTORY_CONTRACT_ADDRESS=$CANVAS_FACTORY_CONTRACT_ADDRESS" > /configs/configs.env echo "REACT_APP_CANVAS_FACTORY_CONTRACT_ADDRESS=$CANVAS_FACTORY_CONTRACT_ADDRESS" >> /configs/configs.env +echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/configs.env +echo "REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/configs.env diff --git a/tests/integration/docker/initialize_worlds.sh b/tests/integration/docker/initialize_worlds.sh index 61770016..362eca45 100755 --- a/tests/integration/docker/initialize_worlds.sh +++ b/tests/integration/docker/initialize_worlds.sh @@ -5,7 +5,12 @@ sleep 10 echo "Deploying the worlds multicanvas application" ./deploy_worlds.sh -echo "Set the contract address" -CONTRACT_ADDRESS=$(cat /configs/configs.env | grep "^CANVAS_FACTORY_CONTRACT_ADDRESS" | cut -d '=' -f2) -echo "Setting the contract address to $CONTRACT_ADDRESS" -curl http://backend:8080/set-factory-contract-address -X POST -d "$CONTRACT_ADDRESS" +echo "Set the factory contract address" +FACTORY_ADDRESS=$(cat /configs/configs.env | grep "^CANVAS_FACTORY_CONTRACT_ADDRESS" | cut -d '=' -f2) +echo "Setting the factory contract address to $FACTORY_ADDRESS" +curl http://backend:8080/set-factory-contract-address -X POST -d "$FACTORY_ADDRESS" + +echo "Set the art peace contract address" +ART_PEACE_ADDRESS=$(cat /configs/configs.env | grep "^ART_PEACE_CONTRACT_ADDRESS" | cut -d '=' -f2) +echo "Setting the art peace contract address to $ART_PEACE_ADDRESS" +curl http://backend:8080/set-contract-address -X POST -d "$ART_PEACE_ADDRESS" From 4eacb76fc7481b44c5829de2f262891a2c9f2833 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 16 Dec 2024 11:31:17 -0600 Subject: [PATCH 5/6] World stencil coloring --- backend/routes/pixel.go | 114 ++---------------- backend/routes/stencils.go | 47 +++++++- backend/routes/templates.go | 2 +- frontend/src/App.js | 80 ++++++------ .../src/tabs/stencils/StencilCreationPanel.js | 2 +- tests/integration/docker/deploy_worlds.sh | 43 ++----- tests/integration/docker/initialize_worlds.sh | 13 +- 7 files changed, 109 insertions(+), 192 deletions(-) diff --git a/backend/routes/pixel.go b/backend/routes/pixel.go index 6dce8aeb..223e42fb 100644 --- a/backend/routes/pixel.go +++ b/backend/routes/pixel.go @@ -2,8 +2,6 @@ package routes import ( "context" - "encoding/json" - "fmt" "net/http" "os" "os/exec" @@ -141,24 +139,11 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) { } type ExtraPixelJson struct { - ExtraPixels []struct { - Position int `json:"position"` - ColorId int `json:"colorId"` - } `json:"extraPixels"` - Timestamp int `json:"timestamp"` + ExtraPixels []map[string]int `json:"extraPixels"` + Timestamp int `json:"timestamp"` } func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) { - // Handle CORS - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - // Disable this in production if routeutils.NonProductionMiddleware(w, r) { return @@ -166,105 +151,28 @@ func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) { jsonBody, err := routeutils.ReadJsonBody[ExtraPixelJson](r) if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, fmt.Sprintf("Invalid JSON request body: %v", err)) - return - } - - // Validate input - if len(jsonBody.ExtraPixels) == 0 { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "No pixels provided") + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid JSON request body") return } shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlaceExtraPixelsDevnet contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - if contract == "" { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Contract address not configured") - return - } - - // Format calldata for the contract - // The contract expects: [pixel_count, pos1, pos2, ..., posN, color1, color2, ..., colorN, timestamp] - var args []string - args = append(args, contract, "place_extra_pixels") - - // Add pixel count - pixelCount := len(jsonBody.ExtraPixels) - args = append(args, strconv.Itoa(pixelCount)) - - // Add positions + positions := strconv.Itoa(len(jsonBody.ExtraPixels)) + colors := strconv.Itoa(len(jsonBody.ExtraPixels)) for _, pixel := range jsonBody.ExtraPixels { - args = append(args, strconv.Itoa(pixel.Position)) + positions += " " + strconv.Itoa(pixel["position"]) + colors += " " + strconv.Itoa(pixel["colorId"]) } - // Add colors - for _, pixel := range jsonBody.ExtraPixels { - args = append(args, strconv.Itoa(pixel.ColorId)) - } - - // Add timestamp - args = append(args, strconv.Itoa(jsonBody.Timestamp)) - - // Execute the command - cmd := exec.Command(shellCmd, args...) - output, err := cmd.CombinedOutput() + cmd := exec.Command(shellCmd, contract, "place_extra_pixels", positions, colors, strconv.Itoa(jsonBody.Timestamp)) + _, err = cmd.Output() if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, - fmt.Sprintf("Failed to place extra pixels on devnet: %v - Output: %s", err, string(output))) + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to place extra pixels on devnet") return } - // Create response structure - response := struct { - Message string `json:"message"` - Data struct { - PixelsPlaced int `json:"pixelsPlaced"` - Positions []int `json:"positions"` - Colors []int `json:"colors"` - Timestamp int `json:"timestamp"` - } `json:"data"` - }{ - Message: "Extra pixels placed successfully", - Data: struct { - PixelsPlaced int `json:"pixelsPlaced"` - Positions []int `json:"positions"` - Colors []int `json:"colors"` - Timestamp int `json:"timestamp"` - }{ - PixelsPlaced: pixelCount, - Positions: extractPositions(jsonBody.ExtraPixels), - Colors: extractColors(jsonBody.ExtraPixels), - Timestamp: jsonBody.Timestamp, - }, - } - - // Write response - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -// Helper functions to extract positions and colors -func extractPositions(pixels []struct { - Position int `json:"position"` - ColorId int `json:"colorId"` -}) []int { - positions := make([]int, len(pixels)) - for i, pixel := range pixels { - positions[i] = pixel.Position - } - return positions -} - -func extractColors(pixels []struct { - Position int `json:"position"` - ColorId int `json:"colorId"` -}) []int { - colors := make([]int, len(pixels)) - for i, pixel := range pixels { - colors[i] = pixel.ColorId - } - return colors + routeutils.WriteResultJson(w, "Extra pixels placed") } func placePixelRedis(w http.ResponseWriter, r *http.Request) { diff --git a/backend/routes/stencils.go b/backend/routes/stencils.go index fab76e5b..69b3d99a 100644 --- a/backend/routes/stencils.go +++ b/backend/routes/stencils.go @@ -28,13 +28,13 @@ func InitStencilsRoutes() { http.HandleFunc("/get-hot-stencils", getHotStencils) http.HandleFunc("/add-stencil-img", addStencilImg) http.HandleFunc("/add-stencil-data", addStencilData) + http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData) if !core.ArtPeaceBackend.BackendConfig.Production { http.HandleFunc("/add-stencil-devnet", addStencilDevnet) http.HandleFunc("/remove-stencil-devnet", removeStencilDevnet) http.HandleFunc("/favorite-stencil-devnet", favoriteStencilDevnet) http.HandleFunc("/unfavorite-stencil-devnet", unfavoriteStencilDevnet) } - http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData) } func InitStencilsStaticRoutes() { @@ -405,7 +405,7 @@ func addStencilImg(w http.ResponseWriter, r *http.Request) { r.Body.Close() - imageData, err := imageToPixelData(fileBytes, 1) + imageData, err := worldImageToPixelData(fileBytes, 1, 0) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert image to pixel data") return @@ -712,6 +712,47 @@ func unfavoriteStencilDevnet(w http.ResponseWriter, r *http.Request) { routeutils.WriteResultJson(w, "Stencil unfavorited in devnet") } +func worldImageToPixelData(imageData []byte, scaleFactor int, worldId int) ([]int, error) { + img, _, err := image.Decode(bytes.NewReader(imageData)) + if err != nil { + return nil, err + } + + colors, err := core.PostgresQuery[ColorType]("SELECT hex FROM WorldsColors WHERE world_id = $1 ORDER BY color_key", worldId) + if err != nil { + return nil, err + } + + colorCount := len(colors) + palette := make([]color.Color, colorCount) + for i := 0; i < colorCount; i++ { + colorHex := colors[i] + palette[i] = hexToRGBA(colorHex) + } + + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + scaledWidth := width / scaleFactor + scaledHeight := height / scaleFactor + pixelData := make([]int, scaledWidth*scaledHeight) + + for y := 0; y < height; y += scaleFactor { + for x := 0; x < width; x += scaleFactor { + newX := x / scaleFactor + newY := y / scaleFactor + rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) + if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent + pixelData[newY*scaledWidth+newX] = 0xFF + } else { + closestIndex := findClosestColor(rgba, palette) + pixelData[newY*scaledWidth+newX] = closestIndex + } + } + } + + return pixelData, nil +} + func getStencilPixelData(w http.ResponseWriter, r *http.Request) { // Get stencil hash from query params hash := r.URL.Query().Get("hash") @@ -729,7 +770,7 @@ func getStencilPixelData(w http.ResponseWriter, r *http.Request) { } // Convert image to pixel data - pixelData, err := imageToPixelData(fileBytes, 1) + pixelData, err := worldImageToPixelData(fileBytes, 1, 0) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image") return diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 998ac43c..bf13dac4 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -29,7 +29,7 @@ func InitTemplateRoutes() { http.HandleFunc("/add-template-data", addTemplateData) http.HandleFunc("/get-template-pixel-data", getTemplatePixelData) if !core.ArtPeaceBackend.BackendConfig.Production { - http.HandleFunc("/add-template-devnet", addTemplateDevnet) + // http.HandleFunc("/add-template-devnet", addTemplateDevnet) http.HandleFunc("/add-faction-template-devnet", addFactionTemplateDevnet) http.HandleFunc("/remove-faction-template-devnet", removeFactionTemplateDevnet) http.HandleFunc("/add-chain-faction-template-devnet", addChainFactionTemplateDevnet) diff --git a/frontend/src/App.js b/frontend/src/App.js index 48b88cea..5bbbe3b7 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -625,27 +625,7 @@ function App() { }; const extraPixelPlaceCall = async (positions, colors, now) => { - if (devnetMode) { - try { - const extraPixels = positions.map((pos, idx) => ({ - position: pos, - colorId: colors[idx] - })); - - const response = await fetchWrapper('place-extra-pixels-devnet', { - mode: 'cors', - method: 'POST', - body: JSON.stringify({ - extraPixels: extraPixels, - timestamp: now - }) - }); - return response; - } catch (error) { - console.error('Failed to place extra pixels:', error); - throw error; - } - } + if (devnetMode) return; if (!address || !artPeaceContract || !account) return; // TODO: Validate inputs const placeExtraPixelsCallData = artPeaceContract.populate( @@ -731,33 +711,49 @@ function App() { let timestamp = Math.floor(Date.now() / 1000); if (!devnetMode) { await extraPixelPlaceCall( - extraPixelsData.map( - (pixel) => pixel.x + pixel.y * canvasConfig.canvas.width - ), + extraPixelsData.map((pixel) => pixel.x + pixel.y * width), extraPixelsData.map((pixel) => pixel.colorId), timestamp ); } else { - const formattedData = { - extraPixels: extraPixelsData.map((pixel) => ({ - position: pixel.x + pixel.y * width, - colorId: pixel.colorId - })), - timestamp: timestamp - }; - - const response = await fetchWrapper('place-extra-pixels-devnet', { - mode: 'cors', - method: 'POST', - body: JSON.stringify(formattedData) - }); - if (response.result) { - console.log(response.result); + if (worldsMode) { + const firstPixel = extraPixelsData[0]; + const formattedData = { + worldId: openedWorldId.toString(), + position: (firstPixel.x + firstPixel.y * width).toString(), + color: firstPixel.colorId.toString(), + timestamp: timestamp.toString() + }; + + const response = await fetchWrapper('place-world-pixel-devnet', { + mode: 'cors', + method: 'POST', + body: JSON.stringify(formattedData) + }); + if (response.result) { + console.log(response.result); + } + } else { + const formattedData = { + extraPixels: extraPixelsData.map((pixel) => ({ + position: pixel.x + pixel.y * width, + colorId: pixel.colorId + })), + timestamp: timestamp + }; + + const response = await fetchWrapper('place-extra-pixels-devnet', { + mode: 'cors', + method: 'POST', + body: JSON.stringify(formattedData) + }); + if (response.result) { + console.log(response.result); + } } } for (let i = 0; i < extraPixelsData.length; i++) { - let position = - extraPixelsData[i].x + extraPixelsData[i].y * canvasConfig.canvas.width; + let position = extraPixelsData[i].x + extraPixelsData[i].y * width; colorPixel(position, extraPixelsData[i].colorId); } if (basePixelUsed) { @@ -1457,7 +1453,7 @@ function App() { isMobile={isMobile} overlayTemplate={overlayTemplate} templatePixels={templatePixels} - width={canvasConfig.canvas.width} + width={width} canvasRef={canvasRef} addExtraPixel={addExtraPixel} addExtraPixels={addExtraPixels} diff --git a/frontend/src/tabs/stencils/StencilCreationPanel.js b/frontend/src/tabs/stencils/StencilCreationPanel.js index 90921ee4..997d9136 100644 --- a/frontend/src/tabs/stencils/StencilCreationPanel.js +++ b/frontend/src/tabs/stencils/StencilCreationPanel.js @@ -121,7 +121,7 @@ const StencilCreationPanel = (props) => { }); props.setTemplateOverlayMode(true); closePanel(); - props.setActiveTab('Canvas'); + props.setActiveTab('Stencils'); } return; } diff --git a/tests/integration/docker/deploy_worlds.sh b/tests/integration/docker/deploy_worlds.sh index f636c444..921c3d6c 100755 --- a/tests/integration/docker/deploy_worlds.sh +++ b/tests/integration/docker/deploy_worlds.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# This script deploys both MultiCanvas and ArtPeace contracts to the StarkNet devnet in docker +# This script deploys the ArtPeace contract to the StarkNet devnet in docker RPC_HOST="devnet" RPC_PORT=5050 @@ -16,6 +16,9 @@ TIMESTAMP=$(date +%s) LOG_DIR=$OUTPUT_DIR/logs/$TIMESTAMP TMP_DIR=$OUTPUT_DIR/tmp/$TIMESTAMP +# TODO: Clean option to remove old logs and state +#rm -rf $OUTPUT_DIR/logs/* +#rm -rf $OUTPUT_DIR/tmp/* mkdir -p $LOG_DIR mkdir -p $TMP_DIR @@ -28,8 +31,6 @@ ACCOUNT_FILE=$TMP_DIR/starknet_accounts.json /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE account add --name $ACCOUNT_NAME --address $ACCOUNT_ADDRESS --private-key $ACCOUNT_PRIVATE_KEY CONTRACT_DIR=$WORK_DIR/onchain - -# First deploy MultiCanvas CANVAS_FACTORY_CLASS_NAME="MultiCanvas" #TODO: Issue if no declare done @@ -39,41 +40,17 @@ echo "Declared class \"$CANVAS_FACTORY_CLASS_NAME\" with hash $CANVAS_FACTORY_CL CALLDATA=$(echo -n $ACCOUNT_ADDRESS) +# Precalculated contract address +# echo "Precalculating contract address..." + +# TODO: calldata passed as parameters echo "Deploying contract \"$CANVAS_FACTORY_CLASS_NAME\"..." +echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $CANVAS_FACTORY_CLASS_HASH --constructor-calldata $CALLDATA" CANVAS_FACTORY_CONTRACT_DEPLOY_RESULT=$(/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $CANVAS_FACTORY_CLASS_HASH --constructor-calldata $CALLDATA | tail -n 1) CANVAS_FACTORY_CONTRACT_ADDRESS=$(echo $CANVAS_FACTORY_CONTRACT_DEPLOY_RESULT | jq -r '.contract_address') echo "Deployed contract \"$CANVAS_FACTORY_CLASS_NAME\" with address $CANVAS_FACTORY_CONTRACT_ADDRESS" -# Then deploy ArtPeace -ART_PEACE_CLASS_NAME="ArtPeace" - -ART_PEACE_CLASS_DECLARE_RESULT=$(cd $CONTRACT_DIR && /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json declare --contract-name $ART_PEACE_CLASS_NAME | tail -n 1) -ART_PEACE_CLASS_HASH=$(echo $ART_PEACE_CLASS_DECLARE_RESULT | jq -r '.class_hash') -echo "Declared class \"$ART_PEACE_CLASS_NAME\" with hash $ART_PEACE_CLASS_HASH" - -CANVAS_CONFIG=$WORK_DIR/configs/canvas.config.json -WIDTH=$(jq -r '.canvas.width' $CANVAS_CONFIG) -HEIGHT=$(jq -r '.canvas.height' $CANVAS_CONFIG) -PLACE_DELAY=30 -COLOR_COUNT=$(jq -r '.colors[]' $CANVAS_CONFIG | wc -l | tr -d ' ') -COLORS=$(jq -r '.colors[]' $CANVAS_CONFIG | sed 's/^/0x/') -VOTABLE_COLOR_COUNT=0 -VOTABLE_COLORS="" -DAILY_NEW_COLORS_COUNT=0 -START_TIME=0 -END_TIME=3000000000 -DAILY_QUESTS_COUNT=0 -DEVNET_MODE=1 - -CALLDATA=$(echo -n $ACCOUNT_ADDRESS $WIDTH $HEIGHT $PLACE_DELAY $COLOR_COUNT $COLORS $VOTABLE_COLOR_COUNT $VOTABLE_COLORS $DAILY_NEW_COLORS_COUNT $START_TIME $END_TIME $DAILY_QUESTS_COUNT $DEVNET_MODE) - -echo "Deploying contract \"$ART_PEACE_CLASS_NAME\"..." -ART_PEACE_CONTRACT_DEPLOY_RESULT=$(/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json deploy --class-hash $ART_PEACE_CLASS_HASH --constructor-calldata $CALLDATA | tail -n 1) -ART_PEACE_CONTRACT_ADDRESS=$(echo $ART_PEACE_CONTRACT_DEPLOY_RESULT | jq -r '.contract_address') -echo "Deployed contract \"$ART_PEACE_CLASS_NAME\" with address $ART_PEACE_CONTRACT_ADDRESS" -# Write both addresses to configs.env +# TODO: Remove these lines? echo "CANVAS_FACTORY_CONTRACT_ADDRESS=$CANVAS_FACTORY_CONTRACT_ADDRESS" > /configs/configs.env echo "REACT_APP_CANVAS_FACTORY_CONTRACT_ADDRESS=$CANVAS_FACTORY_CONTRACT_ADDRESS" >> /configs/configs.env -echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/configs.env -echo "REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/configs.env diff --git a/tests/integration/docker/initialize_worlds.sh b/tests/integration/docker/initialize_worlds.sh index 362eca45..61770016 100755 --- a/tests/integration/docker/initialize_worlds.sh +++ b/tests/integration/docker/initialize_worlds.sh @@ -5,12 +5,7 @@ sleep 10 echo "Deploying the worlds multicanvas application" ./deploy_worlds.sh -echo "Set the factory contract address" -FACTORY_ADDRESS=$(cat /configs/configs.env | grep "^CANVAS_FACTORY_CONTRACT_ADDRESS" | cut -d '=' -f2) -echo "Setting the factory contract address to $FACTORY_ADDRESS" -curl http://backend:8080/set-factory-contract-address -X POST -d "$FACTORY_ADDRESS" - -echo "Set the art peace contract address" -ART_PEACE_ADDRESS=$(cat /configs/configs.env | grep "^ART_PEACE_CONTRACT_ADDRESS" | cut -d '=' -f2) -echo "Setting the art peace contract address to $ART_PEACE_ADDRESS" -curl http://backend:8080/set-contract-address -X POST -d "$ART_PEACE_ADDRESS" +echo "Set the contract address" +CONTRACT_ADDRESS=$(cat /configs/configs.env | grep "^CANVAS_FACTORY_CONTRACT_ADDRESS" | cut -d '=' -f2) +echo "Setting the contract address to $CONTRACT_ADDRESS" +curl http://backend:8080/set-factory-contract-address -X POST -d "$CONTRACT_ADDRESS" From 9ca2cc27eb432e5ef9d996f6caa08b79b4683a1c Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 16 Dec 2024 11:32:03 -0600 Subject: [PATCH 6/6] Go fmt --- backend/routes/stencils.go | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/backend/routes/stencils.go b/backend/routes/stencils.go index 69b3d99a..2f1f48c4 100644 --- a/backend/routes/stencils.go +++ b/backend/routes/stencils.go @@ -713,44 +713,44 @@ func unfavoriteStencilDevnet(w http.ResponseWriter, r *http.Request) { } func worldImageToPixelData(imageData []byte, scaleFactor int, worldId int) ([]int, error) { - img, _, err := image.Decode(bytes.NewReader(imageData)) - if err != nil { - return nil, err - } - - colors, err := core.PostgresQuery[ColorType]("SELECT hex FROM WorldsColors WHERE world_id = $1 ORDER BY color_key", worldId) - if err != nil { - return nil, err - } - - colorCount := len(colors) - palette := make([]color.Color, colorCount) - for i := 0; i < colorCount; i++ { - colorHex := colors[i] - palette[i] = hexToRGBA(colorHex) - } - - bounds := img.Bounds() - width, height := bounds.Max.X, bounds.Max.Y - scaledWidth := width / scaleFactor - scaledHeight := height / scaleFactor - pixelData := make([]int, scaledWidth*scaledHeight) - - for y := 0; y < height; y += scaleFactor { - for x := 0; x < width; x += scaleFactor { - newX := x / scaleFactor - newY := y / scaleFactor - rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) - if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent - pixelData[newY*scaledWidth+newX] = 0xFF - } else { - closestIndex := findClosestColor(rgba, palette) - pixelData[newY*scaledWidth+newX] = closestIndex - } - } - } - - return pixelData, nil + img, _, err := image.Decode(bytes.NewReader(imageData)) + if err != nil { + return nil, err + } + + colors, err := core.PostgresQuery[ColorType]("SELECT hex FROM WorldsColors WHERE world_id = $1 ORDER BY color_key", worldId) + if err != nil { + return nil, err + } + + colorCount := len(colors) + palette := make([]color.Color, colorCount) + for i := 0; i < colorCount; i++ { + colorHex := colors[i] + palette[i] = hexToRGBA(colorHex) + } + + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + scaledWidth := width / scaleFactor + scaledHeight := height / scaleFactor + pixelData := make([]int, scaledWidth*scaledHeight) + + for y := 0; y < height; y += scaleFactor { + for x := 0; x < width; x += scaleFactor { + newX := x / scaleFactor + newY := y / scaleFactor + rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) + if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent + pixelData[newY*scaledWidth+newX] = 0xFF + } else { + closestIndex := findClosestColor(rgba, palette) + pixelData[newY*scaledWidth+newX] = closestIndex + } + } + } + + return pixelData, nil } func getStencilPixelData(w http.ResponseWriter, r *http.Request) {