diff --git a/frontend/src/canvas/Canvas.js b/frontend/src/canvas/Canvas.js
index b8133f9d..e29ae923 100644
--- a/frontend/src/canvas/Canvas.js
+++ b/frontend/src/canvas/Canvas.js
@@ -1,108 +1,120 @@
+
import React, { useCallback, useRef, useEffect, useState } from 'react'
+import { select, zoom, zoomIdentity } from "d3"
import useWebSocket, { ReadyState } from 'react-use-websocket'
import './Canvas.css';
// import TemplateOverlay from './TemplateOverlay.js';
-import canvasConfig from "../configs/canvas.config.json"
-import backendConfig from "../configs/backend.config.json"
+import canvasConfig from "../configs/canvas.config.json";
+import backendConfig from "../configs/backend.config.json";
+
const Canvas = props => {
const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port
- // TODO: Pressing "Canvas" resets the view / positioning
+ //TODO: Pressing "Canvas" resets the view / positioning
+ //TODO: Way to configure tick rates to give smooth xp for all users
+
+ //Todo: Make this dynamic
+ const minScale = 1;
+ const maxScale = 40;
+
+
+ const canvasRef = useRef(null)
+ const canvasPositionRef = useRef(null)
+ const canvasScaleRef = useRef(null)
- const [canvasPositionX, setCanvasPositionX] = useState(0)
- const [canvasPositionY, setCanvasPositionY] = useState(0)
- const [isDragging, setIsDragging] = useState(false)
- const [dragStartX, setDragStartX] = useState(0)
- const [dragStartY, setDragStartY] = useState(0)
- const [canvasScale, setCanvasScale] = useState(6)
- const minScale = 1 // TODO: To config
- const maxScale = 40
- //TODO: Way to configure tick rates to give smooth xp for all users
-
// Read canvas config from environment variable file json
- const width = canvasConfig.canvas.width
- const height = canvasConfig.canvas.height
- const colors = canvasConfig.colors
+ const width = canvasConfig.canvas.width;
+ const height = canvasConfig.canvas.height;
+ const staticColors = canvasConfig.colors;
+
+ const [colors, setColors] = useState([]);
+ const [setupColors, setSetupColors] = useState(false);
- const WS_URL = "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws"
+ useEffect(() => {
+ if (setupColors) {
+ return;
+ }
+ let getColorsEndpoint = backendUrl + "/get-colors";
+ fetch(getColorsEndpoint, { mode: "cors" }).then((response) => {
+ response.json().then((data) => {
+ let colors = [];
+ for (let i = 0; i < data.length; i++) {
+ colors.push(data[i].hex);
+ }
+ setColors(colors);
+ setSetupColors(true);
+ }).catch((error) => {
+ setColors(staticColors);
+ setSetupColors(true);
+ console.error(error);
+ });
+ });
+ // TODO: Return a cleanup function to close the websocket / ...
+ }, [colors, backendUrl, staticColors, setupColors, setColors]);
+
+ const WS_URL =
+ "ws://" + backendConfig.host + ":" + backendConfig.port + "/ws";
const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(
WS_URL,
{
share: false,
shouldReconnect: () => true,
+
},
)
-
- // TODO: Weird positioning behavior when clicking into devtools
-
- // Handle wheel event for zooming
- const handleWheel = (e) => {
- let newScale = canvasScale
- if (e.deltaY < 0) {
- newScale = Math.min(maxScale, newScale + 0.2)
- } else {
- newScale = Math.max(minScale, newScale - 0.2)
- }
- // TODO: Smart positioning of canvas zoom ( zoom to center of mouse pointer )
- //let newCanvasPositionX = canvasPositionX
- //let newCanvasPositionY = canvasPositionY
- //const canvasOriginX = canvasPositionX + width / 2
- //const canvasOriginY = canvasPositionY + height / 2
- //setCanvasPositionX(newCanvasPositionX)
- //setCanvasPositionY(newCanvasPositionY)
-
- setCanvasScale(newScale)
- }
-
- const handlePointerDown = (e) => {
- setIsDragging(true)
- setDragStartX(e.clientX)
- setDragStartY(e.clientY)
- }
-
- const handlePointerUp = () => {
- setIsDragging(false)
- setDragStartX(0)
- setDragStartY(0)
- }
-
- const handlePointerMove = (e) => {
- if (isDragging) {
- // TODO: Prevent dragging outside of canvas container
- setCanvasPositionX(canvasPositionX + e.clientX - dragStartX)
- setCanvasPositionY(canvasPositionY + e.clientY - dragStartY)
- setDragStartX(e.clientX)
- setDragStartY(e.clientY)
- }
- }
+ // TODO: Weird positioning behavior when clicking into devtools
useEffect(() => {
- document.addEventListener('pointerup', handlePointerUp)
+ const canvas = select(canvasPositionRef.current)
+ const Dzoom = zoom().scaleExtent([minScale, maxScale]).on("zoom", zoomHandler)
+
+ // Set default zoom level and center the canvas
+ canvas
+ .call(Dzoom)
+ .call(Dzoom.transform, zoomIdentity.translate(0, 0).scale(4))
return () => {
- document.removeEventListener('pointerup', handlePointerUp)
- }
- }, [])
+ canvas.on(".zoom", null); // Clean up zoom event listeners
+ };
+ }, []);
+
+ const zoomHandler = (event) => {
+ const ele = canvasScaleRef.current
+ const {
+ k: newScale,
+ x: newCanvasPositionX,
+ y: newCanvasPositionY,
+ } = event.transform;
+ const transformValue = `translate(${newCanvasPositionX}px, ${newCanvasPositionY}px) scale(${newScale})`
+ ele.style.transform = transformValue
+ }
const [setup, setSetup] = useState(false)
- const draw = useCallback((ctx, imageData) => {
- ctx.canvas.width = width
- ctx.canvas.height = height
- ctx.putImageData(imageData, 0, 0)
- // TODO: Use image-rendering for supported browsers?
- }, [width, height])
+
+ const draw = useCallback(
+ (ctx, imageData) => {
+ ctx.canvas.width = width;
+ ctx.canvas.height = height;
+ ctx.putImageData(imageData, 0, 0);
+ // TODO: Use image-rendering for supported browsers?
+ },
+ [width, height]
+ );
useEffect(() => {
+ if (!setupColors) {
+ return;
+ }
if (setup) {
- return
+ return;
}
const canvas = props.canvasRef.current
const context = canvas.getContext('2d')
let getCanvasEndpoint = backendUrl + "/getCanvas"
- fetch(getCanvasEndpoint, {mode: 'cors'}).then(response => {
+ fetch(getCanvasEndpoint, { mode: 'cors' }).then(response => {
return response.arrayBuffer()
}).then(data => {
let colorData = new Uint8Array(data, 0, data.byteLength)
@@ -124,32 +136,42 @@ const Canvas = props => {
dataArray.push(value)
}
}
- let imageDataArray = []
+ let imageDataArray = [];
for (let i = 0; i < dataArray.length; i++) {
- const color = "#" + colors[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 color = "#" + colors[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)
- draw(context, imageData)
- setSetup(true)
- }).catch(error => {
+ const uint8ClampedArray = new Uint8ClampedArray(imageDataArray);
+ const imageData = new ImageData(uint8ClampedArray, width, height);
+ draw(context, imageData);
+ setSetup(true);
+ }).catch((error) => {
//TODO: Notifiy user of error
- console.error(error)
+ console.error(error);
});
- console.log("Connect to websocket")
+ console.log("Connect to websocket");
if (readyState === ReadyState.OPEN) {
sendJsonMessage({
event: "subscribe",
data: {
channel: "general",
},
- })
+ });
}
// TODO: Return a cleanup function to close the websocket / ...
- }, [readyState, sendJsonMessage, setup, colors, width, height, backendUrl, draw])
+ }, [
+ readyState,
+ sendJsonMessage,
+ setup,
+ colors,
+ width,
+ height,
+ backendUrl,
+ draw,
+ setupColors,
+ ]);
const colorPixel = useCallback((position, color) => {
const canvas = props.canvasRef.current
@@ -193,21 +215,26 @@ const Canvas = props => {
}
props.setPixelSelection(x, y)
- const position = y * width + x
- let getPixelInfoEndpoint = backendUrl + "/getPixelInfo?position=" + position.toString()
+ const position = y * width + x;
+ let getPixelInfoEndpoint =
+ backendUrl + "/getPixelInfo?position=" + position.toString();
fetch(getPixelInfoEndpoint, {
- mode: 'cors'
- }).then(response => {
- return response.text()
- }).then(data => {
- // TODO: Cache pixel info & clear cache on update from websocket
- // TODO: Dont query if hover select ( until 1s after hover? )
- props.setPixelPlacedBy(data)
- }).catch(error => {
- console.error(error)
- });
-
- }, [props, width, height, backendUrl])
+ mode: "cors",
+ })
+ .then((response) => {
+ return response.text();
+ })
+ .then((data) => {
+ // TODO: Cache pixel info & clear cache on update from websocket
+ // TODO: Dont query if hover select ( until 1s after hover? )
+ props.setPixelPlacedBy(data);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ },
+ [props, width, height, backendUrl]
+ );
const pixelClicked = (e) => {
if (props.nftSelectionMode) {
@@ -303,53 +330,21 @@ const Canvas = props => {
let templatePosition = x + y * width
// TODO: Template preview
- // TODO: Upload template / ask for template name, reward, ...
- //let addTemplateEndpoint = backendUrl + "/add-template-devnet"
- //let template = new Image()
- //// Read templateImage as data url
- //template.src = props.templateImage
- //template.onload = function() {
- // // TODO: Refactor this
- // let templateWidth = this.width
- // let templateHeight = this.height
- // let templateHash = "0" // TODO
- // let templateReward = "0" // TODO
- // let templateRewardToken = "0" // TODO
- // fetch(addTemplateEndpoint, {
- // mode: "cors",
- // method: "POST",
- // body: JSON.stringify({
- // position: templatePosition.toString(),
- // width: templateWidth.toString(),
- // height: templateHeight.toString(),
- // hash: templateHash,
- // reward: templateReward,
- // rewardToken: templateRewardToken,
- // }),
- // }).then(response => {
- // return response.text()
- // }).then(data => {
- // console.log(data)
- // }).catch(error => {
- // console.error("Error adding template")
- // console.error(error)
- // });
- props.setTemplateImagePositionX(x)
- props.setTemplateImagePositionY(y)
- props.setTemplateImagePosition(templatePosition)
- props.setTemplatePlacedMode(true)
- props.setTemplateCreationMode(false)
- // }
+ props.setTemplateImagePositionX(x)
+ props.setTemplateImagePositionY(y)
+ props.setTemplateImagePosition(templatePosition)
+ props.setTemplatePlacedMode(true)
+ props.setTemplateCreationMode(false)
return
}
pixelSelect(e.clientX, e.clientY)
if (props.selectedColorId === -1) {
- return
+ return;
}
if (props.selectedPositionX === null || props.selectedPositionY === null) {
- return
+ return;
}
if (props.extraPixels > 0) {
@@ -367,6 +362,7 @@ const Canvas = props => {
const position = props.selectedPositionX + props.selectedPositionY * width
const colorIdx = props.selectedColorId
let placePixelEndpoint = backendUrl + "/placePixelDevnet"
+
fetch(placePixelEndpoint, {
mode: "cors",
method: "POST",
@@ -374,53 +370,88 @@ const Canvas = props => {
position: position.toString(),
color: colorIdx.toString(),
}),
- }).then(response => {
- return response.text()
- }).then(data => {
- console.log(data)
- }).catch(error => {
- console.error("Error placing pixel")
- console.error(error)
- });
- props.clearPixelSelection()
- props.setSelectedColorId(-1)
+ })
+ .then((response) => {
+ return response.text();
+ })
+ .then((data) => {
+ console.log(data);
+ })
+ .catch((error) => {
+ console.error("Error placing pixel");
+ console.error(error);
+ });
+ props.clearPixelSelection();
+ props.setSelectedColorId(-1);
// TODO: Optimistic update
+
+
}
-
+
// TODO: Deselect pixel when clicking outside of color palette or pixel
// TODO: Show small position vec in bottom right corner of canvas
const getSelectedColor = () => {
- console.log(props.selectedColorId, props.selectedPositionX, props.selectedPositionY)
+ console.log(
+ props.selectedColorId,
+ props.selectedPositionX,
+ props.selectedPositionY
+ );
if (props.selectedPositionX === null || props.selectedPositionY === null) {
- return null
+ return null;
}
if (props.selectedColorId === -1) {
- return null
+ return null;
}
- return "#" + colors[props.selectedColorId] + "FF"
- }
+ return "#" + colors[props.selectedColorId] + "FF";
+ };
const getSelectorsColor = () => {
if (props.selectedPositionX === null || props.selectedPositionY === null) {
- return null
+ return null;
}
if (props.selectedColorId === -1) {
- let color = props.canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data
- return "#" + color[0].toString(16).padStart(2, '0') + color[1].toString(16).padStart(2, '0') + color[2].toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0')
+ let color = canvasRef.current
+ .getContext("2d")
+ .getImageData(
+ props.selectedPositionX,
+ props.selectedPositionY,
+ 1,
+ 1
+ ).data;
+ return (
+ "#" +
+ color[0].toString(16).padStart(2, "0") +
+ color[1].toString(16).padStart(2, "0") +
+ color[2].toString(16).padStart(2, "0") +
+ color[3].toString(16).padStart(2, "0")
+ );
}
- return "#" + colors[props.selectedColorId] + "FF"
- }
+ return "#" + colors[props.selectedColorId] + "FF";
+ };
const getSelectorsColorInverse = () => {
if (props.selectedPositionX === null || props.selectedPositionY === null) {
- return null
+ return null;
}
if (props.selectedColorId === -1) {
- let color = props.canvasRef.current.getContext('2d').getImageData(props.selectedPositionX, props.selectedPositionY, 1, 1).data
- return "#" + (255 - color[0]).toString(16).padStart(2, '0') + (255 - color[1]).toString(16).padStart(2, '0') + (255 - color[2]).toString(16).padStart(2, '0') + color[3].toString(16).padStart(2, '0')
+ let color = canvasRef.current
+ .getContext("2d")
+ .getImageData(
+ props.selectedPositionX,
+ props.selectedPositionY,
+ 1,
+ 1
+ ).data;
+ return (
+ "#" +
+ (255 - color[0]).toString(16).padStart(2, "0") +
+ (255 - color[1]).toString(16).padStart(2, "0") +
+ (255 - color[2]).toString(16).padStart(2, "0") +
+ color[3].toString(16).padStart(2, "0")
+ );
}
- return "#" + colors[props.selectedColorId] + "FF"
- }
+ return "#" + colors[props.selectedColorId] + "FF";
+ };
// TODO
//const templateImage = [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 4, 3]
@@ -503,9 +534,9 @@ const Canvas = props => {
}
if (props.selectedColorId === -1) {
- return
+ return;
}
- pixelSelect(e.clientX, e.clientY)
+ pixelSelect(e.clientX, e.clientY);
};
window.addEventListener("mousemove", setFromEvent);
@@ -516,12 +547,14 @@ const Canvas = props => {
// TODO: both place options
return (
-
-
-
- { props.pixelSelectedMode && (
-
-
+
+
+
+
+ {props.pixelSelectedMode && (
+
)}
);
-}
+};
-export default Canvas
+export default Canvas;
diff --git a/frontend/src/canvas/PixelSelector.js b/frontend/src/canvas/PixelSelector.js
index 00d00de7..12095b63 100644
--- a/frontend/src/canvas/PixelSelector.js
+++ b/frontend/src/canvas/PixelSelector.js
@@ -1,9 +1,10 @@
import React, {useCallback, useEffect, useState} from 'react';
import './PixelSelector.css';
import canvasConfig from '../configs/canvas.config.json';
+import backendConfig from '../configs/backend.config.json';
const PixelSelector = (props) => {
-
+ const backendUrl = "http://" + backendConfig.host + ":" + backendConfig.port;
const [placedTime, setPlacedTime] = useState(0);
const [timeTillNextPlacement, setTimeTillNextPlacement] = useState("XX:XX"); // TODO: get from server on init
// TODO: Animation for swapping selectorMode
@@ -11,8 +12,34 @@ const PixelSelector = (props) => {
const timeBetweenPlacements = 5000; // 5 seconds TODO: make this a config
const updateInterval = 200; // 200ms
- let colors = canvasConfig.colors;
- colors = colors.map(color => `#${color}FF`);
+ let staticColors = canvasConfig.colors;
+ staticColors = staticColors.map(color => `#${color}FF`);
+
+ const [colors, setColors] = useState([]);
+ const [isSetup, setIsSetup] = useState(false);
+
+ useEffect(() => {
+ if (isSetup) {
+ return;
+ }
+ let getColorsEndpoint = backendUrl + "/get-colors";
+ fetch(getColorsEndpoint, { mode: "cors" }).then((response) => {
+ response.json().then((data) => {
+ let colors = [];
+ for (let i = 0; i < data.length; i++) {
+ colors.push(data[i].hex);
+ }
+ colors = colors.map(color => `#${color}FF`);
+ setColors(colors);
+ setIsSetup(true);
+ }).catch((error) => {
+ setColors(staticColors);
+ setIsSetup(true);
+ console.error(error);
+ });
+ });
+ // TODO: Return a cleanup function to close the websocket / ...
+ }, [colors, backendUrl, staticColors, setColors, setIsSetup, isSetup]);
// TODO: implement extraPixels feature(s)
diff --git a/frontend/src/configs/backend.config.json b/frontend/src/configs/backend.config.json
index b8f73c23..228cf22b 100644
--- a/frontend/src/configs/backend.config.json
+++ b/frontend/src/configs/backend.config.json
@@ -4,5 +4,6 @@
"scripts": {
"place_pixel_devnet": "../tests/integration/local/place_pixel.sh",
"add_template_hash_devnet": "../tests/integration/local/add_template_hash.sh"
- }
+ },
+ "production": false
}
diff --git a/frontend/src/tabs/Account.css b/frontend/src/tabs/Account.css
index e69de29b..5c3a638b 100644
--- a/frontend/src/tabs/Account.css
+++ b/frontend/src/tabs/Account.css
@@ -0,0 +1,64 @@
+.Account__flex {
+ display: flex;
+ margin: 8px 4px;
+}
+
+.Account__flex--center {
+ align-items: center;
+}
+
+.Account__wrap {
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.Account__list {
+ padding-left: 20px;
+ list-style-type: none;
+}
+
+.Account__list li {
+ margin-bottom: 10px;
+ text-indent: -8px;
+}
+
+.Account__list li:before {
+ content: "-";
+ text-indent: -8px;
+}
+
+.Account__input {
+ width: 100%;
+ padding: 6px 10px;
+}
+
+.Account__input:focus {
+ border: 1px solid black;
+ outline: black;
+}
+
+.Account__button {
+ background-color: black;
+ color: #efefef;
+ border: 1px solid black;
+ text-transform: uppercase;
+ cursor: pointer;
+ border-radius: 6px;
+}
+
+.Account__button--edit {
+ padding: 2px 8px;
+ font-size: 8px;
+}
+
+.Account__button--submit {
+ padding: 8px 16px;
+ font-size: 10px;
+}
+
+.Account__user {
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+ width: 100%;
+}
diff --git a/frontend/src/tabs/Account.js b/frontend/src/tabs/Account.js
index 4d846979..f4e3d88b 100644
--- a/frontend/src/tabs/Account.js
+++ b/frontend/src/tabs/Account.js
@@ -1,13 +1,81 @@
-import React from 'react'
-import './Account.css';
-import BasicTab from './BasicTab.js';
+import React, { useState, useEffect } from "react";
+import "./Account.css";
+import BasicTab from "./BasicTab.js";
-const Account = props => {
+const Account = (props) => {
// TODO: Create the account tab w/ wallet address, username, pixel info, top X % users ( pixels placed? ), ...
+ const [username, setUsername] = useState("");
+ const [pixelCount, setPixelCount] = useState(2572);
+ const [accountRank, setAccountRank] = useState("");
+ const [isUsernameSaved, saveUsername] = useState(false);
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ setUsername(username);
+ saveUsername(true);
+ };
+
+ const editUsername = (e) => {
+ saveUsername(false);
+ }
+
+ useEffect(() => {
+ if (pixelCount >= 5000) {
+ setAccountRank("Champion");
+ } else if (pixelCount >= 3000) {
+ setAccountRank("Platinum");
+ } else if (pixelCount >= 2000) {
+ setAccountRank("Gold");
+ } else if (pixelCount >= 1000) {
+ setAccountRank("Silver");
+ } else {
+ setAccountRank("Bronze");
+ }
+ });
return (
+
+
Address:
+
+ 0x0000000000000000000000000000000000000000000000000000000000000000
+
+
+
+
Username:
+ {isUsernameSaved ? (
+
+
{username}
+
+
+ ) : (
+
+ )}
+
+
+
Pixel count:
+
{pixelCount}
+
+
+
Current Rank:
+
{accountRank}
+
);
-}
+};
export default Account;
diff --git a/frontend/src/tabs/TabPanel.js b/frontend/src/tabs/TabPanel.js
index c13535be..da677cce 100644
--- a/frontend/src/tabs/TabPanel.js
+++ b/frontend/src/tabs/TabPanel.js
@@ -9,12 +9,20 @@ import Account from './Account.js';
const TabPanel = props => {
return (
-
- { props.activeTab === 'Quests' &&
}
- { props.activeTab === 'Vote' &&
}
- { props.activeTab === 'Templates' &&
}
- { props.activeTab === 'NFTs' &&
}
- { props.activeTab === 'Account' &&
}
+
+ {props.activeTab === "Quests" && (
+
+ )}
+ {props.activeTab === "Vote" && (
+
+ )}
+ {props.activeTab === "Templates" && (
+
+ )}
+ {props.activeTab === "NFTs" && (
+
+ )}
+ {props.activeTab === "Account" &&
}
);
}
diff --git a/frontend/src/tabs/Voting.js b/frontend/src/tabs/Voting.js
index bc6db763..06f4b422 100644
--- a/frontend/src/tabs/Voting.js
+++ b/frontend/src/tabs/Voting.js
@@ -11,42 +11,12 @@ const Voting = props => {
const [votes, setVotes] = useState(colorVotes);
const [userVote, setUserVote] = useState(-1);
// TODO: Pull from API
- const timeTillVote = '05:14:23';
- const [time, setTime] = useState(timeTillVote);
-
- useEffect(() => {
- const interval = setInterval(() => {
- setTime(time => {
- let timeSplit = time.split(':');
- let hours = parseInt(timeSplit[0]);
- let minutes = parseInt(timeSplit[1]);
- let seconds = parseInt(timeSplit[2]);
- if (seconds === 0) {
- if (minutes === 0) {
- if (hours === 0) {
- return '00:00:00';
- }
- hours--;
- minutes = 59;
- seconds = 59;
- } else {
- minutes--;
- seconds = 59;
- }
- } else {
- seconds--;
- }
- return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
- });
- }, 1000);
- return () => clearInterval(interval);
- }, [time]);
return (
Color Vote
Vote for a new palette color.
- Vote closes: {time}
+ Vote closes: {props.timeLeftInDay}
Vote
diff --git a/frontend/src/tabs/quests/Quests.js b/frontend/src/tabs/quests/Quests.js
index 3b024eb1..b59dfc9e 100644
--- a/frontend/src/tabs/quests/Quests.js
+++ b/frontend/src/tabs/quests/Quests.js
@@ -1,54 +1,92 @@
-import React from 'react'
-import './Quests.css';
-import BasicTab from '../BasicTab.js';
-import QuestItem from './QuestItem.js';
+import React, { useState, useEffect } from "react";
+import "./Quests.css";
+import BasicTab from "../BasicTab.js";
+import QuestItem from "./QuestItem.js";
-const Quests = props => {
+const Quests = (props) => {
+ const [dailyQuests, setDailyQuests] = useState([]);
+ const [mainQuests, setMainQuests] = useState([]);
+
+ useEffect(() => {
+ const fetchQuests = async () => {
+ try {
+ // Fetching daily quests from backend
+ const dailyResponse = await fetch('http://localhost:8080/getDailyQuests');
+ const dailyData = await dailyResponse.json();
+ setDailyQuests(dailyData);
+
+ // Fetching main quests from backend
+ const mainResponse = await fetch('http://localhost:8080/getMainQuests');
+ const mainData = await mainResponse.json();
+ setMainQuests(mainData);
+ } catch (error) {
+ console.error('Failed to fetch quests', error);
+ }
+ };
+
+ fetchQuests();
+ }, []);
// TODO: Main quests should be scrollable
// TODO: Main quests should be moved to the bottom on complete
// TODO: Pull quests from backend
// TODO: Links in descriptions
- const dailyQuests = [
+
+
+
+ const localDailyQuests = [
{
title: "Place 10 pixels",
description: "Add 10 pixels on the canvas",
reward: "3",
- status: "completed"
+ status: "completed",
},
{
title: "Build a template",
description: "Create a template for the community to use",
reward: "3",
- status: "claim"
+ status: "claim",
},
{
title: "Deploy a Memecoin",
description: "Create an Unruggable memecoin",
reward: "10",
- status: "completed"
- }
- ]
+ status: "completed",
+ },
+ ];
- const mainQuests = [
+ const localMainQuests = [
{
title: "Tweet #art/peace",
description: "Tweet about art/peace using the hashtag & addr",
reward: "10",
- status: "incomplete"
+ status: "incomplete",
},
{
title: "Place 100 pixels",
description: "Add 100 pixels on the canvas",
reward: "10",
- status: "completed"
+ status: "completed",
},
{
title: "Mint an art/peace NFT",
description: "Mint an NFT using the art/peace theme",
reward: "5",
- status: "incomplete"
+ status: "incomplete",
+ },
+ ];
+
+ const sortByCompleted = (arr) => {
+ if (!arr) return [];
+ const newArray = [];
+ for (let i = 0; i < arr.length; i++) {
+ if (arr[i].status == "completed") {
+ newArray.push(arr[i]);
+ } else {
+ newArray.unshift(arr[i]);
+ }
}
- ]
+ return newArray;
+ };
// TODO: Icons for each tab?
return (
@@ -56,19 +94,49 @@ const Quests = props => {
Dailys
-
XX:XX:XX
+
{props.timeLeftInDay}
- {dailyQuests.map((quest, index) => (
-
+ {sortByCompleted(dailyQuests).map((quest, index) => (
+
))}
-
+ {sortByCompleted(localDailyQuests).map((quest, index) => (
+
+ ))}
+
Main
- {mainQuests.map((quest, index) => (
-
+ {sortByCompleted(mainQuests).map((quest, index) => (
+
+ ))}
+ {sortByCompleted(localMainQuests).map((quest, index) => (
+
))}
);
-}
+};
export default Quests;
diff --git a/indexer/Dockerfile b/indexer/Dockerfile
index 993bcb38..69dad95e 100644
--- a/indexer/Dockerfile
+++ b/indexer/Dockerfile
@@ -1,6 +1,6 @@
FROM quay.io/apibara/sink-webhook:0.6.0 as sink-webhook
WORKDIR /indexer
-COPY ./indexer/docker-script.js .
+COPY ./indexer/script.js .
-CMD ["run", "docker-script.js", "--allow-env", "/deployment/.env"]
+CMD ["run", "script.js", "--allow-env", "/configs/configs.env", "--allow-env-from-env", "BACKEND_TARGET_URL,APIBARA_STREAM_URL"]
diff --git a/indexer/README.md b/indexer/README.md
index 9380a3d1..ebfff6f2 100644
--- a/indexer/README.md
+++ b/indexer/README.md
@@ -6,5 +6,9 @@ This directory contains the Apibara indexer setup for `art/peace`, which indexes
```
# Setup Indexer/DNA w/ docker compose or other options
-apibara run scripts.js
+# Create an indexer.env file with the following :
+# ART_PEACE_CONTRACT_ADDRESS=... # Example: 0x78223f7ab13216727ed426380079c169578cafad83a3178c7b33ba7ca307713
+# APIBARA_STREAM_URL=... # Example: http://localhost:7171
+# BACKEND_TARGET_URL=... # Example: http://localhost:8080/consumeIndexerMsg
+apibara run scripts.js --allow-env indexer.env
```
diff --git a/indexer/docker-script.js b/indexer/docker-script.js
deleted file mode 100644
index f5b12fc6..00000000
--- a/indexer/docker-script.js
+++ /dev/null
@@ -1,40 +0,0 @@
-export const config = {
- streamUrl: "http://art-peace-apibara-1:7171",
- startingBlock: 0,
- network: "starknet",
- finality: "DATA_STATUS_PENDING",
- filter: {
- events: [
- {
- fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"),
- keys: ["0x2D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23"],
- includeReverted: false,
- includeTransaction: false,
- includeReceipt: false,
- },
- {
- fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"),
- keys: ["0x30826E0CD9A517F76E857E3F3100FE5B9098E9F8216D3DB283FB4C9A641232F"],
- includeReverted: false,
- includeTransaction: false,
- includeReceipt: false,
- },
- {
- fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"),
- keys: ["0x3E18EC266FE76A2EFCE73F91228E6E04456B744FC6984C7A6374E417FB4BF59"],
- includeReverted: false,
- includeTransaction: false,
- includeReceipt: false,
- },
- ],
- },
- sinkType: "webhook",
- sinkOptions: {
- targetUrl: "http://art-peace-backend-1:8080/consumeIndexerMsg"
- },
-};
-
-// This transform does nothing.
-export default function transform(block) {
- return block;
-}
diff --git a/indexer/script.js b/indexer/script.js
index c0c0b29b..ce8412c2 100644
--- a/indexer/script.js
+++ b/indexer/script.js
@@ -1,5 +1,5 @@
export const config = {
- streamUrl: "http://localhost:7171",
+ streamUrl: Deno.env.get("APIBARA_STREAM_URL"),
startingBlock: 0,
network: "starknet",
finality: "DATA_STATUS_PENDING",
@@ -12,11 +12,25 @@ export const config = {
includeTransaction: false,
includeReceipt: false,
},
+ {
+ fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"),
+ keys: ["0x30826E0CD9A517F76E857E3F3100FE5B9098E9F8216D3DB283FB4C9A641232F"],
+ includeReverted: false,
+ includeTransaction: false,
+ includeReceipt: false,
+ },
+ {
+ fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"),
+ keys: ["0x3E18EC266FE76A2EFCE73F91228E6E04456B744FC6984C7A6374E417FB4BF59"],
+ includeReverted: false,
+ includeTransaction: false,
+ includeReceipt: false,
+ },
],
},
sinkType: "webhook",
sinkOptions: {
- targetUrl: "http://localhost:8080/consumeIndexerMsg"
+ targetUrl: Deno.env.get("BACKEND_TARGET_URL"),
},
};
diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo
index 4097103e..5c6a3325 100644
--- a/onchain/src/lib.cairo
+++ b/onchain/src/lib.cairo
@@ -3,6 +3,7 @@ pub mod interfaces;
use art_peace::ArtPeace;
use interfaces::{IArtPeace, IArtPeaceDispatcher, IArtPeaceDispatcherTrait, Pixel};
+
mod quests {
pub mod interfaces;
pub mod pixel_quest;
@@ -33,7 +34,22 @@ mod nfts {
};
}
+mod username_store {
+ pub mod interfaces;
+ pub mod username_store;
+
+ use interfaces::{IUsernameStore, IUsernameStoreDispatcher, IUsernameStoreDispatcherTrait};
+ use username_store::UsernameStore;
+}
+
+mod mocks {
+ pub mod erc20_mock;
+}
+
#[cfg(test)]
mod tests {
mod art_peace;
+ mod username_store;
+ pub(crate) mod utils;
}
+
diff --git a/onchain/src/mocks/erc20_mock.cairo b/onchain/src/mocks/erc20_mock.cairo
new file mode 100644
index 00000000..8bd72de4
--- /dev/null
+++ b/onchain/src/mocks/erc20_mock.cairo
@@ -0,0 +1,42 @@
+//
+// https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/tests/mocks/erc20_mocks.cairo
+//
+
+#[starknet::contract]
+pub mod SnakeERC20Mock {
+ use openzeppelin::token::erc20::ERC20Component;
+ use starknet::ContractAddress;
+
+ component!(path: ERC20Component, storage: erc20, event: ERC20Event);
+
+ #[abi(embed_v0)]
+ impl ERC20Impl = ERC20Component::ERC20Impl
;
+ #[abi(embed_v0)]
+ impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl;
+ impl InternalImpl = ERC20Component::InternalImpl;
+
+ #[storage]
+ struct Storage {
+ #[substorage(v0)]
+ erc20: ERC20Component::Storage
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ #[flat]
+ ERC20Event: ERC20Component::Event
+ }
+
+ #[constructor]
+ fn constructor(
+ ref self: ContractState,
+ name: ByteArray,
+ symbol: ByteArray,
+ initial_supply: u256,
+ recipient: ContractAddress
+ ) {
+ self.erc20.initializer(name, symbol);
+ self.erc20._mint(recipient, initial_supply);
+ }
+}
diff --git a/onchain/src/templates/component.cairo b/onchain/src/templates/component.cairo
index e159b069..7fb27ce8 100644
--- a/onchain/src/templates/component.cairo
+++ b/onchain/src/templates/component.cairo
@@ -1,6 +1,9 @@
#[starknet::component]
pub mod TemplateStoreComponent {
use art_peace::templates::interfaces::{ITemplateStore, TemplateMetadata};
+ use core::num::traits::Zero;
+ use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
+ use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
@@ -59,6 +62,11 @@ pub mod TemplateStoreComponent {
let template_id = self.templates_count.read();
self.templates.write(template_id, template_metadata);
self.templates_count.write(template_id + 1);
+
+ if !template_metadata.reward_token.is_zero() && template_metadata.reward != 0 {
+ self.deposit(template_metadata.reward_token, template_metadata.reward);
+ }
+
self.emit(TemplateAdded { id: template_id, metadata: template_metadata });
}
@@ -66,4 +74,27 @@ pub mod TemplateStoreComponent {
self.completed_templates.read(template_id)
}
}
+
+ #[generate_trait]
+ impl InternalImpl<
+ TContractState, +HasComponent
+ > of InternalTrait {
+ fn deposit(
+ ref self: ComponentState,
+ reward_token: ContractAddress,
+ reward_amount: u256
+ ) {
+ let caller_address = get_caller_address();
+ let contract_address = starknet::get_contract_address();
+ assert(!get_caller_address().is_zero(), 'Invalid caller');
+
+ let erc20_dispatcher = IERC20Dispatcher { contract_address: reward_token };
+ let allowance = erc20_dispatcher.allowance(caller_address, contract_address);
+ assert(allowance >= reward_amount, 'Insufficient allowance');
+
+ let success = erc20_dispatcher
+ .transfer_from(caller_address, contract_address, reward_amount);
+ assert(success, 'Transfer failed');
+ }
+ }
}
diff --git a/onchain/src/templates/interfaces.cairo b/onchain/src/templates/interfaces.cairo
index 4aeb4a7a..41f0f967 100644
--- a/onchain/src/templates/interfaces.cairo
+++ b/onchain/src/templates/interfaces.cairo
@@ -1,3 +1,5 @@
+use starknet::ContractAddress;
+
#[derive(Drop, Copy, Serde, starknet::Store)]
pub struct TemplateMetadata {
pub hash: felt252,
@@ -6,7 +8,7 @@ pub struct TemplateMetadata {
pub width: u128,
pub height: u128,
pub reward: u256,
- pub reward_token: starknet::ContractAddress
+ pub reward_token: ContractAddress
}
#[starknet::interface]
diff --git a/onchain/src/tests/art_peace.cairo b/onchain/src/tests/art_peace.cairo
index 17e39a78..616c607f 100644
--- a/onchain/src/tests/art_peace.cairo
+++ b/onchain/src/tests/art_peace.cairo
@@ -1,6 +1,8 @@
use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait};
use art_peace::ArtPeace::InitParams;
use art_peace::quests::pixel_quest::PixelQuest::PixelQuestInitParams;
+use art_peace::mocks::erc20_mock::SnakeERC20Mock;
+use art_peace::tests::utils;
use art_peace::nfts::interfaces::{
IArtPeaceNFTMinterDispatcher, IArtPeaceNFTMinterDispatcherTrait, ICanvasNFTStoreDispatcher,
ICanvasNFTStoreDispatcherTrait, NFTMintParams, NFTMetadata
@@ -13,10 +15,13 @@ use art_peace::templates::interfaces::{
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
+use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait};
+
use snforge_std as snf;
use snforge_std::{CheatTarget, ContractClassTrait};
-use starknet::{ContractAddress, contract_address_const};
+
+use starknet::{ContractAddress, contract_address_const, get_contract_address};
const DAY_IN_SECONDS: u64 = consteval_int!(60 * 60 * 24);
const WIDTH: u128 = 100;
@@ -27,6 +32,10 @@ fn ART_PEACE_CONTRACT() -> ContractAddress {
contract_address_const::<'ArtPeace'>()
}
+fn ERC20_MOCK_CONTRACT() -> ContractAddress {
+ contract_address_const::<'erc20mock'>()
+}
+
fn EMPTY_CALLDATA() -> Span {
array![].span()
}
@@ -187,6 +196,25 @@ fn deploy_nft_contract() -> ContractAddress {
contract.deploy_at(@calldata, NFT_CONTRACT()).unwrap()
}
+
+fn deploy_erc20_mock() -> ContractAddress {
+ let contract = snf::declare("SnakeERC20Mock");
+ let name: ByteArray = "erc20 mock";
+ let symbol: ByteArray = "ERC20MOCK";
+ let initial_supply: u256 = 10 * utils::pow_256(10, 18);
+ let recipient: ContractAddress = get_contract_address();
+
+ let mut calldata: Array = array![];
+ Serde::serialize(@name, ref calldata);
+ Serde::serialize(@symbol, ref calldata);
+ Serde::serialize(@initial_supply, ref calldata);
+ Serde::serialize(@recipient, ref calldata);
+
+ let contract_addr = contract.deploy_at(@calldata, ERC20_MOCK_CONTRACT()).unwrap();
+
+ contract_addr
+}
+
fn warp_to_next_available_time(art_peace: IArtPeaceDispatcher) {
let last_time = art_peace.get_last_placed_time();
snf::start_warp(CheatTarget::One(art_peace.contract_address), last_time + TIME_BETWEEN_PIXELS);
@@ -365,16 +393,13 @@ fn template_full_basic_test() {
assert!(template_store.get_templates_count() == 0, "Templates count is not 0");
+ let erc20_mock: ContractAddress = deploy_erc20_mock();
+
// 2x2 template image
let template_image = array![1, 2, 3, 4];
let template_hash = compute_template_hash(template_image.span());
let template_metadata = TemplateMetadata {
- hash: template_hash,
- position: 0,
- width: 2,
- height: 2,
- reward: 0,
- reward_token: contract_address_const::<0>(),
+ hash: template_hash, position: 0, width: 2, height: 2, reward: 0, reward_token: erc20_mock,
};
template_store.add_template(template_metadata);
@@ -503,3 +528,36 @@ fn nft_mint_test() {
assert!(nft.balance_of(PLAYER1()) == 0, "NFT balance is not correct after transfer");
assert!(nft.balance_of(PLAYER2()) == 1, "NFT balance is not correct after transfer");
}
+
+#[test]
+fn deposit_reward_test() {
+ let art_peace_address = deploy_contract();
+ let art_peace = IArtPeaceDispatcher { contract_address: art_peace_address };
+ let template_store = ITemplateStoreDispatcher { contract_address: art_peace.contract_address };
+
+ let erc20_mock: ContractAddress = deploy_erc20_mock();
+ let reward_amount: u256 = 1 * utils::pow_256(10, 18);
+
+ // 2x2 template image
+ let template_image = array![1, 2, 3, 4];
+ let template_hash = compute_template_hash(template_image.span());
+ let template_metadata = TemplateMetadata {
+ hash: template_hash,
+ position: 0,
+ width: 2,
+ height: 2,
+ reward: reward_amount,
+ reward_token: erc20_mock,
+ };
+
+ IERC20Dispatcher { contract_address: erc20_mock }.approve(art_peace_address, reward_amount);
+
+ template_store.add_template(template_metadata);
+
+ let art_peace_token_balance = IERC20Dispatcher { contract_address: erc20_mock }
+ .balance_of(art_peace_address);
+
+ assert!(
+ art_peace_token_balance == reward_amount, "reward wrongly distributed when adding template"
+ );
+}
diff --git a/onchain/src/tests/username_store.cairo b/onchain/src/tests/username_store.cairo
new file mode 100644
index 00000000..f4f4ee38
--- /dev/null
+++ b/onchain/src/tests/username_store.cairo
@@ -0,0 +1,36 @@
+use snforge_std::{declare, ContractClassTrait};
+use art_peace::username_store::interfaces::{
+ IUsernameStoreDispatcher, IUsernameStoreDispatcherTrait
+};
+use starknet::{ContractAddress, get_caller_address, get_contract_address, contract_address_const};
+
+fn deploy_contract() -> ContractAddress {
+ let contract = declare("UsernameStore");
+
+ return contract.deploy(@ArrayTrait::new()).unwrap();
+}
+
+#[test]
+fn test_claim_username() {
+ let contract_address = deploy_contract();
+ let dispatcher = IUsernameStoreDispatcher { contract_address };
+ dispatcher.claim_username('deal');
+
+ let username_address = dispatcher.get_username('deal');
+
+ assert(contract_address != username_address, 'Username not claimed');
+}
+#[test]
+fn test_transfer_username() {
+ let contract_address = deploy_contract();
+ let dispatcher = IUsernameStoreDispatcher { contract_address };
+ dispatcher.claim_username('devsweet');
+
+ let second_contract_address = contract_address_const::<1>();
+
+ dispatcher.transfer_username('devsweet', second_contract_address);
+
+ let username_address = dispatcher.get_username('devsweet');
+
+ assert(username_address == second_contract_address, 'Username not Transferred');
+}
diff --git a/onchain/src/tests/utils.cairo b/onchain/src/tests/utils.cairo
new file mode 100644
index 00000000..1d9a66bc
--- /dev/null
+++ b/onchain/src/tests/utils.cairo
@@ -0,0 +1,23 @@
+use core::num::traits::Zero;
+
+// Math
+pub(crate) fn pow_256(self: u256, mut exponent: u8) -> u256 {
+ if self.is_zero() {
+ return 0;
+ }
+ let mut result = 1;
+ let mut base = self;
+
+ loop {
+ if exponent & 1 == 1 {
+ result = result * base;
+ }
+
+ exponent = exponent / 2;
+ if exponent == 0 {
+ break result;
+ }
+
+ base = base * base;
+ }
+}
diff --git a/onchain/src/username_store/interfaces.cairo b/onchain/src/username_store/interfaces.cairo
new file mode 100644
index 00000000..04ea5850
--- /dev/null
+++ b/onchain/src/username_store/interfaces.cairo
@@ -0,0 +1,8 @@
+use starknet::ContractAddress;
+
+#[starknet::interface]
+pub trait IUsernameStore {
+ fn claim_username(ref self: TContractState, key: felt252);
+ fn transfer_username(ref self: TContractState, key: felt252, new_Address: ContractAddress);
+ fn get_username(ref self: TContractState, key: felt252) -> ContractAddress;
+}
diff --git a/onchain/src/username_store/username_store.cairo b/onchain/src/username_store/username_store.cairo
new file mode 100644
index 00000000..309a5fe2
--- /dev/null
+++ b/onchain/src/username_store/username_store.cairo
@@ -0,0 +1,79 @@
+pub mod UserNameClaimErrors {
+ pub const USERNAME_CLAIMED: felt252 = 'username_claimed';
+ pub const USERNAME_CANNOT_BE_TRANSFER: felt252 = 'username_cannot_be_transferred';
+}
+
+#[starknet::contract]
+pub mod UsernameStore {
+ use starknet::{get_caller_address, ContractAddress, contract_address_const};
+ use art_peace::username_store::IUsernameStore;
+ use super::UserNameClaimErrors;
+
+ #[storage]
+ struct Storage {
+ usernames: LegacyMap::
+ }
+
+ #[event]
+ #[derive(Drop, starknet::Event)]
+ enum Event {
+ UserNameClaimed: UserNameClaimed,
+ UserNameTransferred: UserNameTransferred
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct UserNameClaimed {
+ #[key]
+ username: felt252,
+ address: ContractAddress
+ }
+
+ #[derive(Drop, starknet::Event)]
+ struct UserNameTransferred {
+ #[key]
+ username: felt252,
+ address: ContractAddress
+ }
+
+ #[abi(embed_v0)]
+ pub impl UsernameStore of IUsernameStore {
+ fn claim_username(ref self: ContractState, key: felt252) {
+ let mut username_address = self.usernames.read(key);
+
+ assert(
+ username_address == contract_address_const::<0>(),
+ UserNameClaimErrors::USERNAME_CLAIMED
+ );
+
+ self.usernames.write(key, get_caller_address());
+
+ self
+ .emit(
+ Event::UserNameClaimed(
+ UserNameClaimed { username: key, address: get_caller_address() }
+ )
+ )
+ }
+
+ fn transfer_username(ref self: ContractState, key: felt252, new_Address: ContractAddress) {
+ let username_address = self.usernames.read(key);
+
+ if username_address != get_caller_address() {
+ core::panic_with_felt252(UserNameClaimErrors::USERNAME_CANNOT_BE_TRANSFER);
+ }
+
+ self.usernames.write(key, new_Address);
+
+ self
+ .emit(
+ Event::UserNameTransferred(
+ UserNameTransferred { username: key, address: new_Address }
+ )
+ )
+ }
+
+ fn get_username(ref self: ContractState, key: felt252) -> ContractAddress {
+ self.usernames.read(key)
+ }
+ }
+}
diff --git a/postgres/init.sql b/postgres/init.sql
index f591448e..420640e9 100644
--- a/postgres/init.sql
+++ b/postgres/init.sql
@@ -41,31 +41,49 @@ CREATE TABLE Days (
);
CREATE INDEX days_dayIndex_index ON Days (dayIndex);
--- TODO: Remove completedStatus & status from Quests?
-CREATE TABLE Quests (
+CREATE TABLE DailyQuests (
key integer NOT NULL PRIMARY KEY,
name text NOT NULL,
description text NOT NULL,
reward integer NOT NULL,
- dayIndex integer NOT NULL,
- completedStatus integer NOT NULL
+ dayIndex integer NOT NULL
);
-CREATE INDEX quests_dayIndex_index ON Quests (dayIndex);
+CREATE INDEX dailyQuests_dayIndex_index ON DailyQuests (dayIndex);
-- TODO: Add calldata field
-CREATE TABLE UserQuests (
+-- Table for storing the daily quests that the user has completed
+CREATE TABLE UserDailyQuests (
key integer NOT NULL PRIMARY KEY,
userAddress char(64) NOT NULL,
questKey integer NOT NULL,
- status integer NOT NULL,
completed boolean NOT NULL,
completedAt timestamp
);
-CREATE INDEX userQuests_userAddress_index ON UserQuests (userAddress);
-CREATE INDEX userQuests_questKey_index ON UserQuests (questKey);
+CREATE INDEX userDailyQuests_userAddress_index ON UserDailyQuests (userAddress);
+CREATE INDEX userDailyQuests_questKey_index ON UserDailyQuests (questKey);
-CREATE TABLE Colors (
+CREATE TABLE MainQuests (
+ key integer NOT NULL PRIMARY KEY,
+ name text NOT NULL,
+ description text NOT NULL,
+ reward integer NOT NULL
+);
+
+-- Table for storing the main quests that the user has completed
+CREATE TABLE UserMainQuests (
key integer NOT NULL PRIMARY KEY,
+ userAddress char(64) NOT NULL,
+ questKey integer NOT NULL,
+ completed boolean NOT NULL,
+ completedAt timestamp
+);
+CREATE INDEX userMainQuests_userAddress_index ON UserMainQuests (userAddress);
+CREATE INDEX userMainQuests_questKey_index ON UserMainQuests (questKey);
+
+-- TODO: key to color_idx
+CREATE TABLE Colors (
+ -- Postgres auto-incrementing primary key
+ key int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
hex text NOT NULL
);
diff --git a/tests/integration/docker/deploy.sh b/tests/integration/docker/deploy.sh
index f6c42029..dc825a95 100755
--- a/tests/integration/docker/deploy.sh
+++ b/tests/integration/docker/deploy.sh
@@ -80,10 +80,10 @@ echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --acc
/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $ART_PEACE_CONTRACT_ADDRESS --function add_nft_contract --calldata $NFT_CONTRACT_ADDRESS
# TODO: Remove these lines?
-echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" > /deployment/.env
-echo "REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /deployment/.env
-echo "NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /deployment/.env
-echo "REACT_APP_NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /deployment/.env
+echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" > /configs/.env
+echo "REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/.env
+echo "NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/.env
+echo "REACT_APP_NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/.env
# TODO
# MULTICALL_TEMPLATE_DIR=$CONTRACT_DIR/tests/multicalls
diff --git a/tests/integration/docker/initialize.sh b/tests/integration/docker/initialize.sh
index 98b8c6d7..cdf1c5d7 100755
--- a/tests/integration/docker/initialize.sh
+++ b/tests/integration/docker/initialize.sh
@@ -9,5 +9,10 @@ echo "Initializing the canvas"
curl http://backend:8080/initCanvas -X POST
echo "Set the contract address"
-CONTRACT_ADDRESS=$(cat /deployment/.env | grep "^ART_PEACE_CONTRACT_ADDRESS" | cut -d '=' -f2)
+CONTRACT_ADDRESS=$(cat /configs/.env | grep "^ART_PEACE_CONTRACT_ADDRESS" | cut -d '=' -f2)
curl http://backend:8080/setContractAddress -X POST -d "$CONTRACT_ADDRESS"
+
+echo "Setup the colors from the color config"
+# flatten colors with quotes and join them with comma and wrap in []
+COLORS=$(cat /configs/canvas.config.json | jq -r '.colors | map("\"\(.)\"") | join(",")')
+curl http://backend:8080/init-colors -X POST -d "[$COLORS]"
diff --git a/tests/integration/local/run.sh b/tests/integration/local/run.sh
index 318fc40f..6e7d4986 100755
--- a/tests/integration/local/run.sh
+++ b/tests/integration/local/run.sh
@@ -77,7 +77,12 @@ INDEXER_SCRIPT_LOG_FILE=$LOG_DIR/indexer_script.log
touch $INDEXER_SCRIPT_LOG_FILE
cd $WORK_DIR/indexer
#TODO: apibara -> postgres automatically?
-ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS apibara run script.js --allow-env-from-env ART_PEACE_CONTRACT_ADDRESS 2>&1 > $INDEXER_SCRIPT_LOG_FILE &
+rm -f $TMP_DIR/indexer.env
+touch $TMP_DIR/indexer.env
+echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> $TMP_DIR/indexer.env
+echo "APIBARA_STREAM_URL=http://localhost:7171" >> $TMP_DIR/indexer.env
+echo "BACKEND_TARGET_URL=http://localhost:8080/consumeIndexerMsg" >> $TMP_DIR/indexer.env
+apibara run script.js --allow-env $TMP_DIR/indexer.env 2>&1 > $INDEXER_SCRIPT_LOG_FILE &
INDEXER_SCRIPT_PID=$!
sleep 2 # Wait for indexer script to start; TODO: Check if indexer script is actually running
@@ -85,6 +90,8 @@ sleep 2 # Wait for indexer script to start; TODO: Check if indexer script is act
echo "Initializing art-peace canvas ..."
curl http://localhost:8080/initCanvas -X POST
curl http://localhost:8080/setContractAddress -X POST -d "$ART_PEACE_CONTRACT_ADDRESS"
+COLORS=$(cat $CANVAS_CONFIG_FILE | jq -r '.colors | map("\"\(.)\"") | join(",")')
+curl http://localhost:8080/init-colors -X POST -d "[$COLORS]"
# Start the art-peace frontend
echo "Starting art-peace frontend ..."
@@ -96,7 +103,7 @@ REACT_CANVAS_CONFIG_FILE=$WORK_DIR/frontend/src/configs/canvas.config.json
REACT_BACKEND_CONFIG_FILE=$WORK_DIR/frontend/src/configs/backend.config.json
cp $CANVAS_CONFIG_FILE $REACT_CANVAS_CONFIG_FILE #TODO: Use a symlink instead?
cp $BACKEND_CONFIG_FILE $REACT_BACKEND_CONFIG_FILE
-REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS REACT_APP_CANVAS_CONFIG_FILE=$REACT_CANVAS_CONFIG_FILE REACT_APP_BACKEND_CONFIG_FILE=$REACT_BACKEND_CONFIG_FILE npm start 2>&1 > $FRONTEND_LOG_FILE &
+npm start 2>&1 > $FRONTEND_LOG_FILE &
FRONTEND_PID=$!
sleep 2 # Wait for frontend to start; TODO: Check if frontend is actually running