diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7453be8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# Frontend ignored dirs +./apps/web/dist/ +./apps/web/node_modules/ diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore index d1bed12..e6fe010 100644 --- a/apps/backend/.gitignore +++ b/apps/backend/.gitignore @@ -1,61 +1,22 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib -# Runtime data -pids -*.pid -*.seed -*.pid.lock +# Test binary, built with `go test -c` +*.test -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# Coverage directory used by tools like istanbul -coverage +# Dependency directories (remove the comment below to include it) +# vendor/ -# nyc test coverage -.nyc_output +# Go workspace files +go.work +go.work.sum -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file +# env file .env - -# next.js build output -.next diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile new file mode 100644 index 0000000..786f2bd --- /dev/null +++ b/apps/backend/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.23.1-alpine + +RUN apk add --no-cache bash curl git jq yq + +# Copy over the configs +WORKDIR /configs +COPY ./apps/backend/configs/docker.config.yaml /configs/config.yaml + +# Copy over the app +WORKDIR /app +COPY ./apps/backend/go.mod ./apps/backend/go.sum ./ +RUN go mod download +COPY ./apps/backend . + +# Build the app & run it +RUN go build -o broly-api ./cmd/broly-api/main.go + +EXPOSE 8080 + +CMD ["./broly-api"] diff --git a/apps/backend/Dockerfile.consumer b/apps/backend/Dockerfile.consumer new file mode 100644 index 0000000..ad0db72 --- /dev/null +++ b/apps/backend/Dockerfile.consumer @@ -0,0 +1,20 @@ +FROM golang:1.23.1-alpine + +RUN apk add --no-cache bash curl git jq yq + +# Copy over the configs +WORKDIR /configs +COPY ./apps/backend/configs/docker.config.yaml /configs/config.yaml + +# Copy over the app +WORKDIR /app +COPY ./apps/backend/go.mod ./apps/backend/go.sum ./ +RUN go mod download +COPY ./apps/backend . + +# Build the app & run it +RUN go build -o consumer ./cmd/consumer/main.go + +EXPOSE 8081 + +CMD ["./consumer"] diff --git a/apps/backend/app.js b/apps/backend/app.js deleted file mode 100644 index dce22bd..0000000 --- a/apps/backend/app.js +++ /dev/null @@ -1,20 +0,0 @@ -var express = require("express"); -var path = require("path"); -var cookieParser = require("cookie-parser"); -var logger = require("morgan"); - -var indexRouter = require("./routes/index"); -var ordersRouter = require("./routes/orders"); - -var app = express(); - -app.use(logger("dev")); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, "public"))); - -app.use("/", indexRouter); -app.use("/api/orders", ordersRouter); - -module.exports = app; diff --git a/apps/backend/bin/www b/apps/backend/bin/www deleted file mode 100755 index 192c6f3..0000000 --- a/apps/backend/bin/www +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var app = require('../app'); -var debug = require('debug')('backend:server'); -var http = require('http'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.PORT || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var server = http.createServer(app); - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - var port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); -} diff --git a/apps/backend/cmd/broly-api/main.go b/apps/backend/cmd/broly-api/main.go new file mode 100644 index 0000000..5523736 --- /dev/null +++ b/apps/backend/cmd/broly-api/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/keep-starknet-strange/broly/backend/internal/config" + "github.com/keep-starknet-strange/broly/backend/internal/db" + "github.com/keep-starknet-strange/broly/backend/routes" +) + +func main() { + config.InitConfig() + + db.InitDB() + defer db.CloseDB() + + routes.InitRoutes() + fmt.Println("Listening on port:", config.Conf.Api.Port) + http.ListenAndServe(fmt.Sprintf(":%d", config.Conf.Api.Port), nil) + fmt.Println("Server stopped") +} diff --git a/apps/backend/cmd/consumer/main.go b/apps/backend/cmd/consumer/main.go new file mode 100644 index 0000000..72c42de --- /dev/null +++ b/apps/backend/cmd/consumer/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/keep-starknet-strange/broly/backend/indexer" + "github.com/keep-starknet-strange/broly/backend/internal/config" + "github.com/keep-starknet-strange/broly/backend/internal/db" +) + +func main() { + config.InitConfig() + + db.InitDB() + defer db.CloseDB() + + indexer.InitIndexerRoutes() + indexer.StartMessageProcessor() + + fmt.Println("Listening on port:", config.Conf.Consumer.Port) + http.ListenAndServe(fmt.Sprintf(":%d", config.Conf.Consumer.Port), nil) + fmt.Println("Server stopped") +} diff --git a/apps/backend/configs/config.yaml b/apps/backend/configs/config.yaml new file mode 100644 index 0000000..6cd60b1 --- /dev/null +++ b/apps/backend/configs/config.yaml @@ -0,0 +1,21 @@ +Api: + Port: 8080 + AllowOrigins: + - '*' + AllowMethods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + AllowHeaders: + - Content-Type + Production: false + Admin: true +Consumer: + Port: 8081 +Postgres: + Host: localhost + Port: 5432 + User: broly-user + Name: broly-db diff --git a/apps/backend/configs/docker.config.yaml b/apps/backend/configs/docker.config.yaml new file mode 100644 index 0000000..c04e896 --- /dev/null +++ b/apps/backend/configs/docker.config.yaml @@ -0,0 +1,21 @@ +Api: + Port: 8080 + AllowOrigins: + - '*' + AllowMethods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + AllowHeaders: + - Content-Type + Production: false + Admin: true +Consumer: + Port: 8081 +Postgres: + Host: broly-postgres-1 + Port: 5432 + User: broly-user + Name: broly-db diff --git a/apps/backend/configs/local.config.yaml b/apps/backend/configs/local.config.yaml new file mode 100644 index 0000000..6cd60b1 --- /dev/null +++ b/apps/backend/configs/local.config.yaml @@ -0,0 +1,21 @@ +Api: + Port: 8080 + AllowOrigins: + - '*' + AllowMethods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + AllowHeaders: + - Content-Type + Production: false + Admin: true +Consumer: + Port: 8081 +Postgres: + Host: localhost + Port: 5432 + User: broly-user + Name: broly-db diff --git a/apps/backend/go.mod b/apps/backend/go.mod new file mode 100644 index 0000000..155da43 --- /dev/null +++ b/apps/backend/go.mod @@ -0,0 +1,19 @@ +module github.com/keep-starknet-strange/broly/backend + +go 1.23.1 + +require ( + github.com/georgysavva/scany/v2 v2.1.3 + github.com/jackc/pgx/v5 v5.7.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 // indirect +) diff --git a/apps/backend/go.sum b/apps/backend/go.sum new file mode 100644 index 0000000..d95727c --- /dev/null +++ b/apps/backend/go.sum @@ -0,0 +1,51 @@ +github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= +github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/georgysavva/scany/v2 v2.1.3 h1:Zd4zm/ej79Den7tBSU2kaTDPAH64suq4qlQdhiBeGds= +github.com/georgysavva/scany/v2 v2.1.3/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/backend/indexer/indexer.go b/apps/backend/indexer/indexer.go new file mode 100644 index 0000000..a9294e8 --- /dev/null +++ b/apps/backend/indexer/indexer.go @@ -0,0 +1,301 @@ +package indexer + +import ( + "fmt" + "net/http" + "sync" + "time" + + routeutils "github.com/keep-starknet-strange/broly/backend/routes/utils" +) + +func InitIndexerRoutes() { + http.HandleFunc("/consume-indexer-msg", consumeIndexerMsg) +} + +type IndexerCursor struct { + OrderKey int `json:"orderKey"` + UniqueKey string `json:"uniqueKey"` +} + +type IndexerEvent struct { + Event struct { + FromAddress string `json:"fromAddress"` + Keys []string `json:"keys"` + Data []string `json:"data"` + } `json:"event"` +} + +type IndexerMessage struct { + Data struct { + Cursor IndexerCursor `json:"cursor"` + EndCursor IndexerCursor `json:"end_cursor"` + Finality string `json:"finality"` + Batch []struct { + Status string `json:"status"` + Events []IndexerEvent `json:"events"` + } `json:"batch"` + } `json:"data"` +} + +var LatestPendingMessage *IndexerMessage +var LastProcessedPendingMessage *IndexerMessage +var PendingMessageLock = &sync.Mutex{} +var LastAcceptedEndKey int +var AcceptedMessageQueue []IndexerMessage +var AcceptedMessageLock = &sync.Mutex{} +var LastFinalizedCursor int +var FinalizedMessageQueue []IndexerMessage +var FinalizedMessageLock = &sync.Mutex{} + +const ( +) + +var eventProcessors = map[string](func(IndexerEvent)){ +} + +var eventReverters = map[string](func(IndexerEvent)){ +} + +var eventRequiresOrdering = map[string]bool{ +} + +const ( + DATA_STATUS_FINALIZED = "DATA_STATUS_FINALIZED" + DATA_STATUS_ACCEPTED = "DATA_STATUS_ACCEPTED" + DATA_STATUS_PENDING = "DATA_STATUS_PENDING" +) + +// TODO: Change to take event +func PrintIndexerError(funcName string, errMsg string, args ...interface{}) { + fmt.Println("Error indexing in "+funcName+": "+errMsg+" -- ", args) +} + +func consumeIndexerMsg(w http.ResponseWriter, r *http.Request) { + message, err := routeutils.ReadJsonBody[IndexerMessage](r) + if err != nil { + PrintIndexerError("consumeIndexerMsg", "error reading indexer message", err) + return + } + + if len(message.Data.Batch) == 0 { + fmt.Println("No events in batch") + return + } + + if message.Data.Finality == DATA_STATUS_FINALIZED { + FinalizedMessageLock.Lock() + FinalizedMessageQueue = append(FinalizedMessageQueue, *message) + FinalizedMessageLock.Unlock() + return + } else if message.Data.Finality == DATA_STATUS_ACCEPTED { + AcceptedMessageLock.Lock() + AcceptedMessageQueue = append(AcceptedMessageQueue, *message) + AcceptedMessageLock.Unlock() + return + } else if message.Data.Finality == DATA_STATUS_PENDING { + PendingMessageLock.Lock() + LatestPendingMessage = message + PendingMessageLock.Unlock() + return + } else { + fmt.Println("Unknown finality status") + } +} + +func ProcessMessageEvents(message IndexerMessage) { + for _, event := range message.Data.Batch[0].Events { + eventKey := event.Event.Keys[0] + eventProcessor, ok := eventProcessors[eventKey] + if !ok { + PrintIndexerError("consumeIndexerMsg", "error processing event", eventKey) + return + } + eventProcessor(event) + } +} + +func EventComparator(event1 IndexerEvent, event2 IndexerEvent) bool { + if event1.Event.FromAddress != event2.Event.FromAddress { + return false + } + + if len(event1.Event.Keys) != len(event2.Event.Keys) { + return false + } + + if len(event1.Event.Data) != len(event2.Event.Data) { + return false + } + + for idx := 0; idx < len(event1.Event.Keys); idx++ { + if event1.Event.Keys[idx] != event2.Event.Keys[idx] { + return false + } + } + + for idx := 0; idx < len(event1.Event.Data); idx++ { + if event1.Event.Data[idx] != event2.Event.Data[idx] { + return false + } + } + + return true +} + +func processMessageEventsWithReverter(oldMessage IndexerMessage, newMessage IndexerMessage) { + var idx int + var latestEventIndex int + var unorderedEvents []IndexerEvent + for idx = 0; idx < len(oldMessage.Data.Batch[0].Events); idx++ { + oldEvent := oldMessage.Data.Batch[0].Events[idx] + newEvent := newMessage.Data.Batch[0].Events[idx] + // Check if events are the same + if EventComparator(oldEvent, newEvent) { + latestEventIndex = idx + continue + } + + // Non-matching events, revert remaining old events based on ordering + // Revert events from end of old events to current event + latestEventIndex = idx + for idx = len(oldMessage.Data.Batch[0].Events) - 1; idx >= latestEventIndex; idx-- { + eventKey := oldMessage.Data.Batch[0].Events[idx].Event.Keys[0] + if eventRequiresOrdering[eventKey] { + // Revert event + eventReverter, ok := eventReverters[eventKey] + if !ok { + PrintIndexerError("consumeIndexerMsg", "error reverting event", eventKey) + return + } + eventReverter(oldMessage.Data.Batch[0].Events[idx]) + } else { + unorderedEvents = append(unorderedEvents, oldMessage.Data.Batch[0].Events[idx]) + } + } + break + } + + // Process new events + for idx = latestEventIndex + 1; idx < len(newMessage.Data.Batch[0].Events); idx++ { + eventKey := newMessage.Data.Batch[0].Events[idx].Event.Keys[0] + + // Check if event is in unordered events + var wasProcessed bool + for idx, unorderedEvent := range unorderedEvents { + if EventComparator(unorderedEvent, newMessage.Data.Batch[0].Events[idx]) { + // Remove event from unordered events + unorderedEvents = append(unorderedEvents[:idx], unorderedEvents[idx+1:]...) + wasProcessed = true + break + } + } + if wasProcessed { + continue + } + + eventProcessor, ok := eventProcessors[eventKey] + if !ok { + PrintIndexerError("consumeIndexerMsg", "error processing event", eventKey) + return + } + eventProcessor(newMessage.Data.Batch[0].Events[idx]) + } + + // Revert remaining unordered events + for _, unorderedEvent := range unorderedEvents { + eventKey := unorderedEvent.Event.Keys[0] + eventReverter, ok := eventReverters[eventKey] + if !ok { + PrintIndexerError("consumeIndexerMsg", "error reverting event", eventKey) + return + } + eventReverter(unorderedEvent) + } +} + +func ProcessMessage(message IndexerMessage) { + // Check if there are pending messages for this start key + // TODO: OrderKey or UniqueKey or both? + if LastProcessedPendingMessage != nil && LastProcessedPendingMessage.Data.Cursor.OrderKey == message.Data.Cursor.OrderKey { + processMessageEventsWithReverter(*LastProcessedPendingMessage, message) + } else { + ProcessMessageEvents(message) + } +} + +func TryProcessFinalizedMessages() bool { + FinalizedMessageLock.Lock() + defer FinalizedMessageLock.Unlock() + + if len(FinalizedMessageQueue) > 0 { + message := FinalizedMessageQueue[0] + FinalizedMessageQueue = FinalizedMessageQueue[1:] + if message.Data.Cursor.OrderKey <= LastFinalizedCursor { + // Skip message + return true + } + ProcessMessage(message) + LastFinalizedCursor = message.Data.Cursor.OrderKey + return true + } + return false +} + +func TryProcessAcceptedMessages() bool { + AcceptedMessageLock.Lock() + defer AcceptedMessageLock.Unlock() + + if len(AcceptedMessageQueue) > 0 { + message := AcceptedMessageQueue[0] + AcceptedMessageQueue = AcceptedMessageQueue[1:] + // TODO: Check if message is already processed? + ProcessMessage(message) + // TODO + LastFinalizedCursor = message.Data.Cursor.OrderKey + return true + } + return false +} + +func TryProcessPendingMessage() bool { + PendingMessageLock.Lock() + defer PendingMessageLock.Unlock() + + if LatestPendingMessage == nil { + return false + } + + ProcessMessage(*LatestPendingMessage) + LastProcessedPendingMessage = LatestPendingMessage + LatestPendingMessage = nil + return true +} + +func StartMessageProcessor() { + // Goroutine to process pending/accepted messages + go func() { + for { + // Check Finalized messages ( for initial load ) + if TryProcessFinalizedMessages() { + continue + } + + // Prioritize accepted messages + if TryProcessAcceptedMessages() { + continue + } + + if TryProcessPendingMessage() { + continue + } + + // No messages to process, sleep for 1 second + time.Sleep(1 * time.Second) + } + }() +} + +// TODO: User might miss some messages between loading canvas and connecting to websocket? +// TODO: Check thread safety of these things + diff --git a/apps/backend/internal/config/config.go b/apps/backend/internal/config/config.go new file mode 100644 index 0000000..c77f179 --- /dev/null +++ b/apps/backend/internal/config/config.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type ApiConfig struct { + Port int `yaml:"Port"` + AllowOrigins []string `yaml:"AllowOrigins"` + AllowMethods []string `yaml:"AllowMethods"` + AllowHeaders []string `yaml:"AllowHeaders"` + Production bool `yaml:"Production"` + Admin bool `yaml:"Admin"` +} + +type ConsumerConfig struct { + Port int `yaml:"Port"` +} + +type PostgresConfig struct { + Host string `yaml:"Host"` + Port int `yaml:"Port"` + User string `yaml:"User"` + Name string `yaml:"Name"` +} + +type Config struct { + Api ApiConfig `yaml:"Api"` + Consumer ConsumerConfig `yaml:"Consumer"` + Postgres PostgresConfig `yaml:"Postgres"` +} + +var Conf *Config + +func InitConfig() { + configPath, ok := os.LookupEnv("CONFIG_PATH") + if !ok { + configPath = "configs/config.yaml" + fmt.Println("CONFIG_PATH not set, using default config.yaml") + } + + yamlFile, err := os.ReadFile(configPath) + if err != nil { + fmt.Println("Error reading config file: ", err) + os.Exit(1) + } + + err = yaml.Unmarshal(yamlFile, &Conf) + if err != nil { + fmt.Println("Error parsing config file: ", err) + os.Exit(1) + } +} diff --git a/apps/backend/internal/db/db.go b/apps/backend/internal/db/db.go new file mode 100644 index 0000000..adb4bba --- /dev/null +++ b/apps/backend/internal/db/db.go @@ -0,0 +1,33 @@ +package db + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/keep-starknet-strange/broly/backend/internal/config" +) + +type Databases struct { + Postgres *pgxpool.Pool +} + +var db *Databases + +func InitDB() { + db = &Databases{} + + postgresConnString := "postgresql://" + config.Conf.Postgres.User + ":" + os.Getenv("POSTGRES_PASSWORD") + "@" + config.Conf.Postgres.Host + ":" + strconv.Itoa(config.Conf.Postgres.Port) + "/" + config.Conf.Postgres.Name + pgPool, err := pgxpool.New(context.Background(), postgresConnString) + if err != nil { + fmt.Println("Error connecting to database: ", err) + os.Exit(1) + } + db.Postgres = pgPool +} + +func CloseDB() { + db.Postgres.Close() +} diff --git a/apps/backend/internal/db/postgres.go b/apps/backend/internal/db/postgres.go new file mode 100644 index 0000000..681eb8c --- /dev/null +++ b/apps/backend/internal/db/postgres.go @@ -0,0 +1,69 @@ +package db + +import ( + "context" + "encoding/json" + + "github.com/georgysavva/scany/v2/pgxscan" +) + +// PostgresQuery is a helper function to run a query on the Postgres database. +// +// Generic Param: +// RowType - Golang struct with json tags to map the query result. +// Params: +// query - Postgres query string w/ $1, $2, etc. placeholders. +// args - Arguments to replace the placeholders in the query. +// Returns: +// []RowType - Slice of RowType structs with the query result. +// error - Error if the query fails. +func PostgresQuery[RowType any](query string, args ...interface{}) ([]RowType, error) { + var result []RowType + err := pgxscan.Select(context.Background(), db.Postgres, &result, query, args...) + if err != nil { + return nil, err + } + + return result, nil +} + +// Same as PostgresQuery, but only returns the first row. +func PostgresQueryOne[RowType any](query string, args ...interface{}) (*RowType, error) { + var result RowType + err := pgxscan.Get(context.Background(), db.Postgres, &result, query, args...) + if err != nil { + return nil, err + } + + return &result, nil +} + +// Same as PostgresQuery, but returns the result as a Marshalled JSON byte array. +func PostgresQueryJson[RowType any](query string, args ...interface{}) ([]byte, error) { + result, err := PostgresQuery[RowType](query, args...) + if err != nil { + return nil, err + } + + json, err := json.Marshal(result) + if err != nil { + return nil, err + } + + return json, nil +} + +// Same as PostgresQueryOne, but returns the result as a Marshalled JSON byte array. +func PostgresQueryOneJson[RowType any](query string, args ...interface{}) ([]byte, error) { + result, err := PostgresQueryOne[RowType](query, args...) + if err != nil { + return nil, err + } + + json, err := json.Marshal(result) + if err != nil { + return nil, err + } + + return json, nil +} diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json deleted file mode 100644 index 9928946..0000000 --- a/apps/backend/package-lock.json +++ /dev/null @@ -1,527 +0,0 @@ -{ - "name": "backend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backend", - "version": "0.0.0", - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", - "morgan": "~1.9.1" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", - "dependencies": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "dependencies": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "bin": { - "mime": "cli.js" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/morgan": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", - "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", - "dependencies": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "dependencies": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "node_modules/statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/apps/backend/package.json b/apps/backend/package.json deleted file mode 100644 index e2e26ac..0000000 --- a/apps/backend/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "backend", - "version": "0.0.0", - "private": true, - "scripts": { - "start": "node ./bin/www" - }, - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", - "morgan": "~1.9.1" - } -} diff --git a/apps/backend/postgres/init.sql b/apps/backend/postgres/init.sql new file mode 100644 index 0000000..06da053 --- /dev/null +++ b/apps/backend/postgres/init.sql @@ -0,0 +1,43 @@ +CREATE TABLE IF NOT EXISTS Users ( + starknet_address char(64) NOT NULL, + bitcoin_address text +); + +CREATE TABLE IF NOT EXISTS Inscriptions ( + inscription_id integer NOT NULL PRIMARY KEY, + owner char(64) NOT NULL, + sat_number integer NOT NULL, + minted_block integer NOT NULL +); +CREATE INDEX IF NOT EXISTS Inscriptions_sat_number ON Inscriptions(sat_number); +CREATE INDEX IF NOT EXISTS Inscriptions_minted_block ON Inscriptions(minted_block); + +CREATE TABLE IF NOT EXISTS InscriptionRequests ( + request_id integer NOT NULL PRIMARY KEY, + owner char(64) NOT NULL +); + +-- TODO: Merge this with InscriptionRequests? +CREATE TABLE IF NOT EXISTS InscriptionRequestsStatus ( + request_id integer NOT NULL PRIMARY KEY, + status integer NOT NULL +); +CREATE INDEX IF NOT EXISTS InscriptionRequestsStatus_status ON InscriptionRequestsStatus(status); + +CREATE TABLE IF NOT EXISTS InscriptionLikes ( + key int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + inscription_id integer NOT NULL, + liker char(64) NOT NULL, + UNIQUE(inscription_id, liker) +); +CREATE INDEX IF NOT EXISTS InscriptionLikes_inscription_id ON InscriptionLikes(inscription_id); +CREATE INDEX IF NOT EXISTS InscriptionLikes_liker ON InscriptionLikes(liker); + +CREATE TABLE IF NOT EXISTS InscriptionSaves ( + key int PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + inscription_id integer NOT NULL, + saver char(64) NOT NULL, + UNIQUE(inscription_id, saver) +); +CREATE INDEX IF NOT EXISTS InscriptionSaves_inscription_id ON InscriptionSaves(inscription_id); +CREATE INDEX IF NOT EXISTS InscriptionSaves_saver ON InscriptionSaves(saver); diff --git a/apps/backend/public/index.html b/apps/backend/public/index.html deleted file mode 100644 index 4b496e3..0000000 --- a/apps/backend/public/index.html +++ /dev/null @@ -1,11 +0,0 @@ - -
-Order on Starknet, write on Bitcoin, get money trustlessly, repeat.
- - diff --git a/apps/backend/public/stylesheets/style.css b/apps/backend/public/stylesheets/style.css deleted file mode 100644 index d20a135..0000000 --- a/apps/backend/public/stylesheets/style.css +++ /dev/null @@ -1,12 +0,0 @@ -body { - padding: 50px; - font: - 14px "Lucida Grande", - Helvetica, - Arial, - sans-serif; -} - -a { - color: #00b7ff; -} diff --git a/apps/backend/routes/index.js b/apps/backend/routes/index.js deleted file mode 100644 index d8c30c2..0000000 --- a/apps/backend/routes/index.js +++ /dev/null @@ -1,14 +0,0 @@ -var express = require("express"); -var router = express.Router(); - -/* GET home page. */ -router.get("/", function (req, res, next) { - res.render("index", { title: "Express" }); -}); - -// Status endpoint -router.get("/status", (req, res) => { - res.json({ status: "healthy" }); -}); - -module.exports = router; diff --git a/apps/backend/routes/orders.js b/apps/backend/routes/orders.js deleted file mode 100644 index 2dc5b3a..0000000 --- a/apps/backend/routes/orders.js +++ /dev/null @@ -1,40 +0,0 @@ -var express = require("express"); -var router = express.Router(); - -// In-memory database -let orders = []; - -// Orders CRUD -router.get("/", (req, res) => { - res.json(orders); -}); - -router.post("/", express.json(), (req, res) => { - const order = { - id: Date.now().toString(), - ...req.body, - status: "pending", - createdAt: new Date().toISOString(), - }; - orders.push(order); - res.status(201).json(order); -}); - -router.get("/:id", (req, res) => { - const order = orders.find((o) => o.id === req.params.id); - if (!order) { - return res.status(404).json({ error: "Order not found" }); - } - res.json(order); -}); - -router.put("/:id", express.json(), (req, res) => { - const index = orders.findIndex((o) => o.id === req.params.id); - if (index === -1) { - return res.status(404).json({ error: "Order not found" }); - } - orders[index] = { ...orders[index], ...req.body }; - res.json(orders[index]); -}); - -module.exports = router; diff --git a/apps/backend/routes/routes.go b/apps/backend/routes/routes.go new file mode 100644 index 0000000..4e1e18b --- /dev/null +++ b/apps/backend/routes/routes.go @@ -0,0 +1,15 @@ +package routes + +import ( + "net/http" + + routeutils "github.com/keep-starknet-strange/broly/backend/routes/utils" +) + +func InitRoutes() { + // Base route needed for health checks + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + routeutils.SetupHeaders(w) + w.WriteHeader(http.StatusOK) + }) +} diff --git a/apps/backend/routes/utils/middleware.go b/apps/backend/routes/utils/middleware.go new file mode 100644 index 0000000..57bca1d --- /dev/null +++ b/apps/backend/routes/utils/middleware.go @@ -0,0 +1,34 @@ +package routeutils + +import ( + "net/http" + + "github.com/keep-starknet-strange/broly/backend/internal/config" +) + +// Middleware functions for routes +// Return true if middleware stops the request + +func NonProductionMiddleware(w http.ResponseWriter, r *http.Request) bool { + if config.Conf.Api.Production { + WriteErrorJson(w, http.StatusNotImplemented, "Route is disabled in production") + return true + } + + return false +} + +func AuthMiddleware(w http.ResponseWriter, r *http.Request) bool { + // TODO: Implement authentication + return false +} + +func AdminMiddleware(w http.ResponseWriter, r *http.Request) bool { + // TODO: Implement admin authentication + if config.Conf.Api.Admin { + return false + } else { + WriteErrorJson(w, http.StatusUnauthorized, "Admin is required") + return true + } +} diff --git a/apps/backend/routes/utils/requests.go b/apps/backend/routes/utils/requests.go new file mode 100644 index 0000000..f003740 --- /dev/null +++ b/apps/backend/routes/utils/requests.go @@ -0,0 +1,30 @@ +package routeutils + +import ( + "encoding/json" + "io" + "net/http" +) + +// ReadJsonBody reads the body of an http.Request and unmarshals it into a struct. +// +// Generic Param: +// bodyType: The type of the struct to unmarshal the body into. +// Parameters: +// r: The http.Request to read the body from. +// Returns: +// *bodyType: A pointer to the unmarshaled body. +func ReadJsonBody[bodyType any](r *http.Request) (*bodyType, error) { + reqBody, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + + var body bodyType + err = json.Unmarshal(reqBody, &body) + if err != nil { + return nil, err + } + + return &body, nil +} diff --git a/apps/backend/routes/utils/responses.go b/apps/backend/routes/utils/responses.go new file mode 100644 index 0000000..d14ffb2 --- /dev/null +++ b/apps/backend/routes/utils/responses.go @@ -0,0 +1,57 @@ +package routeutils + +import ( + "net/http" + "strings" + + "github.com/keep-starknet-strange/broly/backend/internal/config" +) + +func SetupAccessHeaders(w http.ResponseWriter) { + config := config.Conf.Api + + // TODO: Process multiple origins in the future. + if len(config.AllowOrigins) > 0 { + w.Header().Set("Access-Control-Allow-Origin", config.AllowOrigins[0]) + } + methods := strings.Join(config.AllowMethods, ", ") + w.Header().Set("Access-Control-Allow-Methods", methods) + + headers := strings.Join(config.AllowHeaders, ", ") + w.Header().Set("Access-Control-Allow-Headers", headers) +} + +func SetupHeaders(w http.ResponseWriter) { + SetupAccessHeaders(w) + w.Header().Set("Content-Type", "application/json") +} + +func BasicErrorJson(err string) []byte { + return []byte(`{"error": "` + err + `"}`) +} + +func WriteErrorJson(w http.ResponseWriter, errCode int, err string) { + SetupHeaders(w) + w.WriteHeader(errCode) + w.Write(BasicErrorJson(err)) +} + +func BasicResultJson(result string) []byte { + return []byte(`{"result": "` + result + `"}`) +} + +func WriteResultJson(w http.ResponseWriter, result string) { + SetupHeaders(w) + w.WriteHeader(http.StatusOK) + w.Write(BasicResultJson(result)) +} + +func BasicDataJson(data string) []byte { + return []byte(`{"data": ` + data + `}`) +} + +func WriteDataJson(w http.ResponseWriter, data string) { + SetupHeaders(w) + w.WriteHeader(http.StatusOK) + w.Write(BasicDataJson(data)) +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..2a11340 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,18 @@ +FROM node:21.7.1-alpine + +RUN apk add --no-cache bash git jq curl + +WORKDIR /app +COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml ./ + +WORKDIR /app/apps/web +COPY ./apps/web/package.json ./ +RUN npm install +COPY ./apps/web ./ + +EXPOSE 5173 + +SHELL ["/bin/bash", "-c"] +# Clear the entrypoint +ENTRYPOINT [] +CMD ["npm", "run", "dev:host"] diff --git a/apps/web/package.json b/apps/web/package.json index adee9bb..7d15a18 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:host": "vite --host", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" @@ -16,6 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@types/node": "^22.10.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", diff --git a/apps/web/src/api/inscriptions.tsx b/apps/web/src/api/inscriptions.tsx new file mode 100644 index 0000000..3227626 --- /dev/null +++ b/apps/web/src/api/inscriptions.tsx @@ -0,0 +1,63 @@ +import { fetchWrapper, useMock, mockResponse } from "./requests"; +import { mockInscription, mockInscriptionViews, mockRequestViews } from "./mock"; + +export const getNewInscriptions = async (pageLength: number, page: number) => { + if (useMock) return mockResponse(mockInscriptionViews); + return await fetchWrapper( + `get-new-inscriptions?pageLength=${pageLength}&page=${page}` + ); +}; + +export const getHotInscriptions = async (pageLength: number, page: number) => { + if (useMock) return mockResponse(mockInscriptionViews); + return await fetchWrapper( + `get-hot-inscriptions?pageLength=${pageLength}&page=${page}` + ); +} + +export const getMyInscriptions = async (address: string, pageLength: number, page: number) => { + if (useMock) return mockResponse(mockInscriptionViews); + return await fetchWrapper( + `get-my-inscriptions?address=${address}&pageLength=${pageLength}&page=${page}` + ); +} + +export const getMyNewInscriptions = async (address: string, pageLength: number, page: number) => { + if (useMock) return mockResponse(mockInscriptionViews); + return await fetchWrapper( + `get-my-new-inscriptions?address=${address}&pageLength=${pageLength}&page=${page}` + ); +} + +export const getMyTopInscriptions = async (address: string, pageLength: number, page: number) => { + if (useMock) return mockResponse(mockInscriptionViews); + return await fetchWrapper( + `get-my-top-inscriptions?address=${address}&pageLength=${pageLength}&page=${page}` + ); +} + +export const getInscriptionRequests = async (pageLength: number, page: number) => { + if (useMock) return mockResponse(mockRequestViews); + return await fetchWrapper( + `get-inscription-requests?pageLength=${pageLength}&page=${page}` + ); +} + +export const getMyInscriptionRequests = async (address: string, pageLength: number, page: number) => { + if (useMock) return mockResponse(mockRequestViews); + return await fetchWrapper( + `get-my-inscription-requests?address=${address}&pageLength=${pageLength}&page=${page}` + ); +} + +export const uploadInscriptionImg = async (file: File) => { + return await fetchWrapper('upload-inscription-img', { + method: 'POST', + body: file + }); +} + +export const getInscription = async (id: string) => { + if (useMock) return mockResponse(mockInscription); + return await fetchWrapper(`get-inscription?id=${id}`); +} diff --git a/apps/web/src/api/mock.tsx b/apps/web/src/api/mock.tsx new file mode 100644 index 0000000..ac6eb1e --- /dev/null +++ b/apps/web/src/api/mock.tsx @@ -0,0 +1,136 @@ +export const mockAddress = '1234567890abcdef1234567890abcde'; + +export const mockInscriptionViews = [ + { + id: 1, + content: "Hello, Bitcoin!", + type: "text" + }, + { + id: 2, + content: "https://ordiscan.com/content/c17dd02a7f216f4b438ab1a303f518abfc4d4d01dcff8f023cf87c4403cb54cai0", + type: "image" + }, + { + id: 3, + content: "Hello, World 2!\nThis is multiline text.\nThis text is long.\nSo long that you will need to scroll\nif you want to see all of it.\nLorum\nIpsum\nYo yo yo!\nThis is multiline text.\nThis text is long.\nSo long that you will need to scroll\nif you want to see all of it.\nThis is multiline text.\nThis text is long.\n", + type: "text" + }, + { + id: 4, + content: "https://ordiscan.com/content/1008850869eb564cad900c316a02f65854f531b31a2ef96bacecd536be96b031i0", + type: "image" + }, + { + id: 5, + content: "https://ordiscan.com/content/79e63d4fd4f98d239394443798cabf6482821b94042fd299233ff93acb83bf63i0", + type: "image" + }, + { + id: 6, + content: "https://ordiscan.com/content/406e019545eb6e31592ba3859261018f8391af889d2791c2a0c1182964f1339ei0", + type: "image" + }, + { + id: 7, + content: "Bitcoin Message sent from Starknet!", + type: "text" + }, + { + id: 8, + content: "https://www.quantumcats.xyz/collection/vwoieaperz/cat0000.png", + type: "image" + }, + { + id: 9, + content: "0x62000100\nOP_CAT\n0x62000100", + type: "text" + }, + { + id: 10, + content: "https://ordiscan.com/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + type: "image" + }, + { + id: 11, + content: "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks.", + type: "text" + }, + { + id: 12, + content: "100K BTC", + type: "text" + }, + { + id: 13, + content: "https://ordiscan.com/content/e3e29332b269d0ae3fa28ac80427065d31b75f2c92baa729a3f8de363a0d66f6i0", + type: "image" + }, + { + id: 14, + content: "https://www.quantumcats.xyz/collection/vwoieaperz/cat0001.png", + type: "image" + }, + { + id: 15, + content: "https://ordiscan.com/content/31833061114c2ee53d63dba53ef0bc2af741c87463cf573a4e211196883a5f2di0", + type: "gif" + }, + { + id: 16, + content: "BIP-420 - https://github.com/bip420/bip420", + type: "text" + }, +]; + +export const mockRequestViews = [ + { + id: 1, + content: "Hello, Starknet!", + type: "text" + }, + { + id: 2, + content: "https://www.quantumcats.xyz/collection/vwoieaperz/cat0004.png", + type: "image" + }, + { + id: 3, + content: "Hello, World 2!\nThis is multiline text.\nThis text is long.\nSo long that you will need to scroll\nif you want to see all of it.\nLorum\nIpsum\nYo yo yo", + type: "text" + }, + { + id: 4, + content: "https://ordiscan.com/content/2edd2a1972beafeee32c98ca64ea48d1eccd012963bc4066895d74d35ad40209i0", + type: "gif" + }, + { + id: 5, + content: "https://ordiscan.com/content/e4520773d3d7a182ed4bd8875626419db5db3ec73fcacd4cc48ad060afb02a30i0", + type: "image" + } +]; + +export const mockInscription = { + id: 1, + content: "https://www.quantumcats.xyz/collection/vwoieaperz/cat0000.png", + type: "image", + owner: "Brandon", + sat_number: "1,012,345,678,910", + minted: new Date(), + minted_block: 800000, + properties: [ + { + name: "Rarity", + value: "Legendary", + }, + { + name: "Color", + value: "Purple", + }, + { + name: "Shape", + value: "Round", + }, + ] +}; diff --git a/apps/web/src/api/requests.tsx b/apps/web/src/api/requests.tsx new file mode 100644 index 0000000..4575cb4 --- /dev/null +++ b/apps/web/src/api/requests.tsx @@ -0,0 +1,31 @@ +export const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080'; +export const useMock = (import.meta.env.VITE_USE_MOCK === undefined || import.meta.env.VITE_USE_MOCK === '' || import.meta.env.VITE_USE_MOCK === 'true'); + +export const fetchWrapper = async (url: string, options = {}) => { + const controller = new AbortController(); + const signal = controller.signal; + try { + const response = await fetch(`${backendUrl}/${url}`, { + mode: 'cors', + signal, + ...options + }); + if (!response.ok) { + console.log(`Failed to fetch ${url}, got response:`, response); + throw new Error(`Failed to fetch ${url} with status ${response.status}`); + } + return await response.json(); + } catch (err) { + console.log(`Error while fetching ${url}:`, err); + throw err; // Re-throw the error for further handling if needed + } finally { + controller.abort(); // Ensure the request is aborted after completion or error + } +}; + +export const mockResponse = (data: any) => { + return { + data, + status: 200 + }; +} diff --git a/apps/web/src/components/Pagination.tsx b/apps/web/src/components/Pagination.tsx new file mode 100644 index 0000000..f434158 --- /dev/null +++ b/apps/web/src/components/Pagination.tsx @@ -0,0 +1,29 @@ +export function Pagination(props: any) { + const hasMore = () => { + console.log + return ( + props.data.length >= props.stateValue.pageLength * props.stateValue.page + ); + }; + + const handleLoadmore = () => { + props.setState((item: any) => ({ + ...item, + page: props.stateValue.page + 1, + pageLength: props.stateValue.pageLength + })); + }; + + return ( +Create
)} - +