Skip to content

Commit

Permalink
Full integration test which launches devnet, starts indexer w/ event …
Browse files Browse the repository at this point in the history
…watching script, deploys the contract onchain, starts the backend, starts redis, initializes the canvas, starts the frontend, logs everything to an integration test directory, and provides basic scripts and ways to interact with the canvas onchain
  • Loading branch information
b-j-roberts committed Mar 28, 2024
1 parent 34d6728 commit 6205575
Show file tree
Hide file tree
Showing 20 changed files with 962 additions and 1,636 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
./yarn.lock
node_modules/

# Redis
**/dump.rdb

# Development
**/TODO

# TODO
# Frontend
# Backend
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<!--TODO: <img src="book/src/assets/blobstreamSnBanner.png" alt="art_canvas" height="300"/>-->
<img src="resources/art-peace-logo/art-peace.jpg" alt="art_canvas" height="300"/>
<h1>art/peace</h1>

***Collaborative art canvas on Starknet***
Expand Down
223 changes: 202 additions & 21 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,141 @@ package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os/exec"
"strconv"

"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
var client *redis.Client
// Vector of connections
var connections []*websocket.Conn

// Message layout
/*
{
"data": {
"cursor": {
"orderKey": 3,
"uniqueKey": "0x050d47ba775556cd86577692d31a38422af66471dcb85edaea33cde70bfc156c"
},
"end_cursor": {
"orderKey": 4,
"uniqueKey": "0x03b2711fe29eba45f2a0250c34901d15e37b495599fac1a74960a09cc83e1234"
},
"finality": "DATA_STATUS_ACCEPTED",
"batch": [
{
"status": "BLOCK_STATUS_ACCEPTED_ON_L2",
"events": [
{
"event": {
"fromAddress": "0x0474642f7f488d4b49b6e892f3e4a5407c6ad5fe065687f2ebe4e0f7c1309860",
"keys": [
"0x02d7b50ebf415606d77c7e7842546fc13f8acfbfd16f7bcf2bc2d08f54114c23",
"0x0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0",
"0x000000000000000000000000000000000000000000000000000000000000001e"
],
"data": [
"0x0000000000000000000000000000000000000000000000000000000000000001"
]
}
}
]
}
]
}
}
*/

func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
log.Println("Consume indexer msg")
fmt.Println("Consume indexer msg")

requestBody, err := io.ReadAll(r.Body)
if err != nil {
log.Println("Error reading request body: ", err)
fmt.Println("Error reading request body: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

fmt.Println("Received message: ", string(requestBody))

// TODO: Parse message fully, check block status, number, ...
reqBody := map[string]interface{}{}
err = json.Unmarshal(requestBody, &reqBody)
if err != nil {
fmt.Println("Error unmarshalling request body: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Get the field data.batch[0].events[0].event.keys[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]

// Get the field data.batch[0].events[0].event.data[0]
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]

log.Println("Received message: ", string(requestBody))
// Convert hex to int
position, err := strconv.ParseInt(posHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting position 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)
w.WriteHeader(http.StatusInternalServerError)
return
}

colorBitWidth := uint(5) // TODO: Get from request || const / cmdline?
bitfieldType := "u" + strconv.Itoa(int(colorBitWidth))
pos := uint(position) * colorBitWidth

ctx := context.Background()
err = client.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err()
if err != nil {
panic(err)
}

var message = map[string]interface{}{
"position": position,
"color": color,
}
messageBytes, err := json.Marshal(message)
if err != nil {
fmt.Println("Error marshalling message: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Println("Message: ", string(messageBytes), " to clients: ", len(connections))
// Send message to all connected clients
for idx, conn := range connections {
if err := conn.WriteMessage(websocket.TextMessage, messageBytes); err != nil {
fmt.Println(err)
// Remove connection
conn.Close()
connections = append(connections[:idx], connections[idx+1:]...)
}
}
}

func initCanvas(w http.ResponseWriter, r *http.Request) {
log.Println("Initializing Canvas...")
fmt.Println("Initializing Canvas...")

// TODO: Check if canvas already exists

reqBody, err := io.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
panic(err)
}
var jsonBody map[string]uint
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
log.Fatal(err)
panic(err)
}
// TODO: Check if width and height are valid
width := jsonBody["width"]
Expand All @@ -53,35 +153,37 @@ func initCanvas(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
err = client.Set(ctx, "canvas", canvas, 0).Err()
if err != nil {
log.Fatal(err)
panic(err)
}

log.Println("Canvas initialized")
fmt.Println("Canvas initialized")
}

func getCanvas(w http.ResponseWriter, r *http.Request) {
log.Println("Get Canvas")
fmt.Println("Get Canvas")

ctx := context.Background()
val, err := client.Get(ctx, "canvas").Result()
if err != nil {
log.Fatal(err)
panic(err)
}

log.Println("Canvas", val)
fmt.Println("Canvas", val)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte(val))
}

func placePixel(w http.ResponseWriter, r *http.Request) {
log.Println("Place Pixel")
fmt.Println("Place Pixel")

reqBody, err := io.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
panic(err)
}
var jsonBody map[string]uint
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
log.Fatal(err)
panic(err)
}
// TODO: Check if pos and color are valid
// TODO: allow x, y coordinates?
Expand All @@ -94,16 +196,16 @@ func placePixel(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
err = client.BitField(ctx, "canvas", "SET", bitfieldType, pos, color).Err()
if err != nil {
log.Fatal(err)
panic(err)
}
}

