diff --git a/Makefile b/Makefile index c9fb54f2..86c9be7d 100644 --- a/Makefile +++ b/Makefile @@ -66,3 +66,8 @@ init-infra-prod: @echo "Initializing infra..." curl https://api.art-peace.net/init-canvas -X POST curl https://api.art-peace.net/init-quests -X POST -d "@configs/production-quests.config.json" + +update-frontend-contracts: + cat onchain/target/dev/art_peace_ArtPeace.contract_class.json| jq -r '.abi' > frontend/src/contracts/art_peace.abi.json + cat onchain/target/dev/art_peace_CanvasNFT.contract_class.json| jq -r '.abi' > frontend/src/contracts/canvas_nft.abi.json + cat onchain/target/dev/art_peace_UsernameStore.contract_class.json| jq -r '.abi' > frontend/src/contracts/username_store.abi.json diff --git a/backend/config/backend.go b/backend/config/backend.go index a7e2d29b..a7de7734 100644 --- a/backend/config/backend.go +++ b/backend/config/backend.go @@ -11,10 +11,15 @@ type BackendScriptsConfig struct { AddTemplateDevnet string `json:"add_template_devnet"` ClaimTodayQuestDevnet string `json:"claim_today_quest_devnet"` MintNFTDevnet string `json:"mint_nft_devnet"` + LikeNFTDevnet string `json:"like_nft_devnet"` + UnlikeNFTDevnet string `json:"unlike_nft_devnet"` VoteColorDevnet string `json:"vote_color_devnet"` 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 { @@ -48,10 +53,15 @@ var DefaultBackendConfig = BackendConfig{ AddTemplateDevnet: "../scripts/add_template.sh", ClaimTodayQuestDevnet: "../scripts/claim_today_quest.sh", MintNFTDevnet: "../scripts/mint_nft.sh", + LikeNFTDevnet: "../scripts/like_nft.sh", + UnlikeNFTDevnet: "../scripts/unlike_nft.sh", VoteColorDevnet: "../scripts/vote_color.sh", 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..b716f39e --- /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..58da3ea3 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..34714786 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..b39b4453 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 + 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 } - 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..3affbe27 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 e04f4171..7aee683a 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,18 +18,25 @@ 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 { FactionId int `json:"factionId"` - MemberId int `json:"memberId"` Allocation int `json:"allocation"` Name string `json:"name"` - Pool int `json:"pool"` Members int `json:"members"` + Joinable bool `json:"joinable"` Icon string `json:"icon"` Telegram string `json:"telegram"` Twitter string `json:"twitter"` @@ -39,9 +47,9 @@ type FactionUserData struct { type FactionData struct { FactionId int `json:"factionId"` Name string `json:"name"` - Pool int `json:"pool"` Members int `json:"members"` IsMember bool `json:"isMember"` + Joinable bool `json:"joinable"` Icon string `json:"icon"` Telegram string `json:"telegram"` Twitter string `json:"twitter"` @@ -67,7 +75,8 @@ type FactionsConfigItem struct { } type FactionsConfig struct { - Factions []FactionsConfigItem `json:"factions"` + Factions []FactionsConfigItem `json:"factions"` + ChainFactions []string `json:"chain_factions"` } type FactionMemberData struct { @@ -156,9 +165,9 @@ func getMyFactions(w http.ResponseWriter, r *http.Request) { // TODO: Paginate and accumulate the allocations for each faction query := ` - SELECT m.faction_id, m.member_id, m.allocation, f.name, f.pixel_pool as pool, 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.key - 1 + LEFT JOIN factions f ON m.faction_id = f.faction_id LEFT JOIN FactionLinks l ON m.faction_id = l.faction_id WHERE m.user_address = $1 ORDER BY m.faction_id @@ -191,12 +200,12 @@ func getFactions(w http.ResponseWriter, r *http.Request) { offset := (page - 1) * pageLength query := ` - SELECT key - 1 as faction_id, name, pixel_pool as pool, COALESCE((SELECT COUNT(*) FROM factionmembersinfo WHERE faction_id = key - 1), 0) as members, - COALESCE((SELECT COUNT(*) FROM factionmembersinfo WHERE faction_id = key - 1 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 - LEFT JOIN FactionLinks ON key - 1 = faction_id - ORDER BY key + FROM factions f + LEFT JOIN FactionLinks fl ON f.faction_id = fl.faction_id + ORDER BY f.faction_id LIMIT $2 OFFSET $3 ` @@ -208,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,12 +332,11 @@ func getFactionMembers(w http.ResponseWriter, r *http.Request) { SELECT FMI.user_address AS user_address, COALESCE(U.name, '') AS username, - SUM(FMI.allocation) AS total_allocation + F.allocation AS total_allocation FROM FactionMembersInfo FMI LEFT JOIN Users U ON FMI.user_address = U.address + LEFT JOIN Factions F ON F.faction_id = FMI.faction_id WHERE FMI.faction_id = $1 - GROUP BY FMI.user_address, U.name - ORDER BY total_allocation DESC LIMIT $2 OFFSET $3; ` @@ -251,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 6bdeb41c..def5723f 100644 --- a/backend/routes/indexer/faction.go +++ b/backend/routes/indexer/faction.go @@ -12,19 +12,18 @@ func processFactionCreatedEvent(event IndexerEvent) { factionIdHex := event.Event.Keys[1] nameHex := event.Event.Data[0][2:] // Remove 0x prefix leader := event.Event.Data[1][2:] // Remove 0x prefix - poolHex := event.Event.Data[2] - membersCountHex := event.Event.Data[3] - memberAddresses := event.Event.Data[4:] + joinableHex := event.Event.Data[2] + allocationHex := event.Event.Data[3] factionId, err := strconv.ParseInt(factionIdHex, 0, 64) if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to parse factionId", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) + PrintIndexerError("processFactionCreatedEvent", "Failed to parse factionId", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } decodedName, err := hex.DecodeString(nameHex) if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to decode name", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) + PrintIndexerError("processFactionCreatedEvent", "Failed to decode name", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } // Trim off 0s at the start @@ -39,34 +38,25 @@ func processFactionCreatedEvent(event IndexerEvent) { } name := string(trimmedName) - pool, err := strconv.ParseInt(poolHex, 0, 64) + joinableInt, err := strconv.ParseInt(joinableHex, 0, 64) if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to parse pool", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) + PrintIndexerError("processFactionCreatedEvent", "Failed to parse joinable", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } + joinable := joinableInt != 0 - membersCount, err := strconv.ParseInt(membersCountHex, 0, 64) + allocation, err := strconv.ParseInt(allocationHex, 0, 64) if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to parse membersCount", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) + PrintIndexerError("processFactionCreatedEvent", "Failed to parse allocation", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } - allocation := pool / membersCount // Add faction info into postgres - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Factions (name, leader, pixel_pool) VALUES ($1, $2, $3)", name, leader, pool) + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Factions (faction_id, name, leader, joinable, allocation) VALUES ($1, $2, $3, $4, $5)", factionId, name, leader, joinable, allocation) if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to insert faction into postgres", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) + PrintIndexerError("processFactionCreatedEvent", "Failed to insert faction into postgres", factionIdHex, nameHex, leader, joinableHex, allocationHex) return } - - // Add members info into postgres - for i, memberAddress := range memberAddresses { - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO FactionMembersInfo (faction_id, member_id, user_address, allocation, last_placed_time, member_pixels) VALUES ($1, $2, $3, $4, TO_TIMESTAMP($5), $6)", factionId, i, memberAddress[2:], allocation, 0, 0) - if err != nil { - PrintIndexerError("processFactionCreatedEvent", "Failed to insert member into postgres", factionIdHex, nameHex, leader, poolHex, membersCountHex, memberAddresses) - return - } - } } func revertFactionCreatedEvent(event IndexerEvent) { @@ -83,18 +73,158 @@ func revertFactionCreatedEvent(event IndexerEvent) { PrintIndexerError("revertFactionCreatedEvent", "Failed to delete faction from postgres", factionIdHex) return } +} + +func processFactionJoinedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("processFactionJoinedEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM FactionMembersInfo WHERE faction_id = $1", factionId) + _, 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("revertFactionCreatedEvent", "Failed to delete members from postgres", factionIdHex) + PrintIndexerError("processFactionJoinedEvent", "Failed to insert faction member into postgres", factionIdHex, userAddress) return } } -func processMemberReplacedEvent(event IndexerEvent) { - // TODO: Implement +func revertFactionJoinedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("revertFactionJoinedEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM FactionMembersInfo WHERE faction_id = $1 AND user_address = $2", factionId, userAddress) + if err != nil { + PrintIndexerError("revertFactionJoinedEvent", "Failed to delete faction member from postgres", factionIdHex, userAddress) + return + } } -func revertMemberReplacedEvent(event IndexerEvent) { - // TODO: Implement +func processFactionLeftEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("processFactionLeftEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM FactionMembersInfo WHERE faction_id = $1 AND user_address = $2", factionId, userAddress) + if err != nil { + PrintIndexerError("processFactionLeftEvent", "Failed to delete faction member from postgres", factionIdHex, userAddress) + return + } +} + +func revertFactionLeftEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("revertFactionLeftEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } + + // 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, TO_TIMESTAMP($3), $4)", factionId, userAddress, 0, 0) + if err != nil { + PrintIndexerError("revertFactionLeftEvent", "Failed to insert faction member into postgres", factionIdHex, userAddress) + return + } +} + +func processChainFactionCreatedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + nameHex := event.Event.Data[0][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("processChainFactionCreatedEvent", "Failed to parse factionId", factionIdHex, nameHex) + return + } + + decodedName, err := hex.DecodeString(nameHex) + if err != nil { + PrintIndexerError("processChainFactionCreatedEvent", "Failed to decode name", factionIdHex, nameHex) + return + } + // Trim off 0s at the start + trimmedName := []byte{} + trimming := true + for _, b := range decodedName { + if b == 0 && trimming { + continue + } + trimming = false + trimmedName = append(trimmedName, b) + } + name := string(trimmedName) + + // Add faction info into postgres + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO ChainFactions (faction_id, name) VALUES ($1, $2)", factionId, name) + if err != nil { + PrintIndexerError("processChainFactionCreatedEvent", "Failed to insert faction into postgres", factionIdHex, nameHex) + return + } +} + +func revertChainFactionCreatedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("revertChainFactionCreatedEvent", "Failed to parse factionId", factionIdHex) + return + } + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM ChainFactions WHERE faction_id = $1", factionId) + if err != nil { + PrintIndexerError("revertChainFactionCreatedEvent", "Failed to delete faction from postgres", factionIdHex) + return + } +} + +func processChainFactionJoinedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("processChainFactionJoinedEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } + + _, 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 + } +} + +func revertChainFactionJoinedEvent(event IndexerEvent) { + factionIdHex := event.Event.Keys[1] + userAddress := event.Event.Keys[2][2:] // Remove 0x prefix + + factionId, err := strconv.ParseInt(factionIdHex, 0, 64) + if err != nil { + PrintIndexerError("revertChainFactionJoinedEvent", "Failed to parse factionId", factionIdHex, userAddress) + return + } + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM ChainFactionMembersInfo WHERE faction_id = $1 AND user_address = $2", factionId, userAddress) + if err != nil { + PrintIndexerError("revertChainFactionJoinedEvent", "Failed to delete faction member from postgres", factionIdHex, userAddress) + return + } } diff --git a/backend/routes/indexer/nft.go b/backend/routes/indexer/nft.go index 8a4671b8..62738660 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 } @@ -228,3 +261,84 @@ func revertNFTMintedEvent(event IndexerEvent) { // TODO: Mark image as unused? } + +func processNFTLikedEvent(event IndexerEvent) { + tokenIdLowHex := event.Event.Keys[1][2:] // Remove 0x prefix + tokenIdHighHex := event.Event.Keys[2][2:] // Remove 0x prefix + liker := event.Event.Keys[3][2:] // Remove 0x prefix + + tokenIdU256, err := combineLowHigh(tokenIdLowHex, tokenIdHighHex) + if err != nil { + PrintIndexerError("processNFTLikedEvent", "Error converting tokenId hex to int", tokenIdLowHex, liker) + return + } + tokenId := tokenIdU256.Uint64() + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTLikes (nftKey, liker) VALUES ($1, $2) ON CONFLICT DO NOTHING", tokenId, liker) + if err != nil { + PrintIndexerError("processNFTLikedEvent", "Error inserting NFT like into postgres", tokenIdLowHex, liker) + return + } + + // TODO: WebSocket message? +} + +func revertNFTLikedEvent(event IndexerEvent) { + tokenIdLowHex := event.Event.Keys[1][2:] // Remove 0x prefix + tokenIdHighHex := event.Event.Keys[2][2:] // Remove 0x prefix + liker := event.Event.Keys[3][2:] // Remove 0x prefix + + tokenIdU256, err := combineLowHigh(tokenIdLowHex, tokenIdHighHex) + if err != nil { + PrintIndexerError("revertNFTLikedEvent", "Error converting tokenId hex to int", tokenIdLowHex, liker) + return + } + tokenId := tokenIdU256.Uint64() + + // TODO: Check if like exists before event + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM NFTLikes WHERE nftKey = $1 AND liker = $2", tokenId, liker) + if err != nil { + PrintIndexerError("revertNFTLikedEvent", "Error deleting NFT like from postgres", tokenIdLowHex, liker) + return + } +} + +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[3][2:] // Remove 0x prefix + + tokenIdU256, err := combineLowHigh(tokenIdLowHex, tokenIdHighHex) + if err != nil { + PrintIndexerError("processNFTUnlikedEvent", "Error converting tokenId hex to int", tokenIdLowHex, unliker) + return + } + tokenId := tokenIdU256.Uint64() + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM NFTLikes WHERE nftKey = $1 AND liker = $2", tokenId, unliker) + if err != nil { + PrintIndexerError("processNFTUnlikedEvent", "Error deleting NFT like from postgres", tokenIdLowHex, unliker) + return + } + + // TODO: WebSocket message? +} + +func revertNFTUnlikedEvent(event IndexerEvent) { + tokenIdLowHex := event.Event.Keys[1][2:] // Remove 0x prefix + tokenIdHighHex := 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 { + PrintIndexerError("revertNFTUnlikedEvent", "Error converting tokenId hex to int", tokenIdLowHex, unliker) + return + } + tokenId := tokenIdU256.Uint64() + + _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTLikes (nftKey, liker) VALUES ($1, $2) ON CONFLICT DO NOTHING", tokenId, unliker) + if err != nil { + PrintIndexerError("revertNFTUnlikedEvent", "Error inserting NFT like into postgres", tokenIdLowHex, unliker) + return + } +} diff --git a/backend/routes/indexer/pixel.go b/backend/routes/indexer/pixel.go index 836fcefd..d660321a 100644 --- a/backend/routes/indexer/pixel.go +++ b/backend/routes/indexer/pixel.go @@ -143,44 +143,61 @@ 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) + timestamp, err := strconv.ParseInt(timestampHex, 0, 64) if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting faction id hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) + PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting timestamp hex to int", userAddress, timestampHex, memberPixelsHex) return } - memberId, err := strconv.ParseInt(memberIdHex, 0, 64) + memberPixels, err := strconv.ParseInt(memberPixelsHex, 0, 64) if err != nil { - PrintIndexerError("processMemberPixelsPlacedEvent", "Error converting member id 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 user_address = $3", timestamp, memberPixels, userAddress) + if err != nil { + PrintIndexerError("processMemberPixelsPlacedEvent", "Error updating faction member info in postgres", userAddress, timestampHex, memberPixelsHex) + return + } +} + +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("processMemberPixelsPlacedEvent", "Error converting timestamp hex to int", factionIdHex, memberIdHex, timestampHex, memberPixelsHex) + PrintIndexerError("processChainFactionMemberPixelsPlacedEvent", "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("processChainFactionMemberPixelsPlacedEvent", "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 ChainFactionMembersInfo 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("processChainFactionMemberPixelsPlacedEvent", "Error updating chain faction member info in postgres", userAddress, timestampHex, memberPixelsHex) return } } -func revertMemberPixelsPlacedEvent(event IndexerEvent) { +func revertChainFactionPixelsPlacedEvent(event IndexerEvent) { // TODO } diff --git a/backend/routes/indexer/quest.go b/backend/routes/indexer/quest.go index 7c0e76d7..c6d9929a 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 11bb7dd2..1aa24257 100644 --- a/backend/routes/indexer/route.go +++ b/backend/routes/indexer/route.go @@ -54,83 +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" - memberReplacedEvent = "0x01f8936599822d668e09401ffcef1989aca342fb1f003f9b3b1fd1cbf605ed6b" - nftMintedEvent = "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f" - 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, - memberReplacedEvent: processMemberReplacedEvent, - nftMintedEvent: processNFTMintedEvent, - 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, - memberReplacedEvent: revertMemberReplacedEvent, - nftMintedEvent: revertNFTMintedEvent, - 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: false, - memberReplacedEvent: true, - nftMintedEvent: false, - 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 80ada5ec..aac6a465 100644 --- a/backend/routes/nft.go +++ b/backend/routes/nft.go @@ -1,7 +1,7 @@ package routes import ( - "context" + "io" "net/http" "os" "os/exec" @@ -12,17 +12,21 @@ import ( ) func InitNFTRoutes() { + http.HandleFunc("/get-canvas-nft-address", getCanvasNFTAddress) + http.HandleFunc("/set-canvas-nft-address", setCanvasNFTAddress) http.HandleFunc("/get-nft", getNFT) http.HandleFunc("/get-nfts", getNFTs) http.HandleFunc("/get-new-nfts", getNewNFTs) http.HandleFunc("/get-my-nfts", getMyNFTs) http.HandleFunc("/get-nft-likes", getNftLikeCount) - http.HandleFunc("/like-nft", LikeNFT) - http.HandleFunc("/unlike-nft", UnLikeNFT) + // http.HandleFunc("/like-nft", LikeNFT) + // http.HandleFunc("/unlike-nft", UnLikeNFT) http.HandleFunc("/get-top-nfts", getTopNFTs) http.HandleFunc("/get-hot-nfts", getHotNFTs) if !core.ArtPeaceBackend.BackendConfig.Production { http.HandleFunc("/mint-nft-devnet", mintNFTDevnet) + http.HandleFunc("/like-nft-devnet", likeNFTDevnet) + http.HandleFunc("/unlike-nft-devnet", unlikeNFTDevnet) } // Create a static file server for the nft images // TODO: Versioning here? @@ -33,13 +37,36 @@ func InitNFTStaticRoutes() { http.Handle("/nft-meta/", http.StripPrefix("/nft-meta/", http.FileServer(http.Dir("./nfts/meta")))) } +func getCanvasNFTAddress(w http.ResponseWriter, r *http.Request) { + contractAddress := os.Getenv("CANVAS_NFT_CONTRACT_ADDRESS") + routeutils.WriteDataJson(w, "\""+contractAddress+"\"") +} + +// TODO: Set env var on infra level in production +func setCanvasNFTAddress(w http.ResponseWriter, r *http.Request) { + // Only allow admin to set contract address + if routeutils.AdminMiddleware(w, r) { + return + } + + data, err := io.ReadAll(r.Body) + if err != nil { + routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to read reques t body") + return + } + os.Setenv("CANVAS_NFT_CONTRACT_ADDRESS", string(data)) + routeutils.WriteResultJson(w, "Contract address set") +} + type NFTData struct { TokenID int `json:"tokenId"` 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"` @@ -226,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") @@ -239,48 +268,49 @@ func mintNFTDevnet(w http.ResponseWriter, r *http.Request) { routeutils.WriteResultJson(w, "NFT minted on devnet") } -func LikeNFT(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - routeutils.WriteErrorJson(w, http.StatusMethodNotAllowed, "Method not allowed") - return - } - - nftlikeReq, err := routeutils.ReadJsonBody[NFTLikesRequest](r) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to read request body") - return - } - - // TODO: ensure that the nft exists - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTLikes (nftKey, liker) VALUES ($1, $2)", nftlikeReq.NFTKey, nftlikeReq.UserAddress) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "NFT already liked by user") - return - } - - routeutils.WriteResultJson(w, "NFT liked successfully") -} - -func UnLikeNFT(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - routeutils.WriteErrorJson(w, http.StatusMethodNotAllowed, "Method not allowed") - return - } - - nftlikeReq, err := routeutils.ReadJsonBody[NFTLikesRequest](r) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to read request body") - return - } - - _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM nftlikes WHERE nftKey = $1 AND liker = $2", nftlikeReq.NFTKey, nftlikeReq.UserAddress) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to unlike NFT") - return - } - - routeutils.WriteResultJson(w, "NFT unliked successfully") -} +// TODO +// func LikeNFT(w http.ResponseWriter, r *http.Request) { +// if r.Method != http.MethodPost { +// routeutils.WriteErrorJson(w, http.StatusMethodNotAllowed, "Method not allowed") +// return +// } +// +// nftlikeReq, err := routeutils.ReadJsonBody[NFTLikesRequest](r) +// if err != nil { +// routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to read request body") +// return +// } +// +// // TODO: ensure that the nft exists +// _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTLikes (nftKey, liker) VALUES ($1, $2)", nftlikeReq.NFTKey, nftlikeReq.UserAddress) +// if err != nil { +// routeutils.WriteErrorJson(w, http.StatusBadRequest, "NFT already liked by user") +// return +// } +// +// routeutils.WriteResultJson(w, "NFT liked successfully") +// } +// +// func UnLikeNFT(w http.ResponseWriter, r *http.Request) { +// if r.Method != http.MethodPost { +// routeutils.WriteErrorJson(w, http.StatusMethodNotAllowed, "Method not allowed") +// return +// } +// +// nftlikeReq, err := routeutils.ReadJsonBody[NFTLikesRequest](r) +// if err != nil { +// routeutils.WriteErrorJson(w, http.StatusBadRequest, "Failed to read request body") +// return +// } +// +// _, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "DELETE FROM nftlikes WHERE nftKey = $1 AND liker = $2", nftlikeReq.NFTKey, nftlikeReq.UserAddress) +// if err != nil { +// routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to unlike NFT") +// return +// } +// +// routeutils.WriteResultJson(w, "NFT unliked successfully") +// } func getNftLikeCount(w http.ResponseWriter, r *http.Request) { nftkey := r.URL.Query().Get("nft_key") @@ -343,6 +373,61 @@ func getTopNFTs(w http.ResponseWriter, r *http.Request) { routeutils.WriteDataJson(w, string(nfts)) } +func likeNFTDevnet(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, "Failed to read request body") + return + } + + // TODO: Read tokenId into a big.Int + tokenId := (*jsonBody)["tokenId"] + + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.LikeNFTDevnet + contract := os.Getenv("CANVAS_NFT_CONTRACT_ADDRESS") + + cmd := exec.Command(shellCmd, contract, "like_nft", tokenId, "0") + _, err = cmd.Output() + if err != nil { + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to like NFT on devnet") + return + } + + routeutils.WriteResultJson(w, "NFT liked on devnet") +} + +func unlikeNFTDevnet(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, "Failed to read request body") + return + } + + tokenId := (*jsonBody)["tokenId"] + + shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.UnlikeNFTDevnet + contract := os.Getenv("CANVAS_NFT_CONTRACT_ADDRESS") + + 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") + return + } + + routeutils.WriteResultJson(w, "NFT unliked on devnet") +} + func getHotNFTs(w http.ResponseWriter, r *http.Request) { address := r.URL.Query().Get("address") if address == "" { @@ -370,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 ( @@ -388,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..66033863 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" @@ -15,41 +16,61 @@ import ( ) type DailyUserQuest struct { - Name string `json:"name"` - Description string `json:"description"` - Reward int `json:"reward"` - DayIndex int `json:"dayIndex"` - QuestId int `json:"questId"` - Completed bool `json:"completed"` + Name string `json:"name"` + Description string `json:"description"` + Reward int `json:"reward"` + DayIndex int `json:"dayIndex"` + QuestId int `json:"questId"` + Completed bool `json:"completed"` + ClaimParams []ClaimParams `json:"claimParams"` } type DailyQuest struct { - Name string `json:"name"` - Description string `json:"description"` - Reward int `json:"reward"` - DayIndex int `json:"dayIndex"` - QuestId int `json:"questId"` + Name string `json:"name"` + Description string `json:"description"` + Reward int `json:"reward"` + DayIndex int `json:"dayIndex"` + QuestId int `json:"questId"` + ClaimParams []ClaimParams `json:"claimParams"` } type MainUserQuest struct { - QuestId int `json:"questId"` - Name string `json:"name"` - Description string `json:"description"` - Reward int `json:"reward"` - Completed bool `json:"completed"` + QuestId int `json:"questId"` + Name string `json:"name"` + Description string `json:"description"` + Reward int `json:"reward"` + Completed bool `json:"completed"` + ClaimParams []ClaimParams `json:"claimParams"` } type MainQuest struct { - QuestId int `json:"questId"` - Name string `json:"name"` - Description string `json:"description"` - Reward int `json:"reward"` + QuestId int `json:"questId"` + 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,32 @@ 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 +599,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 +639,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 aa45bc63..a269e0c8 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) @@ -50,7 +51,6 @@ func setUsernameStoreAddress(w http.ResponseWriter, r *http.Request) { type MembershipPixelsData struct { FactionId int `json:"factionId"` - MemberId int `json:"memberId"` Allocation int `json:"allocation"` LastPlacedTime *time.Time `json:"lastPlacedTime"` MemberPixels int `json:"memberPixels"` @@ -63,7 +63,23 @@ func getFactionPixels(w http.ResponseWriter, r *http.Request) { return } - membershipPixels, err := core.PostgresQueryJson[MembershipPixelsData]("SELECT faction_id, member_id, allocation, last_placed_time, member_pixels FROM FactionMembersInfo 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 + } + + 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 @@ -167,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 388cb2eb..4f6bfe50 100644 --- a/configs/backend.config.json +++ b/configs/backend.config.json @@ -7,8 +7,13 @@ "place_extra_pixels_devnet": "../tests/integration/local/place_extra_pixels.sh", "add_template_devnet": "../tests/integration/local/add_template.sh", "mint_nft_devnet": "../tests/integration/local/mint_nft.sh", + "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 52decaa5..91600898 100644 --- a/configs/docker-backend.config.json +++ b/configs/docker-backend.config.json @@ -8,10 +8,15 @@ "add_template_devnet": "/scripts/add_template.sh", "claim_today_quest_devnet": "/scripts/claim_today_quest.sh", "mint_nft_devnet": "/scripts/mint_nft.sh", + "like_nft_devnet": "/scripts/like_nft.sh", + "unlike_nft_devnet": "/scripts/unlike_nft.sh", "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.css b/frontend/src/App.css index 56dead20..98be67a2 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -9,6 +9,15 @@ background-color: #fefdfb; } +.App--background { + height: 100vh; + width: 100vw; + + background-size: cover; + background-position: center; + image-rendering: pixelated; +} + .App__panel { position: fixed; z-index: 100; diff --git a/frontend/src/App.js b/frontend/src/App.js index 25b9b380..61fbe279 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -16,9 +16,10 @@ 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'; import NotificationPanel from './tabs/NotificationPanel.js'; import Hamburger from './resources/icons/Hamburger.png'; @@ -85,6 +86,42 @@ function App() { address: process.env.REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS, abi: username_store_abi }); + const { contract: canvasNftContract } = useContract({ + address: process.env.REACT_APP_CANVAS_NFT_CONTRACT_ADDRESS, + 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, { @@ -183,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); @@ -238,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 = () => { @@ -278,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() { @@ -300,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}` @@ -376,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}` @@ -385,6 +494,7 @@ function App() { } setUserFactions(userFactionsResponse.data); } + fetchChainFaction(); fetchUserFactions(); }, [queryAddress]); @@ -469,74 +579,32 @@ function App() { return (
- - - {(!isMobile || activeTab === tabs[0]) && ( - logo - )} -
- + + -
-
+ {(!isMobile || activeTab === tabs[0]) && ( + logo + )}
- - {isFooterSplit && !footerExpanded && ( -
{ - setActiveTab(tabs[0]); - setFooterExpanded(!footerExpanded); - }} - > - Tabs -
- )} - {isFooterSplit && footerExpanded && ( +
+
+
+ {!gameEnded && ( + + )} + {isFooterSplit && !footerExpanded && ( +
{ + setActiveTab(tabs[0]); + setFooterExpanded(!footerExpanded); + }} + > + Tabs +
+ )} + {isFooterSplit && footerExpanded && ( + + )} +
+ {!isFooterSplit && ( )}
- {!isFooterSplit && ( - - )}
); diff --git a/frontend/src/canvas/CanvasContainer.js b/frontend/src/canvas/CanvasContainer.js index 28dce3e2..1936a2da 100644 --- a/frontend/src/canvas/CanvasContainer.js +++ b/frontend/src/canvas/CanvasContainer.js @@ -323,6 +323,7 @@ const CanvasContainer = (props) => { 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/contracts/art_peace.abi.json b/frontend/src/contracts/art_peace.abi.json index 1a0ffd3c..2f889e5d 100644 --- a/frontend/src/contracts/art_peace.abi.json +++ b/frontend/src/contracts/art_peace.abi.json @@ -38,6 +38,20 @@ } ] }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, { "type": "struct", "name": "art_peace::interfaces::Faction", @@ -51,7 +65,11 @@ "type": "core::starknet::contract_address::ContractAddress" }, { - "name": "pixel_pool", + "name": "joinable", + "type": "core::bool" + }, + { + "name": "allocation", "type": "core::integer::u32" } ] @@ -216,78 +234,6 @@ "outputs": [], "state_mutability": "view" }, - { - "type": "function", - "name": "place_pixel_inner", - "inputs": [ - { - "name": "pos", - "type": "core::integer::u128" - }, - { - "name": "color", - "type": "core::integer::u8" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "place_basic_pixel_inner", - "inputs": [ - { - "name": "pos", - "type": "core::integer::u128" - }, - { - "name": "color", - "type": "core::integer::u8" - }, - { - "name": "now", - "type": "core::integer::u64" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "place_member_pixels_inner", - "inputs": [ - { - "name": "faction_id", - "type": "core::integer::u32" - }, - { - "name": "member_id", - "type": "core::integer::u32" - }, - { - "name": "positions", - "type": "core::array::Span::" - }, - { - "name": "colors", - "type": "core::array::Span::" - }, - { - "name": "offset", - "type": "core::integer::u32" - }, - { - "name": "now", - "type": "core::integer::u64" - } - ], - "outputs": [ - { - "type": "core::integer::u32" - } - ], - "state_mutability": "external" - }, { "type": "function", "name": "place_pixel", @@ -444,22 +390,6 @@ ], "state_mutability": "view" }, - { - "type": "function", - "name": "get_user_factions_count", - "inputs": [ - { - "name": "user", - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "outputs": [ - { - "type": "core::integer::u32" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "get_faction", @@ -505,12 +435,12 @@ "type": "core::starknet::contract_address::ContractAddress" }, { - "name": "pool", - "type": "core::integer::u32" + "name": "joinable", + "type": "core::bool" }, { - "name": "members", - "type": "core::array::Span::" + "name": "allocation", + "type": "core::integer::u32" } ], "outputs": [], @@ -518,19 +448,23 @@ }, { "type": "function", - "name": "replace_member", + "name": "init_chain_faction", "inputs": [ { - "name": "faction_id", - "type": "core::integer::u32" - }, + "name": "name", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "join_faction", + "inputs": [ { - "name": "member_id", + "name": "faction_id", "type": "core::integer::u32" - }, - { - "name": "new_member", - "type": "core::starknet::contract_address::ContractAddress" } ], "outputs": [], @@ -538,27 +472,46 @@ }, { "type": "function", - "name": "get_faction_members", + "name": "leave_faction", + "inputs": [], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "join_chain_faction", "inputs": [ { "name": "faction_id", "type": "core::integer::u32" } ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_user_faction", + "inputs": [ + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress" + } + ], "outputs": [ { - "type": "core::array::Span::" + "type": "core::integer::u32" } ], "state_mutability": "view" }, { "type": "function", - "name": "get_faction_member_count", + "name": "get_user_chain_faction", "inputs": [ { - "name": "faction_id", - "type": "core::integer::u32" + "name": "user", + "type": "core::starknet::contract_address::ContractAddress" } ], "outputs": [ @@ -570,15 +523,31 @@ }, { "type": "function", - "name": "get_faction_members_pixels", + "name": "get_user_faction_members_pixels", "inputs": [ { - "name": "faction_id", - "type": "core::integer::u32" + "name": "user", + "type": "core::starknet::contract_address::ContractAddress" }, { - "name": "member_id", + "name": "now", + "type": "core::integer::u64" + } + ], + "outputs": [ + { "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_chain_faction_members_pixels", + "inputs": [ + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress" }, { "name": "now", @@ -1131,20 +1100,6 @@ } ] }, - { - "type": "enum", - "name": "core::bool", - "variants": [ - { - "name": "False", - "type": "()" - }, - { - "name": "True", - "type": "()" - } - ] - }, { "type": "interface", "name": "art_peace::templates::interfaces::ITemplateStore", @@ -1295,6 +1250,23 @@ } ] }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::ColorAdded", + "kind": "struct", + "members": [ + { + "name": "color_key", + "type": "core::integer::u8", + "kind": "key" + }, + { + "name": "color", + "type": "core::integer::u32", + "kind": "data" + } + ] + }, { "type": "event", "name": "art_peace::art_peace::ArtPeace::PixelPlaced", @@ -1341,17 +1313,34 @@ }, { "type": "event", - "name": "art_peace::art_peace::ArtPeace::MemberPixelsPlaced", + "name": "art_peace::art_peace::ArtPeace::FactionPixelsPlaced", "kind": "struct", "members": [ { - "name": "faction_id", - "type": "core::integer::u32", + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", "kind": "key" }, { - "name": "member_id", + "name": "placed_time", + "type": "core::integer::u64", + "kind": "data" + }, + { + "name": "member_pixels", "type": "core::integer::u32", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::ChainFactionPixelsPlaced", + "kind": "struct", + "members": [ + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", "kind": "key" }, { @@ -1485,20 +1474,37 @@ "kind": "data" }, { - "name": "pool", + "name": "joinable", + "type": "core::bool", + "kind": "data" + }, + { + "name": "allocation", "type": "core::integer::u32", "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::ChainFactionCreated", + "kind": "struct", + "members": [ + { + "name": "faction_id", + "type": "core::integer::u32", + "kind": "key" }, { - "name": "members", - "type": "core::array::Span::", + "name": "name", + "type": "core::felt252", "kind": "data" } ] }, { "type": "event", - "name": "art_peace::art_peace::ArtPeace::MemberReplaced", + "name": "art_peace::art_peace::ArtPeace::FactionJoined", "kind": "struct", "members": [ { @@ -1507,14 +1513,43 @@ "kind": "key" }, { - "name": "member_id", + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::FactionLeft", + "kind": "struct", + "members": [ + { + "name": "faction_id", "type": "core::integer::u32", "kind": "key" }, { - "name": "new_member", + "name": "user", "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::art_peace::ArtPeace::ChainFactionJoined", + "kind": "struct", + "members": [ + { + "name": "faction_id", + "type": "core::integer::u32", + "kind": "key" + }, + { + "name": "user", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" } ] }, @@ -1596,6 +1631,11 @@ "type": "art_peace::art_peace::ArtPeace::NewDay", "kind": "nested" }, + { + "name": "ColorAdded", + "type": "art_peace::art_peace::ArtPeace::ColorAdded", + "kind": "nested" + }, { "name": "PixelPlaced", "type": "art_peace::art_peace::ArtPeace::PixelPlaced", @@ -1607,8 +1647,13 @@ "kind": "nested" }, { - "name": "MemberPixelsPlaced", - "type": "art_peace::art_peace::ArtPeace::MemberPixelsPlaced", + "name": "FactionPixelsPlaced", + "type": "art_peace::art_peace::ArtPeace::FactionPixelsPlaced", + "kind": "nested" + }, + { + "name": "ChainFactionPixelsPlaced", + "type": "art_peace::art_peace::ArtPeace::ChainFactionPixelsPlaced", "kind": "nested" }, { @@ -1637,8 +1682,23 @@ "kind": "nested" }, { - "name": "MemberReplaced", - "type": "art_peace::art_peace::ArtPeace::MemberReplaced", + "name": "ChainFactionCreated", + "type": "art_peace::art_peace::ArtPeace::ChainFactionCreated", + "kind": "nested" + }, + { + "name": "FactionJoined", + "type": "art_peace::art_peace::ArtPeace::FactionJoined", + "kind": "nested" + }, + { + "name": "FactionLeft", + "type": "art_peace::art_peace::ArtPeace::FactionLeft", + "kind": "nested" + }, + { + "name": "ChainFactionJoined", + "type": "art_peace::art_peace::ArtPeace::ChainFactionJoined", "kind": "nested" }, { diff --git a/frontend/src/contracts/canvas_nft.abi.json b/frontend/src/contracts/canvas_nft.abi.json new file mode 100644 index 00000000..4831b772 --- /dev/null +++ b/frontend/src/contracts/canvas_nft.abi.json @@ -0,0 +1,852 @@ +[ + { + "type": "impl", + "name": "ERC721Metadata", + "interface_name": "openzeppelin::token::erc721::interface::IERC721Metadata" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "struct", + "name": "core::integer::u256", + "members": [ + { + "name": "low", + "type": "core::integer::u128" + }, + { + "name": "high", + "type": "core::integer::u128" + } + ] + }, + { + "type": "interface", + "name": "openzeppelin::token::erc721::interface::IERC721Metadata", + "items": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "token_uri", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "CanvasNFTAdditional", + "interface_name": "art_peace::nfts::interfaces::ICanvasNFTAdditional" + }, + { + "type": "struct", + "name": "art_peace::nfts::interfaces::NFTMetadata", + "members": [ + { + "name": "position", + "type": "core::integer::u128" + }, + { + "name": "width", + "type": "core::integer::u128" + }, + { + "name": "height", + "type": "core::integer::u128" + }, + { + "name": "image_hash", + "type": "core::felt252" + }, + { + "name": "block_number", + "type": "core::integer::u64" + }, + { + "name": "day_index", + "type": "core::integer::u32" + }, + { + "name": "minter", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "art_peace::nfts::interfaces::ICanvasNFTAdditional", + "items": [ + { + "type": "function", + "name": "set_canvas_contract", + "inputs": [ + { + "name": "canvas_contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "metadata", + "type": "art_peace::nfts::interfaces::NFTMetadata" + }, + { + "name": "receiver", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "CanvasNFTLikeAndUnlike", + "interface_name": "art_peace::nfts::interfaces::ICanvasNFTLikeAndUnlike" + }, + { + "type": "interface", + "name": "art_peace::nfts::interfaces::ICanvasNFTLikeAndUnlike", + "items": [ + { + "type": "function", + "name": "like_nft", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "unlike_nft", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "ERC721Impl", + "interface_name": "openzeppelin::token::erc721::interface::IERC721" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "openzeppelin::token::erc721::interface::IERC721", + "items": [ + { + "type": "function", + "name": "balance_of", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::integer::u256" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "owner_of", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "safe_transfer_from", + "inputs": [ + { + "name": "from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::integer::u256" + }, + { + "name": "data", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "transfer_from", + "inputs": [ + { + "name": "from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "set_approval_for_all", + "inputs": [ + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "approved", + "type": "core::bool" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_approved", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "is_approved_for_all", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "ERC721CamelOnly", + "interface_name": "openzeppelin::token::erc721::interface::IERC721CamelOnly" + }, + { + "type": "interface", + "name": "openzeppelin::token::erc721::interface::IERC721CamelOnly", + "items": [ + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::integer::u256" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "tokenId", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "safeTransferFrom", + "inputs": [ + { + "name": "from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "tokenId", + "type": "core::integer::u256" + }, + { + "name": "data", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "tokenId", + "type": "core::integer::u256" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "setApprovalForAll", + "inputs": [ + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "approved", + "type": "core::bool" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "getApproved", + "inputs": [ + { + "name": "tokenId", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "isApprovedForAll", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "ERC721MetadataCamelOnly", + "interface_name": "openzeppelin::token::erc721::interface::IERC721MetadataCamelOnly" + }, + { + "type": "interface", + "name": "openzeppelin::token::erc721::interface::IERC721MetadataCamelOnly", + "items": [ + { + "type": "function", + "name": "tokenURI", + "inputs": [ + { + "name": "tokenId", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "SRC5Impl", + "interface_name": "openzeppelin::introspection::interface::ISRC5" + }, + { + "type": "interface", + "name": "openzeppelin::introspection::interface::ISRC5", + "items": [ + { + "type": "function", + "name": "supports_interface", + "inputs": [ + { + "name": "interface_id", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "CanvasNFTStoreImpl", + "interface_name": "art_peace::nfts::interfaces::ICanvasNFTStore" + }, + { + "type": "interface", + "name": "art_peace::nfts::interfaces::ICanvasNFTStore", + "items": [ + { + "type": "function", + "name": "get_nft_metadata", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "art_peace::nfts::interfaces::NFTMetadata" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_nft_minter", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_nft_image_hash", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_nft_day_index", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_nfts_count", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u256" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "name", + "type": "core::byte_array::ByteArray" + }, + { + "name": "symbol", + "type": "core::byte_array::ByteArray" + } + ] + }, + { + "type": "event", + "name": "openzeppelin::token::erc721::erc721::ERC721Component::Transfer", + "kind": "struct", + "members": [ + { + "name": "from", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "token_id", + "type": "core::integer::u256", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "openzeppelin::token::erc721::erc721::ERC721Component::Approval", + "kind": "struct", + "members": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "approved", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "token_id", + "type": "core::integer::u256", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "openzeppelin::token::erc721::erc721::ERC721Component::ApprovalForAll", + "kind": "struct", + "members": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "approved", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "openzeppelin::token::erc721::erc721::ERC721Component::Event", + "kind": "enum", + "variants": [ + { + "name": "Transfer", + "type": "openzeppelin::token::erc721::erc721::ERC721Component::Transfer", + "kind": "nested" + }, + { + "name": "Approval", + "type": "openzeppelin::token::erc721::erc721::ERC721Component::Approval", + "kind": "nested" + }, + { + "name": "ApprovalForAll", + "type": "openzeppelin::token::erc721::erc721::ERC721Component::ApprovalForAll", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "openzeppelin::introspection::src5::SRC5Component::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "art_peace::nfts::component::CanvasNFTStoreComponent::CanvasNFTMinted", + "kind": "struct", + "members": [ + { + "name": "token_id", + "type": "core::integer::u256", + "kind": "key" + }, + { + "name": "metadata", + "type": "art_peace::nfts::interfaces::NFTMetadata", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "art_peace::nfts::component::CanvasNFTStoreComponent::Event", + "kind": "enum", + "variants": [ + { + "name": "CanvasNFTMinted", + "type": "art_peace::nfts::component::CanvasNFTStoreComponent::CanvasNFTMinted", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "art_peace::nfts::canvas_nft::CanvasNFT::NFTLiked", + "kind": "struct", + "members": [ + { + "name": "token_id", + "type": "core::integer::u256", + "kind": "key" + }, + { + "name": "user_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::nfts::canvas_nft::CanvasNFT::NFTUnliked", + "kind": "struct", + "members": [ + { + "name": "token_id", + "type": "core::integer::u256", + "kind": "key" + }, + { + "name": "user_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "art_peace::nfts::canvas_nft::CanvasNFT::Event", + "kind": "enum", + "variants": [ + { + "name": "ERC721Event", + "type": "openzeppelin::token::erc721::erc721::ERC721Component::Event", + "kind": "flat" + }, + { + "name": "SRC5Event", + "type": "openzeppelin::introspection::src5::SRC5Component::Event", + "kind": "flat" + }, + { + "name": "NFTEvent", + "type": "art_peace::nfts::component::CanvasNFTStoreComponent::Event", + "kind": "flat" + }, + { + "name": "NFTLiked", + "type": "art_peace::nfts::canvas_nft::CanvasNFT::NFTLiked", + "kind": "nested" + }, + { + "name": "NFTUnliked", + "type": "art_peace::nfts::canvas_nft::CanvasNFT::NFTUnliked", + "kind": "nested" + } + ] + } +] 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 31077aa1..6e563411 100644 --- a/frontend/src/tabs/TabPanel.js +++ b/frontend/src/tabs/TabPanel.js @@ -16,7 +16,11 @@ const TabPanel = (props) => { return (
{ 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} @@ -74,6 +85,7 @@ const TabPanel = (props) => { appear > { {({ timeLeftInDay, newDayAvailable, startNextDay }) => ( { queryAddress={props.queryAddress} setExtraPixels={props.setExtraPixels} extraPixels={props.extraPixels} + gameEnded={props.gameEnded} /> )} @@ -122,15 +136,24 @@ const TabPanel = (props) => { {props.activeTab === 'Factions' && (
)} @@ -139,6 +162,7 @@ const TabPanel = (props) => { {({ timeLeftInDay, newDayAvailable, startNextDay }) => ( { queryAddress={props.queryAddress} address={props.address} artPeaceContract={props.artPeaceContract} + isLastDay={props.isLastDay} + gameEnded={props.gameEnded} /> )} @@ -174,6 +200,7 @@ const TabPanel = (props) => { setLatestMintedTokenId={props.setLatestMintedTokenId} queryAddress={props.queryAddress} isMobile={props.isMobile} + gameEnded={props.gameEnded} />
)} @@ -189,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.css b/frontend/src/tabs/canvas/ExtraPixelsPanel.css index baa92477..937c82c6 100644 --- a/frontend/src/tabs/canvas/ExtraPixelsPanel.css +++ b/frontend/src/tabs/canvas/ExtraPixelsPanel.css @@ -44,6 +44,7 @@ margin: 0; padding: 0 0.5rem; width: 100%; + height: 100%; display: flex; flex-direction: row; 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.css b/frontend/src/tabs/factions/FactionSelector.css index b332c8f1..d60a48f3 100644 --- a/frontend/src/tabs/factions/FactionSelector.css +++ b/frontend/src/tabs/factions/FactionSelector.css @@ -146,7 +146,7 @@ .FactionSelector__info__button__icon { width: 4rem; height: 4rem; - margin: -.5rem; + margin: -0.5rem; } .FactionSelector__name { 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 a8792b4b..c578dc4e 100644 --- a/frontend/src/tabs/nfts/NFTItem.js +++ b/frontend/src/tabs/nfts/NFTItem.js @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { CSSTransition } from 'react-transition-group'; +import { useContractWrite } from '@starknet-react/core'; import './NFTItem.css'; import { fetchWrapper } from '../../services/apiService'; import canvasConfig from '../../configs/canvas.config.json'; @@ -7,50 +8,82 @@ import ShareIcon from '../../resources/icons/Share.png'; import LikeIcon from '../../resources/icons/Like.png'; import LikedIcon from '../../resources/icons/Liked.png'; import Info from '../../resources/icons/Info.png'; +import { devnetMode } from '../../utils/Consts.js'; const NFTItem = (props) => { - const [likes, setLikes] = useState(props.likes); - const [liked, setLiked] = useState(props.liked); + const [calls, setCalls] = useState([]); + const likeNftCall = (tokenId) => { + if (devnetMode) return; + if (!props.address || !props.canvasNftContract) return; + setCalls(props.usernameContract.populateTransaction['like_nft'](tokenId)); + }; + const unlikeNftCall = (tokenId) => { + if (devnetMode) return; + if (!props.address || !props.canvasNftContract) return; + setCalls(props.usernameContract.populateTransaction['unlike_nft'](tokenId)); + }; + useEffect(() => { - setLikes(props.likes); - setLiked(props.liked); - }, [props.likes, props.liked]); + const likeCall = async () => { + if (devnetMode) return; + if (calls.length === 0) return; + await writeAsync(); + console.log('Like call successful:', data, isPending); + // TODO: Update the UI + }; + likeCall(); + }, [calls]); + + const { writeAsync, data, isPending } = useContractWrite({ + calls + }); + + const handleLikePress = async (event) => { + if (props.queryAddress === '0') { + return; + } + event.preventDefault(); + if (!devnetMode) { + if (liked) { + unlikeNftCall(props.tokenId); + } else { + likeNftCall(props.tokenId); + } + return; + } - const handleLike = async () => { - async function fetchLikeNFT() { - const response = await fetchWrapper('like-nft', { + if (!liked) { + let likeResponse = await fetchWrapper('like-nft-devnet', { + mode: 'cors', method: 'POST', body: JSON.stringify({ - nftkey: props.tokenId, - useraddress: props.queryAddress + tokenId: props.tokenId.toString() }) }); - if (response.result) { - // TODO: Update likes on my nfts tab || explore tab if they are the same - setLikes(likes + 1); - setLiked(true); + if (likeResponse.result) { + props.updateLikes(props.tokenId, likes + 1, true); } - } - fetchLikeNFT(); - }; - - const handleUnlike = async () => { - async function fetchUnlikeNFT() { - const response = await fetchWrapper('unlike-nft', { + } else { + let unlikeResponse = await fetchWrapper('unlike-nft-devnet', { + mode: 'cors', method: 'POST', body: JSON.stringify({ - nftkey: props.tokenId, - useraddress: props.queryAddress + tokenId: props.tokenId.toString() }) }); - if (response.result) { - setLikes(likes - 1); - setLiked(false); + if (unlikeResponse.result) { + props.updateLikes(props.tokenId, likes - 1, false); } } - fetchUnlikeNFT(); }; + const [likes, setLikes] = useState(props.likes); + const [liked, setLiked] = useState(props.liked); + useEffect(() => { + setLikes(props.likes); + setLiked(props.liked); + }, [props.likes, props.liked]); + const posx = props.position % canvasConfig.canvas.width; const posy = Math.floor(props.position / canvasConfig.canvas.width); @@ -102,14 +135,15 @@ const NFTItem = (props) => { alt={`nft-image-${props.tokenId}`} className='NFTItem__image' /> +

{props.name}

Share
{ + // TODO: Arrows to control position and size const closePanel = () => { props.setNftMintingMode(false); props.setNftSelectionStarted(false); 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 @@ -21,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) @@ -36,6 +46,7 @@ const NFTMintingPanel = (props) => { console.log('Mint nft successful:', data, isPending); // TODO: Update the UI with the new NFT closePanel(); + props.setActiveTab('NFTs'); }; mintNft(); }, [calls]); @@ -44,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'; @@ -56,15 +69,22 @@ 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) { console.log(response.result); closePanel(); + props.setActiveTab('NFTs'); } }; + const cancel = () => { + props.setNftSelectionStarted(false); + props.setNftSelected(false); + }; + // TODO: Add preview of the NFT && Add input fields for the NFT metadata return (
@@ -76,11 +96,6 @@ const NFTMintingPanel = (props) => {

art/peace NFT Mint

- {props.nftSelected && ( -
submit()}> - Submit -
- )}
{props.nftSelectionStarted === false && ( @@ -134,19 +149,41 @@ const NFTMintingPanel = (props) => {

-

Width

-

- {props.nftWidth} -

-
-
-

Height

+

Size

- {props.nftHeight} + {props.nftWidth} x {props.nftHeight}

+ {props.nftSelected && ( +
+
+

Name

+ setNftName(e.target.value)} + /> +
+
+
cancel()} + > + 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 a86b7e70..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,19 +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)} - > - 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} /> ); })} @@ -55,7 +81,7 @@ const NFTsMainSection = (props) => { }; const NFTsExpandedSection = (props) => { - const imageURL = backendUrl + '/nft-images/'; + const imageURL = nftUrl + '/nft-images/'; return (
@@ -87,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} /> ); })} @@ -130,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 && @@ -178,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 }); } @@ -233,6 +284,7 @@ const NFTs = (props) => { title='NFTs' mainSection={NFTsMainSection} expandedSection={NFTsExpandedSection} + updateLikes={updateLikes} nftMintingMode={props.nftMintingMode} setNftMintingMode={props.setNftMintingMode} nftsCollection={myNFTs} @@ -250,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 57883345..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; @@ -58,6 +63,6 @@ } .Quests__timer--active:active { - transform: scale(1.0) translateY(0rem); + transform: scale(1) translateY(0rem); box-shadow: 0 0 0.3rem rgba(0, 0, 0, 0.3); } 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 ae338a29..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 { @@ -51,7 +53,7 @@ } .Voting__timer--active:active { - transform: scale(1.0) translateY(0rem); + transform: scale(1) translateY(0rem); box-shadow: 0 0 0.3rem rgba(0, 0, 0, 0.3); } 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 30baacd9..5cdffe88 100644 --- a/indexer/prod-script.js +++ b/indexer/prod-script.js @@ -15,6 +15,7 @@ export const config = { includeTransaction: false, includeReceipt: false }, + { // Color Added Event fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), @@ -46,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, @@ -116,10 +127,36 @@ export const config = { includeReceipt: false }, { - // Member Replaced Event - fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), + // Faction Joined Event + keys: [ + "0x01e3fbdf8156ad0dde21e886d61a16d85c9ef54451eb6e253f3f427de32a47ac" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Faction Left Event + keys: [ + "0x014ef8cc25c96157e2a00e9ceaa7c014a162d11d58a98871087ec488a67d7925" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Created Event keys: [ - "0x01f8936599822d668e09401ffcef1989aca342fb1f003f9b3b1fd1cbf605ed6b" + "0x020c994ab49a8316bcc78b06d4ff9929d83b2995af33f480b93e972cedb0c926" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Joined Event + keys: [ + "0x02947960ff713d9b594a3b718b90a45360e46d1bbacef94b727bb0d461d04207" ], includeReverted: false, includeTransaction: false, @@ -135,6 +172,26 @@ export const config = { includeTransaction: false, includeReceipt: false }, + { + // NFT Liked Event + fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"), + keys: [ + "0x028d7ee09447088eecdd12a86c9467a5e9ad18f819a20f9adcf6e34e0bd51453" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // NFT Unliked Event + fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"), + keys: [ + "0x03b57514b19693484c35249c6e8b15bfe6e476205720680c2ff9f02faaf94941" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, { // User Name Claimed Event fromAddress: Deno.env.get("USERNAME_STORE_ADDRESS"), @@ -183,7 +240,6 @@ export const config = { } }; -// This transform does nothing. export default function transform(block) { return block; } diff --git a/indexer/script.js b/indexer/script.js index 179342a6..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, @@ -117,10 +127,36 @@ export const config = { includeReceipt: false }, { - // Member Replaced Event - fromAddress: Deno.env.get("ART_PEACE_CONTRACT_ADDRESS"), + // Faction Joined Event + keys: [ + "0x01e3fbdf8156ad0dde21e886d61a16d85c9ef54451eb6e253f3f427de32a47ac" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Faction Left Event + keys: [ + "0x014ef8cc25c96157e2a00e9ceaa7c014a162d11d58a98871087ec488a67d7925" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Created Event keys: [ - "0x01f8936599822d668e09401ffcef1989aca342fb1f003f9b3b1fd1cbf605ed6b" + "0x020c994ab49a8316bcc78b06d4ff9929d83b2995af33f480b93e972cedb0c926" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // Chain Faction Joined Event + keys: [ + "0x02947960ff713d9b594a3b718b90a45360e46d1bbacef94b727bb0d461d04207" ], includeReverted: false, includeTransaction: false, @@ -136,6 +172,26 @@ export const config = { includeTransaction: false, includeReceipt: false }, + { + // NFT Liked Event + fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"), + keys: [ + "0x028d7ee09447088eecdd12a86c9467a5e9ad18f819a20f9adcf6e34e0bd51453" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, + { + // NFT Unliked Event + fromAddress: Deno.env.get("NFT_CONTRACT_ADDRESS"), + keys: [ + "0x03b57514b19693484c35249c6e8b15bfe6e476205720680c2ff9f02faaf94941" + ], + includeReverted: false, + includeTransaction: false, + includeReceipt: false + }, { // User Name Claimed Event fromAddress: Deno.env.get("USERNAME_STORE_ADDRESS"), diff --git a/onchain/src/art_peace.cairo b/onchain/src/art_peace.cairo index 6e7652f5..e605c997 100644 --- a/onchain/src/art_peace.cairo +++ b/onchain/src/art_peace.cairo @@ -4,7 +4,7 @@ pub mod ArtPeace { use starknet::ContractAddress; use core::poseidon::PoseidonTrait; use core::hash::{HashStateTrait, HashStateExTrait}; - use art_peace::{IArtPeace, Pixel, Faction, MemberMetadata}; + use art_peace::{IArtPeace, Pixel, Faction, ChainFaction, MemberMetadata}; use art_peace::quests::interfaces::{IQuestDispatcher, IQuestDispatcherTrait}; use art_peace::nfts::interfaces::{ IArtPeaceNFTMinter, NFTMetadata, NFTMintParams, ICanvasNFTAdditionalDispatcher, @@ -36,14 +36,18 @@ pub mod ArtPeace { factions_count: u32, // Map: faction id -> faction data factions: LegacyMap::, - // Map: faction id -> amount of members - faction_member_counts: LegacyMap::, - // Map: (faction id, member index) -> member's metadata - faction_members: LegacyMap::<(u32, u32), MemberMetadata>, - // Map: member address -> amount of faction memberships - user_memberships_count: LegacyMap::, - // Map: (member address, membership index) -> (faction id, member index) - user_memberships: LegacyMap::<(ContractAddress, u32), (u32, u32)>, + // Map: members address -> faction id ( 0 => no faction ) + users_faction: LegacyMap::, + // Map: members address -> membership metadata + users_faction_meta: LegacyMap::, + chain_factions_count: u32, + // Map: chain faction id -> chain faction data + chain_factions: LegacyMap::, + // Map: chain members address -> faction id ( 0 => no faction ) + users_chain_faction: LegacyMap::, + // Map: chain members address -> membership metadata + users_chain_faction_meta: LegacyMap::, + // TODO: Extra factions ( assigned at start with larger allocations ) color_count: u8, // Map: color index -> color value in RGBA color_palette: LegacyMap::, @@ -81,13 +85,17 @@ pub mod ArtPeace { ColorAdded: ColorAdded, PixelPlaced: PixelPlaced, BasicPixelPlaced: BasicPixelPlaced, - MemberPixelsPlaced: MemberPixelsPlaced, + FactionPixelsPlaced: FactionPixelsPlaced, + ChainFactionPixelsPlaced: ChainFactionPixelsPlaced, ExtraPixelsPlaced: ExtraPixelsPlaced, DailyQuestClaimed: DailyQuestClaimed, MainQuestClaimed: MainQuestClaimed, VoteColor: VoteColor, FactionCreated: FactionCreated, - MemberReplaced: MemberReplaced, + ChainFactionCreated: ChainFactionCreated, + FactionJoined: FactionJoined, + FactionLeft: FactionLeft, + ChainFactionJoined: ChainFactionJoined, VotableColorAdded: VotableColorAdded, // TODO: Integrate template event #[flat] @@ -127,11 +135,17 @@ pub mod ArtPeace { } #[derive(Drop, starknet::Event)] - struct MemberPixelsPlaced { + struct FactionPixelsPlaced { #[key] - faction_id: u32, + user: ContractAddress, + placed_time: u64, + member_pixels: u32, + } + + #[derive(Drop, starknet::Event)] + struct ChainFactionPixelsPlaced { #[key] - member_id: u32, + user: ContractAddress, placed_time: u64, member_pixels: u32, } @@ -190,17 +204,39 @@ pub mod ArtPeace { faction_id: u32, name: felt252, leader: ContractAddress, - pool: u32, - members: Span, + joinable: bool, + allocation: u32, + } + + #[derive(Drop, starknet::Event)] + struct ChainFactionCreated { + #[key] + faction_id: u32, + name: felt252, + } + + #[derive(Drop, starknet::Event)] + struct FactionJoined { + #[key] + faction_id: u32, + #[key] + user: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct FactionLeft { + #[key] + faction_id: u32, + #[key] + user: ContractAddress, } #[derive(Drop, starknet::Event)] - struct MemberReplaced { + struct ChainFactionJoined { #[key] faction_id: u32, #[key] - member_id: u32, - new_member: ContractAddress, + user: ContractAddress, } #[derive(Drop, Serde)] @@ -228,6 +264,7 @@ pub mod ArtPeace { self.total_pixels.write(init_params.canvas_width * init_params.canvas_height); self.time_between_pixels.write(init_params.time_between_pixels); + self.time_between_member_pixels.write(init_params.time_between_pixels); let color_count: u8 = init_params.color_palette.len().try_into().unwrap(); self.color_count.write(color_count); @@ -319,91 +356,6 @@ pub mod ArtPeace { ); } - fn place_pixel_inner(ref self: ContractState, pos: u128, color: u8) { - self.check_valid_pixel(pos, color); - - let caller = starknet::get_caller_address(); - let pixel = Pixel { color, owner: caller }; - self.canvas.write(pos, pixel); - let day = self.day_index.read(); - self - .user_pixels_placed - .write( - (day, caller, color), self.user_pixels_placed.read((day, caller, color)) + 1 - ); - // TODO: Optimize? - self.emit(PixelPlaced { placed_by: caller, pos, day, color }); - } - - // TODO: Make the function internal only - fn place_basic_pixel_inner(ref self: ContractState, pos: u128, color: u8, now: u64) { - self.place_pixel_inner(pos, color); - let caller = starknet::get_caller_address(); - self.last_placed_time.write(caller, now); - self.emit(BasicPixelPlaced { placed_by: caller, timestamp: now }); - } - - fn place_member_pixels_inner( - ref self: ContractState, - faction_id: u32, - member_id: u32, - positions: Span, - colors: Span, - mut offset: u32, - now: u64 - ) -> u32 { - let pixel_count = positions.len(); - let member_pixels = self.get_faction_members_pixels(faction_id, member_id, now); - let mut member_pixels_left = member_pixels; - while member_pixels_left > 0 { - let pos = *positions.at(offset); - let color = *colors.at(offset); - self.place_pixel_inner(pos, color); - offset += 1; - member_pixels_left -= 1; - if offset == pixel_count { - break; - } - }; - let caller = starknet::get_caller_address(); - if member_pixels != 0 { - // TODO: Optimize - if member_pixels_left == 0 { - let new_member_metadata = MemberMetadata { - address: caller, member_placed_time: now, member_pixels: 0 - }; - self.faction_members.write((faction_id, member_id), new_member_metadata); - self - .emit( - MemberPixelsPlaced { - faction_id, member_id, placed_time: now, member_pixels: 0 - } - ); - } else { - let last_placed_time = self - .faction_members - .read((faction_id, member_id)) - .member_placed_time; - let new_member_metadata = MemberMetadata { - address: caller, - member_placed_time: last_placed_time, - member_pixels: member_pixels_left - }; - self.faction_members.write((faction_id, member_id), new_member_metadata); - self - .emit( - MemberPixelsPlaced { - faction_id, - member_id, - placed_time: last_placed_time, - member_pixels: member_pixels_left - } - ); - } - } - return offset; - } - fn place_pixel(ref self: ContractState, pos: u128, color: u8, now: u64) { self.check_game_running(); self.check_timing(now); @@ -413,7 +365,7 @@ pub mod ArtPeace { 'Pixel not available' ); - self.place_basic_pixel_inner(pos, color, now); + place_basic_pixel_inner(ref self, pos, color, now); } fn place_pixel_xy(ref self: ContractState, x: u128, y: u128, color: u8, now: u64) { @@ -442,27 +394,21 @@ pub mod ArtPeace { if now - self.last_placed_time.read(caller) >= self.time_between_pixels.read() { let pos = *positions.at(pixels_placed); let color = *colors.at(pixels_placed); - self.place_basic_pixel_inner(pos, color, now); + place_basic_pixel_inner(ref self, pos, color, now); pixels_placed += 1; if pixels_placed == pixel_count { return; } } - // Use member pixels if available - let membership_count = self.user_memberships_count.read(caller); - let mut i = 0; - while i < membership_count { - let (faction_id, member_id) = self.user_memberships.read((caller, i)); - pixels_placed = self - .place_member_pixels_inner( - faction_id, member_id, positions, colors, pixels_placed, now - ); - if pixels_placed == pixel_count { - break; - } - i += 1; - }; + 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; } @@ -475,7 +421,7 @@ pub mod ArtPeace { while pixels_placed < pixel_count { let pos = *positions.at(pixels_placed); let color = *colors.at(pixels_placed); - self.place_pixel_inner(pos, color); + place_pixel_inner(ref self, pos, color); pixels_placed += 1; }; let extra_pixels_placed = pixel_count - prior_pixels; @@ -509,10 +455,6 @@ pub mod ArtPeace { self.factions_count.read() } - fn get_user_factions_count(self: @ContractState, user: ContractAddress) -> u32 { - self.user_memberships_count.read(user) - } - fn get_faction(self: @ContractState, faction_id: u32) -> Faction { self.factions.read(faction_id) } @@ -521,138 +463,91 @@ pub mod ArtPeace { self.factions.read(faction_id).leader } + // TODO: Tests fn init_faction( ref self: ContractState, name: felt252, leader: ContractAddress, - pool: u32, - members: Span + joinable: bool, + allocation: u32 ) { - // TODO - //assert( - // starknet::get_caller_address() == self.host.read(), 'Factions are set by the host' - //); + // TODO: Init with members? + assert( + starknet::get_caller_address() == self.host.read(), 'Factions are set by the host' + ); self.check_game_running(); - assert(members.len() <= pool, 'Invalid faction members count'); - let faction_id = self.factions_count.read(); - let faction = Faction { name, leader, pixel_pool: pool }; + let faction_id = self.factions_count.read() + 1; + let faction = Faction { name, leader, joinable, allocation }; self.factions.write(faction_id, faction); - self.factions_count.write(faction_id + 1); - let mut i = 0; - while i < members - .len() { - let member_address = *members.at(i); - let member = MemberMetadata { - address: member_address, member_placed_time: 0, member_pixels: 0 - }; - let member_membership_count = self.user_memberships_count.read(member_address); - self.faction_members.write((faction_id, i), member); - self - .user_memberships - .write((member_address, member_membership_count), (faction_id, i)); - self.user_memberships_count.write(member_address, member_membership_count + 1); - i += 1; - }; - self.faction_member_counts.write(faction_id, members.len()); - self.emit(FactionCreated { faction_id, name, leader, pool, members }); + self.factions_count.write(faction_id); + self.emit(FactionCreated { faction_id, name, leader, joinable, allocation }); } - // TODO: Tests and integration - // TODO: Infinite replacement exploit - fn replace_member( - ref self: ContractState, faction_id: u32, member_id: u32, new_member: ContractAddress - ) { - self.check_game_running(); + fn init_chain_faction(ref self: ContractState, name: felt252) { assert( - starknet::get_caller_address() == self.get_faction_leader(faction_id), - 'Only leader can replace members' + starknet::get_caller_address() == self.host.read(), 'Factions are set by the host' ); - let member_count = self.faction_member_counts.read(faction_id); - assert(member_id < member_count, 'Member ID out of bounds'); + self.check_game_running(); + let faction_id = self.chain_factions_count.read() + 1; + let chain_faction = ChainFaction { name }; + self.chain_factions.write(faction_id, chain_faction); + self.chain_factions_count.write(faction_id); + self.emit(ChainFactionCreated { faction_id, name }); + } - let old_member = self.faction_members.read((faction_id, member_id)); - let old_member_address = old_member.address; + 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( + 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'); + self.users_faction.write(caller, faction_id); + self.emit(FactionJoined { faction_id, user: caller }); + } - let old_member_membership_count = self.user_memberships_count.read(old_member.address); - let mut member_id = 0; - while member_id < old_member_membership_count { - let (fid, mid) = self.user_memberships.read((old_member_address, member_id)); - if fid == faction_id && mid == member_id { - break; - } - member_id += 1; - }; - let last_member_membership = self - .user_memberships - .read((old_member.address, old_member_membership_count - 1)); - self.user_memberships.write((old_member.address, member_id), last_member_membership); - self.user_memberships_count.write(old_member.address, old_member_membership_count - 1); - - let member = MemberMetadata { - address: new_member, member_placed_time: 0, member_pixels: 0 - }; - self.faction_members.write((faction_id, member_id), member); + fn leave_faction(ref self: ContractState) { + self.check_game_running(); + let caller = starknet::get_caller_address(); + let faction_id = self.users_faction.read(caller); + self.users_faction.write(caller, 0); + self.emit(FactionLeft { faction_id, user: caller }); + } - let new_member_membership_count = self.user_memberships_count.read(new_member); - self - .user_memberships - .write((new_member, new_member_membership_count), (faction_id, member_id)); - self.user_memberships_count.write(new_member, new_member_membership_count + 1); - self.emit(MemberReplaced { faction_id, member_id, new_member }); - } - - //fn add_faction_member(ref self: ContractState, faction_id: u32, member: ContractAddress) { - // self.check_game_running(); - // assert( - // starknet::get_caller_address() == self.get_faction_owner(faction_id), - // 'Only the faction owner can add members' - // ); - // let faction = self.factions.read(faction_id); - // let member_count = self.faction_member_counts.read(faction_id); - // assert(member_count < faction.pixel_pool, 'Faction is full'); - // let member_data = MemberMetadata { address: member, member_placed_time: 0, member_pixels: 0 }; - // self.faction_members.write((faction_id, member_count), member_data); - // self.faction_member_counts.write(faction_id, member_count + 1); - //} - - //fn remove_faction_member(ref self: ContractState, faction_id: u32, member_id: u32) { - // self.check_game_running(); - // assert( - // starknet::get_caller_address() == self.get_faction_owner(faction_id), - // 'Only the faction owner can remove members' - // ); - // let member_count = self.faction_member_counts.read(faction_id); - // // Replace the removed member with the last member - // let last_member = self.faction_members.read((faction_id, member_count - 1)); - // self.faction_members.write((faction_id, member_id), last_member); - // self.faction_member_counts.write(faction_id, member_count - 1); - //} - - fn get_faction_members(self: @ContractState, faction_id: u32) -> Span { - let member_count = self.faction_member_counts.read(faction_id); - let mut i = 0; - let mut members = array![]; - while i < member_count { - members.append(self.faction_members.read((faction_id, i)).address); - i += 1; - }; + 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( + 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 }); + } - members.span() + fn get_user_faction(self: @ContractState, user: ContractAddress) -> u32 { + self.users_faction.read(user) } - fn get_faction_member_count(self: @ContractState, faction_id: u32) -> u32 { - self.faction_member_counts.read(faction_id) + fn get_user_chain_faction(self: @ContractState, user: ContractAddress) -> u32 { + self.users_chain_faction.read(user) } - fn get_faction_members_pixels( - self: @ContractState, faction_id: u32, member_id: u32, now: u64 + fn get_user_faction_members_pixels( + self: @ContractState, user: ContractAddress, now: u64 ) -> u32 { - let member_count = self.faction_member_counts.read(faction_id); - let pixel_pool = self.factions.read(faction_id).pixel_pool; - let member_metadata = self.faction_members.read((faction_id, member_id)); - if member_id >= member_count { + let faction_id = self.users_faction.read(user); + if faction_id == 0 { + // 0 => no faction return 0; } + let member_metadata = self.users_faction_meta.read(user); if member_metadata.member_pixels > 0 { // TODO: If member_pixels > 0 && < allocation && enough time has passed, return allocation instead of member_pixels return member_metadata.member_pixels; @@ -662,8 +557,28 @@ pub mod ArtPeace { if time_since_last_pixel < self.time_between_member_pixels.read() { return 0; } else { - // TODO: Think about when pixel_pool % member_count != 0 - return pixel_pool / member_count.into(); + return self.factions.read(faction_id).allocation; + } + } + } + + fn get_chain_faction_members_pixels( + self: @ContractState, user: ContractAddress, now: u64 + ) -> u32 { + let faction_id = self.users_chain_faction.read(user); + if faction_id == 0 { + // 0 => no faction + return 0; + } + let member_metadata = self.users_chain_faction_meta.read(user); + if member_metadata.member_pixels > 0 { + return member_metadata.member_pixels; + } else { + let time_since_last_pixel = now - member_metadata.member_placed_time; + if time_since_last_pixel < self.time_between_member_pixels.read() { + return 0; + } else { + return 2; // Chain faction allocation } } } @@ -950,8 +865,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(), } @@ -1165,6 +1082,10 @@ pub mod ArtPeace { // update palette & votable colors let next_day = day + 1; + let start_day_time = self.start_day_time.read(); + let end_day_time = start_day_time + DAY_IN_SECONDS; + let end_game_time = self.end_time.read(); + let start_new_vote: bool = end_day_time < end_game_time; let mut color_index = self.color_count.read(); let mut next_day_votable_index = 1; votable_index = 1; @@ -1175,7 +1096,7 @@ pub mod ArtPeace { self.color_palette.write(color_index, color); self.emit(ColorAdded { color_key: color_index, color }); color_index += 1; - } else { + } else if start_new_vote { self.votable_colors.write((next_day_votable_index, next_day), color); self .emit( @@ -1188,7 +1109,126 @@ pub mod ArtPeace { votable_index += 1; }; self.color_count.write(color_index); - self.votable_colors_count.write(next_day, next_day_votable_index - 1); + if start_new_vote { + self.votable_colors_count.write(next_day, next_day_votable_index - 1); + } + } + + fn place_pixel_inner(ref self: ContractState, pos: u128, color: u8) { + self.check_valid_pixel(pos, color); + + let caller = starknet::get_caller_address(); + let pixel = Pixel { color, owner: caller }; + self.canvas.write(pos, pixel); + let day = self.day_index.read(); + self + .user_pixels_placed + .write((day, caller, color), self.user_pixels_placed.read((day, caller, color)) + 1); + // TODO: Optimize? + self.emit(PixelPlaced { placed_by: caller, pos, day, color }); } -} + // TODO: Make the function internal + fn place_basic_pixel_inner(ref self: ContractState, pos: u128, color: u8, now: u64) { + place_pixel_inner(ref self, pos, color); + let caller = starknet::get_caller_address(); + self.last_placed_time.write(caller, now); + self.emit(BasicPixelPlaced { placed_by: caller, timestamp: now }); + } + + fn place_user_faction_pixels_inner( + 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); + if faction_pixels == 0 { + return offset; + } + + let pixel_count = positions.len(); + let mut faction_pixels_left = faction_pixels; + while faction_pixels_left > 0 { + let pos = *positions.at(offset); + let color = *colors.at(offset); + place_pixel_inner(ref self, pos, color); + offset += 1; + faction_pixels_left -= 1; + if offset == pixel_count { + break; + } + }; + let caller = starknet::get_caller_address(); + if faction_pixels_left == 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 }); + } else { + 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 + }; + self.users_faction_meta.write(caller, new_member_metadata); + self + .emit( + FactionPixelsPlaced { + user: caller, + placed_time: last_placed_time, + member_pixels: faction_pixels_left + } + ); + } + return offset; + } + + fn place_chain_faction_pixels_inner( + ref self: ContractState, positions: Span, colors: Span, mut offset: u32, now: u64 + ) -> u32 { + let pixel_count = positions.len(); + let caller = starknet::get_caller_address(); + let member_pixels = self.get_chain_faction_members_pixels(caller, now); + let mut member_pixels_left = member_pixels; + while member_pixels_left > 0 { + let pos = *positions.at(offset); + let color = *colors.at(offset); + place_pixel_inner(ref self, pos, color); + offset += 1; + member_pixels_left -= 1; + if offset == pixel_count { + break; + } + }; + let caller = starknet::get_caller_address(); + if member_pixels != 0 { + if member_pixels_left == 0 { + let new_member_metadata = MemberMetadata { + member_placed_time: now, member_pixels: 0 + }; + self.users_chain_faction_meta.write(caller, new_member_metadata); + self + .emit( + ChainFactionPixelsPlaced { + user: caller, placed_time: now, member_pixels: 0 + } + ); + } else { + let last_placed_time = self + .users_chain_faction_meta + .read(caller) + .member_placed_time; + let new_member_metadata = MemberMetadata { + member_placed_time: last_placed_time, member_pixels: member_pixels_left + }; + self.users_chain_faction_meta.write(caller, new_member_metadata); + self + .emit( + ChainFactionPixelsPlaced { + user: caller, + placed_time: last_placed_time, + member_pixels: member_pixels_left + } + ); + } + } + return offset; + } +} diff --git a/onchain/src/interfaces.cairo b/onchain/src/interfaces.cairo index f8b33698..a4e8512e 100644 --- a/onchain/src/interfaces.cairo +++ b/onchain/src/interfaces.cairo @@ -10,12 +10,17 @@ pub struct Pixel { pub struct Faction { pub name: felt252, pub leader: starknet::ContractAddress, - pub pixel_pool: u32 + pub joinable: bool, + pub allocation: u32 +} + +#[derive(Drop, Serde, starknet::Store)] +pub struct ChainFaction { + pub name: felt252, } #[derive(Drop, Serde, starknet::Store)] pub struct MemberMetadata { - pub address: starknet::ContractAddress, pub member_placed_time: u64, pub member_pixels: u32 } @@ -39,17 +44,6 @@ pub trait IArtPeace { fn check_timing(self: @TContractState, now: u64); // Place pixels on the canvas - fn place_pixel_inner(ref self: TContractState, pos: u128, color: u8); - fn place_basic_pixel_inner(ref self: TContractState, pos: u128, color: u8, now: u64); - fn place_member_pixels_inner( - ref self: TContractState, - faction_id: u32, - member_id: u32, - positions: Span, - colors: Span, - offset: u32, - now: u64 - ) -> u32; fn place_pixel(ref self: TContractState, pos: u128, color: u8, now: u64); fn place_pixel_xy(ref self: TContractState, x: u128, y: u128, color: u8, now: u64); fn place_pixel_blocktime(ref self: TContractState, pos: u128, color: u8); @@ -66,28 +60,26 @@ pub trait IArtPeace { // Faction stuff fn get_factions_count(self: @TContractState) -> u32; - fn get_user_factions_count(self: @TContractState, user: starknet::ContractAddress) -> u32; fn get_faction(self: @TContractState, faction_id: u32) -> Faction; fn get_faction_leader(self: @TContractState, faction_id: u32) -> starknet::ContractAddress; fn init_faction( ref self: TContractState, name: felt252, leader: starknet::ContractAddress, - pool: u32, - members: Span - ); - fn replace_member( - ref self: TContractState, - faction_id: u32, - member_id: u32, - new_member: starknet::ContractAddress + joinable: bool, + allocation: u32 ); - fn get_faction_members( - self: @TContractState, faction_id: u32 - ) -> Span; - fn get_faction_member_count(self: @TContractState, faction_id: u32) -> u32; - fn get_faction_members_pixels( - self: @TContractState, faction_id: u32, member_id: u32, now: u64 + fn init_chain_faction(ref self: TContractState, name: felt252); + fn join_faction(ref self: TContractState, faction_id: u32); + fn leave_faction(ref self: TContractState); + 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; // Get color info diff --git a/onchain/src/lib.cairo b/onchain/src/lib.cairo index 93a9ce68..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, 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/canvas_nft.cairo b/onchain/src/nfts/canvas_nft.cairo index daeef160..399bbc4c 100644 --- a/onchain/src/nfts/canvas_nft.cairo +++ b/onchain/src/nfts/canvas_nft.cairo @@ -55,15 +55,17 @@ mod CanvasNFT { #[derive(Drop, starknet::Event)] struct NFTLiked { #[key] - user_address: ContractAddress, - token_id: u256 + token_id: u256, + #[key] + user_address: ContractAddress } #[derive(Drop, starknet::Event)] struct NFTUnliked { #[key] - user_address: ContractAddress, - token_id: u256 + token_id: u256, + #[key] + user_address: ContractAddress } diff --git a/onchain/src/nfts/component.cairo b/onchain/src/nfts/component.cairo index 87b71993..db4b1055 100644 --- a/onchain/src/nfts/component.cairo +++ b/onchain/src/nfts/component.cairo @@ -41,6 +41,11 @@ 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/faction_quest.cairo b/onchain/src/quests/faction_quest.cairo index 9cef785c..ec8d6381 100644 --- a/onchain/src/quests/faction_quest.cairo +++ b/onchain/src/quests/faction_quest.cairo @@ -42,9 +42,9 @@ pub mod FactionQuest { contract_address: self.art_peace.read() }; - let user_factions_count = art_peace_dispatcher.get_user_factions_count(user); + let user_faction = art_peace_dispatcher.get_user_faction(user); - if (user_factions_count == 0) { + if user_faction == 0 { return false; } 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/art_peace.cairo b/onchain/src/tests/art_peace.cairo index e1519e45..eab7df94 100644 --- a/onchain/src/tests/art_peace.cairo +++ b/onchain/src/tests/art_peace.cairo @@ -354,7 +354,7 @@ fn nft_mint_test() { nft_minter.add_nft_contract(utils::NFT_CONTRACT()); snf::stop_prank(CheatTarget::One(nft_minter.contract_address)); - let mint_params = NFTMintParams { position: 10, width: 16, height: 16, }; + let mint_params = NFTMintParams { position: 10, width: 16, height: 16, name: 'test' }; snf::start_prank(CheatTarget::One(nft_minter.contract_address), utils::PLAYER1()); nft_minter.mint_nft(mint_params); snf::stop_prank(CheatTarget::One(nft_minter.contract_address)); @@ -363,7 +363,9 @@ fn nft_mint_test() { position: 10, width: 16, height: 16, + name: 'test', image_hash: 0, + day_index: 0, block_number: 2000, // TODO minter: utils::PLAYER1(), }; diff --git a/onchain/src/tests/chain_faction_quest.cairo b/onchain/src/tests/chain_faction_quest.cairo new file mode 100644 index 00000000..1771bfa4 --- /dev/null +++ b/onchain/src/tests/chain_faction_quest.cairo @@ -0,0 +1,93 @@ +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..a1788858 100644 --- a/onchain/src/tests/faction_quest.cairo +++ b/onchain/src/tests/faction_quest.cairo @@ -50,9 +50,12 @@ fn faction_quest_test() { ) }; - snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); + 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)); - art_peace.init_faction('TestFaction', utils::HOST(), 20, array![utils::PLAYER1()].span()); + 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..9402358c 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,24 @@ fn deploy_nft_quest() -> ContractAddress { canvas_nft: utils::NFT_CONTRACT(), art_peace: utils::ART_PEACE_CONTRACT(), reward: reward_amt, + is_daily: false, + day_index: 0, + } + .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, + day_index: 0, } .serialize(ref nft_quest_calldata); @@ -29,8 +47,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 +65,25 @@ 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![nft_quest].span(), array![].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( @@ -66,7 +100,7 @@ fn nft_quest_test() { let calldata: Array = array![0]; snf::start_prank(CheatTarget::One(art_peace.contract_address), utils::PLAYER1()); - let mint_params = NFTMintParams { height: 2, width: 2, position: 10 }; + let mint_params = NFTMintParams { height: 2, width: 2, position: 10, name: 'test' }; art_peace_nft_minter.mint_nft(mint_params); art_peace.claim_main_quest(0, calldata.span()); @@ -80,7 +114,7 @@ fn nft_quest_test() { #[test] #[should_panic(expected: ('Quest not claimable',))] fn nft_quest_claim_if_not_claimable_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( array![].span(), array![nft_mint_quest].span() diff --git a/postgres/init.sql b/postgres/init.sql index 749b3e74..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 ); @@ -181,13 +209,19 @@ CREATE INDEX nftLikes_nft_key_index ON NFTLikes (nftKey); CREATE INDEX nftLikes_liker_index ON NFTLikes (liker); CREATE TABLE Factions ( - -- Postgres auto-incrementing primary key - key integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + faction_id integer NOT NULL PRIMARY KEY, name text NOT NULL, leader char(64) NOT NULL, - pixel_pool integer NOT NULL + joinable boolean NOT NULL, + allocation integer NOT NULL ); CREATE INDEX factions_leader_index ON Factions (leader); +CREATE INDEX factions_joinable_index ON Factions (joinable); + +CREATE TABLE ChainFactions ( + faction_id integer NOT NULL PRIMARY KEY, + name text NOT NULL +); CREATE TABLE FactionLinks ( faction_id integer NOT NULL, @@ -200,19 +234,47 @@ 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, - member_id integer NOT NULL, user_address char(64) NOT NULL, - allocation integer NOT NULL, last_placed_time timestamp NOT NULL, member_pixels integer NOT NULL, - UNIQUE (faction_id, member_id) + UNIQUE (faction_id, user_address) ); CREATE INDEX factionMembersInfo_faction_id_index ON FactionMembersInfo (faction_id); CREATE INDEX factionMembersInfo_user_address_index ON FactionMembersInfo (user_address); +CREATE TABLE ChainFactionMembersInfo ( + faction_id integer NOT NULL, + user_address char(64) NOT NULL, + last_placed_time timestamp NOT NULL, + 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 5766a658..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 ' ') @@ -106,8 +106,10 @@ echo "ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" > /configs/configs echo "REACT_APP_ART_PEACE_CONTRACT_ADDRESS=$ART_PEACE_CONTRACT_ADDRESS" >> /configs/configs.env echo "NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/configs.env echo "REACT_APP_NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/configs.env +echo "CANVAS_NFT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/configs.env +echo "REACT_APP_CANVAS_NFT_CONTRACT_ADDRESS=$NFT_CONTRACT_ADDRESS" >> /configs/configs.env echo "USERNAME_STORE_ADDRESS=$USERNAME_STORE_ADDRESS" >> /configs/configs.env -echo "REACT_APP_USERNAME_STORE_ADDRESS=$USERNAME_STORE_ADDRESS" >> /configs/configs.env +echo "REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS=$USERNAME_STORE_ADDRESS" >> /configs/configs.env # TODO # MULTICALL_TEMPLATE_DIR=$CONTRACT_DIR/tests/multicalls 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/initialize.sh b/tests/integration/docker/initialize.sh index cf0c3365..9bc07392 100755 --- a/tests/integration/docker/initialize.sh +++ b/tests/integration/docker/initialize.sh @@ -16,6 +16,10 @@ echo "Set the username store address" USERNAME_STORE_ADDRESS=$(cat /configs/configs.env | grep "^USERNAME_STORE_ADDRESS" | cut -d '=' -f2) curl http://backend:8080/set-username-store-address -X POST -d "$USERNAME_STORE_ADDRESS" +echo "Set the canvas nft address" +CANVAS_NFT_ADDRESS=$(cat /configs/configs.env | grep "^CANVAS_NFT_ADDRESS" | cut -d '=' -f2) +curl http://backend:8080/set-canvas-nft-address -X POST -d "$CANVAS_NFT_ADDRESS" + echo "Setup the quests from the quest config" QUESTS_CONFIG_FILE="/configs/quests.config.json" curl http://backend:8080/init-quests -X POST -d "@$QUESTS_CONFIG_FILE" 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/like_nft.sh b/tests/integration/docker/like_nft.sh new file mode 100755 index 00000000..63bd103f --- /dev/null +++ b/tests/integration/docker/like_nft.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 $4" > $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 > $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 diff --git a/tests/integration/docker/unlike_nft.sh b/tests/integration/docker/unlike_nft.sh new file mode 100755 index 00000000..63bd103f --- /dev/null +++ b/tests/integration/docker/unlike_nft.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 $4" > $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 > $LOG_DIR/output.json