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]) && (
-
- )}
-
-
+
+
-
-
+ {(!isMobile || activeTab === tabs[0]) && (
+
+ )}
-
- {isFooterSplit && !footerExpanded && (
-
{
- setActiveTab(tabs[0]);
- setFooterExpanded(!footerExpanded);
- }}
- >
-
-
- )}
- {isFooterSplit && footerExpanded && (
+
+
+
+ {!gameEnded && (
+
+ )}
+ {isFooterSplit && !footerExpanded && (
+
{
+ setActiveTab(tabs[0]);
+ setFooterExpanded(!footerExpanded);
+ }}
+ >
+
+
+ )}
+ {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}
-
-
-
+ {!props.gameEnded && (
+
+
+
+ )}
) : (
@@ -386,15 +389,20 @@ const Account = (props) => {
)}
-
-
-
-
-
{accountRank}
+
+
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)}
>
@@ -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}
{
+ // 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