Skip to content

Commit

Permalink
Feat: backend (#269)
Browse files Browse the repository at this point in the history
Added a backend package that will read indexer data and forward it.

### Endpoints

#### Deploy
- /deploy - Returns all deploys
- /deploy/:token - Returns specific token deploy
- /deploy/owner/:owner - Returns token deploys of an owner

#### Launch
- /launch - Returns all launches
- /launch/:token - Returns specific token launch

#### Transfer
- /transfer - Returns all transfers
- /transfer/:token - Returns transfers of a specific token
  • Loading branch information
ugur-eren authored Jul 18, 2024
1 parent 2e1f96a commit 59a1d2e
Show file tree
Hide file tree
Showing 18 changed files with 770 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PORT=3001
INDEXER_DB_CONNECTION_STRING=postgresql://admin:password@localhost:5432/indexer
3 changes: 3 additions & 0 deletions packages/backend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['../../.eslintrc.js', '@uniswap/eslint-config/node'],
}
31 changes: 31 additions & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "backend",
"version": "0.1.0",
"license": "MIT",
"scripts": {
"start": "tsx src/index.ts",
"build": "pkgroll",
"lint": "eslint . --max-warnings=0 --ignore-path ../../.eslintignore",
"lint:fix": "eslint src --fix"
},
"dependencies": {
"cors": "^2.8.5",
"drizzle-orm": "^0.32.0",
"express": "^4.19.2",
"helmet": "^7.1.0",
"pg": "^8.12.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"@types/pg": "^8.11.6",
"@uniswap/eslint-config": "^1.2.0",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.23.0",
"eslint": "^8.0.0",
"pkgroll": "^2.0.2",
"tsx": "^4.7.3",
"typescript": "^5.4.5"
}
}
22 changes: 22 additions & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'dotenv/config'

import cors from 'cors'
import express from 'express'
import helmet from 'helmet'

import router from './router'

const app = express()

app.use(cors())
app.use(helmet())
app.use(express.urlencoded({ extended: false }))
app.use(express.json())

app.use('/', router)

const PORT = Number(process.env.PORT) || 3001

app.listen(PORT, () => {
console.info(`Express server started listening on port ${PORT}`)
})
16 changes: 16 additions & 0 deletions packages/backend/src/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from 'express'

import deploy from './routes/deploy'
import health from './routes/health'
import launch from './routes/launch'
import transfer from './routes/transfer'

const Router = express.Router()

Router.use('/health', health)

Router.use('/deploy', deploy)
Router.use('/launch', launch)
Router.use('/transfer', transfer)

export default Router
60 changes: 60 additions & 0 deletions packages/backend/src/routes/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { eq } from 'drizzle-orm'
import express from 'express'

import { db, deploy } from '../services/db'
import { ErrorCode } from '../utils/error'
import { isValidStarknetAddress } from '../utils/helpers'
import { HTTPStatus } from '../utils/http'

const Router = express.Router()

