Skip to content

Commit

Permalink
NFTs frontend and devnet interaction, templates upload on frontend an…
Browse files Browse the repository at this point in the history
…d devnet interaction, various patches, ...
  • Loading branch information
b-j-roberts committed Apr 22, 2024
1 parent f2b6d11 commit 3b2e739
Show file tree
Hide file tree
Showing 54 changed files with 2,764 additions and 324 deletions.
8 changes: 6 additions & 2 deletions backend/config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (

type BackendScriptsConfig struct {
PlacePixelDevnet string `json:"place_pixel_devnet"`
AddTemplateHashDevnet string `json:"add_template_hash_devnet"`
PlaceExtraPixelsDevnet string `json:"place_extra_pixels_devnet"`
AddTemplateDevnet string `json:"add_template_devnet"`
MintNFTDevnet string `json:"mint_nft_devnet"`
}

type BackendConfig struct {
Expand All @@ -21,7 +23,9 @@ var DefaultBackendConfig = BackendConfig{
Port: 8080,
Scripts: BackendScriptsConfig{
PlacePixelDevnet: "../scripts/place_pixel.sh",
AddTemplateHashDevnet: "../scripts/add_template_hash.sh",
PlaceExtraPixelsDevnet: "../scripts/place_extra_pixels.sh",
AddTemplateDevnet: "../scripts/add_template.sh",
MintNFTDevnet: "../scripts/mint_nft.sh",
},
}

Expand Down
10 changes: 5 additions & 5 deletions backend/core/databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
"strconv"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"

"github.com/keep-starknet-strange/art-peace/backend/config"
Expand All @@ -15,7 +15,7 @@ type Databases struct {
DatabaseConfig *config.DatabaseConfig

Redis *redis.Client
Postgres *pgx.Conn
Postgres *pgxpool.Pool
}

func NewDatabases(databaseConfig *config.DatabaseConfig) *Databases {
Expand All @@ -31,16 +31,16 @@ func NewDatabases(databaseConfig *config.DatabaseConfig) *Databases {

// Connect to Postgres
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)
pgPool, err := pgxpool.New(context.Background(), postgresConnString)
if err != nil {
panic(err)
}
d.Postgres = pgConn
d.Postgres = pgPool

return d
}

func (d *Databases) Close() {
d.Redis.Close()
d.Postgres.Close(context.Background())
d.Postgres.Close()
}
2 changes: 2 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ require (
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
Expand Down
282 changes: 278 additions & 4 deletions backend/routes/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package routes

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"image"
"image/color"
"image/png"
"io"
"net/http"
"os"
"strconv"

"github.com/gorilla/websocket"
Expand Down Expand Up @@ -55,6 +60,13 @@ func InitIndexerRoutes() {
}
*/

const (
pixelPlacedEvent = "0x02d7b50ebf415606d77c7e7842546fc13f8acfbfd16f7bcf2bc2d08f54114c23"
nftMintedEvent = "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f"
templateAddedEvent = "0x03e18ec266fe76a2efce73f91228e6e04456b744fc6984c7a6374e417fb4bf59"
)


// TODO: User might miss some messages between loading canvas and connecting to websocket?
func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
requestBody, err := io.ReadAll(r.Body)
Expand All @@ -73,11 +85,28 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
return
}

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]
events := reqBody["data"].(map[string]interface{})["batch"].([]interface{})[0].(map[string]interface{})["events"].([]interface{})

for _, event := range events {
eventKey := event.(map[string]interface{})["event"].(map[string]interface{})["keys"].([]interface{})[0].(string)
if eventKey == pixelPlacedEvent {
processPixelPlacedEvent(event.(map[string]interface{}), w)
} else if eventKey == nftMintedEvent {
processNFTMintedEvent(event.(map[string]interface{}), w)
} else if eventKey == templateAddedEvent {
processTemplateAddedEvent(event.(map[string]interface{}), w)
} else {
fmt.Println("Unknown event key: ", eventKey)
}
}
}

