Skip to content

Commit

Permalink
Docker compose integration tests, cleanup, refactor, readmes, diagram…
Browse files Browse the repository at this point in the history
…s, and many features like quests, frontend tabs, ...
  • Loading branch information
b-j-roberts committed Apr 7, 2024
1 parent f69b4ac commit 9ebeafd
Show file tree
Hide file tree
Showing 91 changed files with 2,774 additions and 977 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ node_modules/
# Development
**/TODO

# TODO
# Frontend
build/
node_modules/

# Backend
art-peace-backend
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#TODO: setup all, list dependencies, etc

build: backend-build frontend-build contracts-build
test: contracts-test

Expand Down
72 changes: 57 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,77 @@

## Overview

`art/peace` is a collaborative art game where users can place pixels on a large canvas and receive rewards for collaborating. The app will run over X days, and end with a final snapshot of the board. The goal is to give users the feeling of collectively building on a highly responsive art canvas, which they can explore, interact with, and compete on.
`art/peace` is a collaborative art game where users can place pixels on a large shared canvas and receive rewards for collaborating to build art. The game will run over X days, and end with a final snapshot of the board. The goal is to give users the feeling of collectively building on a highly responsive art canvas, which they can explore, interact with, and compete on.

Some of the features include :

- **Placing Pixels** : This will be the main interaction type, where every X minutes a user will be allowed to place a pixel onto the canvas. ( on-chain interaction )
- **Color Voting** : In addition to the base colors, there will be a vote to add new colors to the palette every day. ( on-chain interaction )
- **Quests** : Tasks to get extra pixels to place on-top of the one every X minutes. ( on-chain interaction )
- **Templates** : Overlayable artworks to help communities collaborate on an art piece. Bounties can be added to a template to incentivize creation. ( off-chain interaction until settling )
- **Placing Pixels** : This will be the main user interaction, where every X minutes a user will be allowed to place a pixel onto the canvas.
- **Quests** : Tasks to get extra pixels to place on-top of the one every X minutes.
- **Voting** : In addition to the base colors, there will be a vote to add new colors to the palette every day.
- **Templates** : Artwork templates used to help communities collaborate on an art piece. Bounties can be added to a template to incentivize creation.
- **NFTs** : Mint NFTs from the canvas.

## References
## Running

