diff --git a/backend/cmd/video-gen/video.go b/backend/cmd/video-gen/video.go new file mode 100644 index 00000000..6620761d --- /dev/null +++ b/backend/cmd/video-gen/video.go @@ -0,0 +1,45 @@ +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" + "github.com/keep-starknet-strange/art-peace/backend/routes/indexer" +) + +func main() { + 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") + + flag.Parse() + + 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) + } + + databases := core.NewDatabases(databaseConfig) + defer databases.Close() + + core.ArtPeaceBackend = core.NewBackend(databases, canvasConfig, backendConfig, true) + + routes.InitBaseRoutes() + routes.InitCanvasRoutes() + indexer.InitIndexerRoutes() + indexer.StartMessageProcessor() + + core.ArtPeaceBackend.Start(core.ArtPeaceBackend.BackendConfig.ConsumerPort) +} diff --git a/backend/core/backend.go b/backend/core/backend.go index 53a038e3..62c460db 100644 --- a/backend/core/backend.go +++ b/backend/core/backend.go @@ -11,8 +11,7 @@ import ( ) type Backend struct { - Databases *Databases - // TODO: Is this thread safe? + Databases *Databases WSConnections []*websocket.Conn WSConnectionsLock sync.Mutex @@ -41,7 +40,6 @@ func (b *Backend) Start(port int) { func (b *Backend) GetBackendUrl() string { if b.BackendConfig.Production { - // TODO: To config return "https://api.art-peace.net" } else { return fmt.Sprintf("http://%s:%d", b.BackendConfig.Host, b.BackendConfig.Port) diff --git a/backend/routes/contract.go b/backend/routes/contract.go index 8b50cd8b..2b857cb5 100644 --- a/backend/routes/contract.go +++ b/backend/routes/contract.go @@ -22,7 +22,6 @@ func getContractAddress(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, "\""+contractAddress+"\"") } -// TODO: Set env var on infra level in production func setContractAddress(w http.ResponseWriter, r *http.Request) { // Only allow admin to set contract address if routeutils.AdminMiddleware(w, r) { diff --git a/backend/routes/factions.go b/backend/routes/factions.go index 3857be2f..76f36eb4 100644 --- a/backend/routes/factions.go +++ b/backend/routes/factions.go @@ -93,7 +93,7 @@ func initFactions(w http.ResponseWriter, r *http.Request) { return } - // TODO: check if quests already exist + // TODO: check if factions already exist factionJson, err := routeutils.ReadJsonBody[FactionsConfig](r) if err != nil { routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to parse request body") diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 62738660..c93ecca2 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -186,7 +186,6 @@ func processNFTMintedEvent(event IndexerEvent) { x := position % int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) y := position / int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) - // TODO: Name from onchain mint event metadata := map[string]interface{}{ "name": name, "description": "User minted art/peace NFT from the canvas.", diff --git a/backend/routes/nft.go b/backend/routes/nft.go index aac6a465..9d2e70e4 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -42,7 +42,6 @@ func getCanvasNFTAddress(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, "\""+contractAddress+"\"") } -// TODO: Set env var on infra level in production func setCanvasNFTAddress(w http.ResponseWriter, r *http.Request) { // Only allow admin to set contract address if routeutils.AdminMiddleware(w, r) { diff --git a/backend/routes/quests.go b/backend/routes/quests.go index 0a403376..8df85bda 100644 --- a/backend/routes/quests.go +++ b/backend/routes/quests.go @@ -262,7 +262,6 @@ func GetDailyQuests(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(jsonQuests)) } -// TODO: Here func GetMainQuests(w http.ResponseWriter, r *http.Request) { quests, err := core.PostgresQuery[MainQuest]("SELECT key - 1 as quest_id, name, description, reward FROM MainQuests ORDER BY quest_id ASC") if err != nil { @@ -314,6 +313,7 @@ func GetMainUserQuests(w http.ResponseWriter, r *http.Request) { // Add claim params to quests for _, questClaimParam := range questClaimParams { + // TODO: Assumes no gaps in quest ids quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) } diff --git a/backend/routes/user.go b/backend/routes/user.go index 767b1b40..12ba6b3d 100644 --- a/backend/routes/user.go +++ b/backend/routes/user.go @@ -34,7 +34,6 @@ func getUsernameStoreAddress(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, "\""+contractAddress+"\"") } -// TODO: Set env var on infra level in production func setUsernameStoreAddress(w http.ResponseWriter, r *http.Request) { // Only allow admin to set contract address if routeutils.AdminMiddleware(w, r) { diff --git a/backend/video/indexer.env b/backend/video/indexer.env new file mode 100644 index 00000000..60dec162 --- /dev/null +++ b/backend/video/indexer.env @@ -0,0 +1,3 @@ +ART_PEACE_CONTRACT_ADDRESS='0x0384e5fa826e1151715dbc3df0bd1b3aaec234dfc344394d02c39b670e354c48' +CONSUMER_TARGET_URL='http://localhost:8081/consume-indexer-msg' +APIBARA_STREAM_URL='https://mainnet.starknet.a5a.ch' diff --git a/backend/video/video.go b/backend/video/video.go new file mode 100644 index 00000000..d98ded8f --- /dev/null +++ b/backend/video/video.go @@ -0,0 +1,80 @@ +package video + +import ( + "context" + "fmt" + "image" + "image/color" + "image/png" + "os" + "strconv" + + "github.com/keep-starknet-strange/art-peace/backend/core" +) + +func GenerateImageFromCanvas(orderId int) { + ctx := context.Background() + + colorWidth := core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth + canvasWidth := int(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) + canvasHeight := int(core.ArtPeaceBackend.CanvasConfig.Canvas.Height) + + // TODO: Make generic & initialize only once + colorPalette := make([]color.RGBA, 0) + colorPaletteHex := []string{"fafafa", "080808", "ba2112", "ff403d", "ff7714", "ffd115", "f5ff05", "199f27", "00ef3f", "152665", "1542ff", "5cfffe", "a13dff", "ff7ad7", "c1d9e6", "ea1608", "1991f4", "3c3c84", "ff5c5d", "fde578", "74401b", "f86949", "46b093", "d4d7d9"} + for _, colorHex := range colorPaletteHex { + r, err := strconv.ParseInt(colorHex[0:2], 16, 64) + if err != nil { + fmt.Println("Failed to parse hex color: ", colorHex, " Error: ", err) + return + } + g, err := strconv.ParseInt(colorHex[2:4], 16, 64) + if err != nil { + fmt.Println("Failed to parse hex color: ", colorHex, " Error: ", err) + return + } + b, err := strconv.ParseInt(colorHex[4:6], 16, 64) + if err != nil { + fmt.Println("Failed to parse hex color: ", colorHex, " Error: ", err) + return + } + colorPalette = append(colorPalette, color.RGBA{uint8(r), uint8(g), uint8(b), 255}) + } + generatedImage := image.NewRGBA(image.Rect(0, 0, canvasWidth, canvasHeight)) + bitfieldType := "u" + strconv.Itoa(int(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)) + 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() + if err != nil { + fmt.Println("Failed to get bitfield value. Error: ", err) + return + } + color := colorPalette[val[0]] + generatedImage.Set(x, y, color) + } + } + + if _, err := os.Stat("images"); os.IsNotExist(err) { + err := os.Mkdir("images", os.ModePerm) + if err != nil { + fmt.Println("Failed to create images directory. Error: ", err) + return + } + } + + fileName := fmt.Sprintf("images/%d.png", orderId) + f, err := os.Create(fileName) + if err != nil { + fmt.Println("Failed to create image file. Error: ", err) + return + } + defer f.Close() + + if err := png.Encode(f, generatedImage); err != nil { + fmt.Println("Failed to encode image. Error: ", err) + return + } + fmt.Println("Generated image for orderId: ", orderId) +} diff --git a/configs/factions.config.json b/configs/factions.config.json index 8b005c4b..dc210009 100644 --- a/configs/factions.config.json +++ b/configs/factions.config.json @@ -4,7 +4,7 @@ "id": 1, "name": "Ducks Everywhere", "icon": "$BACKEND_URL/faction-images/ducks-everywhere.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -20,7 +20,7 @@ "id": 2, "name": "WASD", "icon": "$BACKEND_URL/faction-images/wasd.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -36,7 +36,7 @@ "id": 3, "name": "Influence", "icon": "$BACKEND_URL/faction-images/influence.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -52,7 +52,7 @@ "id": 4, "name": "Ark Project", "icon": "$BACKEND_URL/faction-images/ark-project.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -68,7 +68,7 @@ "id": 5, "name": "Everai", "icon": "$BACKEND_URL/faction-images/everai.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -84,7 +84,7 @@ "id": 6, "name": "2077 Collective", "icon": "$BACKEND_URL/faction-images/2077.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { @@ -100,7 +100,7 @@ "id": 7, "name": "Argent", "icon": "$BACKEND_URL/faction-images/argent.png", - "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", + "leader": "0x05bd7adfE8AfaA58300aDC72bF5584b191E236987Fe16A217b1a3e067869A0Aa", "joinable": true, "allocation": 1, "links": { diff --git a/frontend/src/utils/TimerInjector.js b/frontend/src/utils/TimerInjector.js index a0caf688..bfd36900 100644 --- a/frontend/src/utils/TimerInjector.js +++ b/frontend/src/utils/TimerInjector.js @@ -88,7 +88,7 @@ export const TimerInjector = ({ children, props, isLastDay, endTimestamp }) => { } const now = new Date(); let thisDayEnd = now; - console.log(props); + // TODO: isLastDay || game ended if (isLastDay) { thisDayEnd = new Date(endTimestamp * 1000); } else { diff --git a/indexer/video-script.js b/indexer/video-script.js new file mode 100644 index 00000000..cff54d83 --- /dev/null +++ b/indexer/video-script.js @@ -0,0 +1,28 @@ +export const config = { + streamUrl: Deno.env.get("APIBARA_STREAM_URL"), + startingBlock: 650_000, + network: "starknet", + finality: "DATA_STATUS_PENDING", + filter: { + events: [ + { + // Pixel Placed Event + fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), + keys: [ + "0x2D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23" + ], + 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/instructions/helm-deploy-steps.txt b/infra/instructions/helm-deploy-steps.txt new file mode 100644 index 00000000..73405569 --- /dev/null +++ b/infra/instructions/helm-deploy-steps.txt @@ -0,0 +1,83 @@ +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 username store to sepolia + ./tests/integration/sepolia/deploy-username-store.sh + save address +deploy art peace to sepolia + vim ./tests/integration/sepolia/deploy.sh + change start & end time + ./tests/integration/sepolia/deploy.sh + save address +deploy canvas nft to sepolia + ./tests/integration/sepolia/deploy-canvas-nft.sh + save address + and set in art_peace contract +deploy quests + vim ./tests/integration/sepolia/deploy_quests.sh + change art peace address, canvas contract address, username store, ... + ./tests/integration/sepolia/deploy_quests.sh + +setup nft contract +setup factions + vim ./tests/integration/sepolia/setup_factions.sh + change art-peace contract + ./tests/integration/sepolia/setup_factions.sh + +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 + 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 + REACT_APP_STARKNET_CONTRACT_ADDRESS= + REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS= + REACT_APP_CANVAS_NFT_CONTRACT_ADDRESS= + ART_PEACE_END_TIME= + ART_PEACE_HOST= + + + 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 + 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 + + setup canvas, quests, and faction backend + + + + +video: + redis-server + redis-cli del canvas + backend/video/video.go - set color palette + backend/routes/indexer/route.go - add video.GenerateImageFromCanvas(message.Data.Cursor.OrderKey) after LastFinalizedCursor = message.Data.Cursor.OrderKey + go run cmd/video-gen/video.go + curl http://localhost:8081/init-canvas + setup indexer.env for art peace contract + AUTH_TOKEN=dna_abc apibara run indexer/video-script.js --allow-env backend/video/indexer.env + render video from frames in blender diff --git a/infra/instructions/kube-deploy-steps.txt b/infra/instructions/kube-deploy-steps.txt new file mode 100644 index 00000000..0259b30b --- /dev/null +++ b/infra/instructions/kube-deploy-steps.txt @@ -0,0 +1,91 @@ +setup .env w/ STARKNET_KEYSTORE=$HOME/.starkli-sepolia/starkli-keystore.json + STARKNET_ACCOUNT=$HOME/.starkli-sepolia/starkli-account.json + DOCKER_VERSION=new version +source .env + +deploy username store to sepolia + ./tests/integration/sepolia/deploy-username-store.sh + save address +deploy art peace to sepolia + vim ./tests/integration/sepolia/deploy.sh + change end time + ./tests/integration/sepolia/deploy.sh + save address +deploy canvas nft to sepolia + ./tests/integration/sepolia/deploy-canvas-nft.sh + save address + and set in art_peace contract +deploy quests + vim ./tests/integration/sepolia/deploy-daily-quests.sh + change art peace address + ./tests/integration/sepolia/deploy-daily-quests.sh + save address(es) + add quests to art peace + +copy abis to frontend + jq -r '.abi' onchain/target/dev/art_peace_UsernameStore.contract_class.json > frontend/src/contracts/username_store.abi.json + jq -r '.abi' onchain/target/dev/art_peace_ArtPeace.contract_class.json > frontend/src/contracts/art_peace.abi.json + +build prod docker images w/ new version & push to docker hub + docker build . -f backend/Dockerfile.prod -t "brandonjroberts/art-peace-backend:$DOCKER_VERSION" + docker build . -f backend/Dockerfile.consumer.prod -t "brandonjroberts/art-peace-consumer:$DOCKER_VERSION" + docker build . -f indexer/Dockerfile.prod -t "brandonjroberts/art-peace-indexer:$DOCKER_VERSION" + docker push "brandonjroberts/art-peace-backend:$DOCKER_VERSION" + docker push "brandonjroberts/art-peace-consumer:$DOCKER_VERSION" + docker push "brandonjroberts/art-peace-indexer:$DOCKER_VERSION" + +clean old infra / volumes if needed + cloud console + + kubectl delete deployment.apps/backend deployment.apps/consumer deployment.apps/indexer deployment.apps/postgres deployment.apps/redis + kubectl delete pvc postgres-volume-claim redis-volume-claim nft-volume-claim + kubectl get pv + kubectl delete pv's above --grace-period=0 --force + +apply kubectl changes + clone / pull latest main / branch art-peace + git clone https://github.com/keep-starknet-strange/art-peace.git + cd art-peace & checkout + cd infra + change configmap values + vim backend/backend-configmap.yaml + vim consumer/consumer-configmap.yaml + vim indexer/indexer-configmap.yaml + vim postgres/postgres-configmap.yaml + change image tags in deployments + vim backend/backend-deployment.yaml + vim consumer/consumer-deployment.yaml + vim indexer/indexer-deployment.yaml + apply all + kubectl apply -f backend/backend-configmap.yaml + kubectl apply -f consumer/consumer-configmap.yaml + kubectl apply -f indexer/indexer-configmap.yaml + kubectl apply -f postgres/postgres-configmap.yaml + kubectl apply -f redis/redis-configmap.yaml + kubectl apply -f postgres/psql-pvc.yaml + kubectl apply -f redis/redis-pvc.yaml + kubectl apply -f backend/nfts-pvc.yaml + kubectl apply -f postgres/psql-deployment.yaml + + 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 apply -f redis/redis-deployment.yaml + kubectl apply -f backend/backend-deployment.yaml + kubectl apply -f consumer/consumer-deployment.yaml + kubectl apply -f indexer/indexer-deployment.yaml + +init backend w/ commands if needed + curl https://api.art-peace.net/init-canvas -X POST + export COLORS=$(cat ../configs/canvas.config.json | jq -r '.colors | map("\"\(.)\"") | join(",")') + curl https://api.art-peace.net/init-colors -X POST -d "[$COLORS]" + export VOTABLE_COLORS=$(cat ../configs/canvas.config.json | jq -r '.votableColors | map("\"\(.)\"") | join(",")') + curl https://api.art-peace.net/init-votable-colors -X POST -d "[$VOTABLE_COLORS]" + export QUESTS_CONFIG_FILE="../configs/quests.config.json" + curl https://api.art-peace.net/init-quests -X POST -d "@$QUESTS_CONFIG_FILE" + +change frontend contract addresses in vercel + https://vercel.com/keep-starknet-strange/art-peace/settings + REACT_APP_STARKNET_CONTRACT_ADDRESS= + REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS= diff --git a/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index f05ea958..4a7c180c 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -824,6 +824,7 @@ pub mod ArtPeace { let mut i = self.main_quests_count.read(); let end = i + quests.len(); while i < end { + // TODO: This should be i - self.main_quests_count.read() self.main_quests.write(i, *quests.at(i)); i += 1; }; diff --git a/onchain/src/nfts/canvas_nft.cairo b/onchain/src/nfts/canvas_nft.cairo index 11b3ad6d..2bdb1f51 100644 --- a/onchain/src/nfts/canvas_nft.cairo +++ b/onchain/src/nfts/canvas_nft.cairo @@ -71,7 +71,6 @@ mod CanvasNFT { #[constructor] fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray) { - // TODO: allow changing base_uri let base_uri = "https://api.art-peace.net/nft-meta/nft-"; self.erc721.initializer(name, symbol, base_uri); } diff --git a/onchain/src/quests/unruggable_quest.cairo b/onchain/src/quests/unruggable_quest.cairo index 8949d414..151810f1 100644 --- a/onchain/src/quests/unruggable_quest.cairo +++ b/onchain/src/quests/unruggable_quest.cairo @@ -51,10 +51,6 @@ pub mod UnruggableQuest { contract_address: coin_address_as_felt252.try_into().unwrap() }; - if coin.owner() != user { - return false; - } - if coin.is_launched() != true { return false; }