func processPixelPlacedEvent(event map[string]interface{}, w http.ResponseWriter) {
address := event["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]
posHex := event["event"].(map[string]interface{})["keys"].([]interface{})[2]
dayIdxHex := event["event"].(map[string]interface{})["keys"].([]interface{})[3]
colorHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]

// Convert hex to int
position, err := strconv.ParseInt(posHex.(string), 0, 64)
Expand Down Expand Up @@ -140,3 +169,248 @@ func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) {
}
}
}

/*
indexer-1 | [
indexer-1 | [Object: null prototype] {
indexer-1 | event: [Object: null prototype] {
indexer-1 | fromAddress: "0x07163dbc0d5dc7e65c8fb9697dbd778e70fa9fef66f12a018a69751eb53fec5a",
indexer-1 | keys: [
indexer-1 | "0x030826e0cd9a517f76e857e3f3100fe5b9098e9f8216d3db283fb4c9a641232f",
indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000",
indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000"
indexer-1 | ],
indexer-1 | data: [
indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000091",
indexer-1 | "0x000000000000000000000000000000000000000000000000000000000000000e",
indexer-1 | "0x000000000000000000000000000000000000000000000000000000000000000d",
indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000000",
indexer-1 | "0x0000000000000000000000000000000000000000000000000000000000000006",
indexer-1 | "0x0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0"
indexer-1 | ],
indexer-1 | index: "1"
indexer-1 | }
indexer-1 | }
indexer-1 | ]
*/

func processNFTMintedEvent(event map[string]interface{}, w http.ResponseWriter) {
// TODO: combine high and low token ids
tokenIdLowHex := event["event"].(map[string]interface{})["keys"].([]interface{})[1]
tokenIdHighHex := event["event"].(map[string]interface{})["keys"].([]interface{})[2]

positionHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]
widthHex := event["event"].(map[string]interface{})["data"].([]interface{})[1]
heightHex := event["event"].(map[string]interface{})["data"].([]interface{})[2]
imageHashHex := event["event"].(map[string]interface{})["data"].([]interface{})[3]
blockNumberHex := event["event"].(map[string]interface{})["data"].([]interface{})[4]
minterHex := event["event"].(map[string]interface{})["data"].([]interface{})[5]

fmt.Println("NFT minted with token id low: ", tokenIdLowHex, " and token id high: ", tokenIdHighHex)