func getPixel(w http.ResponseWriter, r *http.Request) {
log.Println("Get Pixel")
fmt.Println("Get Pixel")

position, err := strconv.Atoi(r.URL.Query().Get("position"))
if err != nil {
log.Fatal(err)
panic(err)
}
colorBitWidth := uint(5) // TODO: Get from request || const / cmdline?
bitfieldType := "u" + strconv.Itoa(int(colorBitWidth))
Expand All @@ -112,10 +214,87 @@ func getPixel(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
val, err := client.BitField(ctx, "canvas", "GET", bitfieldType, pos).Result()
if err != nil {
log.Fatal(err)
panic(err)
}

fmt.Println("Pixel", val)
}

func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
fmt.Println("Place Pixel")

reqBody, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
fmt.Println(reqBody)
var jsonBody map[string]string
err = json.Unmarshal(reqBody, &jsonBody)
if err != nil {
panic(err)
}
fmt.Println(jsonBody)

x, err := strconv.Atoi(jsonBody["x"])
if err != nil {
panic(err)
}
y, err := strconv.Atoi(jsonBody["y"])
if err != nil {
panic(err)
}
// Use shell / bash to ls files in directory
shellCmd := "../tests/integration/local/place_pixel.sh"
position := x + y * 16 // TODO: Hardcoded for now
fmt.Println("Running shell command: ", shellCmd, jsonBody["contract"], "place_pixel", strconv.Itoa(int(position)), jsonBody["color"])
cmd := exec.Command(shellCmd, jsonBody["contract"], "place_pixel", strconv.Itoa(int(position)), jsonBody["color"])
out, err := cmd.Output()
if err != nil {
fmt.Println("Error executing shell command: ", err)
panic(err)
}
fmt.Println(string(out))

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Pixel placed"))
}

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}

func wsReader(conn *websocket.Conn) {
for {
fmt.Println("Reading message")
messageType, p, err := conn.ReadMessage()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("read", string(p), "messageType", messageType)

//if err := conn.WriteMessage(messageType, p); err != nil {
// fmt.Println(err)
// return
//}
//fmt.Println("sent", string(p))
}
}

func wsEndpoint(w http.ResponseWriter, r *http.Request) {
fmt.Println("Websocket endpoint")
upgrader.CheckOrigin = func(r *http.Request) bool { return true }

ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println(err)
}

log.Println("Pixel", val)
fmt.Println("Client Connected")
// TODO: disconnecting / removing connections
connections = append(connections, ws)
wsReader(ws)
}

func main() {
Expand All @@ -132,8 +311,10 @@ func main() {
http.HandleFunc("/getCanvas", getCanvas)
http.HandleFunc("/placePixel", placePixel)
http.HandleFunc("/getPixel", getPixel)
http.HandleFunc("/placePixelDevnet", placePixelDevnet)
http.HandleFunc("/ws", wsEndpoint)

// TODO: hardcode port
log.Println("Listening on port 8080")
fmt.Println("Listening on port 8080")
http.ListenAndServe(":8080", nil)
}
2 changes: 2 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ require github.com/redis/go-redis/v9 v9.5.1
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gorilla/websocket v1.5.1 // indirect
golang.org/x/net v0.17.0 // indirect
)
4 changes: 4 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
Loading

0 comments on commit 6205575

Please sign in to comment.