Router.get('/', async (req, res) => {
try {
const deploys = await db.select().from(deploy)

res.status(HTTPStatus.OK).send(deploys)
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

Router.get('/:token', async (req, res) => {
try {
const { token } = req.params

if (!isValidStarknetAddress(token)) {
res.status(HTTPStatus.BadRequest).send({ code: ErrorCode.BAD_REQUEST, message: 'Invalid token address' })
return
}

const deploys = await db.select().from(deploy).where(eq(deploy.token, token)).limit(1)

if (!deploys[0]) {
res.status(HTTPStatus.NotFound).send({ code: ErrorCode.TOKEN_NOT_FOUND, message: 'Token not found' })
return
}

res.status(HTTPStatus.OK).send(deploys[0])
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

Router.get('/owner/:owner', async (req, res) => {
try {
const { owner } = req.params

if (!isValidStarknetAddress(owner)) {
res.status(HTTPStatus.BadRequest).send({ code: ErrorCode.BAD_REQUEST, message: 'Invalid owner address' })
return
}

const deploys = await db.select().from(deploy).where(eq(deploy.owner, owner))

res.status(HTTPStatus.OK).send(deploys)
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

export default Router
11 changes: 11 additions & 0 deletions packages/backend/src/routes/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from 'express'

import { HTTPStatus } from '../utils/http'

const Router = express.Router()

Router.get('/', async (req, res) => {
res.status(HTTPStatus.OK).send()
})

export default Router
43 changes: 43 additions & 0 deletions packages/backend/src/routes/launch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { eq } from 'drizzle-orm'
import express from 'express'

import { db, launch } from '../services/db'
import { ErrorCode } from '../utils/error'
import { isValidStarknetAddress } from '../utils/helpers'
import { HTTPStatus } from '../utils/http'

const Router = express.Router()

Router.get('/', async (req, res) => {
try {
const launches = await db.select().from(launch)

res.status(HTTPStatus.OK).send(launches)
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

Router.get('/:token', async (req, res) => {
try {
const { token } = req.params

if (!isValidStarknetAddress(token)) {
res.status(HTTPStatus.BadRequest).send({ code: ErrorCode.BAD_REQUEST, message: 'Invalid token address' })
return
}

const launches = await db.select().from(launch).where(eq(launch.token, token)).limit(1)

if (!launches[0]) {
res.status(HTTPStatus.NotFound).send({ code: ErrorCode.TOKEN_NOT_FOUND, message: 'Token not found' })
return
}

res.status(HTTPStatus.OK).send(launches[0])
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

export default Router
38 changes: 38 additions & 0 deletions packages/backend/src/routes/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { eq } from 'drizzle-orm'
import express from 'express'

import { db, transfer } from '../services/db'
import { ErrorCode } from '../utils/error'
import { isValidStarknetAddress } from '../utils/helpers'
import { HTTPStatus } from '../utils/http'

const Router = express.Router()

Router.get('/', async (req, res) => {
try {
const transfers = await db.select().from(transfer)

res.status(HTTPStatus.OK).send(transfers)
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

Router.get('/:token', async (req, res) => {
try {
const { token } = req.params

if (!isValidStarknetAddress(token)) {
res.status(HTTPStatus.BadRequest).send({ code: ErrorCode.BAD_REQUEST, message: 'Invalid token address' })
return
}

const transfers = await db.select().from(transfer).where(eq(transfer.token, token))

res.status(HTTPStatus.OK).send(transfers)
} catch (error) {
res.status(HTTPStatus.InternalServerError).send(error)
}
})

export default Router
52 changes: 52 additions & 0 deletions packages/backend/src/services/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { drizzle } from 'drizzle-orm/node-postgres'
import { bigint, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { Pool } from 'pg'

if (!process.env.INDEXER_DB_CONNECTION_STRING) {
throw new Error('INDEXER_DB_CONNECTION_STRING environment variable is not set')
}

const pool = new Pool({
connectionString: process.env.INDEXER_DB_CONNECTION_STRING,
})

export const db = drizzle(pool)

const commonSchema = {
cursor: bigint('_cursor', { mode: 'number' }),
createdAt: timestamp('created_at', { mode: 'date', withTimezone: false }),

network: text('network'),
blockHash: text('block_hash'),
blockNumber: bigint('block_number', { mode: 'number' }),
blockTimestamp: timestamp('block_timestamp', { mode: 'date', withTimezone: false }),
transactionHash: text('transaction_hash'),
}

export const deploy = pgTable('unrugmeme_deploy', {
...commonSchema,

token: text('memecoin_address').primaryKey(),
owner: text('owner_address'),
name: text('name'),
symbol: text('symbol'),
initialSupply: text('initial_supply'),
})

export const launch = pgTable('unrugmeme_launch', {
...commonSchema,

token: text('memecoin_address').primaryKey(),
quoteToken: text('quote_token'),
exchangeName: text('exchange_name'),
})

export const transfer = pgTable('unrugmeme_transfers', {
...commonSchema,

transferId: text('transfer_id').primaryKey(),
from: text('from_address'),
to: text('to_address'),
token: text('memecoin_address'),
amount: text('amount'),
})
7 changes: 7 additions & 0 deletions packages/backend/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const ErrorCodesArray = ['BAD_REQUEST', 'TOKEN_NOT_FOUND'] as const

export type ErrorCode = (typeof ErrorCodesArray)[number]

export const ErrorCode = Object.fromEntries(ErrorCodesArray.map((code) => [code, code])) as {
[code in ErrorCode]: code
}
12 changes: 12 additions & 0 deletions packages/backend/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Checks if a given string is a valid StarkNet address.
*
* A valid StarkNet address must start with '0x' followed by 63 or 64 hexadecimal characters.
*
* @param address - The string to be tested against the StarkNet address format.
* @returns `true` if the string is a valid StarkNet address, otherwise `false`.
*/
export function isValidStarknetAddress(address: string): boolean {
const regex = /^0x[0-9a-fA-F]{50,64}$/
return regex.test(address)
}
28 changes: 28 additions & 0 deletions packages/backend/src/utils/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const HTTPStatus = {
OK: 200,
Created: 201,
Accepted: 202,
NoContent: 204,
ResetContent: 205,
PartialContent: 206,

MovedPermanently: 301,
Found: 302,
SeeOther: 303,
TemporaryRedirect: 307,
PermanentRedirect: 308,

BadRequest: 400,
Unauthorized: 401,
Forbidden: 403,
NotFound: 404,
NotAcceptable: 406,
Timeout: 408,
Gone: 410,
TooManyRequests: 429,

InternalServerError: 500,
NotImplemented: 501,
BadGateway: 502,
ServiceUnavailable: 503,
}
9 changes: 9 additions & 0 deletions packages/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"baseUrl": "."
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
13 changes: 13 additions & 0 deletions packages/indexers/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ services:
networks:
- backend

unruggableMemecoin-deploy-indexer:
environment:
- AUTH_TOKEN=${AUTH_TOKEN}
image: quay.io/apibara/sink-postgres:latest
command: 'run ./indexer/unruggableMemecoin-deploy.indexer.ts --connection-string postgresql://admin:password@postgres:5432/indexer -A ${AUTH_TOKEN}'
volumes:
- ./src:/indexer
depends_on:
- postgres
networks:
- backend
restart: on-failure

unruggableMemecoin-launch-indexer:
environment:
- AUTH_TOKEN=${AUTH_TOKEN}
Expand Down
15 changes: 15 additions & 0 deletions packages/indexers/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ create table unrugmeme_transfers(
_cursor bigint
);

create table unrugmeme_deploy(
network text,
block_hash text,
block_number bigint,
block_timestamp timestamp,
transaction_hash text,
memecoin_address text unique primary key,
owner_address text,
name text,
symbol text,
initial_supply text,
created_at timestamp default current_timestamp,
_cursor bigint
);

create table unrugmeme_launch(
network text,
block_hash text,
Expand Down
Loading

0 comments on commit 59a1d2e

Please sign in to comment.