tokenId, err := strconv.ParseInt(tokenIdLowHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting token id low hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

position, err := strconv.ParseInt(positionHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting position hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

width, err := strconv.ParseInt(widthHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting width hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

height, err := strconv.ParseInt(heightHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting height hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

blockNumber, err := strconv.ParseInt(blockNumberHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting block number hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

minter := minterHex.(string)[2:]

fmt.Println("NFT minted with position: ", position, " width: ", width, " height: ", height, " image hash: ", imageHashHex, " block number: ", blockNumber, " minter: ", minter, "tokenId", tokenId)
// Set NFT in postgres
_, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO NFTs (key, position, width, height, imageHash, blockNumber, minter) VALUES ($1, $2, $3, $4, $5, $6, $7)", tokenId, position, width, height, imageHashHex, blockNumber, minter)
if err != nil {
fmt.Println("Error inserting NFT into postgres: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// TODO: get image from canvas through starknet rpc? What to do about pending transactions?

// Load image from redis
ctx := context.Background()
// TODO: Better way to get image
canvas, err := core.ArtPeaceBackend.Databases.Redis.Get(ctx, "canvas").Result()
if err != nil {
panic(err)
}

colorPaletteHex := core.ArtPeaceBackend.CanvasConfig.Colors
colorPalette := make([]color.RGBA, len(colorPaletteHex))
for idx, colorHex := range colorPaletteHex {
r, err := strconv.ParseInt(colorHex[0:2], 16, 64)
if err != nil {
fmt.Println("Error converting red hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
g, err := strconv.ParseInt(colorHex[2:4], 16, 64)
if err != nil {
fmt.Println("Error converting green hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
b, err := strconv.ParseInt(colorHex[4:6], 16, 64)
if err != nil {
fmt.Println("Error converting blue hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
colorPalette[idx] = color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
}
bitWidth := int64(core.ArtPeaceBackend.CanvasConfig.ColorsBitWidth)
startX := int64(position % int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width))
startY := int64(position / int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width))
oneByteBitOffset := int64(8 - bitWidth)
twoByteBitOffset := int64(16 - bitWidth)
generatedImage := image.NewRGBA(image.Rect(0, 0, int(width), int(height)))
for y := startY; y < startY+height; y++ {
for x := startX; x < startX+width; x++ {
pos := y*int64(core.ArtPeaceBackend.CanvasConfig.Canvas.Width) + x
bitPos := pos * bitWidth
bytePos := bitPos / 8
bitOffset := bitPos % 8
if bitOffset <= oneByteBitOffset {
colorIdx := (canvas[bytePos] >> (oneByteBitOffset - bitOffset)) & 0b11111
generatedImage.Set(int(x-startX), int(y-startY), colorPalette[colorIdx])
} else {
colorIdx := (((uint16(canvas[bytePos]) << 8) | uint16(canvas[bytePos+1])) >> (twoByteBitOffset - bitOffset)) & 0b11111
generatedImage.Set(int(x-startX), int(y-startY), colorPalette[colorIdx])
}
}
}

// TODO: Path to save image
// Save image to disk
filename := fmt.Sprintf("nft-%d.png", tokenId)
file, err := os.Create(filename)
if err != nil {
fmt.Println("Error creating file: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()

err = png.Encode(file, generatedImage)
if err != nil {
fmt.Println("Error encoding image: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("NFT minted"))
}

func processTemplateAddedEvent(event map[string]interface{}, w http.ResponseWriter) {
templateIdHex := event["event"].(map[string]interface{})["keys"].([]interface{})[1]
templateHashHex := event["event"].(map[string]interface{})["data"].([]interface{})[0]
templateNameHex := event["event"].(map[string]interface{})["data"].([]interface{})[1]
templatePositionHex := event["event"].(map[string]interface{})["data"].([]interface{})[2]
templateWidthHex := event["event"].(map[string]interface{})["data"].([]interface{})[3]
templateHeightHex := event["event"].(map[string]interface{})["data"].([]interface{})[4]
// TODO: Combine low and high token ids
templateRewardHighHex := event["event"].(map[string]interface{})["data"].([]interface{})[5]
templateRewardLowHex := event["event"].(map[string]interface{})["data"].([]interface{})[6]
templateRewardTokenHex := event["event"].(map[string]interface{})["data"].([]interface{})[7]

fmt.Println("Template added with template id: ", templateIdHex, " template hash: ", templateHashHex, "reward: ", templateRewardLowHex, templateRewardHighHex, "name:", templateNameHex)

templateId, err := strconv.ParseInt(templateIdHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting template id hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

// Parse template name hex as bytes encoded in utf-8
decodedName, err := hex.DecodeString(templateNameHex.(string)[2:])
if err != nil {
fmt.Println("Error decoding template name hex: ", err)
w.WriteHeader(http.StatusInternalServerError)
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)
}
templateName := string(trimmedName)

templatePosition, err := strconv.ParseInt(templatePositionHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting template position hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

templateWidth, err := strconv.ParseInt(templateWidthHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting template width hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

templateHeight, err := strconv.ParseInt(templateHeightHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting template height hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

templateReward, err := strconv.ParseInt(templateRewardLowHex.(string), 0, 64)
if err != nil {
fmt.Println("Error converting template reward hex to int: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

templateRewardToken := templateRewardTokenHex.(string)[2:]

// Add template to postgres
_, err = core.ArtPeaceBackend.Databases.Postgres.Exec(context.Background(), "INSERT INTO Templates (key, name, hash, position, width, height, reward, rewardToken) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", templateId, templateName, templateHashHex, templatePosition, templateWidth, templateHeight, templateReward, templateRewardToken)
if err != nil {
fmt.Println("Error inserting template into postgres: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Template added"))
}
Loading

0 comments on commit 3b2e739

Please sign in to comment.