From 55c4ae3ec4edad36244ff36caac5a560fc2126ea Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 28 Jun 2024 19:39:16 -0500 Subject: [PATCH] Factions, quests, touch ups, ux, and other features/improvements fully integrated --- backend/config/backend.go | 6 + backend/quests/claim.go | 31 ++ backend/quests/inputs.go | 12 + backend/quests/quests.go | 22 +- backend/quests/status.go | 50 ++- backend/routes/contract.go | 40 ++ backend/routes/factions.go | 200 ++++++++- backend/routes/indexer/faction.go | 9 +- backend/routes/indexer/nft.go | 83 ++-- backend/routes/indexer/pixel.go | 57 ++- backend/routes/indexer/quest.go | 10 +- backend/routes/indexer/route.go | 176 ++++---- backend/routes/nft.go | 15 +- backend/routes/quests.go | 213 ++++++++- backend/routes/user.go | 21 +- configs/backend.config.json | 5 +- configs/docker-backend.config.json | 5 +- configs/factions.config.json | 47 +- configs/production-quests.config.json | 69 +-- configs/quests.config.json | 127 +++++- docker-compose.yml | 1 + frontend/src/App.js | 168 +++++-- frontend/src/canvas/CanvasContainer.js | 2 + frontend/src/footer/PixelSelector.js | 1 + frontend/src/services/apiService.js | 16 + frontend/src/tabs/TabPanel.js | 23 + frontend/src/tabs/account/Account.css | 16 +- frontend/src/tabs/account/Account.js | 66 ++- frontend/src/tabs/canvas/ExtraPixelsPanel.js | 125 +++++- frontend/src/tabs/factions/FactionItem.js | 135 ++++-- frontend/src/tabs/factions/FactionSelector.js | 2 +- frontend/src/tabs/factions/Factions.css | 18 +- frontend/src/tabs/factions/Factions.js | 414 +++++++++++++----- frontend/src/tabs/nfts/NFTItem.css | 38 ++ frontend/src/tabs/nfts/NFTItem.js | 12 +- frontend/src/tabs/nfts/NFTMintingPanel.js | 24 +- frontend/src/tabs/nfts/NFTs.css | 22 +- frontend/src/tabs/nfts/NFTs.js | 82 +++- frontend/src/tabs/quests/QuestItem.css | 3 + frontend/src/tabs/quests/QuestItem.js | 104 +++-- frontend/src/tabs/quests/Quests.css | 5 + frontend/src/tabs/quests/Quests.js | 84 +--- frontend/src/tabs/voting/VoteItem.js | 2 +- frontend/src/tabs/voting/Voting.css | 2 + frontend/src/tabs/voting/Voting.js | 122 ++++-- frontend/src/utils/Consts.js | 4 + indexer/prod-script.js | 25 +- indexer/script.js | 23 +- onchain/src/art_peace.cairo | 86 ++-- onchain/src/interfaces.cairo | 8 +- onchain/src/lib.cairo | 5 +- onchain/src/nfts/component.cairo | 7 + onchain/src/nfts/interfaces.cairo | 4 + onchain/src/quests/chain_faction_quest.cairo | 67 +++ onchain/src/quests/nft_quest.cairo | 13 + onchain/src/tests/chain_faction_quest.cairo | 92 ++++ onchain/src/tests/faction_quest.cairo | 8 +- onchain/src/tests/nft_quest.cairo | 43 +- postgres/init.sql | 50 +++ tests/integration/docker/deploy.sh | 2 +- tests/integration/docker/deploy_quests.sh | 4 +- .../integration/docker/join_chain_faction.sh | 36 ++ tests/integration/docker/join_faction.sh | 36 ++ tests/integration/docker/leave_faction.sh | 36 ++ tests/integration/docker/mint_nft.sh | 4 +- tests/integration/docker/setup_factions.sh | 26 +- 66 files changed, 2539 insertions(+), 725 deletions(-) create mode 100644 backend/quests/claim.go create mode 100644 onchain/src/quests/chain_faction_quest.cairo create mode 100644 onchain/src/tests/chain_faction_quest.cairo create mode 100755 tests/integration/docker/join_chain_faction.sh create mode 100755 tests/integration/docker/join_faction.sh create mode 100755 tests/integration/docker/leave_faction.sh diff --git a/backend/config/backend.go b/backend/config/backend.go index 1153b0d1..fdea3e9f 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -17,6 +17,9 @@ type BackendScriptsConfig struct { NewUsernameDevnet string `json:"new_username_devnet"` ChangeUsernameDevnet string `json:"change_username_devnet"` IncreaseDayDevnet string `json:"increase_day_devnet"` + JoinChainFactionDevnet string `json:"join_chain_faction_devnet"` + JoinFactionDevnet string `json:"join_faction_devnet"` + LeaveFactionDevnet string `json:"leave_faction_devnet"` } type WebSocketConfig struct { @@ -56,6 +59,9 @@ var DefaultBackendConfig = BackendConfig{ NewUsernameDevnet: "../scripts/new_username.sh", ChangeUsernameDevnet: "../scripts/change_username.sh", IncreaseDayDevnet: "../scripts/increase_day_index.sh", + JoinChainFactionDevnet: "../scripts/join_chain_faction.sh", + JoinFactionDevnet: "../scripts/join_faction.sh", + LeaveFactionDevnet: "../scripts/leave_faction.sh", }, Production: false, WebSocket: WebSocketConfig{ diff --git a/backend/quests/claim.go b/backend/quests/claim.go new file mode 100644 index 00000000..6b1e07dd --- /dev/null +++ b/backend/quests/claim.go @@ -0,0 +1,31 @@ +package quests + +import "github.com/keep-starknet-strange/art-peace/backend/core" + +var QuestClaimData = map[int]func(*Quest, string) []int { + NFTMintQuestType: NFTMintQuestClaimData, +} + +func (q *Quest) GetQuestClaimData(user string) []int { + if f, ok := QuestClaimData[q.Type]; ok { + return f(q, user) + } + return nil +} + +func NFTMintQuestClaimData(q *Quest, user string) []int { + nftQuestInputs := NewNFTQuestInputs(q.InputData) + if nftQuestInputs.IsDaily { + tokenId, err := core.PostgresQueryOne[int]("SELECT token_id FROM NFTs WHERE minter = $1 AND day_index = $2", user, nftQuestInputs.ClaimDay) + if err != nil { + return nil + } + return []int{*tokenId} + } else { + tokenId, err := core.PostgresQueryOne[int]("SELECT token_id FROM NFTs WHERE minter = $1", user) + if err != nil { + return nil + } + return []int{*tokenId} + } +} diff --git a/backend/quests/inputs.go b/backend/quests/inputs.go index 4406d59b..8e2b2857 100644 --- a/backend/quests/inputs.go +++ b/backend/quests/inputs.go @@ -16,6 +16,11 @@ type HodlQuestInputs struct { Amount int } +type NFTQuestInputs struct { + IsDaily bool + ClaimDay uint32 +} + func NewPixelQuestInputs(encodedInputs []int) *PixelQuestInputs { return &PixelQuestInputs{ PixelsNeeded: uint32(encodedInputs[0]), @@ -37,3 +42,10 @@ func NewHodlQuestInputs(encodedInputs []int) *HodlQuestInputs { Amount: encodedInputs[0], } } + +func NewNFTQuestInputs(encodedInputs []int) *NFTQuestInputs { + return &NFTQuestInputs{ + IsDaily: encodedInputs[0] == 1, + ClaimDay: uint32(encodedInputs[1]), + } +} diff --git a/backend/quests/quests.go b/backend/quests/quests.go index e44debed..691d5a28 100644 --- a/backend/quests/quests.go +++ b/backend/quests/quests.go @@ -12,21 +12,23 @@ const ( TemplateQuestType UnruggableQuestType VoteQuestType + ChainFactionQuestType FactionQuestType UsernameQuestType ) var OnchainQuestTypes = map[string]int{ - "AuthorityQuest": AuthorityQuestType, - "HodlQuest": HodlQuestType, - "NFTMintQuest": NFTMintQuestType, - "PixelQuest": PixelQuestType, - "RainbowQuest": RainbowQuestType, - "TemplateQuest": TemplateQuestType, - "UnruggableQuest": UnruggableQuestType, - "VoteQuest": VoteQuestType, - "FactionQuest": FactionQuestType, - "UsernameQuest": UsernameQuestType, + "AuthorityQuest": AuthorityQuestType, + "HodlQuest": HodlQuestType, + "NFTMintQuest": NFTMintQuestType, + "PixelQuest": PixelQuestType, + "RainbowQuest": RainbowQuestType, + "TemplateQuest": TemplateQuestType, + "UnruggableQuest": UnruggableQuestType, + "VoteQuest": VoteQuestType, + "ChainFactionQuest": ChainFactionQuestType, + "FactionQuest": FactionQuestType, + "UsernameQuest": UsernameQuestType, } type Quest struct { diff --git a/backend/quests/status.go b/backend/quests/status.go index 30a9d109..fc985c0e 100644 --- a/backend/quests/status.go +++ b/backend/quests/status.go @@ -5,16 +5,17 @@ import ( ) var QuestChecks = map[int]func(*Quest, string) (int, int){ - AuthorityQuestType: CheckAuthorityStatus, - HodlQuestType: CheckHodlStatus, - NFTMintQuestType: CheckNftStatus, - PixelQuestType: CheckPixelStatus, - RainbowQuestType: CheckRainbowStatus, - TemplateQuestType: CheckTemplateStatus, - UnruggableQuestType: CheckUnruggableStatus, - VoteQuestType: CheckVoteStatus, - FactionQuestType: CheckFactionStatus, - UsernameQuestType: CheckUsernameStatus, + AuthorityQuestType: CheckAuthorityStatus, + HodlQuestType: CheckHodlStatus, + NFTMintQuestType: CheckNftStatus, + PixelQuestType: CheckPixelStatus, + RainbowQuestType: CheckRainbowStatus, + TemplateQuestType: CheckTemplateStatus, + UnruggableQuestType: CheckUnruggableStatus, + VoteQuestType: CheckVoteStatus, + FactionQuestType: CheckFactionStatus, + ChainFactionQuestType: CheckChainFactionStatus, + UsernameQuestType: CheckUsernameStatus, } func (q *Quest) CheckStatus(user string) (progress int, needed int) { @@ -41,12 +42,20 @@ func CheckHodlStatus(q *Quest, user string) (progress int, needed int) { } func CheckNftStatus(q *Quest, user string) (progress int, needed int) { - nfts_minted_by_user, err := core.PostgresQueryOne[int]("SELECT COUNT(*) FROM NFTs WHERE minter = $1", user) - - if err != nil { - return 0, 1 - } - return *nfts_minted_by_user, 1 + nftQuestInputs := NewNFTQuestInputs(q.InputData) + if nftQuestInputs.IsDaily { + nfts_minted_by_user, err := core.PostgresQueryOne[int]("SELECT COUNT(*) FROM NFTs WHERE minter = $1 AND day_index = $2", user, nftQuestInputs.ClaimDay) + if err != nil { + return 0, 1 + } + return *nfts_minted_by_user, 1 + } else { + nfts_minted_by_user, err := core.PostgresQueryOne[int]("SELECT COUNT(*) FROM NFTs WHERE minter = $1", user) + if err != nil { + return 0, 1 + } + return *nfts_minted_by_user, 1 + } } func CheckPixelStatus(q *Quest, user string) (progress int, needed int) { @@ -95,6 +104,15 @@ func CheckVoteStatus(q *Quest, user string) (progress int, needed int) { return *count, 1 } +func CheckChainFactionStatus(q *Quest, user string) (progress int, needed int) { + count, err := core.PostgresQueryOne[int]("SELECT COUNT(*) FROM ChainFactionMembersInfo WHERE user_address = $1", user) + if err != nil { + return 0, 1 + } + + return *count, 1 +} + func CheckFactionStatus(q *Quest, user string) (progress int, needed int) { count, err := core.PostgresQueryOne[int]("SELECT COUNT(*) FROM FactionMembersInfo WHERE user_address = $1", user) if err != nil { diff --git a/backend/routes/contract.go b/backend/routes/contract.go index 8994c506..1ab60ceb 100644 --- a/backend/routes/contract.go +++ b/backend/routes/contract.go @@ -1,16 +1,20 @@ package routes import ( + "encoding/json" "io" "net/http" "os" + "strconv" + "github.com/keep-starknet-strange/art-peace/backend/core" routeutils "github.com/keep-starknet-strange/art-peace/backend/routes/utils" ) func InitContractRoutes() { http.HandleFunc("/get-contract-address", getContractAddress) http.HandleFunc("/set-contract-address", setContractAddress) + http.HandleFunc("/get-game-data", getGameData) } func getContractAddress(w http.ResponseWriter, r *http.Request) { @@ -33,3 +37,39 @@ func setContractAddress(w http.ResponseWriter, r *http.Request) { os.Setenv("ART_PEACE_CONTRACT_ADDRESS", string(data)) routeutils.WriteResultJson(w, "Contract address set") } + +type GameData struct { + Day int `json:"day"` + EndTime int `json:"endTime"` +} + +func getGameData(w http.ResponseWriter, r *http.Request) { + day, err := core.PostgresQueryOne[int](`SELECT day_index from days ORDER BY day_index DESC LIMIT 1`) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get day") + return + } + + endTime := os.Getenv("ART_PEACE_END_TIME") + if endTime == "" { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get end time") + return + } + endTimeInt, err := strconv.Atoi(endTime) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert end time to int") + return + } + + gameData := GameData{ + Day: *day, + EndTime: endTimeInt, + } + jsonGameData, err := json.Marshal(gameData) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal game data") + return + } + + routeutils.WriteDataJson(w, string(jsonGameData)) +} diff --git a/backend/routes/factions.go b/backend/routes/factions.go index f38194ef..9aac2972 100644 --- a/backend/routes/factions.go +++ b/backend/routes/factions.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "os" + "os/exec" "strconv" "github.com/keep-starknet-strange/art-peace/backend/core" @@ -17,9 +18,17 @@ func InitFactionRoutes() { http.HandleFunc("/upload-faction-icon", uploadFactionIcon) http.HandleFunc("/get-my-factions", getMyFactions) http.HandleFunc("/get-factions", getFactions) + http.HandleFunc("/get-my-chain-factions", getMyChainFactions) + http.HandleFunc("/get-chain-factions", getChainFactions) + http.HandleFunc("/get-chain-faction-members", getChainFactionMembers) http.HandleFunc("/get-faction-members", getFactionMembers) // Create a static file server for the nft images http.Handle("/faction-images/", http.StripPrefix("/faction-images/", http.FileServer(http.Dir("./factions")))) + if !core.ArtPeaceBackend.BackendConfig.Production { + http.HandleFunc("/join-chain-faction-devnet", joinChainFactionDevnet) + http.HandleFunc("/join-faction-devnet", joinFactionDevnet) + http.HandleFunc("/leave-faction-devnet", leaveFactionDevnet) + } } type FactionUserData struct { @@ -27,6 +36,7 @@ type FactionUserData struct { Allocation int `json:"allocation"` Name string `json:"name"` Members int `json:"members"` + Joinable bool `json:"joinable"` Icon string `json:"icon"` Telegram string `json:"telegram"` Twitter string `json:"twitter"` @@ -39,6 +49,7 @@ type FactionData struct { Name string `json:"name"` Members int `json:"members"` IsMember bool `json:"isMember"` + Joinable bool `json:"joinable"` Icon string `json:"icon"` Telegram string `json:"telegram"` Twitter string `json:"twitter"` @@ -65,6 +76,7 @@ type FactionsConfigItem struct { type FactionsConfig struct { Factions []FactionsConfigItem `json:"factions"` + ChainFactions []string `json:"chain_factions"` } type FactionMemberData struct { @@ -153,7 +165,7 @@ func getMyFactions(w http.ResponseWriter, r *http.Request) { // TODO: Paginate and accumulate the allocations for each faction query := ` - SELECT m.faction_id, f.allocation, f.name, COALESCE((SELECT COUNT(*) FROM factionmembersinfo WHERE faction_id = m.faction_id), 0) as members, COALESCE(icon, '') as icon, COALESCE(telegram, '') as telegram, COALESCE(twitter, '') as twitter, COALESCE(github, '') as github, COALESCE(site, '') as site + SELECT m.faction_id, f.allocation, f.name, COALESCE((SELECT COUNT(*) FROM factionmembersinfo WHERE faction_id = m.faction_id), 0) as members, f.joinable, COALESCE(icon, '') as icon, COALESCE(telegram, '') as telegram, COALESCE(twitter, '') as twitter, COALESCE(github, '') as github, COALESCE(site, '') as site FROM factionmembersinfo m LEFT JOIN factions f ON m.faction_id = f.faction_id LEFT JOIN FactionLinks l ON m.faction_id = l.faction_id @@ -188,11 +200,11 @@ func getFactions(w http.ResponseWriter, r *http.Request) { offset := (page - 1) * pageLength query := ` - SELECT faction_id, name, COALESCE((SELECT COUNT(*) FROM factionmembersinfo WHERE faction_id = key - 1), 0) as members, - COALESCE((SELECT COUNT(*) FROM factionmembersinfo fm WHERE f.faction_id = fm.faction_id AND user_address = $1), 0) > 0 as is_member, + SELECT f.faction_id, name, COALESCE((SELECT COUNT(*) FROM factionmembersinfo fm WHERE f.faction_id = fm.faction_id), 0) as members, + COALESCE((SELECT COUNT(*) FROM factionmembersinfo fm WHERE f.faction_id = fm.faction_id AND user_address = $1), 0) > 0 as is_member, f.joinable, COALESCE(icon, '') as icon, COALESCE(telegram, '') as telegram, COALESCE(twitter, '') as twitter, COALESCE(github, '') as github, COALESCE(site, '') as site FROM factions f - LEFT JOIN FactionLinks ON f.faction_id = fm.faction_id + LEFT JOIN FactionLinks fl ON f.faction_id = fl.faction_id ORDER BY f.faction_id LIMIT $2 OFFSET $3 ` @@ -205,6 +217,96 @@ func getFactions(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(factions)) } +func getMyChainFactions(w http.ResponseWriter, r *http.Request) { + address := r.URL.Query().Get("address") + if address == "" { + address = "0" + } + + query := ` + SELECT f.faction_id, name, COALESCE((SELECT COUNT(*) FROM chainfactionmembersinfo fm WHERE f.faction_id = fm.faction_id), 0) as members, + COALESCE((SELECT COUNT(*) FROM chainfactionmembersinfo fm WHERE f.faction_id = fm.faction_id AND user_address = $1), 0) > 0 as is_member, true as joinable, + COALESCE(icon, '') as icon, COALESCE(telegram, '') as telegram, COALESCE(twitter, '') as twitter, COALESCE(github, '') as github, COALESCE(site, '') as site + FROM chainfactionmembersinfo m + LEFT JOIN ChainFactions f ON m.faction_id = f.faction_id + LEFT JOIN ChainFactionLinks l ON m.faction_id = l.faction_id + WHERE m.user_address = $1 + ORDER BY m.faction_id + ` + + factions, err := core.PostgresQueryJson[FactionData](query, address) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to retrieve factions") + return + } + routeutils.WriteDataJson(w, string(factions)) +} + +func getChainFactions(w http.ResponseWriter, r *http.Request) { + address := r.URL.Query().Get("address") + if address == "" { + address = "0" + } + + query := ` + SELECT f.faction_id, name, COALESCE((SELECT COUNT(*) FROM chainfactionmembersinfo fm WHERE f.faction_id = fm.faction_id), 0) as members, + COALESCE((SELECT COUNT(*) FROM chainfactionmembersinfo fm WHERE f.faction_id = fm.faction_id AND user_address = $1), 0) > 0 as is_member, true as joinable, + COALESCE(icon, '') as icon, COALESCE(telegram, '') as telegram, COALESCE(twitter, '') as twitter, COALESCE(github, '') as github, COALESCE(site, '') as site + FROM ChainFactions f + LEFT JOIN ChainFactionLinks fl ON f.faction_id = fl.faction_id + ORDER BY f.faction_id + ` + + factions, err := core.PostgresQueryJson[FactionData](query, address) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to retrieve factions") + return + } + routeutils.WriteDataJson(w, string(factions)) +} + +func getChainFactionMembers(w http.ResponseWriter, r *http.Request) { + factionID, err := strconv.Atoi(r.URL.Query().Get("factionId")) + if err != nil || factionID < 0 { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid faction ID") + return + } + + pageLength, err := strconv.Atoi(r.URL.Query().Get("pageLength")) + if err != nil || pageLength <= 0 { + pageLength = 10 + } + if pageLength > 50 { + pageLength = 50 + } + + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil || page <= 0 { + page = 1 + } + offset := (page - 1) * pageLength + + query := ` + SELECT + CFMI.user_address AS user_address, + COALESCE(U.name, '') AS username, + 2 AS total_allocation + FROM ChainFactionMembersInfo CFMI + LEFT JOIN Users U ON CFMI.user_address = U.address + WHERE CFMI.faction_id = $1 + LIMIT $2 OFFSET $3; + ` + + members, err := core.PostgresQueryJson[FactionMemberData](query, factionID, pageLength, offset) + + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to retrieve factions") + return + } + + routeutils.WriteDataJson(w, string(members)) +} + func getFactionMembers(w http.ResponseWriter, r *http.Request) { factionID, err := strconv.Atoi(r.URL.Query().Get("factionId")) if err != nil || factionID < 0 { @@ -233,10 +335,8 @@ func getFactionMembers(w http.ResponseWriter, r *http.Request) { F.allocation AS total_allocation FROM FactionMembersInfo FMI LEFT JOIN Users U ON FMI.user_address = U.address - WHERE FMI.faction_id = $1 LEFT JOIN Factions F ON F.faction_id = FMI.faction_id - GROUP BY FMI.user_address, U.name - ORDER BY total_allocation DESC + WHERE FMI.faction_id = $1 LIMIT $2 OFFSET $3; ` @@ -249,3 +349,89 @@ func getFactionMembers(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(members)) } + +func joinChainFactionDevnet(w http.ResponseWriter, r *http.Request) { + // Disable this in production + if routeutils.NonProductionMiddleware(w, r) { + return + } + + jsonBody, err := routeutils.ReadJsonBody[map[string]string](r) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid JSON request body") + return + } + + chainId := (*jsonBody)["chainId"] + if chainId == "" { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Missing chainId parameter") + return + } + + if len(chainId) > 31 { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "chainId too long (max 31 characters)") + return + } + + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.JoinChainFactionDevnet + contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") + + cmd := exec.Command(shellCmd, contract, "join_chain_faction", chainId) + _, err = cmd.Output() + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to join chain faction on devnet") + return + } + + routeutils.WriteResultJson(w, "Joined chain faction successfully") +} + +func joinFactionDevnet(w http.ResponseWriter, r *http.Request) { + // Disable this in production + if routeutils.NonProductionMiddleware(w, r) { + return + } + + jsonBody, err := routeutils.ReadJsonBody[map[string]string](r) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid JSON request body") + return + } + + factionId := (*jsonBody)["factionId"] + if factionId == "" { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Missing factionId parameter") + return + } + + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.JoinFactionDevnet + contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") + + cmd := exec.Command(shellCmd, contract, "join_faction", factionId) + _, err = cmd.Output() + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to join faction on devnet") + return + } + + routeutils.WriteResultJson(w, "Joined faction successfully") +} + +func leaveFactionDevnet(w http.ResponseWriter, r *http.Request) { + // Disable this in production + if routeutils.NonProductionMiddleware(w, r) { + return + } + + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.LeaveFactionDevnet + contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") + + cmd := exec.Command(shellCmd, contract, "leave_faction") + _, err := cmd.Output() + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to leave faction on devnet") + return + } + + routeutils.WriteResultJson(w, "Left faction successfully") +} diff --git a/backend/routes/indexer/faction.go b/backend/routes/indexer/faction.go index d1a1b8df..c392f3fe 100644 --- a/backend/routes/indexer/faction.go +++ b/backend/routes/indexer/faction.go @@ -38,11 +38,12 @@ func processFactionCreatedEvent(event IndexerEvent) { } name := string(trimmedName) - joinable, err := strconv.ParseBool(joinableHex) + joinableInt, err := strconv.ParseInt(joinableHex, 0, 64) if err != nil { PrintIndexerError("processFactionCreatedEvent", "Failed to parse joinable", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } + joinable := joinableInt != 0 allocation, err := strconv.ParseInt(allocationHex, 0, 64) if err != nil { @@ -84,7 +85,7 @@ func processFactionJoinedEvent(event IndexerEvent) { return } - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO FactionMembersInfo (faction_id, user_address, last_placed_time, member_pixels) VALUES ($1, $2, $3, $4)", factionId, userAddress, 0, 0) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO FactionMembersInfo (faction_id, user_address, last_placed_time, member_pixels) VALUES ($1, $2, TO_TIMESTAMP($3), $4)", factionId, userAddress, 0, 0) if err != nil { PrintIndexerError("processFactionJoinedEvent", "Failed to insert faction member into postgres", factionIdHex, userAddress) return @@ -136,7 +137,7 @@ func revertFactionLeftEvent(event IndexerEvent) { } // TODO: Stash the last_placed_time and member_pixels in the event data - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO FactionMembersInfo (faction_id, user_address, last_placed_time, member_pixels) VALUES ($1, $2, $3, $4)", factionId, userAddress, 0, 0) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO FactionMembersInfo (faction_id, user_address, last_placed_time, member_pixels) VALUES ($1, $2, TO_TIMESTAMP($3), $4)", factionId, userAddress, 0, 0) if err != nil { PrintIndexerError("revertFactionLeftEvent", "Failed to insert faction member into postgres", factionIdHex, userAddress) return @@ -204,7 +205,7 @@ func processChainFactionJoinedEvent(event IndexerEvent) { return } - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO ChainFactionMembersInfo (faction_id, user_address, last_placed_time, members_pixels) VALUES ($1, $2, $3, $4)", factionId, userAddress, 0, 0) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO ChainFactionMembersInfo (faction_id, user_address, last_placed_time, member_pixels) VALUES ($1, $2, TO_TIMESTAMP($3), $4)", factionId, userAddress, 0, 0) if err != nil { PrintIndexerError("processChainFactionJoinedEvent", "Failed to insert faction member into postgres", factionIdHex, userAddress) return diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 6cac263f..86f6bd87 100644 --- a/backend/routes/indexer/nft.go +++ b/backend/routes/indexer/nft.go @@ -2,6 +2,7 @@ package indexer import ( "context" + "encoding/hex" "encoding/json" "fmt" "image" @@ -20,46 +21,70 @@ func processNFTMintedEvent(event IndexerEvent) { positionHex := event.Event.Data[0] widthHex := event.Event.Data[1] heightHex := event.Event.Data[2] - imageHashHex := event.Event.Data[3] - blockNumberHex := event.Event.Data[4] - minter := event.Event.Data[5][2:] // Remove 0x prefix + nameHex := event.Event.Data[3][2:] // Remove 0x prefix + imageHashHex := event.Event.Data[4] + blockNumberHex := event.Event.Data[5] + dayIndexHex := event.Event.Data[6] + minter := event.Event.Data[7][2:] // Remove 0x prefix // combine high and low token ids tokenIdU256, err := combineLowHigh(tokenIdLowHex, tokenIdHighHex) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error combining high and low tokenId hex", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error combining high and low tokenId hex", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } tokenId := tokenIdU256.Uint64() position, err := strconv.ParseInt(positionHex, 0, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting position hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting position hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } width, err := strconv.ParseInt(widthHex, 0, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting width hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting width hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } height, err := strconv.ParseInt(heightHex, 0, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting height hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting height hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } + decodedName, err := hex.DecodeString(nameHex) + if err != nil { + PrintIndexerError("processNFTMintedEvent", "Error decoding name hex", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) + return + } + trimmedName := []byte{} + trimming := true + for _, b := range decodedName { + if b == 0 && trimming { + continue + } + trimming = false + trimmedName = append(trimmedName, b) + } + name := string(trimmedName) + blockNumber, err := strconv.ParseInt(blockNumberHex, 0, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting block number hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting block number hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } + dayIndex, err := strconv.ParseInt(dayIndexHex, 0, 64) + if err != nil { + PrintIndexerError("processNFTMintedEvent", "Error converting day index hex to int", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) + return + } + // Set NFT in postgres - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTs (token_id, position, width, height, image_hash, block_number, minter, owner) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", tokenId, position, width, height, imageHashHex, blockNumber, minter, minter) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTs (token_id, position, width, height, name, image_hash, block_number, day_index, minter, owner) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", tokenId, position, width, height, name, imageHashHex, blockNumber, dayIndex, minter, minter) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error inserting NFT into postgres", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error inserting NFT into postgres", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } @@ -67,13 +92,13 @@ func processNFTMintedEvent(event IndexerEvent) { ctx := context.Background() canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result() if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error getting canvas from redis", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error getting canvas from redis", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } colorPaletteHex, err := core.PostgresQuery[string]("SELECT hex FROM colors ORDER BY color_key") if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error getting color palette from postgres", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error getting color palette from postgres", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } @@ -81,17 +106,17 @@ func processNFTMintedEvent(event IndexerEvent) { for idx, colorHex := range colorPaletteHex { r, err := strconv.ParseInt(colorHex[0:2], 16, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting red hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting red hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } g, err := strconv.ParseInt(colorHex[2:4], 16, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting green hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting green hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } b, err := strconv.ParseInt(colorHex[4:6], 16, 64) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error converting blue hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error converting blue hex to int when creating palette", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } colorPalette[idx] = color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255} @@ -121,7 +146,7 @@ func processNFTMintedEvent(event IndexerEvent) { if _, err := os.Stat("nfts"); os.IsNotExist(err) { err = os.MkdirAll("nfts", os.ModePerm) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error creating nfts directory", tokenIdLowHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error creating nfts directory", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } } @@ -129,7 +154,7 @@ func processNFTMintedEvent(event IndexerEvent) { if _, err := os.Stat("nfts/images"); os.IsNotExist(err) { err = os.MkdirAll("nfts/images", os.ModePerm) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error creating nfts/images directory", tokenIdLowHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error creating nfts/images directory", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } } @@ -137,7 +162,7 @@ func processNFTMintedEvent(event IndexerEvent) { if _, err := os.Stat("nfts/meta"); os.IsNotExist(err) { err = os.MkdirAll("nfts/meta", os.ModePerm) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error creating nfts/meta directory", tokenIdLowHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error creating nfts/meta directory", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } } @@ -146,14 +171,14 @@ func processNFTMintedEvent(event IndexerEvent) { filename := fmt.Sprintf("nfts/images/nft-%d.png", tokenId) file, err := os.Create(filename) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error creating file", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error creating file", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } defer file.Close() err = png.Encode(file, generatedImage) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error encoding image", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error encoding image", tokenIdLowHex, tokenIdHighHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } @@ -163,7 +188,7 @@ func processNFTMintedEvent(event IndexerEvent) { y := position / int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) // TODO: Name from onchain mint event metadata := map[string]interface{}{ - "name": fmt.Sprintf("art/peace #%d", tokenId), + "name": name, "description": "User minted art/peace NFT from the canvas.", "image": fmt.Sprintf("%s/nft-images/nft-%d.png", core.ArtPeaceBackend.GetBackendUrl(), tokenId), "attributes": []map[string]interface{}{ @@ -176,26 +201,34 @@ func processNFTMintedEvent(event IndexerEvent) { "value": fmt.Sprintf("%d", height), }, { - "trait_type": "position", + "trait_type": "Position", "value": fmt.Sprintf("(%d, %d)", x, y), }, + { + "trait_type": "Day Index", + "value": fmt.Sprintf("%d", dayIndex), + }, { "trait_type": "Minter", "value": minter, }, + { + "trait_type": "Token ID", + "value": fmt.Sprintf("%d", tokenId), + }, }, } metadataFile, err := json.MarshalIndent(metadata, "", " ") if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error generating NFT metadata", tokenIdLowHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error generating NFT metadata", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } metadataFilename := fmt.Sprintf("nfts/meta/nft-%d.json", tokenId) err = os.WriteFile(metadataFilename, metadataFile, 0644) if err != nil { - PrintIndexerError("processNFTMintedEvent", "Error writing NFT metadata file", tokenIdLowHex, positionHex, widthHex, heightHex, imageHashHex, blockNumberHex, minter) + PrintIndexerError("processNFTMintedEvent", "Error writing NFT metadata file", tokenIdLowHex, positionHex, widthHex, heightHex, nameHex, imageHashHex, blockNumberHex, minter) return } @@ -273,7 +306,7 @@ func revertNFTLikedEvent(event IndexerEvent) { func processNFTUnlikedEvent(event IndexerEvent) { tokenIdLowHex := event.Event.Keys[1][2:] // Remove 0x prefix tokenIdHighHex := event.Event.Keys[2][2:] // Remove 0x prefix - unliker := event.Event.Keys[2][2:] // Remove 0x prefix + unliker := event.Event.Keys[3][2:] // Remove 0x prefix tokenIdU256, err := combineLowHigh(tokenIdLowHex, tokenIdHighHex) if err != nil { diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 836fcefd..1dbaaa0a 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -143,47 +143,64 @@ func revertBasicPixelPlacedEvent(event IndexerEvent) { // TODO: check ordering of this and revertPixelPlacedEvent } -func processMemberPixelsPlacedEvent(event IndexerEvent) { - factionIdHex := event.Event.Keys[1] - memberIdHex := event.Event.Keys[2] +func processFactionPixelsPlacedEvent(event IndexerEvent) { + // TODO: Faction id + userAddress := event.Event.Keys[1][2:] // Remove 0x prefix timestampHex := event.Event.Data[0] memberPixelsHex := event.Event.Data[1] - factionId, err := strconv.ParseInt(factionIdHex, 0, 64) - if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting faction id hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) - return - } - - memberId, err := strconv.ParseInt(memberIdHex, 0, 64) - if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting member id hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) - return - } - timestamp, err := strconv.ParseInt(timestampHex, 0, 64) if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting timestamp hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) + PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting timestamp hex to int", userAddress, timestampHex, memberPixelsHex) return } memberPixels, err := strconv.ParseInt(memberPixelsHex, 0, 64) if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting member pixels hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) + PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting member pixels hex to int", userAddress, timestampHex, memberPixelsHex) return } - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "UPDATE FactionMembersInfo SET last_placed_time = TO_TIMESTAMP($1), member_pixels = $2 WHERE faction_id = $3 AND member_id = $4", timestamp, memberPixels, factionId, memberId) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "UPDATE FactionMembersInfo SET last_placed_time = TO_TIMESTAMP($1), member_pixels = $2 WHERE user_address = $3", timestamp, memberPixels, userAddress) if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error updating faction member info in postgres", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) + PrintIndexerError("processMemberPixelsPlacedEvent", "Error updating faction member info in postgres", userAddress, timestampHex, memberPixelsHex) return } } -func revertMemberPixelsPlacedEvent(event IndexerEvent) { +func revertFactionPixelsPlacedEvent(event IndexerEvent) { // TODO } +func processChainFactionPixelsPlacedEvent(event IndexerEvent) { + // TODO: Faction id + userAddress := event.Event.Keys[1][2:] // Remove 0x prefix + timestampHex := event.Event.Data[0] + memberPixelsHex := event.Event.Data[1] + + timestamp, err := strconv.ParseInt(timestampHex, 0, 64) + if err != nil { + PrintIndexerError("processChainFactionMemberPixelsPlacedEvent", "Error converting timestamp hex to int", userAddress, timestampHex, memberPixelsHex) + return + } + + memberPixels, err := strconv.ParseInt(memberPixelsHex, 0, 64) + if err != nil { + PrintIndexerError("processChainFactionMemberPixelsPlacedEvent", "Error converting member pixels hex to int", userAddress, timestampHex, memberPixelsHex) + return + } + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "UPDATE ChainFactionMembersInfo SET last_placed_time = TO_TIMESTAMP($1), member_pixels = $2 WHERE user_address = $3", timestamp, memberPixels, userAddress) + if err != nil { + PrintIndexerError("processChainFactionMemberPixelsPlacedEvent", "Error updating chain faction member info in postgres", userAddress, timestampHex, memberPixelsHex) + return + } +} + +func revertChainFactionPixelsPlacedEvent(event IndexerEvent) { + // TODO +} + func processExtraPixelsPlacedEvent(event IndexerEvent) { address := event.Event.Keys[1][2:] // Remove 0x prefix extraPixelsHex := event.Event.Data[0] diff --git a/backend/routes/indexer/quest.go b/backend/routes/indexer/quest.go index 7c0e76d7..ea6e2cfb 100644 --- a/backend/routes/indexer/quest.go +++ b/backend/routes/indexer/quest.go @@ -40,8 +40,14 @@ func processDailyQuestClaimedEvent(event IndexerEvent) { } if calldataLen > 0 { - // TODO : Fix these - calldata = event.Event.Data[2:][2:] // Remove 0x prefix + for i := 2; i < len(event.Event.Data); i++ { + calldataInt, err := strconv.ParseInt(event.Event.Data[i], 0, 64) + if err != nil { + PrintIndexerError("processDailyQuestClaimedEvent", "Failed to parse calldata", dayIndexHex, questIdHex, user, rewardHex, calldataLenHex, calldata) + return + } + calldata = append(calldata, strconv.FormatInt(calldataInt, 10)) + } } // TODO: Add calldata field & completed_at field diff --git a/backend/routes/indexer/route.go b/backend/routes/indexer/route.go index df10c1b1..f5da5fb2 100644 --- a/backend/routes/indexer/route.go +++ b/backend/routes/indexer/route.go @@ -54,99 +54,107 @@ var FinalizedMessageQueue []IndexerMessage var FinalizedMessageLock = &sync.Mutex{} const ( - newDayEvent = "0x00df776faf675d0c64b0f2ec596411cf1509d3966baba3478c84771ddbac1784" - colorAddedEvent = "0x0004a301e4d01f413a1d4d0460c4ba976e23392f49126d90f5bd45de7dd7dbeb" - pixelPlacedEvent = "0x02d7b50ebf415606d77c7e7842546fc13f8acfbfd16f7bcf2bc2d08f54114c23" - basicPixelPlacedEvent = "0x03089ae3085e1c52442bb171f26f92624095d32dc8a9c57c8fb09130d32daed8" - memberPixelsPlacedEvent = "0x0165248ea72ba05120b18ec02e729e1f03a465f728283e6bb805bb284086c859" - extraPixelsPlacedEvent = "0x000e8f5c4e6f651bf4c7b093805f85c9b8ec2ec428210f90a4c9c135c347f48c" - dailyQuestClaimedEvent = "0x02025eddbc0f68a923d76519fb336e0fe1e0d6b9053ab3a504251bbd44201b10" - mainQuestClaimedEvent = "0x0121172d5bc3847c8c39069075125e53d3225741d190df6d52194cb5dd5d2049" - voteColorEvent = "0x02407c82b0efa2f6176a075ba5a939d33eefab39895fabcf3ac1c5e897974a40" - votableColorAddedEvent = "0x0115b3bc605487276e022f4bec68b316e7a6b3615fb01afee58241fd1d40e3e5" - factionCreatedEvent = "0x00f3878d4c85ed94271bb611f83d47ea473bae501ffed34cd21b73206149f692" - factionJoinedEvent = "0x01e3fbdf8156ad0dde21e886d61a16d85c9ef54451eb6e253f3f427de32a47ac" - factionLeftEvent = "0x014ef8cc25c96157e2a00e9ceaa7c014a162d11d58a98871087ec488a67d7925" - chainFactionJoinedEvent = "0x02947960ff713d9b594a3b718b90a45360e46d1bbacef94b727bb0d461d04207" - nftMintedEvent = "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f" - nftLikedEvent = "0x028d7ee09447088eecdd12a86c9467a5e9ad18f819a20f9adcf6e34e0bd51453" - nftUnlikedEvent = "0x03b57514b19693484c35249c6e8b15bfe6e476205720680c2ff9f02faaf94941" - usernameClaimedEvent = "0x019be6537c04b790ae4e3a06d6e777ec8b2e9950a01d76eed8a2a28941cc511c" - usernameChangedEvent = "0x03c44b98666b0a27eadcdf5dc42449af5f907b19523858368c4ffbc7a2625dab" - templateAddedEvent = "0x03e18ec266fe76a2efce73f91228e6e04456b744fc6984c7a6374e417fb4bf59" - nftTransferEvent = "0x0099cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9" + newDayEvent = "0x00df776faf675d0c64b0f2ec596411cf1509d3966baba3478c84771ddbac1784" + colorAddedEvent = "0x0004a301e4d01f413a1d4d0460c4ba976e23392f49126d90f5bd45de7dd7dbeb" + pixelPlacedEvent = "0x02d7b50ebf415606d77c7e7842546fc13f8acfbfd16f7bcf2bc2d08f54114c23" + basicPixelPlacedEvent = "0x03089ae3085e1c52442bb171f26f92624095d32dc8a9c57c8fb09130d32daed8" + factionPixelsPlacedEvent = "0x02838056c6784086957f2252d4a36a24d554ea2db7e09d2806cc69751d81f0a2" + chainFactionPixelsPlacedEvent = "0x02e4d1feaacd0627a6c7d5002564bdb4ca4877d47f00cad4714201194690a7a9" + extraPixelsPlacedEvent = "0x000e8f5c4e6f651bf4c7b093805f85c9b8ec2ec428210f90a4c9c135c347f48c" + dailyQuestClaimedEvent = "0x02025eddbc0f68a923d76519fb336e0fe1e0d6b9053ab3a504251bbd44201b10" + mainQuestClaimedEvent = "0x0121172d5bc3847c8c39069075125e53d3225741d190df6d52194cb5dd5d2049" + voteColorEvent = "0x02407c82b0efa2f6176a075ba5a939d33eefab39895fabcf3ac1c5e897974a40" + votableColorAddedEvent = "0x0115b3bc605487276e022f4bec68b316e7a6b3615fb01afee58241fd1d40e3e5" + factionCreatedEvent = "0x00f3878d4c85ed94271bb611f83d47ea473bae501ffed34cd21b73206149f692" + factionJoinedEvent = "0x01e3fbdf8156ad0dde21e886d61a16d85c9ef54451eb6e253f3f427de32a47ac" + factionLeftEvent = "0x014ef8cc25c96157e2a00e9ceaa7c014a162d11d58a98871087ec488a67d7925" + chainFactionCreatedEvent = "0x020c994ab49a8316bcc78b06d4ff9929d83b2995af33f480b93e972cedb0c926" + chainFactionJoinedEvent = "0x02947960ff713d9b594a3b718b90a45360e46d1bbacef94b727bb0d461d04207" + nftMintedEvent = "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f" + nftLikedEvent = "0x028d7ee09447088eecdd12a86c9467a5e9ad18f819a20f9adcf6e34e0bd51453" + nftUnlikedEvent = "0x03b57514b19693484c35249c6e8b15bfe6e476205720680c2ff9f02faaf94941" + usernameClaimedEvent = "0x019be6537c04b790ae4e3a06d6e777ec8b2e9950a01d76eed8a2a28941cc511c" + usernameChangedEvent = "0x03c44b98666b0a27eadcdf5dc42449af5f907b19523858368c4ffbc7a2625dab" + templateAddedEvent = "0x03e18ec266fe76a2efce73f91228e6e04456b744fc6984c7a6374e417fb4bf59" + nftTransferEvent = "0x0099cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9" ) var eventProcessors = map[string](func(IndexerEvent)){ - newDayEvent: processNewDayEvent, - colorAddedEvent: processColorAddedEvent, - pixelPlacedEvent: processPixelPlacedEvent, - basicPixelPlacedEvent: processBasicPixelPlacedEvent, - memberPixelsPlacedEvent: processMemberPixelsPlacedEvent, - extraPixelsPlacedEvent: processExtraPixelsPlacedEvent, - dailyQuestClaimedEvent: processDailyQuestClaimedEvent, - mainQuestClaimedEvent: processMainQuestClaimedEvent, - voteColorEvent: processVoteColorEvent, - votableColorAddedEvent: processVotableColorAddedEvent, - factionCreatedEvent: processFactionCreatedEvent, - factionJoinedEvent: processFactionJoinedEvent, - factionLeftEvent: processFactionLeftEvent, - chainFactionJoinedEvent: processChainFactionJoinedEvent, - nftMintedEvent: processNFTMintedEvent, - nftLikedEvent: processNFTLikedEvent, - nftUnlikedEvent: processNFTUnlikedEvent, - usernameClaimedEvent: processUsernameClaimedEvent, - usernameChangedEvent: processUsernameChangedEvent, - templateAddedEvent: processTemplateAddedEvent, - nftTransferEvent: processNFTTransferEvent, + newDayEvent: processNewDayEvent, + colorAddedEvent: processColorAddedEvent, + pixelPlacedEvent: processPixelPlacedEvent, + basicPixelPlacedEvent: processBasicPixelPlacedEvent, + factionPixelsPlacedEvent: processFactionPixelsPlacedEvent, + chainFactionPixelsPlacedEvent: processChainFactionPixelsPlacedEvent, + extraPixelsPlacedEvent: processExtraPixelsPlacedEvent, + dailyQuestClaimedEvent: processDailyQuestClaimedEvent, + mainQuestClaimedEvent: processMainQuestClaimedEvent, + voteColorEvent: processVoteColorEvent, + votableColorAddedEvent: processVotableColorAddedEvent, + factionCreatedEvent: processFactionCreatedEvent, + factionJoinedEvent: processFactionJoinedEvent, + factionLeftEvent: processFactionLeftEvent, + chainFactionCreatedEvent: processChainFactionCreatedEvent, + chainFactionJoinedEvent: processChainFactionJoinedEvent, + nftMintedEvent: processNFTMintedEvent, + nftLikedEvent: processNFTLikedEvent, + nftUnlikedEvent: processNFTUnlikedEvent, + usernameClaimedEvent: processUsernameClaimedEvent, + usernameChangedEvent: processUsernameChangedEvent, + templateAddedEvent: processTemplateAddedEvent, + nftTransferEvent: processNFTTransferEvent, } var eventReverters = map[string](func(IndexerEvent)){ - newDayEvent: revertNewDayEvent, - colorAddedEvent: revertColorAddedEvent, - pixelPlacedEvent: revertPixelPlacedEvent, - basicPixelPlacedEvent: revertBasicPixelPlacedEvent, - memberPixelsPlacedEvent: revertMemberPixelsPlacedEvent, - extraPixelsPlacedEvent: revertExtraPixelsPlacedEvent, - dailyQuestClaimedEvent: revertDailyQuestClaimedEvent, - mainQuestClaimedEvent: revertMainQuestClaimedEvent, - voteColorEvent: revertVoteColorEvent, - votableColorAddedEvent: revertVotableColorAddedEvent, - factionCreatedEvent: revertFactionCreatedEvent, - factionJoinedEvent: revertFactionJoinedEvent, - factionLeftEvent: revertFactionLeftEvent, - chainFactionJoinedEvent: revertChainFactionJoinedEvent, - nftMintedEvent: revertNFTMintedEvent, - nftLikedEvent: revertNFTLikedEvent, - nftUnlikedEvent: revertNFTUnlikedEvent, - usernameClaimedEvent: revertUsernameClaimedEvent, - usernameChangedEvent: revertUsernameChangedEvent, - templateAddedEvent: revertTemplateAddedEvent, - nftTransferEvent: revertNFTTransferEvent, + newDayEvent: revertNewDayEvent, + colorAddedEvent: revertColorAddedEvent, + pixelPlacedEvent: revertPixelPlacedEvent, + basicPixelPlacedEvent: revertBasicPixelPlacedEvent, + factionPixelsPlacedEvent: revertFactionPixelsPlacedEvent, + chainFactionPixelsPlacedEvent: revertChainFactionPixelsPlacedEvent, + extraPixelsPlacedEvent: revertExtraPixelsPlacedEvent, + dailyQuestClaimedEvent: revertDailyQuestClaimedEvent, + mainQuestClaimedEvent: revertMainQuestClaimedEvent, + voteColorEvent: revertVoteColorEvent, + votableColorAddedEvent: revertVotableColorAddedEvent, + factionCreatedEvent: revertFactionCreatedEvent, + factionJoinedEvent: revertFactionJoinedEvent, + factionLeftEvent: revertFactionLeftEvent, + chainFactionCreatedEvent: revertChainFactionCreatedEvent, + chainFactionJoinedEvent: revertChainFactionJoinedEvent, + nftMintedEvent: revertNFTMintedEvent, + nftLikedEvent: revertNFTLikedEvent, + nftUnlikedEvent: revertNFTUnlikedEvent, + usernameClaimedEvent: revertUsernameClaimedEvent, + usernameChangedEvent: revertUsernameChangedEvent, + templateAddedEvent: revertTemplateAddedEvent, + nftTransferEvent: revertNFTTransferEvent, } var eventRequiresOrdering = map[string]bool{ - newDayEvent: false, - colorAddedEvent: true, - pixelPlacedEvent: true, - basicPixelPlacedEvent: false, - memberPixelsPlacedEvent: false, - extraPixelsPlacedEvent: false, - dailyQuestClaimedEvent: false, - mainQuestClaimedEvent: false, - voteColorEvent: true, - votableColorAddedEvent: true, - factionCreatedEvent: true, - factionJoinedEvent: true, - factionLeftEvent: true, - chainFactionJoinedEvent: true, - nftMintedEvent: false, - nftLikedEvent: true, - nftUnlikedEvent: true, - usernameClaimedEvent: false, - usernameChangedEvent: true, - templateAddedEvent: false, - nftTransferEvent: true, + newDayEvent: false, + colorAddedEvent: true, + pixelPlacedEvent: true, + basicPixelPlacedEvent: false, + factionPixelsPlacedEvent: false, + chainFactionPixelsPlacedEvent: false, + extraPixelsPlacedEvent: false, + dailyQuestClaimedEvent: false, + mainQuestClaimedEvent: false, + voteColorEvent: true, + votableColorAddedEvent: true, + factionCreatedEvent: true, + factionJoinedEvent: true, + factionLeftEvent: true, + chainFactionCreatedEvent: true, + chainFactionJoinedEvent: true, + nftMintedEvent: false, + nftLikedEvent: true, + nftUnlikedEvent: true, + usernameClaimedEvent: false, + usernameChangedEvent: true, + templateAddedEvent: false, + nftTransferEvent: true, } const ( diff --git a/backend/routes/nft.go b/backend/routes/nft.go index 7e3a7534..473878bd 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -63,8 +63,10 @@ type NFTData struct { Position int `json:"position"` Width int `json:"width"` Height int `json:"height"` + Name string `json:"name"` ImageHash string `json:"imageHash"` BlockNumber int `json:"blockNumber"` + DayIndex int `json:"dayIndex"` Minter string `json:"minter"` Owner string `json:"owner"` Likes int `json:"likes"` @@ -251,10 +253,12 @@ func mintNFTDevnet(w http.ResponseWriter, r *http.Request) { return } + name := (*jsonBody)["name"] + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.MintNFTDevnet contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "mint_nft", strconv.Itoa(position), strconv.Itoa(width), strconv.Itoa(height)) + cmd := exec.Command(shellCmd, contract, "mint_nft", strconv.Itoa(position), strconv.Itoa(width), strconv.Itoa(height), name) _, err = cmd.Output() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to mint NFT on devnet") @@ -414,7 +418,7 @@ func unlikeNFTDevnet(w http.ResponseWriter, r *http.Request) { shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.UnlikeNFTDevnet contract := os.Getenv("CANVAS_NFT_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "unlike_nft", tokenId) + cmd := exec.Command(shellCmd, contract, "unlike_nft", tokenId, "0") _, err = cmd.Output() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to unlike NFT on devnet") @@ -451,7 +455,10 @@ func getHotNFTs(w http.ResponseWriter, r *http.Request) { SELECT nfts.*, COALESCE(like_count, 0) AS likes, - COALESCE((SELECT true FROM nftlikes WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), false) as liked + COALESCE(( + SELECT true FROM nftlikes + WHERE liker = $1 AND nftlikes.nftkey = nfts.token_id), + false) as liked FROM nfts LEFT JOIN ( @@ -469,7 +476,7 @@ func getHotNFTs(w http.ResponseWriter, r *http.Request) { ) latestlikes GROUP BY nftkey ) rank ON nfts.token_id = rank.nftkey - ORDER BY rank DESC + ORDER BY COALESCE(rank, 0) DESC LIMIT $3 OFFSET $4;` nfts, err := core.PostgresQueryJson[NFTData](query, address, hotLimit, pageLength, offset) if err != nil { diff --git a/backend/routes/quests.go b/backend/routes/quests.go index abc725f4..9f106251 100644 --- a/backend/routes/quests.go +++ b/backend/routes/quests.go @@ -3,6 +3,7 @@ package routes import ( "context" "encoding/json" + "fmt" "net/http" "os" "os/exec" @@ -21,6 +22,7 @@ type DailyUserQuest struct { DayIndex int `json:"dayIndex"` QuestId int `json:"questId"` Completed bool `json:"completed"` + ClaimParams []ClaimParams `json:"claimParams"` } type DailyQuest struct { @@ -29,6 +31,7 @@ type DailyQuest struct { Reward int `json:"reward"` DayIndex int `json:"dayIndex"` QuestId int `json:"questId"` + ClaimParams []ClaimParams `json:"claimParams"` } type MainUserQuest struct { @@ -37,6 +40,7 @@ type MainUserQuest struct { Description string `json:"description"` Reward int `json:"reward"` Completed bool `json:"completed"` + ClaimParams []ClaimParams `json:"claimParams"` } type MainQuest struct { @@ -44,12 +48,29 @@ type MainQuest struct { Name string `json:"name"` Description string `json:"description"` Reward int `json:"reward"` + ClaimParams []ClaimParams `json:"claimParams"` } type QuestContractConfig struct { - Type string `json:"type"` - InitParams []string `json:"initParams"` - StoreParams []int `json:"storeParams"` + Type string `json:"type"` + InitParams []string `json:"initParams"` + StoreParams []int `json:"storeParams"` + ClaimParams []ClaimParamConfig `json:"claimParams"` +} + +type ClaimParams struct { + QuestId int `json:"questId"` + ClaimType string `json:"claimType"` + Name string `json:"name"` + Example string `json:"example"` + Input bool `json:"input"` +} + +type ClaimParamConfig struct { + Type string `json:"type"` + Name string `json:"name"` + Example string `json:"example"` + Input bool `json:"input"` } type QuestConfig struct { @@ -85,9 +106,10 @@ type QuestTypes struct { } type QuestProgress struct { - QuestId int `json:"questId"` - Progress int `json:"progress"` - Needed int `json:"needed"` + QuestId int `json:"questId"` + Progress int `json:"progress"` + Needed int `json:"needed"` + Calldata []int `json:"calldata"` } func InitQuestsRoutes() { @@ -155,6 +177,17 @@ func InitQuests(w http.ResponseWriter, r *http.Request) { paramIdx++ } + + claimParamIdx := 0 + for _, claimParam := range questConfig.ContractConfig.ClaimParams { + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO DailyQuestsClaimParams (day_index, quest_id, claim_key, claim_type, name, example, input) VALUES ($1, $2, $3, $4, $5, $6, $7)", dailyQuestConfig.Day-1, idx, claimParamIdx, claimParam.Type, claimParam.Name, claimParam.Example, claimParam.Input) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to insert daily quest claim param") + return + } + + claimParamIdx++ + } } } @@ -183,29 +216,79 @@ func InitQuests(w http.ResponseWriter, r *http.Request) { paramIdx++ } + + claimParamIdx := 0 + for _, claimParam := range questConfig.ContractConfig.ClaimParams { + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO MainQuestsClaimParams (quest_id, claim_key, claim_type, name, example, input) VALUES ($1, $2, $3, $4, $5, $6)", idx, claimParamIdx, claimParam.Type, claimParam.Name, claimParam.Example, claimParam.Input) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to insert main quest claim param") + return + } + + claimParamIdx++ + } } routeutils.WriteResultJson(w, "Initialized quests successfully") } func GetDailyQuests(w http.ResponseWriter, r *http.Request) { - quests, err := core.PostgresQueryJson[DailyQuest]("SELECT name, description, reward, day_index, quest_id FROM DailyQuests ORDER BY day_index ASC") + quests, err := core.PostgresQuery[DailyQuest]("SELECT name, description, reward, day_index, quest_id FROM DailyQuests ORDER BY day_index ASC") if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get daily quests") return } - routeutils.WriteDataJson(w, string(quests)) + // Get claim params + questClaimParams, err := core.PostgresQuery[ClaimParams]("SELECT quest_id, claim_type, name, example, input FROM DailyQuestsClaimParams ORDER BY quest_id ASC, claim_key ASC") + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get daily quests claim params") + return + } + + // Add claim params to quests + for _, questClaimParam := range questClaimParams { + quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) + } + + // Json quest data + jsonQuests, err := json.Marshal(quests) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal completed daily quests") + return + } + + routeutils.WriteDataJson(w, string(jsonQuests)) } +// TODO: Here func GetMainQuests(w http.ResponseWriter, r *http.Request) { - quests, err := core.PostgresQueryJson[MainQuest]("SELECT key - 1 as quest_id, name, description, reward FROM MainQuests ORDER BY quest_id ASC") + quests, err := core.PostgresQuery[MainQuest]("SELECT key - 1 as quest_id, name, description, reward FROM MainQuests ORDER BY quest_id ASC") if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get main quests") return } - routeutils.WriteDataJson(w, string(quests)) + // Get claim params + questClaimParams, err := core.PostgresQuery[ClaimParams]("SELECT quest_id, claim_type, name, example, input FROM MainQuestsClaimParams ORDER BY quest_id ASC, claim_key ASC") + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get main quests claim params") + return + } + + // Add claim params to quests + for _, questClaimParam := range questClaimParams { + quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) + } + + // Json quest data + jsonQuests, err := json.Marshal(quests) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal completed main quests") + return + } + + routeutils.WriteDataJson(w, string(jsonQuests)) } func GetMainUserQuests(w http.ResponseWriter, r *http.Request) { @@ -215,13 +298,32 @@ func GetMainUserQuests(w http.ResponseWriter, r *http.Request) { return } - quests, err := core.PostgresQueryJson[MainUserQuest]("SELECT m.name, m.description, m.reward, m.key - 1 as quest_id, COALESCE(u.completed, false) as completed FROM MainQuests m LEFT JOIN UserMainQuests u ON u.quest_id = m.key - 1 AND u.user_address = $1", userAddress) + quests, err := core.PostgresQuery[MainUserQuest]("SELECT m.name, m.description, m.reward, m.key - 1 as quest_id, COALESCE(u.completed, false) as completed FROM MainQuests m LEFT JOIN UserMainQuests u ON u.quest_id = m.key - 1 AND u.user_address = $1", userAddress) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get main user quests") return } - routeutils.WriteDataJson(w, string(quests)) + // Get claim params + questClaimParams, err := core.PostgresQuery[ClaimParams]("SELECT quest_id, claim_type, name, example, input FROM MainQuestsClaimParams ORDER BY quest_id ASC, claim_key ASC") + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get main user quests claim params") + return + } + + // Add claim params to quests + for _, questClaimParam := range questClaimParams { + quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) + } + + // Json quest data + jsonQuests, err := json.Marshal(quests) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal completed main quests") + return + } + + routeutils.WriteDataJson(w, string(jsonQuests)) } func GetDailyQuestProgress(w http.ResponseWriter, r *http.Request) { @@ -260,10 +362,15 @@ func GetDailyQuestProgress(w http.ResponseWriter, r *http.Request) { return } progress, needed := questItem.CheckStatus(userAddress) + var calldata []int + if progress >= needed { + calldata = questItem.GetQuestClaimData(userAddress) + } result = append(result, QuestProgress{ QuestId: quest.QuestId, Progress: progress, Needed: needed, + Calldata: calldata, }) } @@ -299,10 +406,15 @@ func GetTodayQuestProgress(w http.ResponseWriter, r *http.Request) { return } progress, needed := questItem.CheckStatus(userAddress) + var calldata []int + if progress >= needed { + calldata = questItem.GetQuestClaimData(userAddress) + } result = append(result, QuestProgress{ QuestId: quest.QuestId, Progress: progress, Needed: needed, + Calldata: calldata, }) } @@ -336,10 +448,15 @@ func GetMainQuestProgress(w http.ResponseWriter, r *http.Request) { return } progress, needed := questItem.CheckStatus(userAddress) + var calldata []int + if progress >= needed { + calldata = questItem.GetQuestClaimData(userAddress) + } result = append(result, QuestProgress{ QuestId: quest.QuestId, Progress: progress, Needed: needed, + Calldata: calldata, }) } @@ -354,7 +471,7 @@ func GetMainQuestProgress(w http.ResponseWriter, r *http.Request) { // Get today's quests based on the current day index. func getTodaysQuests(w http.ResponseWriter, r *http.Request) { - quests, err := core.PostgresQueryJson[DailyQuest]("SELECT name, description, reward, day_index, quest_id FROM DailyQuests WHERE day_index = (SELECT MAX(day_index) FROM Days) ORDER BY quest_id ASC") + quests, err := core.PostgresQuery[DailyQuest]("SELECT name, description, reward, day_index, quest_id FROM DailyQuests WHERE day_index = (SELECT MAX(day_index) FROM Days) ORDER BY quest_id ASC") if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get today's quests") return @@ -364,7 +481,27 @@ func getTodaysQuests(w http.ResponseWriter, r *http.Request) { return } - routeutils.WriteDataJson(w, string(quests)) + // Get claim params + questClaimParams, err := core.PostgresQuery[ClaimParams]("SELECT quest_id, claim_type, name, example, input FROM DailyQuestsClaimParams WHERE day_index = (SELECT MAX(day_index) FROM Days) ORDER BY quest_id ASC, claim_key ASC") + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get today's quests claim params") + return + } + + // Add claim params to quests + for _, questClaimParam := range questClaimParams { + quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) + } + + // Json quest data + jsonQuests, err := json.Marshal(quests) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal completed daily quests") + return + } + fmt.Println(string(jsonQuests)) + + routeutils.WriteDataJson(w, string(jsonQuests)) } func getTodaysUserQuests(w http.ResponseWriter, r *http.Request) { @@ -374,13 +511,33 @@ func getTodaysUserQuests(w http.ResponseWriter, r *http.Request) { return } - quests, err := core.PostgresQueryJson[DailyUserQuest]("SELECT d.name, d.description, d.reward, d.day_index, d.quest_id, COALESCE(u.completed, false) as completed FROM DailyQuests d LEFT JOIN UserDailyQuests u ON d.quest_id = u.quest_id AND d.day_index = u.day_index AND u.user_address = $1 WHERE d.day_index = (SELECT MAX(day_index) FROM Days)", userAddress) + quests, err := core.PostgresQuery[DailyUserQuest]("SELECT d.name, d.description, d.reward, d.day_index, d.quest_id, COALESCE(u.completed, false) as completed FROM DailyQuests d LEFT JOIN UserDailyQuests u ON d.quest_id = u.quest_id AND d.day_index = u.day_index AND u.user_address = $1 WHERE d.day_index = (SELECT MAX(day_index) FROM Days)", userAddress) if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get today's user quests") return } - - routeutils.WriteDataJson(w, string(quests)) + + // Get claim params + questClaimParams, err := core.PostgresQuery[ClaimParams]("SELECT quest_id, claim_type, name, example, input FROM DailyQuestsClaimParams WHERE day_index = (SELECT MAX(day_index) FROM Days) ORDER BY quest_id ASC, claim_key ASC") + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to get today's user quests claim params") + return + } + + // Add claim params to quests + for _, questClaimParam := range questClaimParams { + quests[questClaimParam.QuestId].ClaimParams = append(quests[questClaimParam.QuestId].ClaimParams, questClaimParam) + } + + + // Json quest data + jsonQuests, err := json.Marshal(quests) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to marshal completed daily quests") + return + } + + routeutils.WriteDataJson(w, string(jsonQuests)) } func GetCompletedMainQuests(w http.ResponseWriter, r *http.Request) { @@ -443,10 +600,19 @@ func ClaimTodayQuestDevnet(w http.ResponseWriter, r *http.Request) { return } + calldataVal := (*jsonBody)["calldata"] + calldata := "" + // TODO: More generic + if calldataVal != "" { + calldata = "1 " + calldataVal + } else { + calldata = "0" + } + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.ClaimTodayQuestDevnet contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "claim_today_quest", strconv.Itoa(questId), "0") + cmd := exec.Command(shellCmd, contract, "claim_today_quest", strconv.Itoa(questId), calldata) _, err = cmd.Output() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to claim today quest on devnet") @@ -474,10 +640,19 @@ func ClaimMainQuestDevnet(w http.ResponseWriter, r *http.Request) { return } + calldataVal := (*jsonBody)["calldata"] + calldata := "" + // TODO: More generic + if calldataVal != "" { + calldata = "1 " + calldataVal + } else { + calldata = "0" + } + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.ClaimTodayQuestDevnet // TODO contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS") - cmd := exec.Command(shellCmd, contract, "claim_main_quest", strconv.Itoa(questId), "0") + cmd := exec.Command(shellCmd, contract, "claim_main_quest", strconv.Itoa(questId), calldata) _, err = cmd.Output() if err != nil { routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to claim main quest on devnet") diff --git a/backend/routes/user.go b/backend/routes/user.go index 48797996..fb62831b 100644 --- a/backend/routes/user.go +++ b/backend/routes/user.go @@ -17,6 +17,7 @@ func InitUserRoutes() { http.HandleFunc("/get-username-store-address", getUsernameStoreAddress) http.HandleFunc("/set-username-store-address", setUsernameStoreAddress) http.HandleFunc("/get-last-placed-time", getLastPlacedTime) + http.HandleFunc("/get-chain-faction-pixels", getChainFactionPixels) http.HandleFunc("/get-faction-pixels", getFactionPixels) http.HandleFunc("/get-extra-pixels", getExtraPixels) http.HandleFunc("/get-username", getUsername) @@ -62,7 +63,7 @@ func getFactionPixels(w http.ResponseWriter, r *http.Request) { return } - membershipPixels, err := core.PostgresQueryJson[MembershipPixelsData]("SELECT faction_id, allocation, last_placed_time, member_pixels FROM FactionMembersInfo FMI LEFT JOIN Factions F ON F.faction_id = FMI.faction_id WHERE user_address = $1", address) + membershipPixels, err := core.PostgresQueryJson[MembershipPixelsData]("SELECT F.faction_id, allocation, last_placed_time, member_pixels FROM FactionMembersInfo FMI LEFT JOIN Factions F ON F.faction_id = FMI.faction_id WHERE user_address = $1", address) if err != nil { routeutils.WriteDataJson(w, "[]") return @@ -71,6 +72,22 @@ func getFactionPixels(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(membershipPixels)) } +func getChainFactionPixels(w http.ResponseWriter, r *http.Request) { + address := r.URL.Query().Get("address") + if address == "" { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Missing address parameter") + return + } + + membershipPixels, err := core.PostgresQueryJson[MembershipPixelsData]("SELECT F.faction_id, 2 as allocation, last_placed_time, member_pixels FROM ChainFactionMembersInfo FMI LEFT JOIN ChainFactions F ON F.faction_id = FMI.faction_id WHERE user_address = $1", address) + if err != nil { + routeutils.WriteDataJson(w, "[]") + return + } + + routeutils.WriteDataJson(w, string(membershipPixels)) +} + func getExtraPixels(w http.ResponseWriter, r *http.Request) { address := r.URL.Query().Get("address") if address == "" { @@ -166,7 +183,7 @@ func newUsernameDevnet(w http.ResponseWriter, r *http.Request) { cmd := exec.Command(shellCmd, contract, "claim_username", username) _, err = cmd.Output() if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to place pixel on devnet") + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to claim username on devnet") return } diff --git a/configs/backend.config.json b/configs/backend.config.json index 7985695f..4f6bfe50 100644 --- a/configs/backend.config.json +++ b/configs/backend.config.json @@ -10,7 +10,10 @@ "like_nft_devnet": "../tests/integration/local/like_nft.sh", "unlike_nft_devnet": "../tests/integration/local/unlike_nft.sh", "vote_color_devnet": "../tests/integration/local/vote_color.sh", - "increase_day_devnet": "../tests/integration/local/increase_day_index.sh" + "increase_day_devnet": "../tests/integration/local/increase_day_index.sh", + "join_chain_faction_devnet": "../tests/integration/local/join_chain_faction.sh", + "join_faction_devnet": "../tests/integration/local/join_faction.sh", + "leave_faction_devnet": "../tests/integration/local/leave_faction.sh" }, "production": false, "websocket": { diff --git a/configs/docker-backend.config.json b/configs/docker-backend.config.json index 330b576d..91600898 100644 --- a/configs/docker-backend.config.json +++ b/configs/docker-backend.config.json @@ -13,7 +13,10 @@ "vote_color_devnet": "/scripts/vote_color.sh", "new_username_devnet": "/scripts/new_username.sh", "change_username_devnet": "/scripts/change_username.sh", - "increase_day_devnet": "/scripts/increase_day_index.sh" + "increase_day_devnet": "/scripts/increase_day_index.sh", + "join_chain_faction_devnet": "/scripts/join_chain_faction.sh", + "join_faction_devnet": "/scripts/join_faction.sh", + "leave_faction_devnet": "/scripts/leave_faction.sh" }, "production": false, "websocket": { diff --git a/configs/factions.config.json b/configs/factions.config.json index b0c01eb4..19394c30 100644 --- a/configs/factions.config.json +++ b/configs/factions.config.json @@ -1,13 +1,12 @@ { "factions": [ { - "id": 0, + "id": 1, "name": "Early Birds", "icon": "$BACKEND_URL/faction-images/early-bird.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 2, - "per_member": true, "joinable": false, + "allocation": 2, "links": { "telegram": "", "twitter": "", @@ -26,13 +25,12 @@ ] }, { - "id": 1, + "id": 2, "name": "Keep Starknet Strange", "icon": "$BACKEND_URL/faction-images/keep-starknet-strange.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 4, - "per_member": true, "joinable": false, + "allocation": 5, "links": { "telegram": "", "twitter": "", @@ -45,13 +43,12 @@ ] }, { - "id": 2, + "id": 3, "name": "Contributors", "icon": "$BACKEND_URL/faction-images/contributors.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 2, - "per_member": true, "joinable": false, + "allocation": 2, "links": { "telegram": "", "twitter": "", @@ -67,13 +64,12 @@ ] }, { - "id": 3, + "id": 4, "name": "Ducks Everywhere", "icon": "$BACKEND_URL/faction-images/ducks-everywhere.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 100, - "per_member": false, "joinable": true, + "allocation": 1, "links": { "telegram": "https://t.me/duckseverywhere", "twitter": "https://twitter.com/DucksEverywher2", @@ -81,18 +77,15 @@ "site": "https://linktr.ee/duckseverywhere" }, "members": [ - "0x01bf5fad6815868d6fe067905548285596cf311641169544109a7a5394c2565f", - "0x0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0" ] }, { - "id": 4, + "id": 5, "name": "PixeLaw", "icon": "$BACKEND_URL/faction-images/pixelaw.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 100, - "per_member": false, "joinable": true, + "allocation": 1, "links": { "telegram": "https://t.me/pixelaw", "twitter": "https://twitter.com/0xPixeLAW", @@ -100,18 +93,15 @@ "site": "https://www.pixelaw.xyz" }, "members": [ - "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "0x01bf5fad6815868d6fe067905548285596cf311641169544109a7a5394c2565f" ] }, { - "id": 5, + "id": 6, "name": "WASD", "icon": "$BACKEND_URL/faction-images/wasd.png", "leader": "0x07c313ea8b45044c2272b77ec7332b65bdfef089c4de0fffab3de3fd6b85d124", - "pool": 100, - "per_member": false, "joinable": true, + "allocation": 1, "links": { "telegram": "https://t.me/wasd", "twitter": "https://twitter.com/WASD_0x", @@ -119,10 +109,17 @@ "site": "https://bento.me/wasd" }, "members": [ - "0x034be07b6e7eeb280eb15d000d9eb53a63e5614e9886b74284991098c30a614a", - "0x01bf5fad6815868d6fe067905548285596cf311641169544109a7a5394c2565f", - "0x0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0" ] } + ], + "chain_factions": [ + "Starknet", + "Solana", + "Bitcoin", + "Base", + "ZkSync", + "Polygon", + "Optimism", + "Scroll" ] } diff --git a/configs/production-quests.config.json b/configs/production-quests.config.json index 07d0ffe0..64862885 100644 --- a/configs/production-quests.config.json +++ b/configs/production-quests.config.json @@ -38,11 +38,11 @@ } }, { - "name": "Join a faction", - "description": "Represent a community by joining their faction on the factions tab", + "name": "Represent your chain", + "description": "Join a faction to represent your favorite chain in the factions tab", "reward": 3, "questContract": { - "type": "FactionQuest", + "type": "ChainFactionQuest", "initParams": [ "$ART_PEACE_CONTRACT", "$REWARD" @@ -82,9 +82,18 @@ "initParams": [ "$CANVAS_NFT_CONTRACT", "$ART_PEACE_CONTRACT", - "$REWARD" + "$REWARD", + "0", + "0" ], - "storeParams": [] + "storeParams": [3,4], + "claimParams": [ + { + "type": "int", + "name": "Token ID", + "input": false + } + ] } }, { @@ -170,9 +179,18 @@ "initParams": [ "$CANVAS_NFT_CONTRACT", "$ART_PEACE_CONTRACT", - "$REWARD" + "$REWARD", + "1", + "$DAY_IDX" ], - "storeParams": [] + "storeParams": [3,4], + "claimParams": [ + { + "type": "int", + "name": "Token ID", + "input": false + } + ] } } ] @@ -182,25 +200,24 @@ "main": { "mainQuests": [ { - "name": "HODL", - "description": "Accumulate 10 extra pixels in your account", - "reward": 5, + "name": "The Rainbow", + "description": "Place at least 1 pixel of each color", + "reward": 10, "questContract": { - "type": "HodlQuest", + "type": "RainbowQuest", "initParams": [ "$ART_PEACE_CONTRACT", - "$REWARD", - "10" + "$REWARD" ], - "storeParams": [2] + "storeParams": [] } }, { - "name": "Represent your chain", - "description": "Join a faction to represent your favorite chain in the factions tab", - "reward": 5, + "name": "Join a Faction", + "description": "Represent a community by joining their faction on the factions tab", + "reward": 3, "questContract": { - "type": "ChainFactionQuest", + "type": "FactionQuest", "initParams": [ "$ART_PEACE_CONTRACT", "$REWARD" @@ -209,16 +226,17 @@ } }, { - "name": "The Rainbow", - "description": "Place at least 1 pixel of each color", - "reward": 10, + "name": "HODL", + "description": "Accumulate 10 extra pixels in your account", + "reward": 5, "questContract": { - "type": "RainbowQuest", + "type": "HodlQuest", "initParams": [ "$ART_PEACE_CONTRACT", - "$REWARD" + "$REWARD", + "10" ], - "storeParams": [] + "storeParams": [2] } }, { @@ -236,7 +254,8 @@ { "type": "address", "name": "MemeCoin Address", - "example": "0x02D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23" + "example": "0x02D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23", + "input": true } ] } diff --git a/configs/quests.config.json b/configs/quests.config.json index 0247d994..64862885 100644 --- a/configs/quests.config.json +++ b/configs/quests.config.json @@ -1,6 +1,6 @@ { "daily": { - "dailyQuestsCount": 2, + "dailyQuestsCount": 3, "dailyQuests": [ { "day": 1, @@ -36,6 +36,19 @@ ], "storeParams": [] } + }, + { + "name": "Represent your chain", + "description": "Join a faction to represent your favorite chain in the factions tab", + "reward": 3, + "questContract": { + "type": "ChainFactionQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD" + ], + "storeParams": [] + } } ] }, @@ -69,9 +82,32 @@ "initParams": [ "$CANVAS_NFT_CONTRACT", "$ART_PEACE_CONTRACT", - "$REWARD" + "$REWARD", + "0", + "0" ], - "storeParams": [] + "storeParams": [3,4], + "claimParams": [ + { + "type": "int", + "name": "Token ID", + "input": false + } + ] + } + }, + { + "name": "Cast your vote", + "description": "Vote to add a color to the palette in the vote tab", + "reward": 3, + "questContract": { + "type": "VoteQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD", + "$DAY_IDX" + ], + "storeParams": [2] } } ] @@ -96,6 +132,20 @@ ], "storeParams": [2,3,4,5,6] } + }, + { + "name": "Last color vote", + "description": "Cast your vote in the last color vote in the vote tab", + "reward": 3, + "questContract": { + "type": "VoteQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD", + "$DAY_IDX" + ], + "storeParams": [2] + } } ] }, @@ -119,6 +169,29 @@ ], "storeParams": [2,3,4,5,6] } + }, + { + "name": "Finalize your art piece", + "description": "Mint an NFT of your artwork to keep it forever", + "reward": 5, + "questContract": { + "type": "NFTMintQuest", + "initParams": [ + "$CANVAS_NFT_CONTRACT", + "$ART_PEACE_CONTRACT", + "$REWARD", + "1", + "$DAY_IDX" + ], + "storeParams": [3,4], + "claimParams": [ + { + "type": "int", + "name": "Token ID", + "input": false + } + ] + } } ] } @@ -139,6 +212,33 @@ "storeParams": [] } }, + { + "name": "Join a Faction", + "description": "Represent a community by joining their faction on the factions tab", + "reward": 3, + "questContract": { + "type": "FactionQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD" + ], + "storeParams": [] + } + }, + { + "name": "HODL", + "description": "Accumulate 10 extra pixels in your account", + "reward": 5, + "questContract": { + "type": "HodlQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD", + "10" + ], + "storeParams": [2] + } + }, { "name": "Deploy a Memecoin", "description": "Create your own [Unruggable memecoin](https://www.unruggable.meme/)", @@ -154,10 +254,29 @@ { "type": "address", "name": "MemeCoin Address", - "example": "0x02D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23" + "example": "0x02D7B50EBF415606D77C7E7842546FC13F8ACFBFD16F7BCF2BC2D08F54114C23", + "input": true } ] } + }, + { + "name": "Place 100 pixels", + "description": "Add 100 pixels on the canvas", + "reward": 15, + "questContract": { + "type": "PixelQuest", + "initParams": [ + "$ART_PEACE_CONTRACT", + "$REWARD", + "100", + "0", + "0", + "0", + "0" + ], + "storeParams": [2,3,4,5,6] + } } ] } diff --git a/docker-compose.yml b/docker-compose.yml index 5070b14f..347dfc28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,7 @@ services: restart: always environment: - POSTGRES_PASSWORD=password + - ART_PEACE_END_TIME=3000000000 volumes: - nfts:/app/nfts consumer: diff --git a/frontend/src/App.js b/frontend/src/App.js index 119d9850..61fbe279 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -16,7 +16,7 @@ import { usePreventZoom, useLockScroll } from './utils/Window.js'; import { backendUrl, wsUrl, devnetMode } from './utils/Consts.js'; import logo from './resources/logo.png'; import canvasConfig from './configs/canvas.config.json'; -import { fetchWrapper } from './services/apiService.js'; +import { fetchWrapper, getTodaysStartTime } from './services/apiService.js'; import art_peace_abi from './contracts/art_peace.abi.json'; import username_store_abi from './contracts/username_store.abi.json'; import canvas_nft_abi from './contracts/canvas_nft.abi.json'; @@ -91,6 +91,38 @@ function App() { abi: canvas_nft_abi }); + const [currentDay, setCurrentDay] = useState(0); + const [isLastDay, setIsLastDay] = useState(false); + const [gameEnded, setGameEnded] = useState(false); + useEffect(() => { + const fetchGameData = async () => { + let response = await fetchWrapper('get-game-data'); + if (!response.data) { + return; + } + setCurrentDay(response.data.day); + if (devnetMode) { + const days = 4; + if (response.data.day >= days) { + setGameEnded(true); + } else if (response.data.day === days - 1) { + setIsLastDay(true); + } + } else { + let now = new Date(); + const result = await getTodaysStartTime(); + let dayEnd = new Date(result.data); + dayEnd.setHours(dayEnd.getHours() + 24); + if (now.getTime() >= response.data.endTime) { + setGameEnded(true); + } else if (dayEnd.getTime() >= response.data.endTime) { + setIsLastDay(true); + } + } + }; + fetchGameData(); + }, []); + // Websocket const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(wsUrl, { share: false, @@ -188,6 +220,8 @@ function App() { const [lastPlacedTime, setLastPlacedTime] = useState(0); const [basePixelUp, setBasePixelUp] = useState(false); + const [chainFactionPixelsData, setChainFactionPixelsData] = useState([]); + const [chainFactionPixels, setChainFactionPixels] = useState([]); const [factionPixelsData, setFactionPixelsData] = useState([]); const [factionPixels, setFactionPixels] = useState([]); const [extraPixels, setExtraPixels] = useState(0); @@ -243,6 +277,45 @@ function App() { return () => clearInterval(interval); }, [lastPlacedTime]); + const [chainFactionPixelTimers, setChainFactionPixelTimers] = useState([]); + useEffect(() => { + const updateChainFactionPixelTimers = () => { + let newChainFactionPixelTimers = []; + let newChainFactionPixels = []; + for (let i = 0; i < chainFactionPixelsData.length; i++) { + let memberPixels = chainFactionPixelsData[i].memberPixels; + if (memberPixels !== 0) { + newChainFactionPixelTimers.push('00:00'); + newChainFactionPixels.push(memberPixels); + continue; + } + let lastPlacedTime = new Date(chainFactionPixelsData[i].lastPlacedTime); + let timeSinceLastPlacement = Date.now() - lastPlacedTime; + let chainFactionPixelAvailable = + timeSinceLastPlacement > timeBetweenPlacements; + if (chainFactionPixelAvailable) { + newChainFactionPixelTimers.push('00:00'); + newChainFactionPixels.push(chainFactionPixelsData[i].allocation); + } else { + let secondsTillPlacement = Math.floor( + (timeBetweenPlacements - timeSinceLastPlacement) / 1000 + ); + newChainFactionPixelTimers.push( + `${Math.floor(secondsTillPlacement / 60)}:${secondsTillPlacement % 60 < 10 ? '0' : ''}${secondsTillPlacement % 60}` + ); + newChainFactionPixels.push(0); + } + } + setChainFactionPixelTimers(newChainFactionPixelTimers); + setChainFactionPixels(newChainFactionPixels); + }; + const interval = setInterval(() => { + updateChainFactionPixelTimers(); + }, updateInterval); + updateChainFactionPixelTimers(); + return () => clearInterval(interval); + }, [chainFactionPixelsData]); + const [factionPixelTimers, setFactionPixelTimers] = useState([]); useEffect(() => { const updateFactionPixelTimers = () => { @@ -283,14 +356,21 @@ function App() { }, [factionPixelsData]); useEffect(() => { + let totalChainFactionPixels = 0; + for (let i = 0; i < chainFactionPixels.length; i++) { + totalChainFactionPixels += chainFactionPixels[i]; + } let totalFactionPixels = 0; for (let i = 0; i < factionPixels.length; i++) { totalFactionPixels += factionPixels[i]; } setAvailablePixels( - (basePixelUp ? 1 : 0) + totalFactionPixels + extraPixels + (basePixelUp ? 1 : 0) + + totalChainFactionPixels + + totalFactionPixels + + extraPixels ); - }, [basePixelUp, factionPixels, extraPixels]); + }, [basePixelUp, chainFactionPixels, factionPixels, extraPixels]); useEffect(() => { async function fetchExtraPixelsEndpoint() { @@ -305,6 +385,18 @@ function App() { } fetchExtraPixelsEndpoint(); + async function fetchChainFactionPixelsEndpoint() { + let chainFactionPixelsResponse = await fetchWrapper( + `get-chain-faction-pixels?address=${queryAddress}` + ); + if (!chainFactionPixelsResponse.data) { + setChainFactionPixelsData([]); + return; + } + setChainFactionPixelsData(chainFactionPixelsResponse.data); + } + fetchChainFactionPixelsEndpoint(); + async function fetchFactionPixelsEndpoint() { let factionPixelsResponse = await fetchWrapper( `get-faction-pixels?address=${queryAddress}` @@ -381,6 +473,18 @@ function App() { const [chainFaction, setChainFaction] = useState(null); const [userFactions, setUserFactions] = useState([]); useEffect(() => { + async function fetchChainFaction() { + let chainFactionResponse = await fetchWrapper( + `get-my-chain-factions?address=${queryAddress}` + ); + if (!chainFactionResponse.data) { + return; + } + if (chainFactionResponse.data.length === 0) { + return; + } + setChainFaction(chainFactionResponse.data[0]); + } async function fetchUserFactions() { let userFactionsResponse = await fetchWrapper( `get-my-factions?address=${queryAddress}` @@ -390,6 +494,7 @@ function App() { } setUserFactions(userFactionsResponse.data); } + fetchChainFaction(); fetchUserFactions(); }, [queryAddress]); @@ -480,6 +585,7 @@ function App() { animationDuration={5000} />
@@ -607,28 +723,30 @@ function App() { alignItems: `${footerExpanded && isFooterSplit ? 'flex-end' : 'center'}` }} > - + {!gameEnded && ( + + )} {isFooterSplit && !footerExpanded && (
{ if (!devnetMode) { props.setSelectedColorId(-1); + props.colorPixel(position, colorId); placePixelCall(position, colorId, timestamp); props.clearPixelSelection(); props.setLastPlacedTime(timestamp * 1000); @@ -331,6 +332,7 @@ const CanvasContainer = (props) => { if (props.selectedColorId !== -1) { props.setSelectedColorId(-1); + props.colorPixel(position, colorId); const response = await fetchWrapper(`place-pixel-devnet`, { mode: 'cors', method: 'POST', diff --git a/frontend/src/footer/PixelSelector.js b/frontend/src/footer/PixelSelector.js index 89d19114..0fea4c48 100644 --- a/frontend/src/footer/PixelSelector.js +++ b/frontend/src/footer/PixelSelector.js @@ -26,6 +26,7 @@ const PixelSelector = (props) => { return; } } else { + // TODO: Use lowest timer out of base, chain, faction, ... setPlacementTimer(props.basePixelTimer); } }, [ diff --git a/frontend/src/services/apiService.js b/frontend/src/services/apiService.js index 01219b6f..c46fd735 100644 --- a/frontend/src/services/apiService.js +++ b/frontend/src/services/apiService.js @@ -72,6 +72,10 @@ export const getFactions = async (query) => { ); }; +export const getChainFactions = async (query) => { + return await fetchWrapper(`get-chain-factions?address=${query.queryAddress}`); +}; + export const getNewNftsFn = async (params) => { const { page, pageLength, queryAddress } = params; return await fetchWrapper( @@ -101,3 +105,15 @@ export const getHotNftsFn = async (params) => { `get-hot-nfts?address=${queryAddress}&page=${page}&pageLength=${pageLength}` ); }; + +export const getChainFactionMembers = async (query) => { + return await fetchWrapper( + `get-chain-faction-members?factionId=${query.factionId}&page=${query.page}&pageLength=${query.pageLength}` + ); +}; + +export const getFactionMembers = async (query) => { + return await fetchWrapper( + `get-faction-members?factionId=${query.factionId}&page=${query.page}&pageLength=${query.pageLength}` + ); +}; diff --git a/frontend/src/tabs/TabPanel.js b/frontend/src/tabs/TabPanel.js index 089e186a..6e563411 100644 --- a/frontend/src/tabs/TabPanel.js +++ b/frontend/src/tabs/TabPanel.js @@ -41,6 +41,7 @@ const TabPanel = (props) => { appear > { setLastPlacedTime={props.setLastPlacedTime} basePixelUp={props.basePixelUp} basePixelTimer={props.basePixelTimer} + chainFaction={props.chainFaction} + chainFactionPixels={props.chainFactionPixels} factionPixels={props.factionPixels} + setChainFactionPixels={props.setChainFactionPixels} setFactionPixels={props.setFactionPixels} + chainFactionPixelTimers={props.chainFactionPixelTimers} factionPixelTimers={props.factionPixelTimers} + chainFactionPixelsData={props.chainFactionPixelsData} factionPixelsData={props.factionPixelsData} + setChainFactionPixelsData={props.setChainFactionPixelsData} setFactionPixelsData={props.setFactionPixelsData} extraPixels={props.extraPixels} setPixelSelection={props.setPixelSelection} @@ -107,6 +114,7 @@ const TabPanel = (props) => { {({ timeLeftInDay, newDayAvailable, startNextDay }) => ( { queryAddress={props.queryAddress} setExtraPixels={props.setExtraPixels} extraPixels={props.extraPixels} + gameEnded={props.gameEnded} /> )} @@ -127,15 +136,24 @@ const TabPanel = (props) => { {props.activeTab === 'Factions' && (
)} @@ -144,6 +162,7 @@ const TabPanel = (props) => { {({ timeLeftInDay, newDayAvailable, startNextDay }) => ( { queryAddress={props.queryAddress} address={props.address} artPeaceContract={props.artPeaceContract} + isLastDay={props.isLastDay} + gameEnded={props.gameEnded} /> )} @@ -179,6 +200,7 @@ const TabPanel = (props) => { setLatestMintedTokenId={props.setLatestMintedTokenId} queryAddress={props.queryAddress} isMobile={props.isMobile} + gameEnded={props.gameEnded} />
)} @@ -194,6 +216,7 @@ const TabPanel = (props) => { connectWallet={props.connectWallet} connectors={props.connectors} isMobile={props.isMobile} + gameEnded={props.gameEnded} />
)} diff --git a/frontend/src/tabs/account/Account.css b/frontend/src/tabs/account/Account.css index 4d3d7e9f..e5646065 100644 --- a/frontend/src/tabs/account/Account.css +++ b/frontend/src/tabs/account/Account.css @@ -69,6 +69,9 @@ justify-content: center; padding: 1rem 0.5rem; margin: 0 1.5rem; +} + +.Account__item__separator { border-bottom: 1px solid rgba(0, 0, 0, 0.3); } @@ -192,7 +195,14 @@ } .Account__disconnect__button { - margin: auto; - padding: 0.7rem 1rem; - width: 50%; + padding: 0.7rem 2rem; + margin: 0 0 0 1rem; +} + +.Account__footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin: 0 1rem; } diff --git a/frontend/src/tabs/account/Account.js b/frontend/src/tabs/account/Account.js index 517ac524..dae2e404 100644 --- a/frontend/src/tabs/account/Account.js +++ b/frontend/src/tabs/account/Account.js @@ -5,6 +5,7 @@ import BasicTab from '../BasicTab.js'; import '../../utils/Styles.css'; import { backendUrl, devnetMode } from '../../utils/Consts.js'; import { fetchWrapper } from '../../services/apiService.js'; +import { encodeToLink } from '../../utils/encodeToLink'; import BeggarRankImg from '../../resources/ranks/Beggar.png'; import OwlRankImg from '../../resources/ranks/Owl.png'; import CrownRankImg from '../../resources/ranks/Crown.png'; @@ -342,16 +343,18 @@ const Account = (props) => {

Username

{username}

-
- edit -
+ {!props.gameEnded && ( +
+ edit +
+ )}
) : ( @@ -386,15 +389,20 @@ const Account = (props) => { )} -
-
-
- rank -

{accountRank}

+
+

Rank

+
+
+
+ rank +

+ {accountRank} +

+
@@ -438,11 +446,21 @@ const Account = (props) => {
-
disconnectWallet()} - > - Logout +
+
+

+
+
disconnectWallet()} + > + Logout +
)} diff --git a/frontend/src/tabs/canvas/ExtraPixelsPanel.js b/frontend/src/tabs/canvas/ExtraPixelsPanel.js index d6f6507b..a2b4582a 100644 --- a/frontend/src/tabs/canvas/ExtraPixelsPanel.js +++ b/frontend/src/tabs/canvas/ExtraPixelsPanel.js @@ -79,9 +79,74 @@ const ExtraPixelsPanel = (props) => { console.log(response.result); } } + for (let i = 0; i < props.extraPixelsData.length; i++) { + let position = + props.extraPixelsData[i].x + + props.extraPixelsData[i].y * canvasConfig.canvas.width; + props.colorPixel(position, props.extraPixelsData[i].colorId); + } if (basePixelUsed) { props.setLastPlacedTime(timestamp * 1000); } + if (chainFactionPixelsUsed > 0) { + let chainFactionIndex = 0; + let chainFactionUsedCounter = 0; + let newChainFactionPixels = []; + let newChainFactionPixelsData = []; + while (chainFactionIndex < props.chainFactionPixels.length) { + if (chainFactionUsedCounter >= chainFactionPixelsUsed) { + newChainFactionPixels.push( + props.chainFactionPixels[chainFactionIndex] + ); + newChainFactionPixelsData.push( + props.chainFactionPixelsData[chainFactionIndex] + ); + chainFactionIndex++; + continue; + } + let currChainFactionPixelsUsed = Math.min( + chainFactionPixelsUsed - chainFactionUsedCounter, + props.chainFactionPixels[chainFactionIndex] + ); + if (currChainFactionPixelsUsed <= 0) { + newChainFactionPixels.push( + props.chainFactionPixels[chainFactionIndex] + ); + newChainFactionPixelsData.push( + props.chainFactionPixelsData[chainFactionIndex] + ); + chainFactionIndex++; + continue; + } + if ( + currChainFactionPixelsUsed === + props.chainFactionPixels[chainFactionIndex] + ) { + newChainFactionPixels.push(0); + let newChainFactionData = + props.chainFactionPixelsData[chainFactionIndex]; + newChainFactionData.lastPlacedTime = timestamp * 1000; + newChainFactionData.memberPixels = 0; + newChainFactionPixelsData.push(newChainFactionData); + } else { + newChainFactionPixels.push( + props.chainFactionPixels[chainFactionIndex] - + currChainFactionPixelsUsed + ); + let newChainFactionData = + props.chainFactionPixelsData[chainFactionIndex]; + newChainFactionData.memberPixels = + props.chainFactionPixels[chainFactionIndex] - + currChainFactionPixelsUsed; + newChainFactionPixelsData.push(newChainFactionData); + } + chainFactionUsedCounter += currChainFactionPixelsUsed; + chainFactionIndex++; + } + props.setChainFactionPixels(newChainFactionPixels); + props.setChainFactionPixelsData(newChainFactionPixelsData); + } + // TODO: Click faction pixels button to expand out info here if (factionPixelsUsed > 0) { // TODO: Will order always be the same? @@ -138,7 +203,10 @@ const ExtraPixelsPanel = (props) => { }; const [basePixelUsed, setBasePixelUsed] = React.useState(false); + const [totalChainFactionPixels, setTotalChainFactionPixels] = + React.useState(0); const [totalFactionPixels, setTotalFactionPixels] = React.useState(0); + const [chainFactionPixelsUsed, setChainFactionPixelsUsed] = React.useState(0); const [factionPixelsUsed, setFactionPixelsUsed] = React.useState(0); const [extraPixelsUsed, setExtraPixelsUsed] = React.useState(0); React.useEffect(() => { @@ -151,11 +219,24 @@ const ExtraPixelsPanel = (props) => { setBasePixelUsed(false); } } + let allChainFactionPixels = 0; + for (let i = 0; i < props.chainFactionPixels.length; i++) { + allChainFactionPixels += props.chainFactionPixels[i]; + } + setTotalChainFactionPixels(allChainFactionPixels); let allFactionPixels = 0; for (let i = 0; i < props.factionPixels.length; i++) { allFactionPixels += props.factionPixels[i]; } setTotalFactionPixels(allFactionPixels); + if (allChainFactionPixels > 0) { + let chainFactionsPixelsUsed = Math.min( + pixelsUsed, + totalChainFactionPixels + ); + setChainFactionPixelsUsed(chainFactionsPixelsUsed); + pixelsUsed -= chainFactionsPixelsUsed; + } if (allFactionPixels > 0) { let factionsPixelsUsed = Math.min(pixelsUsed, totalFactionPixels); setFactionPixelsUsed(factionsPixelsUsed); @@ -170,6 +251,9 @@ const ExtraPixelsPanel = (props) => { const [factionPixelsExpanded, setFactionPixelsExpanded] = React.useState(false); + const getChainFactionName = (_index) => { + return props.chainFaction.name; + }; const getFactionName = (index) => { /* TODO: Animate expanding */ const id = props.userFactions.findIndex( @@ -219,18 +303,51 @@ const ExtraPixelsPanel = (props) => {

)}
- {totalFactionPixels > 0 && ( + {(props.chainFactionPixels.length > 0 || + props.factionPixels.length > 0) && (
setFactionPixelsExpanded(!factionPixelsExpanded)} >

Faction

- {totalFactionPixels - factionPixelsUsed} /  - {totalFactionPixels} + {totalChainFactionPixels + + totalFactionPixels - + chainFactionPixelsUsed - + factionPixelsUsed} + /  + {totalChainFactionPixels + totalFactionPixels}

{factionPixelsExpanded && (
+ {props.chainFactionPixels.map((chainFactionPixel, index) => { + return ( +
+

+ {getChainFactionName(index)} +

+

+ {chainFactionPixel === 0 + ? props.chainFactionPixelTimers[index] + : chainFactionPixel + 'px'} +

+
+ ); + })} {props.factionPixels.map((factionPixel, index) => { return (
{ // TODO: Faction owner tabs: allocations, ... const factionsSubTabs = ['templates', 'info']; const [activeTab, setActiveTab] = useState(factionsSubTabs[0]); - // TODO: Think what info to show for faction ( members, pixels, pool, ... ) const [_leader, _setLeader] = useState('Brandon'); // TODO: Fetch leader & show in members info - const [pool, _setPool] = useState(10); const [members, setMembers] = useState([]); + const [membersPagination, setMembersPagination] = useState({ + pageLength: 10, + page: 1 + }); + useEffect(() => { + let newPagination = { + pageLength: 10, + page: 1 + }; + setMembersPagination(newPagination); + }, [props.faction]); + useEffect(() => { const createShorthand = (name) => { if (name.length > 12) { @@ -24,50 +39,42 @@ const FactionItem = (props) => { return name; } }; - // TODO: Fetch members - const memberData = [ - { - name: 'Brandon', - allocation: 3 - }, - { - name: 'John', - allocation: 2 - }, - { - name: 'Mark', - allocation: 2 - }, - { - name: 'David', - allocation: 2 - }, - { - name: '0x12928349872394827349827349287234982374982734479234', - allocation: 1 - }, - { - name: 'Alex', - allocation: 0 - }, - { - name: '0x159234987239482734982734928723498237498273447923a4', - allocation: 0 - }, - { - name: 'Smith', - allocation: 0 + async function getMembers() { + try { + let result = []; + if (props.isChain) { + result = await getChainFactionMembers({ + factionId: props.faction.factionId, + page: membersPagination.page, + pageLength: membersPagination.pageLength + }); + } else { + result = await getFactionMembers({ + factionId: props.faction.factionId, + page: membersPagination.page, + pageLength: membersPagination.pageLength + }); + } + if (!result.data || result.data.length === 0) { + setMembers([]); + return; + } + let shortenedMembers = []; + result.data.forEach((member) => { + let name = + member.username == '' ? '0x' + member.userAddress : member.username; + shortenedMembers.push({ + name: createShorthand(name), + allocation: member.totalAllocation + }); + }); + setMembers(shortenedMembers); + } catch (error) { + console.log(error); } - ]; - let shortenedMembers = []; - memberData.forEach((member) => { - shortenedMembers.push({ - name: createShorthand(member.name), - allocation: member.allocation - }); - }); - setMembers(shortenedMembers); - }, [props.faction]); + } + getMembers(); + }, [props.faction, membersPagination.page, membersPagination.pageLength]); const factionTemplates = [ { @@ -100,6 +107,27 @@ const FactionItem = (props) => { } ]; + const [canJoin, setCanJoin] = useState(true); + useEffect(() => { + if (props.queryAddress === '0' || props.gameEnded) { + setCanJoin(false); + return; + } + if (props.faction.isMember || !props.faction.joinable) { + setCanJoin(false); + return; + } + if (props.isChain && props.userInChainFaction) { + setCanJoin(false); + return; + } + if (!props.isChain && props.userInFaction) { + setCanJoin(false); + return; + } + setCanJoin(true); + }, [props]); + return (
@@ -188,11 +216,17 @@ const FactionItem = (props) => {

)} - {!props.faction.isMember && ( + {canJoin && (
props.joinFaction(props.faction.factionId)} + onClick={() => { + if (props.isChain) { + props.joinChain(props.faction.factionId); + } else { + props.joinFaction(props.faction.factionId); + } + }} >

Join

@@ -253,7 +287,7 @@ const FactionItem = (props) => {

- Pool: {pool}px + Pool: {props.faction.members * props.factionAlloc}px

Alloc

@@ -270,6 +304,11 @@ const FactionItem = (props) => {
); })} +
)} diff --git a/frontend/src/tabs/factions/FactionSelector.js b/frontend/src/tabs/factions/FactionSelector.js index 08564d6c..1dae7b4c 100644 --- a/frontend/src/tabs/factions/FactionSelector.js +++ b/frontend/src/tabs/factions/FactionSelector.js @@ -13,7 +13,7 @@ const FactionSelector = (props) => { if (e.target.classList.contains('FactionSelector__link__icon')) { return; } - props.selectFaction(props.factionId); + props.selectFaction(props, props.isChain); }; return ( diff --git a/frontend/src/tabs/factions/Factions.css b/frontend/src/tabs/factions/Factions.css index 28b906db..936be7b0 100644 --- a/frontend/src/tabs/factions/Factions.css +++ b/frontend/src/tabs/factions/Factions.css @@ -17,6 +17,19 @@ margin-bottom: 0.5rem; } +.Factions__heading { + padding: 0 1rem; +} + +.Factions__header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0 1rem 0 0; + margin-bottom: 0; +} + .Factions__header__buttons { display: flex; flex-direction: row; @@ -77,10 +90,13 @@ max-height: 47vh; width: 100%; padding: 0.5rem 2rem; + overflow: scroll; +} + +.Factions__all__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr)); align-items: flex-start; - overflow: scroll; } .Factions__joiner { diff --git a/frontend/src/tabs/factions/Factions.js b/frontend/src/tabs/factions/Factions.js index 1b30ac1c..7d8e1e8f 100644 --- a/frontend/src/tabs/factions/Factions.js +++ b/frontend/src/tabs/factions/Factions.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useContractWrite } from '@starknet-react/core'; import './Factions.css'; import FactionSelector from './FactionSelector.js'; import FactionItem from './FactionItem.js'; @@ -12,66 +13,20 @@ import Polygon from '../../resources/chains/Polygon.png'; import Solana from '../../resources/chains/Solana.png'; import ZkSync from '../../resources/chains/ZkSync.png'; import { PaginationView } from '../../ui/pagination.js'; -import { getFactions } from '../../services/apiService.js'; -import { convertUrl } from '../../utils/Consts.js'; +import { getFactions, getChainFactions } from '../../services/apiService.js'; +import { devnetMode, convertUrl } from '../../utils/Consts.js'; +import { fetchWrapper } from '../../services/apiService.js'; const FactionsMainSection = (props) => { // TODO: convertUrl when fetching from server - useState(() => { - let newFactions = []; - if (!props.userFactions) { - return; - } - props.userFactions.forEach((faction) => { - if ( - newFactions.findIndex((f) => f.factionId === faction.factionId) !== -1 - ) { - return; - } - let totalAllocation = 0; - props.factionPixelsData.forEach((factionData) => { - if (factionData.factionId === faction.factionId) { - totalAllocation += factionData.allocation; - } - }); - let newFaction = { - factionId: faction.factionId, - name: faction.name, - icon: faction.icon, - pixels: totalAllocation, - members: faction.members, - isMember: true, - telegram: faction.telegram, - twitter: faction.twitter, - github: faction.github, - site: faction.site - }; - newFactions.push(newFaction); - }); - props.setMyFactions(newFactions); - }, [props.userFactions]); - - const joinFaction = (factionId) => { - // TODO: Join faction - let newFactions = [...props.allFactions]; - let idx = newFactions.findIndex((f) => f.factionId === factionId); - if (idx === -1) { - return; - } - newFactions[idx].isMember = true; - - let newMyFactions = [...props.myFactions]; - newMyFactions.push(newFactions[idx]); - props.setMyFactions(newMyFactions); - props.setAllFactions(newFactions); - }; - return (
{
{props.selectedFaction === null && (
+
+

My Factions

+ {!props.expanded && ( +
{ + props.setExpanded(true); + props.setExploreMode(true); + }} + > + Explore +
+ )} +
{props.chainFaction && ( )} - {props.myFactions.map((faction, idx) => ( + {props.userFactions.map((faction, idx) => ( { twitter={faction.twitter} github={faction.github} site={faction.site} + isChain={false} /> ))}

0 + props.chainFaction || props.userFactions.length > 0 ? 'none' : 'block', textAlign: 'center' }} > - Join a faction to represent your community + {props.queryAddress === '0' + ? 'Login with your wallet to see your factions' + : 'Join a faction to represent your community'}

)} {props.selectedFaction !== null && ( 0} + userInChainFaction={props.chainFaction !== null} + factionAlloc={props.selectedFactionType === 'chain' ? 2 : 1} + isChain={props.selectedFactionType === 'chain'} + gameEnded={props.gameEnded} /> )}
@@ -139,11 +131,8 @@ const FactionsMainSection = (props) => { ); }; // TODO: MyFactions pagination -// TODO: Pool const FactionsExpandedSection = (props) => { - // TODO: Load from server - React.useEffect(() => { if (!props.expanded) { return; @@ -151,7 +140,7 @@ const FactionsExpandedSection = (props) => { async function getAllFactions() { try { const result = await getFactions({ - address: props.queryAddress, + queryAddress: props.queryAddress, page: props.allFactionsPagination.page, pageLength: props.allFactionsPagination.pageLength }); @@ -166,7 +155,20 @@ const FactionsExpandedSection = (props) => { console.log('Error fetching Nfts', error); } } + async function getAllChainFactions() { + try { + const result = await getChainFactions({ + queryAddress: props.queryAddress + }); + if (result.data) { + props.setChainFactions(result.data); + } + } catch (error) { + console.log('Error fetching Nfts', error); + } + } getAllFactions(); + getAllChainFactions(); }, [ props.queryAddress, props.expanded, @@ -179,30 +181,32 @@ const FactionsExpandedSection = (props) => { return (
- {props.chainFaction === null && props.exploreMode === false ? ( + {props.chainFaction === null && + props.exploreMode === false && + !props.gameEnded ? (

Pick a faction to represent your favorite chain

props.setExploreMode(true)} > Explore
- {props.chainOptions.map((chain, idx) => ( + {props.chainFactions.map((chain, idx) => (
props.joinChain(idx)} + onClick={() => props.joinChain(idx + 1)} >
icon

@@ -239,23 +243,49 @@ const FactionsExpandedSection = (props) => {

- {props.allFactions.map((faction, idx) => ( - - ))} +
+ {props.allFactions.map((faction, idx) => ( + + ))} +
+

Chain Factions

+
+ {props.chainFactions.map((chain, idx) => ( + + ))} +
)} @@ -263,29 +293,171 @@ const FactionsExpandedSection = (props) => { ); }; +const chainIcons = { + Starknet: Starknet, + Solana: Solana, + Bitcoin: Bitcoin, + Base: Base, + ZkSync: ZkSync, + Polygon: Polygon, + Optimism: Optimism, + Scroll: Scroll +}; + const Factions = (props) => { const [expanded, setExpanded] = useState(false); const [exploreMode, setExploreMode] = useState(false); - const [myFactions, setMyFactions] = useState([]); + const [chainFactions, setChainFactions] = useState([]); const [allFactions, setAllFactions] = useState([]); - const chainOptions = [ - { name: 'Starknet', icon: Starknet }, - { name: 'Solana', icon: Solana }, - { name: 'Bitcoin', icon: Bitcoin }, - { name: 'Base', icon: Base }, - { name: 'ZkSync', icon: ZkSync }, - { name: 'Polygon', icon: Polygon }, - { name: 'Optimism', icon: Optimism }, - { name: 'Scroll', icon: Scroll } - ]; - const joinChain = (chainId) => { - props.setChainFaction(chainOptions[chainId]); - setExpanded(false); + const [calls, setCalls] = useState([]); + const joinChainCall = (chainId) => { + if (props.gameEnded) return; + if (devnetMode) return; + if (!props.address || !props.artPeaceContract) return; + if (chainId === 0) return; + setCalls( + props.usernameContract.populateTransaction['join_chain_faction'](chainId) + ); + }; + const joinFactionCall = (factionId) => { + if (devnetMode) return; + if (!props.address || !props.artPeaceContract) return; + if (factionId === 0) return; + setCalls( + props.usernameContract.populateTransaction['join_faction'](factionId) + ); + }; + + useEffect(() => { + const factionCall = async () => { + if (devnetMode) return; + if (calls.length === 0) return; + await writeAsync(); + console.log('Faction call successful:', data, isPending); + }; + factionCall(); + }, [calls]); + + const { writeAsync, data, isPending } = useContractWrite({ + calls + }); + + const joinChain = async (chainId) => { + if (!devnetMode) { + joinChainCall(chainId); + setExpanded(false); + let newChainFactions = chainFactions.map((chain) => { + if (chain.factionId === chainId) { + chain.isMember = true; + chain.members += 1; + } + return chain; + }); + let chain = chainFactions.find((chain) => chain.factionId === chainId); + if (chain) { + props.setChainFaction(chain); + let newChainFactionPixelsData = { + allocation: 2, + factionId: chainId, + lastPlacedTime: new Date(0).getTime(), + memberPixels: 0 + }; + props.setChainFactionPixelsData([newChainFactionPixelsData]); + } + setChainFactions(newChainFactions); + return; + } + let joinChainResponse = await fetchWrapper('join-chain-faction-devnet', { + mode: 'cors', + method: 'POST', + body: JSON.stringify({ + chainId: chainId.toString() + }) + }); + if (joinChainResponse.result) { + setExpanded(false); + let newChainFactions = chainFactions.map((chain) => { + if (chain.factionId === chainId) { + chain.isMember = true; + chain.members += 1; + } + return chain; + }); + let chain = chainFactions.find((chain) => chain.factionId === chainId); + if (chain) { + props.setChainFaction(chain); + let newChainFactionPixelsData = { + allocation: 2, + factionId: chainId, + lastPlacedTime: new Date(0).getTime(), + memberPixels: 0 + }; + props.setChainFactionPixelsData([newChainFactionPixelsData]); + } + setChainFactions(newChainFactions); + } + }; + + const joinFaction = async (factionId) => { + if (!devnetMode) { + joinFactionCall(factionId); + let newAllFactions = allFactions.map((faction) => { + if (faction.factionId === factionId) { + faction.isMember = true; + faction.members += 1; + } + return faction; + }); + let faction = allFactions.find( + (faction) => faction.factionId === factionId + ); + let newUserFactions = [...props.userFactions, faction]; + props.setUserFactions(newUserFactions); + // TODO: Hardcoded + let newFactionPixelsData = { + allocation: 1, + factionId: factionId, + lastPlacedTime: new Date(0).getTime(), + memberPixels: 0 + }; + props.setFactionPixelsData([newFactionPixelsData]); + setAllFactions(newAllFactions); + return; + } + let joinFactionResponse = await fetchWrapper('join-faction-devnet', { + mode: 'cors', + method: 'POST', + body: JSON.stringify({ + factionId: factionId.toString() + }) + }); + if (joinFactionResponse.result) { + let newAllFactions = allFactions.map((faction) => { + if (faction.factionId === factionId) { + faction.isMember = true; + faction.members += 1; + } + return faction; + }); + let faction = allFactions.find( + (faction) => faction.factionId === factionId + ); + let newUserFactions = [...props.userFactions, faction]; + props.setUserFactions(newUserFactions); + let newFactionPixelsData = { + allocation: 1, + factionId: factionId, + lastPlacedTime: new Date(0).getTime(), + memberPixels: 0 + }; + props.setFactionPixelsData([newFactionPixelsData]); + setAllFactions(newAllFactions); + } }; useEffect(() => { - if (!props.chainFaction) { + if (props.queryAddress !== '0' && !props.chainFaction) { setExpanded(true); } }, [props.chainFaction]); @@ -300,21 +472,10 @@ const Factions = (props) => { }); const [selectedFaction, setSelectedFaction] = useState(null); - const selectFaction = (factionId) => { - // TODO: Make this more efficient - for (let i = 0; i < myFactions.length; i++) { - if (myFactions[i].factionId === factionId) { - setSelectedFaction(myFactions[i]); - return; - } - } - - for (let i = 0; i < allFactions.length; i++) { - if (allFactions[i].factionId === factionId) { - setSelectedFaction(allFactions[i]); - return; - } - } + const [selectedFactionType, setSelectedFactionType] = useState(null); + const selectFaction = (faction, isChain) => { + setSelectedFaction(faction); + setSelectedFactionType(isChain ? 'chain' : 'faction'); }; const clearFactionSelection = () => { @@ -328,18 +489,27 @@ const Factions = (props) => { expandedSection={FactionsExpandedSection} setActiveTab={props.setActiveTab} userFactions={props.userFactions} + setUserFactions={props.setUserFactions} + chainFactionPixels={props.chainFactionPixels} factionPixels={props.factionPixels} + chainFactionPixelsData={props.chainFactionPixelsData} + setChainFactionPixelsData={props.setChainFactionPixelsData} factionPixelsData={props.factionPixelsData} + setFactionPixelsData={props.setFactionPixelsData} expanded={expanded} setExpanded={setExpanded} exploreMode={exploreMode} setExploreMode={setExploreMode} chainFaction={props.chainFaction} joinChain={joinChain} - chainOptions={chainOptions} - canExpand={props.chainFaction !== null || exploreMode} - myFactions={myFactions} - setMyFactions={setMyFactions} + joinFaction={joinFaction} + chainFactions={chainFactions} + setChainFactions={setChainFactions} + queryAddress={props.queryAddress} + canExpand={ + (props.queryAddress !== '0' && props.chainFaction !== null) || + exploreMode + } myFactionsPagination={myFactionsPagination} setMyFactionsPagination={setMyFactionsPagination} allFactionsPagination={allFactionsPagination} @@ -347,11 +517,13 @@ const Factions = (props) => { allFactions={allFactions} setAllFactions={setAllFactions} selectedFaction={selectedFaction} + selectedFactionType={selectedFactionType} selectFaction={selectFaction} clearFactionSelection={clearFactionSelection} setTemplateOverlayMode={props.setTemplateOverlayMode} setOverlayTemplate={props.setOverlayTemplate} isMobile={props.isMobile} + gameEnded={props.gameEnded} /> ); }; diff --git a/frontend/src/tabs/nfts/NFTItem.css b/frontend/src/tabs/nfts/NFTItem.css index b639f853..b9475408 100644 --- a/frontend/src/tabs/nfts/NFTItem.css +++ b/frontend/src/tabs/nfts/NFTItem.css @@ -35,6 +35,7 @@ display: flex; flex-direction: row; justify-content: right; + align-items: center; padding: 0; margin: 0; } @@ -66,6 +67,22 @@ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); } +.NFTItem__button--disabled { + background-color: rgba(255, 255, 255, 0.3) !important; + border: 1px solid rgba(0, 0, 0, 0.1) !important; + box-shadow: none !important; +} + +.NFTItem__button--disabled:hover { + transform: none !important; + box-shadow: none !important; +} + +.NFTItem__button--disabled:active { + transform: none !important; + box-shadow: none !important; +} + .NFTItem__info { position: absolute; top: 0; @@ -140,6 +157,26 @@ margin: 0.5rem 0; } +.NFTItem__name { + position: absolute; + top: 0; + left: 0; + margin: 0.5rem; + max-width: 80%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background-image: linear-gradient( + to bottom right, + rgba(255, 255, 255, 0.7), + rgba(255, 255, 255, 0.8) + ); + border-radius: 1.5rem; + border: 1px solid rgba(0, 0, 0, 0.3); + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3); + padding: 0.5rem 1rem; +} + .list-transition-enter, .list-transition-appear { transform: translateX(120%); @@ -161,3 +198,4 @@ transform: translateX(120%); transition: all 150ms; } + diff --git a/frontend/src/tabs/nfts/NFTItem.js b/frontend/src/tabs/nfts/NFTItem.js index 078e6597..c578dc4e 100644 --- a/frontend/src/tabs/nfts/NFTItem.js +++ b/frontend/src/tabs/nfts/NFTItem.js @@ -39,6 +39,9 @@ const NFTItem = (props) => { }); const handleLikePress = async (event) => { + if (props.queryAddress === '0') { + return; + } event.preventDefault(); if (!devnetMode) { if (liked) { @@ -58,8 +61,7 @@ const NFTItem = (props) => { }) }); if (likeResponse.result) { - setLikes(likes + 1); - setLiked(true); + props.updateLikes(props.tokenId, likes + 1, true); } } else { let unlikeResponse = await fetchWrapper('unlike-nft-devnet', { @@ -70,8 +72,7 @@ const NFTItem = (props) => { }) }); if (unlikeResponse.result) { - setLikes(likes - 1); - setLiked(false); + props.updateLikes(props.tokenId, likes - 1, false); } } }; @@ -134,13 +135,14 @@ const NFTItem = (props) => { alt={`nft-image-${props.tokenId}`} className='NFTItem__image' /> +

{props.name}

Share
{ props.setNftSelected(false); }; + const toHex = (str) => { + let hex = '0x'; + for (let i = 0; i < str.length; i++) { + hex += '' + str.charCodeAt(i).toString(16); + } + return hex; + }; + const [calls, setCalls] = useState([]); - const mintNftCall = (position, width, height) => { + const mintNftCall = (position, width, height, name) => { if (devnetMode) return; if (!props.address || !props.artPeaceContract) return; // TODO: Validate the position, width, and height @@ -22,7 +30,8 @@ const NFTMintingPanel = (props) => { let mintParams = { position: position, width: width, - height: height + height: height, + name: toHex(name) }; setCalls( props.artPeaceContract.populateTransaction['mint_nft'](mintParams) @@ -46,9 +55,11 @@ const NFTMintingPanel = (props) => { calls }); + const [nftName, setNftName] = useState(''); const submit = async () => { + if (nftName.length === 0 || nftName.length > 31) return; if (!devnetMode) { - mintNftCall(props.nftPosition, props.nftWidth, props.nftHeight); + mintNftCall(props.nftPosition, props.nftWidth, props.nftHeight, nftName); return; } let mintNFTEndpoint = 'mint-nft-devnet'; @@ -58,7 +69,8 @@ const NFTMintingPanel = (props) => { body: JSON.stringify({ position: props.nftPosition.toString(), width: props.nftWidth.toString(), - height: props.nftHeight.toString() + height: props.nftHeight.toString(), + name: toHex(nftName) }) }); if (response.result) { @@ -152,6 +164,8 @@ const NFTMintingPanel = (props) => { className='Text__small Input__primary NFTMintingPanel__form__input' type='text' placeholder='NFT name...' + value={nftName} + onChange={(e) => setNftName(e.target.value)} />
@@ -162,7 +176,7 @@ const NFTMintingPanel = (props) => { Cancel
31 ? 'Button__disabled' : ''}`} onClick={() => submit()} > Submit diff --git a/frontend/src/tabs/nfts/NFTs.css b/frontend/src/tabs/nfts/NFTs.css index eb86ca55..1116c541 100644 --- a/frontend/src/tabs/nfts/NFTs.css +++ b/frontend/src/tabs/nfts/NFTs.css @@ -1,11 +1,18 @@ .NFTs__main { position: relative; - width: min(100%, 40rem); + width: 100%; margin: 0 auto; transition: all 0.5s; } +.NFTs__main__expanded { + position: relative; + width: min(100%, 40rem); + margin: 0 auto; + transition: all 0.5s; +} + .NFTs__container { height: 55vh; display: grid; @@ -20,7 +27,7 @@ display: flex; flex-direction: row; justify-content: space-between; - align-items: center; + align-items: space-between; padding: 0.5rem; margin: 0 0.5rem; width: calc(100% - 1rem); @@ -28,13 +35,22 @@ .NFTs__heading { font-size: 1.6rem; - text-align: center; padding: 0; padding-bottom: 0.5rem; margin: 0; text-decoration: underline; } +.NFTS__nowallet { + text-align: center; +} + +.NFTs__buttons { + display: flex; + flex-direction: row; + align-items: center; +} + .NFTs__button { padding: 1rem; font-size: 1.2rem; diff --git a/frontend/src/tabs/nfts/NFTs.js b/frontend/src/tabs/nfts/NFTs.js index 0b80bccb..b7aecfec 100644 --- a/frontend/src/tabs/nfts/NFTs.js +++ b/frontend/src/tabs/nfts/NFTs.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import './NFTs.css'; import ExpandableTab from '../ExpandableTab.js'; import NFTItem from './NFTItem.js'; -import { backendUrl } from '../../utils/Consts.js'; +import { nftUrl } from '../../utils/Consts.js'; import { fetchWrapper, getMyNftsFn, @@ -14,22 +14,43 @@ import { import { PaginationView } from '../../ui/pagination.js'; const NFTsMainSection = (props) => { - const imageURL = backendUrl + '/nft-images/'; + const imageURL = nftUrl + '/nft-images/'; return ( -
+

My Collection

-
{ - props.setNftMintingMode(true); - props.setActiveTab('Canvas'); - }} - > - Mint +
+ {!props.gameEnded && props.queryAddress !== '0' && ( +
{ + props.setNftMintingMode(true); + props.setActiveTab('Canvas'); + }} + > + Mint +
+ )} + {!props.expanded && ( +
{ + props.setExpanded(true); + }} + > + Explore +
+ )}
+ {props.queryAddress === '0' && ( +

+ Please login with your wallet to view your NFTs +

+ )} {props.nftsCollection.map((nft, index) => { return ( { image={imageURL + 'nft-' + nft.tokenId + '.png'} width={nft.width} height={nft.height} + name={nft.name} blockNumber={nft.blockNumber} likes={nft.likes} liked={nft.liked} minter={nft.minter} queryAddress={props.queryAddress} + updateLikes={props.updateLikes} /> ); })} @@ -58,7 +81,7 @@ const NFTsMainSection = (props) => { }; const NFTsExpandedSection = (props) => { - const imageURL = backendUrl + '/nft-images/'; + const imageURL = nftUrl + '/nft-images/'; return (
@@ -90,11 +113,13 @@ const NFTsExpandedSection = (props) => { image={imageURL + 'nft-' + nft.tokenId + '.png'} width={nft.width} height={nft.height} + name={nft.name} blockNumber={nft.blockNumber} likes={nft.likes} liked={nft.liked} minter={nft.minter} queryAddress={props.queryAddress} + updateLikes={props.updateLikes} /> ); })} @@ -133,6 +158,25 @@ const NFTs = (props) => { } }; + const updateLikes = (tokenId, likes, liked) => { + let newMyNFTs = myNFTs.map((nft) => { + if (nft.tokenId === tokenId) { + return { ...nft, likes: likes, liked: liked }; + } + return nft; + }); + + let newAllNFTs = allNFTs.map((nft) => { + if (nft.tokenId === tokenId) { + return { ...nft, likes: likes, liked: liked }; + } + return nft; + }); + + setMyNFTs(newMyNFTs); + setAllNFTs(newAllNFTs); + }; + useEffect(() => { if ( props.latestMintedTokenId !== null && @@ -181,22 +225,26 @@ const NFTs = (props) => { if (activeFilter === 'hot') { result = await getHotNftsFn({ page: allNftPagination.page, - pageLength: allNftPagination.pageLength + pageLength: allNftPagination.pageLength, + queryAddress: props.queryAddress }); } else if (activeFilter === 'new') { result = await getNewNftsFn({ page: allNftPagination.page, - pageLength: allNftPagination.pageLength + pageLength: allNftPagination.pageLength, + queryAddress: props.queryAddress }); } else if (activeFilter === 'top') { result = await getTopNftsFn({ page: allNftPagination.page, - pageLength: allNftPagination.pageLength + pageLength: allNftPagination.pageLength, + queryAddress: props.queryAddress }); } else { result = await getNftsFn({ page: allNftPagination.page, - pageLength: allNftPagination.pageLength + pageLength: allNftPagination.pageLength, + queryAddress: props.queryAddress }); } @@ -236,6 +284,7 @@ const NFTs = (props) => { title='NFTs' mainSection={NFTsMainSection} expandedSection={NFTsExpandedSection} + updateLikes={updateLikes} nftMintingMode={props.nftMintingMode} setNftMintingMode={props.setNftMintingMode} nftsCollection={myNFTs} @@ -253,6 +302,7 @@ const NFTs = (props) => { setActiveFilter={setActiveFilter} filters={filters} isMobile={props.isMobile} + gameEnded={props.gameEnded} /> ); }; diff --git a/frontend/src/tabs/quests/QuestItem.css b/frontend/src/tabs/quests/QuestItem.css index a0daf948..304d9d29 100644 --- a/frontend/src/tabs/quests/QuestItem.css +++ b/frontend/src/tabs/quests/QuestItem.css @@ -166,6 +166,9 @@ .QuestItem__form__input { flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .QuestItem__form__submit { diff --git a/frontend/src/tabs/quests/QuestItem.js b/frontend/src/tabs/quests/QuestItem.js index c8795d64..3fde26b3 100644 --- a/frontend/src/tabs/quests/QuestItem.js +++ b/frontend/src/tabs/quests/QuestItem.js @@ -9,20 +9,9 @@ import { devnetMode } from '../../utils/Consts.js'; const QuestItem = (props) => { // TODO: Flash red on quest if clicked and not completed w/ no args const [expanded, setExpanded] = useState(false); - const _expandQuest = () => { - if (props.status == 'completed') { - return; - } - - if (props.args == null || props.args.length == 0) { - return; - } - setExpanded(!expanded); - }; - const [inputsValidated, setInputsValidated] = useState(false); const validateInputs = (event) => { - if (props.args == null || props.args.length == 0) { + if (props.claimParams == null || props.claimParams.length == 0) { return; } @@ -34,24 +23,24 @@ const QuestItem = (props) => { if (input.value == '') { validated = false; } - // Switch based on props.args.type[inputIndex] - if (props.args[inputIndex].inputType == 'address') { + // Switch based on props.claimParams.claimType[inputIndex] + if (props.claimParams[inputIndex].claimType == 'address') { // Starts w/ 0x and is 65 || 66 hex characters long let hexPattern = /^0x[0-9a-fA-F]{63,64}$/; if (!hexPattern.test(input.value)) { validated = false; } - } else if (props.args[inputIndex].inputType == 'text') { + } else if (props.claimParams[inputIndex].claimType == 'text') { // Any string < 32 characters if (input.value.length >= 32) { validated = false; } - } else if (props.args[inputIndex].inputType == 'number') { + } else if (props.claimParams[inputIndex].claimType == 'number') { // Any number if (isNaN(input.value)) { validated = false; } - } else if (props.args[inputIndex].type == 'twitter') { + } else if (props.claimParams[inputIndex].claimType == 'twitter') { // Starts w/ @ and is < 16 characters if (!input.value.startsWith('@') || input.value.length >= 16) { validated = false; @@ -76,10 +65,15 @@ const QuestItem = (props) => { ); }; - const claimMainQuest = () => { + const claimMainQuestCall = (quest_id, calldata) => { if (devnetMode) return; if (!props.address || !props.artPeaceContract) return; - setCalls(props.artPeaceContract.populateTransaction['claim_main_quest']()); + setCalls( + props.artPeaceContract.populateTransaction['claim_main_quest']( + quest_id, + calldata + ) + ); }; useEffect(() => { @@ -95,15 +89,36 @@ const QuestItem = (props) => { calls }); + const [canClaim, setCanClaim] = useState(false); const claimOrExpand = async () => { + if (!canClaim || props.gameEnded || props.queryAddress === '0') { + return; + } if (props.status == 'completed') { return; } + let questCalldata = []; + if (props.claimParams && props.claimParams.length > 0) { + if (inputsValidated) { + let component = event.target.closest('.QuestItem'); + let inputs = component.querySelectorAll('.QuestItem__form__input'); + inputs.forEach((input) => { + questCalldata.push(input.value); + }); + setExpanded(!expanded); + } else if (props.claimParams[0].input) { + setExpanded(!expanded); + return; + } + } + if (props.calldata) { + questCalldata = props.calldata; + } if (!devnetMode) { if (props.type === 'daily') { - claimTodayQuestCall(props.questId, []); + claimTodayQuestCall(props.questId, questCalldata); } else if (props.type === 'main') { - claimMainQuest(); + claimMainQuestCall(props.questId, questCalldata); } else { console.log('Quest type not recognized'); } @@ -122,15 +137,14 @@ const QuestItem = (props) => { mode: 'cors', method: 'POST', body: JSON.stringify({ - // TODO - questId: props.questId.toString() + questId: props.questId.toString(), + calldata: questCalldata.length > 0 ? questCalldata[0].toString() : '' }) }); if (response.result) { console.log(response.result); props.markCompleted(props.questId, props.type); } - // TODO: Expand if not claimable && has args }; const [percentCompletion, setPercentCompletion] = useState(0); @@ -161,12 +175,38 @@ const QuestItem = (props) => { } if (props.status === 'completed') { setProgressionColor('rgba(32, 225, 32, 0.80)'); + } else if ( + props.claimParams && + props.claimParams.length > 0 && + props.claimParams[0].input + ) { + setProgressionColor(`hsla(${0.5 * 60}, 100%, 60%, 0.78)`); } else { setProgressionColor(`hsla(${(percent / 100) * 60}, 100%, 60%, 0.78)`); } setPercentCompletion(percent); }, [props.progress, props.needed, props.status]); + useEffect(() => { + if (props.gameEnded || props.queryAddress === '0') { + setCanClaim(false); + return; + } + if (props.status === 'completed') { + setCanClaim(false); + return; + } + if (props.claimParams && props.claimParams.length > 0) { + if (props.claimParams[0].input) { + setCanClaim(true); + } else { + setCanClaim(props.progress >= props.needed); + } + return; + } + setCanClaim(props.progress >= props.needed); + }, [props]); + // TODO: Claimable if progress >= needed // TODO: 100% to the top of list return ( @@ -186,9 +226,7 @@ const QuestItem = (props) => {
{ } >
- {props.args && - props.args.map((arg, idx) => ( + {props.claimParams && + props.claimParams.map((arg, idx) => (
))} - {props.args && ( + {props.claimParams && ( diff --git a/frontend/src/tabs/quests/Quests.css b/frontend/src/tabs/quests/Quests.css index bfe3c4ff..569717f5 100644 --- a/frontend/src/tabs/quests/Quests.css +++ b/frontend/src/tabs/quests/Quests.css @@ -10,6 +10,11 @@ padding: 0.5rem; } +.Quests__nowallet { + padding: 0.5rem; + text-align: center; +} + .Quests__timer { margin: 1rem 0 0.5rem 1rem; padding: 0.5rem 1rem; diff --git a/frontend/src/tabs/quests/Quests.js b/frontend/src/tabs/quests/Quests.js index e7740187..6d72e259 100644 --- a/frontend/src/tabs/quests/Quests.js +++ b/frontend/src/tabs/quests/Quests.js @@ -31,8 +31,7 @@ const Quests = (props) => { setMainQuests(combineQuests(mainQuestsInfo, mainQuestsStatus)); }, [todaysQuestsInfo, mainQuestsInfo, todaysQuestsStatus, mainQuestsStatus]); - // TODO: remove local quests - const createArgs = (labels, placeholders, types) => { + const _createArgs = (labels, placeholders, types) => { const args = []; for (let i = 0; i < labels.length; i++) { args.push({ @@ -44,67 +43,6 @@ const Quests = (props) => { return args; }; - const localDailyQuests = [ - { - name: 'Place 10 pixels', - description: - 'Add 10 pixels on the canvas [art/peace theme](https://www.google.com/)', - reward: '3', - completed: false, - progress: 0, - needed: 10 - }, - { - name: 'Build a template', - description: 'Create a template for the community to use', - reward: '3', - completed: false, - progress: 1, - needed: 20 - }, - { - name: 'Deploy a Memecoin', - description: 'Create an Unruggable memecoin', - reward: '10', - completed: false, - args: createArgs(['MemeCoin Address'], ['0x1234'], ['address']), - progress: 1, - needed: 1 - } - ]; - - const localMainQuests = [ - { - name: 'Tweet #art/peace', - description: 'Tweet about art/peace using the hashtag & addr', - reward: '10', - completed: true, - args: createArgs( - ['Twitter Handle', 'Address', 'test'], - ['@test', '0x1234', 'asdioj'], - ['twitter', 'address', 'text'] - ), - progress: 13, - needed: 13 - }, - { - name: 'Place 100 pixels', - description: 'Add 100 pixels on the canvas', - reward: '10', - completed: false, - progress: 98, - needed: 100 - }, - { - name: 'Mint art/peace NFT', - description: 'Mint an NFT using the art/peace theme', - reward: '5', - completed: false, - progress: 14, - needed: 13 - } - ]; - useEffect(() => { const fetchQuests = async () => { try { @@ -115,7 +53,7 @@ const Quests = (props) => { let dailyData = await dailyResponse.json(); dailyData = dailyData.data; if (!dailyData) { - dailyData = localDailyQuests; + dailyData = []; } setTodaysQuestsInfo(sortByCompleted(dailyData)); @@ -126,8 +64,7 @@ const Quests = (props) => { let mainData = await mainResponse.json(); mainData = mainData.data; if (!mainData) { - // TODO: remove this & use [] - mainData = localMainQuests; + mainData = []; } setMainQuestsInfo(sortByCompleted(mainData)); } catch (error) { @@ -202,9 +139,14 @@ const Quests = (props) => { return (
+ {props.queryAddress === '0' && ( +

+ Please login with your wallet to view your quests. +

+ )}
{ {todaysQuests.map((quest, index) => ( { artPeaceContract={props.artPeaceContract} progress={quest.progress} needed={quest.needed} + calldata={quest.calldata} + claimParams={quest.claimParams} type='daily' + gameEnded={props.gameEnded} /> ))} @@ -240,6 +186,7 @@ const Quests = (props) => { {mainQuests.map((quest, index) => ( { artPeaceContract={props.artPeaceContract} progress={quest.progress} needed={quest.needed} + calldata={quest.calldata} + claimParams={quest.claimParams} type='main' + gameEnded={props.gameEnded} /> ))}
diff --git a/frontend/src/tabs/voting/VoteItem.js b/frontend/src/tabs/voting/VoteItem.js index b6e7ddbf..1e64e5c8 100644 --- a/frontend/src/tabs/voting/VoteItem.js +++ b/frontend/src/tabs/voting/VoteItem.js @@ -5,7 +5,7 @@ const VoteItem = (props) => { return (
props.castVote(props.index)} > {props.userVote === props.index && ( diff --git a/frontend/src/tabs/voting/Voting.css b/frontend/src/tabs/voting/Voting.css index 5d79ec9c..1e988295 100644 --- a/frontend/src/tabs/voting/Voting.css +++ b/frontend/src/tabs/voting/Voting.css @@ -1,6 +1,8 @@ .Voting__description { padding: 0 0.5rem 0 1rem; margin: 0.7rem 0; + line-height: 1.6rem; + text-align: center; } .Voting__timer { diff --git a/frontend/src/tabs/voting/Voting.js b/frontend/src/tabs/voting/Voting.js index 2ac231d5..c528534f 100644 --- a/frontend/src/tabs/voting/Voting.js +++ b/frontend/src/tabs/voting/Voting.js @@ -59,6 +59,9 @@ const Voting = (props) => { }, [props.queryAddress]); const castVote = async (index) => { + if (props.queryAddress === '0') { + return; // Prevent voting if not logged in + } if (userVote === index) { return; // Prevent re-voting for the same index } @@ -91,6 +94,13 @@ const Voting = (props) => { }; useEffect(() => { + if (props.isLastDay || props.gameEnded) { + setVotableColorApiState((prevState) => ({ + ...prevState, + data: [] + })); + return; + } const fetchVotableColors = async () => { try { setVotableColorApiState((prevState) => ({ @@ -115,48 +125,86 @@ const Voting = (props) => { } }; fetchVotableColors(); - }, []); + }, [props.isLastDay, props.gameEnded]); return ( -
-

Vote closes

-

props.startNextDay()} - > - {props.timeLeftInDay} -

-
-

- Vote for a new palette color -

+ {props.isLastDay && !props.gameEnded && ( +
+
+

Voting has ended

+

props.startNextDay()} + > + {props.timeLeftInDay} +

+
+
+

+ Check back tomorrow after the game ends to vote for the best NFTs. +

+
+
+ )} + {!props.isLastDay && !props.gameEnded && ( +
+
+

Time left to vote

+

props.startNextDay()} + > + {props.timeLeftInDay} +

+
+

+ {props.queryAddress === '0' + ? 'Please login with your wallet to vote' + : 'Vote for a new palette color'} +

-
- {votableColorApiState.data && votableColorApiState.data.length ? ( - votableColorApiState.data.map((color, index) => ( - - )) - ) : ( -
- No Color Added Yet +
+ {votableColorApiState.data && votableColorApiState.data.length ? ( + votableColorApiState.data.map((color, index) => ( + + )) + ) : ( +
+ No Color Added Yet +
+ )}
- )} -
+
+ )} ); }; diff --git a/frontend/src/utils/Consts.js b/frontend/src/utils/Consts.js index 0ca7015c..cad5b29c 100644 --- a/frontend/src/utils/Consts.js +++ b/frontend/src/utils/Consts.js @@ -8,6 +8,10 @@ export const wsUrl = backendConfig.production ? 'wss://' + backendConfig.host + '/ws' : 'ws://' + backendConfig.host + ':' + backendConfig.consumer_port + '/ws'; +export const nftUrl = backendConfig.production + ? 'https://' + backendConfig.host + : 'http://' + backendConfig.host + ':' + backendConfig.consumer_port; + export const devnetMode = backendConfig.production === false; export const convertUrl = (url) => { diff --git a/indexer/prod-script.js b/indexer/prod-script.js index 7fec6fa1..5cdffe88 100644 --- a/indexer/prod-script.js +++ b/indexer/prod-script.js @@ -1,6 +1,6 @@ export const config = { streamUrl: Deno.env.get("APIBARA_STREAM_URL"), - startingBlock: 70000, + startingBlock: 65_000, network: "starknet", finality: "DATA_STATUS_PENDING", filter: { @@ -47,10 +47,20 @@ export const config = { includeReceipt: false }, { - // Member Pixels Placed Event + // Faction Pixels Placed Event fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), keys: [ - "0x0165248ea72ba05120b18ec02e729e1f03a465f728283e6bb805bb284086c859" + "0x02838056c6784086957f2252d4a36a24d554ea2db7e09d2806cc69751d81f0a2" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Pixels Placed Event + fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), + keys: [ + "0x02e4d1feaacd0627a6c7d5002564bdb4ca4877d47f00cad4714201194690a7a9" ], includeReverted: false, includeTransaction: false, @@ -134,6 +144,15 @@ export const config = { includeTransaction: false, includeReceipt: false }, + { + // Chain Faction Created Event + keys: [ + "0x020c994ab49a8316bcc78b06d4ff9929d83b2995af33f480b93e972cedb0c926" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, { // Chain Faction Joined Event keys: [ diff --git a/indexer/script.js b/indexer/script.js index ca032be9..233a0b6d 100644 --- a/indexer/script.js +++ b/indexer/script.js @@ -47,10 +47,20 @@ export const config = { includeReceipt: false }, { - // Member Pixels Placed Event + // Faction Pixels Placed Event fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), keys: [ - "0x0165248ea72ba05120b18ec02e729e1f03a465f728283e6bb805bb284086c859" + "0x02838056c6784086957f2252d4a36a24d554ea2db7e09d2806cc69751d81f0a2" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Pixels Placed Event + fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), + keys: [ + "0x02e4d1feaacd0627a6c7d5002564bdb4ca4877d47f00cad4714201194690a7a9" ], includeReverted: false, includeTransaction: false, @@ -134,6 +144,15 @@ export const config = { includeTransaction: false, includeReceipt: false }, + { + // Chain Faction Created Event + keys: [ + "0x020c994ab49a8316bcc78b06d4ff9929d83b2995af33f480b93e972cedb0c926" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, { // Chain Faction Joined Event keys: [ diff --git a/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index fc34009e..4af670ba 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -401,24 +401,22 @@ pub mod ArtPeace { } } - // Use member pixels if available - let last_member_placed_time = self.get_user_last_placed_time(caller); - if now - last_member_placed_time >= self.time_between_member_pixels.read() { - pixels_placed = - place_user_faction_pixels_inner(ref self, positions, colors, pixels_placed, now); - if pixels_placed == pixel_count { - return; - } + pixels_placed = + place_chain_faction_pixels_inner( + ref self, positions, colors, pixels_placed, now + ); + if pixels_placed == pixel_count { + return; } - let last_chain_member_placed_time = self.get_user_last_placed_time(caller); - if now - last_chain_member_placed_time >= self.time_between_member_pixels.read() { - pixels_placed = - place_chain_faction_pixels_inner(ref self, positions, colors, pixels_placed, now); - if pixels_placed == pixel_count { - return; - } + pixels_placed = + place_user_faction_pixels_inner( + ref self, positions, colors, pixels_placed, now + ); + if pixels_placed == pixel_count { + return; } + // TODO: place_extra_pixels_inner // Use extra pixels let extra_pixels = self.extra_pixels.read(caller); @@ -504,7 +502,11 @@ pub mod ArtPeace { fn join_faction(ref self: ContractState, faction_id: u32) { self.check_game_running(); assert(faction_id != 0, 'Faction 0 is not joinable'); - assert(faction_id < self.factions_count.read(), 'Faction does not exist'); + assert(faction_id <= self.factions_count.read(), 'Faction does not exist'); + assert( + self.users_faction.read(starknet::get_caller_address()) == 0, + 'User already in a faction' + ); let caller = starknet::get_caller_address(); let faction = self.factions.read(faction_id); assert(faction.joinable, 'Faction is not joinable'); @@ -523,7 +525,11 @@ pub mod ArtPeace { fn join_chain_faction(ref self: ContractState, faction_id: u32) { self.check_game_running(); assert(faction_id != 0, 'Faction 0 is not joinable'); - assert(faction_id < self.chain_factions_count.read(), 'Faction does not exist'); + assert(faction_id <= self.chain_factions_count.read(), 'Faction does not exist'); + assert( + self.users_chain_faction.read(starknet::get_caller_address()) == 0, + 'User already in a chain faction' + ); let caller = starknet::get_caller_address(); self.users_chain_faction.write(caller, faction_id); self.emit(ChainFactionJoined { faction_id, user: caller }); @@ -863,8 +869,10 @@ pub mod ArtPeace { position: mint_params.position, width: mint_params.width, height: mint_params.height, + name: mint_params.name, image_hash: 0, // TODO block_number: starknet::get_block_number(), + day_index: self.day_index.read(), minter: starknet::get_caller_address(), }; ICanvasNFTAdditionalDispatcher { contract_address: self.nft_contract.read(), } @@ -1119,9 +1127,7 @@ pub mod ArtPeace { let day = self.day_index.read(); self .user_pixels_placed - .write( - (day, caller, color), self.user_pixels_placed.read((day, caller, color)) + 1 - ); + .write((day, caller, color), self.user_pixels_placed.read((day, caller, color)) + 1); // TODO: Optimize? self.emit(PixelPlaced { placed_by: caller, pos, day, color }); } @@ -1135,11 +1141,7 @@ pub mod ArtPeace { } fn place_user_faction_pixels_inner( - ref self: ContractState, - positions: Span, - colors: Span, - mut offset: u32, - now: u64 + ref self: ContractState, positions: Span, colors: Span, mut offset: u32, now: u64 ) -> u32 { let faction_pixels = self .get_user_faction_members_pixels(starknet::get_caller_address(), now); @@ -1161,26 +1163,13 @@ pub mod ArtPeace { }; let caller = starknet::get_caller_address(); if faction_pixels_left == 0 { - let new_member_metadata = MemberMetadata { - member_placed_time: now, member_pixels: 0 - }; + let new_member_metadata = MemberMetadata { member_placed_time: now, member_pixels: 0 }; self.users_faction_meta.write(caller, new_member_metadata); - self - .emit( - FactionPixelsPlaced { - user: caller, - placed_time: now, - member_pixels: 0 - } - ); + self.emit(FactionPixelsPlaced { user: caller, placed_time: now, member_pixels: 0 }); } else { - let last_placed_time = self - .users_faction_meta - .read(caller) - .member_placed_time; + let last_placed_time = self.users_faction_meta.read(caller).member_placed_time; let new_member_metadata = MemberMetadata { - member_placed_time: last_placed_time, - member_pixels: faction_pixels_left + member_placed_time: last_placed_time, member_pixels: faction_pixels_left }; self.users_faction_meta.write(caller, new_member_metadata); self @@ -1196,11 +1185,7 @@ pub mod ArtPeace { } fn place_chain_faction_pixels_inner( - ref self: ContractState, - positions: Span, - colors: Span, - mut offset: u32, - now: u64 + ref self: ContractState, positions: Span, colors: Span, mut offset: u32, now: u64 ) -> u32 { let pixel_count = positions.len(); let caller = starknet::get_caller_address(); @@ -1226,9 +1211,7 @@ pub mod ArtPeace { self .emit( ChainFactionPixelsPlaced { - user: caller, - placed_time: now, - member_pixels: 0 + user: caller, placed_time: now, member_pixels: 0 } ); } else { @@ -1237,8 +1220,7 @@ pub mod ArtPeace { .read(caller) .member_placed_time; let new_member_metadata = MemberMetadata { - member_placed_time: last_placed_time, - member_pixels: member_pixels_left + member_placed_time: last_placed_time, member_pixels: member_pixels_left }; self.users_chain_faction_meta.write(caller, new_member_metadata); self diff --git a/onchain/src/interfaces.cairo b/onchain/src/interfaces.cairo index fbef23e7..a4e8512e 100644 --- a/onchain/src/interfaces.cairo +++ b/onchain/src/interfaces.cairo @@ -75,8 +75,12 @@ pub trait IArtPeace { fn join_chain_faction(ref self: TContractState, faction_id: u32); fn get_user_faction(self: @TContractState, user: starknet::ContractAddress) -> u32; fn get_user_chain_faction(self: @TContractState, user: starknet::ContractAddress) -> u32; - fn get_user_faction_members_pixels(self: @TContractState, user: starknet::ContractAddress, now: u64) -> u32; - fn get_chain_faction_members_pixels(self: @TContractState, user: starknet::ContractAddress, now: u64) -> u32; + fn get_user_faction_members_pixels( + self: @TContractState, user: starknet::ContractAddress, now: u64 + ) -> u32; + fn get_chain_faction_members_pixels( + self: @TContractState, user: starknet::ContractAddress, now: u64 + ) -> u32; // Get color info fn get_color_count(self: @TContractState) -> u8; diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index a58c131b..db846dce 100644 --- a/onchain/src/lib.cairo +++ b/onchain/src/lib.cairo @@ -2,7 +2,8 @@ pub mod art_peace; pub mod interfaces; use art_peace::ArtPeace; use interfaces::{ - IArtPeace, IArtPeaceDispatcher, IArtPeaceDispatcherTrait, Pixel, Faction, ChainFaction, MemberMetadata + IArtPeace, IArtPeaceDispatcher, IArtPeaceDispatcherTrait, Pixel, Faction, ChainFaction, + MemberMetadata }; mod quests { @@ -16,6 +17,7 @@ mod quests { pub mod nft_quest; pub mod hodl_quest; pub mod faction_quest; + pub mod chain_faction_quest; pub mod vote_quest; use interfaces::{ @@ -72,6 +74,7 @@ mod tests { pub(crate) mod hodl_quest; pub(crate) mod pixel_quest; pub(crate) mod faction_quest; + pub(crate) mod chain_faction_quest; pub(crate) mod rainbow_quest; pub(crate) mod template_quest; pub(crate) mod unruggable_quest; diff --git a/onchain/src/nfts/component.cairo b/onchain/src/nfts/component.cairo index 87b71993..7b6d0006 100644 --- a/onchain/src/nfts/component.cairo +++ b/onchain/src/nfts/component.cairo @@ -41,6 +41,13 @@ pub mod CanvasNFTStoreComponent { return metadata.minter; } + fn get_nft_day_index( + self: @ComponentState, token_id: u256 + ) -> u32 { + let metadata: NFTMetadata = self.nfts_data.read(token_id); + return metadata.day_index; + } + fn get_nft_image_hash(self: @ComponentState, token_id: u256) -> felt252 { let metadata: NFTMetadata = self.nfts_data.read(token_id); return metadata.image_hash; diff --git a/onchain/src/nfts/interfaces.cairo b/onchain/src/nfts/interfaces.cairo index 3e1406a4..96e0b7a7 100644 --- a/onchain/src/nfts/interfaces.cairo +++ b/onchain/src/nfts/interfaces.cairo @@ -3,6 +3,7 @@ pub struct NFTMintParams { pub position: u128, pub width: u128, pub height: u128, + pub name: felt252, } #[derive(Drop, Copy, Serde, PartialEq, starknet::Store)] @@ -10,8 +11,10 @@ pub struct NFTMetadata { pub position: u128, pub width: u128, pub height: u128, + pub name: felt252, pub image_hash: felt252, pub block_number: u64, + pub day_index: u32, pub minter: starknet::ContractAddress, } @@ -21,6 +24,7 @@ pub trait ICanvasNFTStore { fn get_nft_metadata(self: @TContractState, token_id: u256) -> NFTMetadata; fn get_nft_minter(self: @TContractState, token_id: u256) -> starknet::ContractAddress; fn get_nft_image_hash(self: @TContractState, token_id: u256) -> felt252; + fn get_nft_day_index(self: @TContractState, token_id: u256) -> u32; // Returns the number of NFTs stored in the contract state. fn get_nfts_count(self: @TContractState) -> u256; diff --git a/onchain/src/quests/chain_faction_quest.cairo b/onchain/src/quests/chain_faction_quest.cairo new file mode 100644 index 00000000..cf8c0631 --- /dev/null +++ b/onchain/src/quests/chain_faction_quest.cairo @@ -0,0 +1,67 @@ +#[starknet::contract] +pub mod ChainFactionQuest { + use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait}; + use art_peace::quests::{IQuest}; + + use starknet::{ContractAddress, get_caller_address}; + + #[storage] + struct Storage { + art_peace: ContractAddress, + reward: u32, + claimed: LegacyMap, + } + + #[derive(Drop, Serde)] + pub struct ChainFactionQuestInitParams { + pub art_peace: ContractAddress, + pub reward: u32, + } + + #[constructor] + fn constructor(ref self: ContractState, init_params: ChainFactionQuestInitParams) { + self.art_peace.write(init_params.art_peace); + self.reward.write(init_params.reward); + } + + + #[abi(embed_v0)] + impl ChainFactionQuest of IQuest { + fn get_reward(self: @ContractState) -> u32 { + self.reward.read() + } + + fn is_claimable( + self: @ContractState, user: ContractAddress, calldata: Span + ) -> bool { + if self.claimed.read(user) { + return false; + } + + let art_peace_dispatcher = IArtPeaceDispatcher { + contract_address: self.art_peace.read() + }; + + let user_faction = art_peace_dispatcher.get_user_chain_faction(user); + + if user_faction == 0 { + return false; + } + + return true; + } + + + fn claim(ref self: ContractState, user: ContractAddress, calldata: Span) -> u32 { + assert(get_caller_address() == self.art_peace.read(), 'Only ArtPeace can claim quests'); + + assert(self.is_claimable(user, calldata), 'Quest not claimable'); + + self.claimed.write(user, true); + let reward = self.reward.read(); + + reward + } + } +} + diff --git a/onchain/src/quests/nft_quest.cairo b/onchain/src/quests/nft_quest.cairo index 1a7965ed..3b01f44e 100644 --- a/onchain/src/quests/nft_quest.cairo +++ b/onchain/src/quests/nft_quest.cairo @@ -10,6 +10,8 @@ pub mod NFTMintQuest { canvas_nft: ContractAddress, art_peace: ContractAddress, reward: u32, + is_daily: bool, + day_index: u32, claimed: LegacyMap, } @@ -18,6 +20,8 @@ pub mod NFTMintQuest { pub canvas_nft: ContractAddress, pub art_peace: ContractAddress, pub reward: u32, + pub is_daily: bool, + pub day_index: u32, } #[constructor] @@ -25,6 +29,8 @@ pub mod NFTMintQuest { self.canvas_nft.write(init_params.canvas_nft); self.art_peace.write(init_params.art_peace); self.reward.write(init_params.reward); + self.is_daily.write(init_params.is_daily); + self.day_index.write(init_params.day_index); } #[abi(embed_v0)] @@ -50,6 +56,13 @@ pub mod NFTMintQuest { return false; } + if self.is_daily.read() { + let day_index = nft_store.get_nft_day_index(token_id); + if day_index != self.day_index.read() { + return false; + } + } + return true; } diff --git a/onchain/src/tests/chain_faction_quest.cairo b/onchain/src/tests/chain_faction_quest.cairo new file mode 100644 index 00000000..7a02e4f0 --- /dev/null +++ b/onchain/src/tests/chain_faction_quest.cairo @@ -0,0 +1,92 @@ +use art_peace::{IArtPeaceDispatcher, IArtPeaceDispatcherTrait}; +use art_peace::quests::chain_faction_quest::ChainFactionQuest::ChainFactionQuestInitParams; +use art_peace::tests::art_peace::deploy_with_quests_contract; +use art_peace::tests::utils; +use snforge_std as snf; +use snforge_std::{CheatTarget, ContractClassTrait, declare}; +use starknet::{ContractAddress, contract_address_const}; + + +const reward_amt: u32 = 10; + +fn deploy_chain_faction_quest_main() -> ContractAddress { + let contract = declare("ChainFactionQuest"); + + let mut hodl_quest_calldata = array![]; + ChainFactionQuestInitParams { art_peace: utils::ART_PEACE_CONTRACT(), reward: reward_amt, } + .serialize(ref hodl_quest_calldata); + + contract.deploy(@hodl_quest_calldata).unwrap() +} + + +#[test] +fn deploy_chain_faction_quest_main_test() { + let chain_faction_quest = deploy_chain_faction_quest_main(); + + let art_peace = IArtPeaceDispatcher { + contract_address: deploy_with_quests_contract(array![].span(), array![chain_faction_quest].span()) + }; + + let zero_address = contract_address_const::<0>(); + + assert!( + art_peace.get_days_quests(0) == array![zero_address, zero_address, zero_address].span(), + "Daily quests were not set correctly" + ); + assert!( + art_peace.get_main_quests() == array![chain_faction_quest].span(), + "Main quests were not set correctly" + ); +} + +#[test] +fn chain_faction_quest_test() { + let chain_faction_quest_contract_address = deploy_chain_faction_quest_main(); + + let art_peace = IArtPeaceDispatcher { + contract_address: deploy_with_quests_contract( + array![].span(), array![chain_faction_quest_contract_address].span() + ) + }; + + + snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::HOST()); + art_peace.init_chain_faction('TestFaction'); + snf::stop_prank(CheatTarget::One(art_peace.contract_address)); + + snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); + art_peace.join_chain_faction(1); + + art_peace.claim_main_quest(0, utils::EMPTY_CALLDATA()); + + assert!( + art_peace.get_extra_pixels_count() == reward_amt, + "Extra pixels are wrong after main quest claim" + ); + snf::stop_prank(CheatTarget::One(art_peace.contract_address)); +} + + +#[test] +#[should_panic(expected: 'Quest not claimable')] +fn chain_faction_quest_is_not_claimable_test() { + let chain_faction_quest_contract_address = deploy_chain_faction_quest_main(); + + let art_peace = IArtPeaceDispatcher { + contract_address: deploy_with_quests_contract( + array![].span(), array![chain_faction_quest_contract_address].span() + ) + }; + + snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); + + art_peace.claim_main_quest(0, utils::EMPTY_CALLDATA()); + + assert!( + art_peace.get_extra_pixels_count() == reward_amt, + "Extra pixels are wrong after main quest claim" + ); + snf::stop_prank(CheatTarget::One(art_peace.contract_address)); +} + diff --git a/onchain/src/tests/faction_quest.cairo b/onchain/src/tests/faction_quest.cairo index cbf59df3..d583d67b 100644 --- a/onchain/src/tests/faction_quest.cairo +++ b/onchain/src/tests/faction_quest.cairo @@ -50,9 +50,13 @@ fn faction_quest_test() { ) }; - snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); - art_peace.init_faction('TestFaction', utils::HOST(), 20, array![utils::PLAYER1()].span()); + snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::HOST()); + art_peace.init_faction('TestFaction', utils::HOST(), true, 1); + snf::stop_prank(CheatTarget::One(art_peace.contract_address)); + + snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); + art_peace.join_faction(1); art_peace.claim_main_quest(0, utils::EMPTY_CALLDATA()); diff --git a/onchain/src/tests/nft_quest.cairo b/onchain/src/tests/nft_quest.cairo index 38bf7ab6..5057220b 100644 --- a/onchain/src/tests/nft_quest.cairo +++ b/onchain/src/tests/nft_quest.cairo @@ -14,7 +14,7 @@ use starknet::{ContractAddress, contract_address_const}; const reward_amt: u32 = 18; -fn deploy_nft_quest() -> ContractAddress { +fn deploy_normal_nft_quest() -> ContractAddress { let contract = snf::declare("NFTMintQuest"); let mut nft_quest_calldata = array![]; @@ -22,6 +22,22 @@ fn deploy_nft_quest() -> ContractAddress { canvas_nft: utils::NFT_CONTRACT(), art_peace: utils::ART_PEACE_CONTRACT(), reward: reward_amt, + is_daily: false, + } + .serialize(ref nft_quest_calldata); + + contract.deploy(@nft_quest_calldata).unwrap() +} + +fn deploy_daily_nft_quest() -> ContractAddress { + let contract = snf::declare("NFTMintQuest"); + + let mut nft_quest_calldata = array![]; + NFTMintQuestInitParams { + canvas_nft: utils::NFT_CONTRACT(), + art_peace: utils::ART_PEACE_CONTRACT(), + reward: reward_amt, + is_daily: true, } .serialize(ref nft_quest_calldata); @@ -29,8 +45,8 @@ fn deploy_nft_quest() -> ContractAddress { } #[test] -fn deploy_nft_quest_test() { - let nft_quest = deploy_nft_quest(); +fn deploy_normal_nft_quest_test() { + let nft_quest = deploy_normal_nft_quest(); let art_peace = IArtPeaceDispatcher { contract_address: deploy_with_quests_contract(array![].span(), array![nft_quest].span()) }; @@ -47,9 +63,28 @@ fn deploy_nft_quest_test() { ); } +#[test] +fn deploy_daily_nft_quest_test() { + let nft_quest = deploy_daily_nft_quest(); + let art_peace = IArtPeaceDispatcher { + contract_address: deploy_with_quests_contract(array![].span(), array![nft_quest].span()) + }; + + let zero_address = contract_address_const::<0>(); + + assert!( + art_peace.get_days_quests(0) == array![nft_quest, zero_address, zero_address].span(), + "Daily quests were not set correctly" + ); + assert!( + art_peace.get_main_quests() == array![].span(), + "Main quests were not set correctly" + ); +} + #[test] fn nft_quest_test() { - let nft_mint_quest = deploy_nft_quest(); + let nft_mint_quest = deploy_normal_nft_quest(); let art_peace = IArtPeaceDispatcher { contract_address: deploy_with_quests_contract( diff --git a/postgres/init.sql b/postgres/init.sql index 2e4d9e6c..72dbbcf0 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -65,6 +65,20 @@ CREATE INDEX dailyQuestsInput_day_index_index ON DailyQuestsInput (day_index); CREATE INDEX dailyQuestsInput_quest_id_index ON DailyQuestsInput (quest_id); CREATE INDEX dailyQuestsInput_input_key_index ON DailyQuestsInput (input_key); +CREATE TABLE DailyQuestsClaimParams ( + day_index integer NOT NULL, + quest_id integer NOT NULL, + claim_key integer NOT NULL, + claim_type text NOT NULL, + name text NOT NULL, + example text, + input boolean NOT NULL, + PRIMARY KEY (day_index, quest_id, claim_key) +); +CREATE INDEX dailyQuestsClaimParams_day_index_index ON DailyQuestsClaimParams (day_index); +CREATE INDEX dailyQuestsClaimParams_quest_id_index ON DailyQuestsClaimParams (quest_id); +CREATE INDEX dailyQuestsClaimParams_claim_key_index ON DailyQuestsClaimParams (claim_key); + -- Table for storing the daily quests that the user has completed CREATE TABLE UserDailyQuests ( -- Postgres auto-incrementing primary key @@ -97,6 +111,18 @@ CREATE TABLE MainQuestsInput ( CREATE INDEX mainQuestsInput_quest_id_index ON MainQuestsInput (quest_id); CREATE INDEX mainQuestsInput_input_key_index ON MainQuestsInput (input_key); +CREATE TABLE MainQuestsClaimParams ( + quest_id integer NOT NULL, + claim_key integer NOT NULL, + claim_type text NOT NULL, + name text NOT NULL, + example text, + input boolean NOT NULL, + PRIMARY KEY (quest_id, claim_key) +); +CREATE INDEX mainQuestsClaimParams_quest_id_index ON MainQuestsClaimParams (quest_id); +CREATE INDEX mainQuestsClaimParams_claim_key_index ON MainQuestsClaimParams (claim_key); + -- Table for storing the main quests that the user has completed CREATE TABLE UserMainQuests ( -- Postgres auto-incrementing primary key @@ -165,8 +191,10 @@ CREATE TABLE NFTs ( position integer NOT NULL, width integer NOT NULL, height integer NOT NULL, + name text NOT NULL, image_hash text NOT NULL, block_number integer NOT NULL, + day_index integer NOT NULL, minter char(64) NOT NULL, owner char(64) NOT NULL ); @@ -206,6 +234,17 @@ CREATE TABLE FactionLinks ( ); CREATE INDEX factionLinks_faction_id_index ON FactionLinks (faction_id); +CREATE TABLE ChainFactionLinks ( + faction_id integer NOT NULL, + icon text NOT NULL, + telegram text, + twitter text, + github text, + site text, + PRIMARY KEY (faction_id) +); +CREATE INDEX chainFactionLinks_faction_id_index ON ChainFactionLinks (faction_id); + CREATE TABLE FactionMembersInfo ( faction_id integer NOT NULL, user_address char(64) NOT NULL, @@ -223,8 +262,19 @@ CREATE TABLE ChainFactionMembersInfo ( member_pixels integer NOT NULL, UNIQUE (faction_id, user_address) ); +CREATE INDEX chainFactionMembersInfo_faction_id_index ON ChainFactionMembersInfo (faction_id); +CREATE INDEX chainFactionMembersInfo_user_address_index ON ChainFactionMembersInfo (user_address); CREATE TABLE FactionTemplates ( template_key integer NOT NULL, faction_id integer NOT NULL ); +CREATE INDEX factionTemplates_template_key_index ON FactionTemplates (template_key); +CREATE INDEX factionTemplates_faction_id_index ON FactionTemplates (faction_id); + +CREATE TABLE ChainFactionTemplates ( + template_key integer NOT NULL, + faction_id integer NOT NULL +); +CREATE INDEX chainFactionTemplates_template_key_index ON ChainFactionTemplates (template_key); +CREATE INDEX chainFactionTemplates_faction_id_index ON ChainFactionTemplates (faction_id); diff --git a/tests/integration/docker/deploy.sh b/tests/integration/docker/deploy.sh index 12e8f5bc..3ed57769 100755 --- a/tests/integration/docker/deploy.sh +++ b/tests/integration/docker/deploy.sh @@ -42,7 +42,7 @@ CANVAS_CONFIG=$WORK_DIR/configs/canvas.config.json QUESTS_CONFIG=$WORK_DIR/configs/quests.config.json WIDTH=$(jq -r '.canvas.width' $CANVAS_CONFIG) HEIGHT=$(jq -r '.canvas.height' $CANVAS_CONFIG) -PLACE_DELAY=0x00 +PLACE_DELAY=120 COLOR_COUNT=$(jq -r '.colors[]' $CANVAS_CONFIG | wc -l | tr -d ' ') COLORS=$(jq -r '.colors[]' $CANVAS_CONFIG | sed 's/^/0x/') VOTABLE_COLOR_COUNT=$(jq -r '.votableColors[]' $CANVAS_CONFIG | wc -l | tr -d ' ') diff --git a/tests/integration/docker/deploy_quests.sh b/tests/integration/docker/deploy_quests.sh index 4a6ae4c9..b855a11f 100755 --- a/tests/integration/docker/deploy_quests.sh +++ b/tests/integration/docker/deploy_quests.sh @@ -71,7 +71,7 @@ for entry in $(echo $DAILY_QUESTS | jq -r '.[] | @base64'); do echo " Contract type: $QUEST_TYPE" CALLDATA=$(echo -n $QUEST_INIT_PARAMS | jq -r '[.[]] | join(" ")') echo " Contract calldata: $CALLDATA" - CLASS_HASH_IDX=$(echo ${DECLARED_CONTRACT_TYPES[@]} | tr ' ' '\n' | grep -n $QUEST_TYPE | cut -d: -f1) + CLASS_HASH_IDX=$(echo ${DECLARED_CONTRACT_TYPES[@]} | tr ' ' '\n' | grep -n ^$QUEST_TYPE$ | cut -d: -f1) echo " Class hash index: $CLASS_HASH_IDX" CLASS_HASH=${DECLARED_CONTRACT_HASHES[$CLASS_HASH_IDX-1]} echo " Using class hash $CLASS_HASH" @@ -112,7 +112,7 @@ for entry in $(echo $MAIN_QUESTS | jq -r '.[] | @base64'); do echo " Contract type: $QUEST_TYPE" CALLDATA=$(echo -n $QUEST_INIT_PARAMS | jq -r '[.[]] | join(" ")') echo " Contract calldata: $CALLDATA" - CLASS_HASH_IDX=$(echo ${DECLARED_CONTRACT_TYPES[@]} | tr ' ' '\n' | grep -n $QUEST_TYPE | cut -d: -f1) + CLASS_HASH_IDX=$(echo ${DECLARED_CONTRACT_TYPES[@]} | tr ' ' '\n' | grep -n ^$QUEST_TYPE$ | cut -d: -f1) echo " Class hash index: $CLASS_HASH_IDX" CLASS_HASH=${DECLARED_CONTRACT_HASHES[$CLASS_HASH_IDX-1]} echo " Using class hash $CLASS_HASH" diff --git a/tests/integration/docker/join_chain_faction.sh b/tests/integration/docker/join_chain_faction.sh new file mode 100755 index 00000000..431e5a69 --- /dev/null +++ b/tests/integration/docker/join_chain_faction.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# This script runs the integration tests. + +# TODO: Host? +RPC_HOST="devnet" +RPC_PORT=5050 + +RPC_URL=http://$RPC_HOST:$RPC_PORT + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +WORK_DIR=$SCRIPT_DIR/.. + +#TODO: 2 seperate directories when called from the test script +OUTPUT_DIR=$HOME/.art-peace-tests +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 + +ACCOUNT_NAME=art_peace_acct +ACCOUNT_ADDRESS=0x328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0 +ACCOUNT_PRIVATE_KEY=0x856c96eaa4e7c40c715ccc5dacd8bf6e +ACCOUNT_PROFILE=starknet-devnet +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 + +#TODO: rename script and make more generic +echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $1 --function $2 --calldata $3" > $LOG_DIR/cmd.txt +/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $1 --function $2 --calldata $3 > $LOG_DIR/output.json diff --git a/tests/integration/docker/join_faction.sh b/tests/integration/docker/join_faction.sh new file mode 100755 index 00000000..431e5a69 --- /dev/null +++ b/tests/integration/docker/join_faction.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# This script runs the integration tests. + +# TODO: Host? +RPC_HOST="devnet" +RPC_PORT=5050 + +RPC_URL=http://$RPC_HOST:$RPC_PORT + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +WORK_DIR=$SCRIPT_DIR/.. + +#TODO: 2 seperate directories when called from the test script +OUTPUT_DIR=$HOME/.art-peace-tests +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 + +ACCOUNT_NAME=art_peace_acct +ACCOUNT_ADDRESS=0x328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0 +ACCOUNT_PRIVATE_KEY=0x856c96eaa4e7c40c715ccc5dacd8bf6e +ACCOUNT_PROFILE=starknet-devnet +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 + +#TODO: rename script and make more generic +echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $1 --function $2 --calldata $3" > $LOG_DIR/cmd.txt +/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $1 --function $2 --calldata $3 > $LOG_DIR/output.json diff --git a/tests/integration/docker/leave_faction.sh b/tests/integration/docker/leave_faction.sh new file mode 100755 index 00000000..431e5a69 --- /dev/null +++ b/tests/integration/docker/leave_faction.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# This script runs the integration tests. + +# TODO: Host? +RPC_HOST="devnet" +RPC_PORT=5050 + +RPC_URL=http://$RPC_HOST:$RPC_PORT + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +WORK_DIR=$SCRIPT_DIR/.. + +#TODO: 2 seperate directories when called from the test script +OUTPUT_DIR=$HOME/.art-peace-tests +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 + +ACCOUNT_NAME=art_peace_acct +ACCOUNT_ADDRESS=0x328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0 +ACCOUNT_PRIVATE_KEY=0x856c96eaa4e7c40c715ccc5dacd8bf6e +ACCOUNT_PROFILE=starknet-devnet +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 + +#TODO: rename script and make more generic +echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $1 --function $2 --calldata $3" > $LOG_DIR/cmd.txt +/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $1 --function $2 --calldata $3 > $LOG_DIR/output.json diff --git a/tests/integration/docker/mint_nft.sh b/tests/integration/docker/mint_nft.sh index b0635f4f..66a7a51b 100755 --- a/tests/integration/docker/mint_nft.sh +++ b/tests/integration/docker/mint_nft.sh @@ -32,5 +32,5 @@ 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 #TODO: rename script and make more generic -echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $1 --function $2 --calldata $3 $4 $5" > $LOG_DIR/cmd.txt -/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $1 --function $2 --calldata $3 $4 $5 > $LOG_DIR/output.json +echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $1 --function $2 --calldata $3 $4 $5 $6" > $LOG_DIR/cmd.txt +/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $1 --function $2 --calldata $3 $4 $5 $6 > $LOG_DIR/output.json diff --git a/tests/integration/docker/setup_factions.sh b/tests/integration/docker/setup_factions.sh index 847f1f7e..6cf6b585 100755 --- a/tests/integration/docker/setup_factions.sh +++ b/tests/integration/docker/setup_factions.sh @@ -26,24 +26,28 @@ for entry in $(cat $FACTIONS_CONFIG_FILE | jq -r '.factions.[] | @base64'); do FACTION_ID=$(_jq '.id') FACTION_NAME=$(_jq '.name') FACTION_LEADER=$(_jq '.leader') - FACTION_POOL=$(_jq '.pool') - FACTION_PER_MEMBER=$(_jq '.per_member') - FACTION_MEMBERS=$(_jq '.members') + JOINABLE=$(_jq '.joinable') + ALLOCATION=$(_jq '.allocation') # Add faction onchain FACTION_NAME_HEX=0x$(echo -n $FACTION_NAME | xxd -p) - FACTION_MEMBERS_COUNT=$(echo $FACTION_MEMBERS | jq '. | length') - FACTION_MEMBERS_CALLDATA=$(echo $FACTION_MEMBERS | jq -r '[.[]] | join(" ")') - - if [ $FACTION_PER_MEMBER == "true" ]; then - POOL=$(($FACTION_POOL * $FACTION_MEMBERS_COUNT)) - else - POOL=$FACTION_POOL + FACTION_JOINABLE_HEX=1 + if [ "$JOINABLE" = "false" ]; then + FACTION_JOINABLE_HEX=0 fi - CALLDATA="$FACTION_NAME_HEX $FACTION_LEADER $POOL $FACTION_MEMBERS_COUNT $FACTION_MEMBERS_CALLDATA" + CALLDATA="$FACTION_NAME_HEX $FACTION_LEADER $FACTION_JOINABLE_HEX $ALLOCATION" echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $ART_PEACE_CONTRACT_ADDRESS --function init_faction --calldata $CALLDATA" /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $ART_PEACE_CONTRACT_ADDRESS --function init_faction --calldata $CALLDATA done +for entry in $(cat $FACTIONS_CONFIG_FILE | jq -r '.chain_factions.[]'); do + FACTION_NAME=$entry + FACTION_NAME_HEX=0x$(echo -n $FACTION_NAME | xxd -p) + + CALLDATA="$FACTION_NAME_HEX" + echo "/root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME invoke --contract-address $ART_PEACE_CONTRACT_ADDRESS --function init_chain_faction --calldata $CALLDATA" + /root/.local/bin/sncast --url $RPC_URL --accounts-file $ACCOUNT_FILE --account $ACCOUNT_NAME --wait --json invoke --contract-address $ART_PEACE_CONTRACT_ADDRESS --function init_chain_faction --calldata $CALLDATA +done + # #TODO: rename script and make more generic