- [r/place technical document](https://www.redditinc.com/blog/how-we-built-rplace)
TODO: Note build and run for each modules

## Build
#### Docker ( Recommended )
```
docker compose up
```

To build the project, run:
To stop the run use `Ctrl-C`.

```bash
scarb build
For a complete reset of the state and rebuild of the containers use :
```
# WARNING! This will clear the state (volumes) of all the DBs and the Devnet
docker compose down --volumes
docker compose build
```

## Test
#### Local
```
# Must install all the dependencies first
# Change the user on `configs/database.config.json` for postgres
make integration-test-local
```

To test the project, run ( uses `snforge` ):
To stop the run use `Ctrl-C`.

```bash
scarb test
## Build

TODO: Note build and run for each modules
#### Docker ( Recommended )
```
docker compose build
```

#### Local
Use the `make X-build` command for each corresponding module `X`. See the **Modules** section below for more details.

## Modules

- **Onchain:** [Starknet contract(s)](./onchain/) for trustless onchain interactions.
- **Backend:** [Monolithic Go backend](./backend/) for managing requests, interactions, and DBs.
- **Frontend:** [Reactjs application](./frontend/) for users to interact with.
- **Indexer:** [Apibara indexer](./indexer/) for monitoring Starknet events and forwarding to the DBs.
- **Postgres:** DB for storing general data used for analytics, frontend, and backend.
- **Redis:** In memory DB used to store the compressed `Canvas` data for fast retrieval
- **tests:** Integration tests for local, docker, ...

![art/peace diagram](./docs/diagrams/art-peace-diagram.png)

## Dependencies

Its recommended to use `docker compose` when building and running, so the only dependencies would be [docker](https://docs.docker.com/desktop/) and [docker compose](https://docs.docker.com/compose/install/linux/)

Howeveer, it might be worth running only certain modules for development/testing sometimes. Each module has various dependencies, check [dependencies.txt](./dependencies.txt) for more details.

## References

- [Diagrams](./docs/diagrams/)
- [r/place technical document](https://www.redditinc.com/blog/how-we-built-rplace)

## Contributors ✨

Thanks goes to these wonderful people. Follow the [contributors guide](https://github.com/keep-starknet-strange/art-peace/blob/main/CONTRIBUTING.md) if you'd like to take part.
Expand Down
35 changes: 35 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM golang:1.21.7-alpine
# TODO: Add psql to the image?
# TODO: depends on in docker compose?

RUN apk add --no-cache bash curl git jq

SHELL ["/bin/bash", "-c"]
RUN curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | bash -s -- -v 2.6.3
RUN curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | bash
# TODO: Source not working properly & requiring manual /root/.local/bin/ paths
RUN source /root/.profile
RUN /bin/bash /root/.local/bin/snfoundryup --version 0.20.0

# Copy over the configs
WORKDIR /configs
COPY ./configs/ .
COPY ./configs/docker-database.config.json ./database.config.json
COPY ./configs/docker-backend.config.json ./backend.config.json

# Copy over the scripts
WORKDIR /scripts
COPY ./tests/integration/docker/ .

# Copy over the app
WORKDIR /app
COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
COPY ./backend .

# Build the app & run it
RUN go build -o main .

EXPOSE 8080

CMD ["./main"]
16 changes: 16 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# art/peace Backend

This directory contains the Go backend for `art/peace`, which provides routes for managing and retrieving `art/peace` info from the Redis and Postgres DBs. Also contains other utilities to do things like get the contract address, use devnet transaction invoke scripts for easy devnet testing, maintain websocket connections for pixel updates, and more.

## Running

```
go run main.go
```

## Build

```
go mod download
go build
```
10 changes: 5 additions & 5 deletions backend/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ type Backend struct {
WSConnections []*websocket.Conn

CanvasConfig *config.CanvasConfig
Port int
BackendConfig *config.BackendConfig
}

var ArtPeaceBackend *Backend

func NewBackend(databases *Databases, canvasConfig *config.CanvasConfig, port int) *Backend {
func NewBackend(databases *Databases, canvasConfig *config.CanvasConfig, backendConfig *config.BackendConfig) *Backend {
return &Backend{
Databases: databases,
CanvasConfig: canvasConfig,
Port: port,
BackendConfig: backendConfig,
}
}

func (b *Backend) Start() {
fmt.Println("Listening on port", b.Port)
http.ListenAndServe(fmt.Sprintf(":%d", b.Port), nil)
fmt.Println("Listening on port", b.BackendConfig.Port)
http.ListenAndServe(fmt.Sprintf(":%d", b.BackendConfig.Port), nil)
}
3 changes: 2 additions & 1 deletion backend/backend/databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backend

import (
"context"
"os"
"strconv"

"github.com/jackc/pgx/v5"
Expand Down Expand Up @@ -29,7 +30,7 @@ func NewDatabases(databaseConfig *config.DatabaseConfig) *Databases {
})

// Connect to Postgres
postgresConnString := "postgresql://" + databaseConfig.Postgres.User + "@" + databaseConfig.Postgres.Host + ":" + strconv.Itoa(databaseConfig.Postgres.Port) + "/" + databaseConfig.Postgres.Database
postgresConnString := "postgresql://" + databaseConfig.Postgres.User + ":" + os.Getenv("POSTGRES_PASSWORD") + "@" + databaseConfig.Postgres.Host + ":" + strconv.Itoa(databaseConfig.Postgres.Port) + "/" + databaseConfig.Postgres.Database
pgConn, err := pgx.Connect(context.Background(), postgresConnString)
if err != nil {
panic(err)
Expand Down
45 changes: 45 additions & 0 deletions backend/config/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package config

import (
"encoding/json"
"os"
)

type BackendScriptsConfig struct {
PlacePixelDevnet string `json:"place_pixel_devnet"`
AddTemplateHashDevnet string `json:"add_template_hash_devnet"`
}

type BackendConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Scripts BackendScriptsConfig `json:"scripts"`
}

var DefaultBackendConfig = BackendConfig{
Host: "localhost",
Port: 8080,
Scripts: BackendScriptsConfig{
PlacePixelDevnet: "../scripts/place_pixel.sh",
AddTemplateHashDevnet: "../scripts/add_template_hash.sh",
},
}

var DefaultBackendConfigPath = "../configs/backend.config.json"

func LoadBackendConfig(backendConfigPath string) (*BackendConfig, error) {
file, err := os.Open(backendConfigPath)
if err != nil {
return nil, err
}
defer file.Close()

decoder := json.NewDecoder(file)
config := BackendConfig{}
err = decoder.Decode(&config)
if err != nil {
return nil, err
}

return &config, nil
}
3 changes: 1 addition & 2 deletions backend/config/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ var DefaultDatabaseConfig = DatabaseConfig{
Postgres: PostgresConfig{
Host: "localhost",
Port: 5432,
// TODO: Add non-hardcoded default user
User: "brandonroberts",
User: "art-peace-user",
Database: "art-peace-db",
},
}
Expand Down
9 changes: 7 additions & 2 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func main() {
canvasConfigFilename := flag.String("canvas-config", config.DefaultCanvasConfigPath, "Canvas config file")
databaseConfigFilename := flag.String("database-config", config.DefaultDatabaseConfigPath, "Database config file")
port := flag.Int("port", 8080, "Port to listen on")
backendConfigFilename := flag.String("backend-config", config.DefaultBackendConfigPath, "Backend config file")
flag.Parse()

canvasConfig, err := config.LoadCanvasConfig(*canvasConfigFilename)
Expand All @@ -24,11 +24,16 @@ func main() {
panic(err)
}

backendConfig, err := config.LoadBackendConfig(*backendConfigFilename)
if err != nil {
panic(err)
}

databases := backend.NewDatabases(databaseConfig)
defer databases.Close()

routes.InitRoutes()

backend.ArtPeaceBackend = backend.NewBackend(databases, canvasConfig, *port)
backend.ArtPeaceBackend = backend.NewBackend(databases, canvasConfig, backendConfig)
backend.ArtPeaceBackend.Start()
}
29 changes: 29 additions & 0 deletions backend/routes/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package routes

import (
"io/ioutil"
"net/http"
"os"
)

func InitContractRoutes() {
http.HandleFunc("/getContractAddress", getContractAddress)
http.HandleFunc("/setContractAddress", setContractAddress)
}

func getContractAddress(w http.ResponseWriter, r *http.Request) {
contractAddress := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")
w.Write([]byte(contractAddress))
}

func setContractAddress(w http.ResponseWriter, r *http.Request) {
// TODO: Add authentication
data, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid request"))
return
}
os.Setenv("ART_PEACE_CONTRACT_ADDRESS", string(data))
w.Write([]byte("Contract address set successfully"))
}
14 changes: 13 additions & 1 deletion backend/routes/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
address := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[1]
address = address.(string)[2:]
posHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[2]
dayIdxHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[3]
colorHex := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})[0].(map[string]interface{})["event"].(map[string]interface{})["data"].([]interface{})[0]

// Convert hex to int
Expand All @@ -85,6 +86,12 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
return
}
dayIdx, err := strconv.ParseInt(dayIdxHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting day index hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
color, err := strconv.ParseInt(colorHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting color hex to int: ", err)
Expand All @@ -105,7 +112,12 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
}

// Set pixel in postgres
_, err = backend.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Pixels (address, position, color) VALUES ($1, $2, $3)", address, position, color)
_, err = backend.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Pixels (address, position, day, color) VALUES ($1, $2, $3, $4)", address, position, dayIdx, color)
if err != nil {
fmt.Println("Error inserting pixel into postgres: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// Send message to all connected clients
var message = map[string]interface{}{
Expand Down
10 changes: 4 additions & 6 deletions backend/routes/pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strconv"

Expand Down Expand Up @@ -54,7 +55,6 @@ func getPixelInfo(w http.ResponseWriter, r *http.Request) {
}

func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
// TODO: Disable this in production
reqBody, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
Expand All @@ -65,7 +65,6 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
panic(err)
}

// TODO: Pass position instead of x, y?
x, err := strconv.Atoi(jsonBody["x"])
if err != nil {
panic(err)
Expand All @@ -74,9 +73,10 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
shellCmd := "../tests/integration/local/place_pixel.sh"
shellCmd := backend.ArtPeaceBackend.BackendConfig.Scripts.PlacePixelDevnet
position := x + y * int(backend.ArtPeaceBackend.CanvasConfig.Canvas.Width)
cmd := exec.Command(shellCmd, jsonBody["contract"], "place_pixel", strconv.Itoa(position), jsonBody["color"])
contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")
cmd := exec.Command(shellCmd, contract, "place_pixel", strconv.Itoa(position), jsonBody["color"])
_, err = cmd.Output()
if err != nil {
fmt.Println("Error executing shell command: ", err)
Expand All @@ -98,8 +98,6 @@ func placePixelRedis(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
// TODO: Check if pos and color are valid
// TODO: allow x, y coordinates?
position := jsonBody["position"]
color := jsonBody["color"]
bitfieldType := "u" + strconv.Itoa(int(backend.ArtPeaceBackend.CanvasConfig.ColorsBitWidth))
Expand Down
Loading

0 comments on commit 9ebeafd

Please sign in to comment.