diff --git a/bun.lockb b/bun.lockb index 7fad050..8ffcda2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b20b8df..1e2bcea 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dotenv": "^16.3.1", "ethers": "^6.8.0", "hono": "^3.7.2", + "tslog": "^4.9.2", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/config.ts b/src/config.ts index ff180e5..dd65b04 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,7 +11,7 @@ export const DEFAULT_DB_NAME = "clickhouse_sink"; export const DEFAULT_DB_USERNAME = "default"; export const DEFAULT_DB_PASSWORD = ""; export const DEFAULT_MAX_ELEMENTS_QUERIES = 10; -export const DEFAULT_VERBOSE = false; +export const DEFAULT_VERBOSE = true; const CommanderSchema = z.object({ NODE_ENV: z.string().optional(), @@ -23,7 +23,8 @@ const CommanderSchema = z.object({ password: z.string().default(DEFAULT_DB_PASSWORD), maxElementsQueried: z.coerce.number().default(DEFAULT_MAX_ELEMENTS_QUERIES).describe( 'Maximum number of query elements when using arrays as parameters' - ) + ), + verbose: z.boolean().default(DEFAULT_VERBOSE), }); export function decode(data: unknown) { diff --git a/src/index.ts b/src/index.ts index 4b295d7..0cfcafc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,33 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { TypedResponse } from 'hono'; -import { serveStatic } from 'hono/bun' -import { logger } from 'hono/logger'; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { TypedResponse } from "hono"; +import { serveStatic } from "hono/bun"; +import { logger } from "./logger"; import pkg from "../package.json"; -import * as routes from './routes'; +import * as routes from "./routes"; import { type SupplyResponseSchema, - type SupplySchema, type ContractSchema, type ContractResponseSchema, type BalanceSchema, type BalanceResponseSchema -} from './schemas'; -import { getTotalSupply, getContract, getBalance } from './queries'; -import config from './config' -import { HTTPException } from 'hono/http-exception'; + type SupplySchema, + type ContractSchema, + type ContractResponseSchema, + type BalanceSchema, + type BalanceResponseSchema, +} from "./schemas"; +import { getTotalSupply, getContract, getBalance } from "./queries"; +import config from "./config"; +import { HTTPException } from "hono/http-exception"; import { banner } from "./banner"; export function generateApp() { - const app = new OpenAPIHono(); - if (config.NODE_ENV !== "production") - app.use('*', logger()); - - app.use('/swagger/*', serveStatic({ root: './' })) + app.use("/swagger/*", serveStatic({ root: "./" })); - app.doc('/openapi', { - openapi: '3.0.0', + app.doc("/openapi", { + openapi: "3.0.0", info: { version: pkg.version, - title: 'ERC20 API', + title: "ERC20 API", }, }); @@ -40,65 +40,58 @@ export function generateApp() { error_code = err.status; } + logger.error(error_message); return c.json({ error_message }, error_code); }); - app.openapi(routes.indexRoute, (c) => { return { - response: c.text(banner()) + response: c.text(banner()), } as TypedResponse; }); - app.openapi(routes.TotalSupplyQueryRoute, async (c) => { // @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200) - const { address, block, contract } = c.req.valid('query') as SupplySchema; + const { address, block, contract } = c.req.valid("query") as SupplySchema; if (contract) { let supply = await getTotalSupply(address, block); let contract_info = await getContract(address); - let result = Object.assign({}, supply, contract_info) + let result = Object.assign({}, supply, contract_info); return { - response: c.json(result) + response: c.json(result), } as TypedResponse; - } - else { + } else { return { - response: c.json(await getTotalSupply(address, block)) + response: c.json(await getTotalSupply(address, block)), } as TypedResponse; } - }); - - - - app.openapi(routes.ContractQueryRoute, async (c) => { - const { address } = c.req.valid('query') as ContractSchema; + const { address } = c.req.valid("query") as ContractSchema; return { - response: c.json(await getContract(address)) + response: c.json(await getContract(address)), } as TypedResponse; }); - app.openapi(routes.BalanceQueryRoute, async (c) => { // @ts-expect-error: Suppress type of parameter expected to be never (see https://github.com/honojs/middleware/issues/200) - const { wallet, address, block } = c.req.valid('query') as BalanceSchema; + const { wallet, address, block } = c.req.valid("query") as BalanceSchema; return { - response: c.json(await getBalance(wallet, address, block)) + response: c.json(await getBalance(wallet, address, block)), } as TypedResponse; }); - return app; } +if (config.verbose) logger.enable(); +logger.info( + `Server listening on http://${config.hostname}${config.port}/` +); Bun.serve({ port: config.port, hostname: config.hostname, - fetch: generateApp().fetch -} -) + fetch: generateApp().fetch, +}); -console.log("Server listening on http://" + config.hostname + ":" + config.port + "/") diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..60b635a --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,21 @@ +import { Logger, type ILogObj } from "tslog"; +import { name } from "../package.json" assert { type: "json" }; + +class TsLogger extends Logger { + constructor() { + super(); + this.settings.minLevel = 5; + this.settings.name = name; + } + + public enable(type: "pretty" | "json" = "pretty") { + this.settings.type = type; + this.settings.minLevel = 0; + } + + public disable() { + this.settings.type = "hidden"; + } +} + +export const logger = new TsLogger(); \ No newline at end of file diff --git a/src/queries.ts b/src/queries.ts index 87b31bb..395acfb 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -10,7 +10,7 @@ const client = createClient({ }); function formatAddress(address: string) { - if (address.startsWith('0x')) { + if (address.startsWith("0x")) { // Remove the "0x" prefix and return the address return address.slice(2); } @@ -48,7 +48,6 @@ export async function getTotalSupply( return { error: "Invalid Address" }; } } - } export async function getContract(address: string | undefined) { @@ -68,7 +67,6 @@ export async function getContract(address: string | undefined) { return { error: "Invalid Address" }; } } - } export async function getBalance( @@ -156,5 +154,4 @@ export async function getBalance( return { error: "Invalid Wallet" }; } } - } diff --git a/src/routes.ts b/src/routes.ts index c25e0a9..c029482 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,71 +1,66 @@ -import { createRoute } from '@hono/zod-openapi'; -import * as schemas from './schemas'; - - +import { createRoute } from "@hono/zod-openapi"; +import * as schemas from "./schemas"; export const indexRoute = createRoute({ - method: 'get', - path: '/', + method: "get", + path: "/", responses: { 200: { - description: 'Index page banner.', + description: "Index page banner.", }, }, }); - export const TotalSupplyQueryRoute = createRoute({ - method: 'get', - path: '/supply', + method: "get", + path: "/supply", request: { query: schemas.SupplySchema, }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: schemas.SupplyResponseSchema, }, }, - description: 'Get the total supply of an ERC20 contract', + description: "Get the total supply of an ERC20 contract", }, }, }); - export const ContractQueryRoute = createRoute({ - method: 'get', - path: '/contract', + method: "get", + path: "/contract", request: { query: schemas.ContractSchema, }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: schemas.ContractResponseSchema, }, }, - description: 'Get the ERC20 contract information', + description: "Get the ERC20 contract information", }, }, }); - export const BalanceQueryRoute = createRoute({ - method: 'get', - path: '/balance', + method: "get", + path: "/balance", request: { query: schemas.BalanceSchema, }, responses: { 200: { content: { - 'application/json': { + "application/json": { schema: schemas.BalanceResponseSchema, }, }, - description: 'Get the ERC20 contract information', + description: "Get the ERC20 contract information", }, }, -}); \ No newline at end of file +}); diff --git a/src/schemas.ts b/src/schemas.ts index cc5118e..6bed760 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,151 +1,141 @@ -import { z } from '@hono/zod-openapi'; -import { ethers } from 'ethers' - +import { z } from "@hono/zod-openapi"; +import { ethers } from "ethers"; export const ContractSchema = z.object({ - address: z.string().refine((val) => ethers.isAddress(val)) + address: z + .string() + .refine((val) => ethers.isAddress(val)) .openapi({ param: { - name: 'address', - in: 'query', + name: "address", + in: "query", }, - example: 'dAC17F958D2ee523a2206206994597C13D831ec7', - }) + example: "dAC17F958D2ee523a2206206994597C13D831ec7", + }), }); export type ContractSchema = z.infer; export const ContractResponseSchema = z.object({ - address: z.string() - .openapi({ - example: 'dAC17F958D2ee523a2206206994597C13D831ec7', - }) - , - name: z.string() - .openapi({ - example: 'Tether USD', - }) - , - symbol: z.string() - .openapi({ - example: 'USDT', - }) - , - - decimals: z.string().or(z.number()) - .openapi({ - example: '6', - }) - , + address: z.string().openapi({ + example: "dAC17F958D2ee523a2206206994597C13D831ec7", + }), + name: z.string().openapi({ + example: "Tether USD", + }), + symbol: z.string().openapi({ + example: "USDT", + }), + decimals: z.string().or(z.number()).openapi({ + example: "6", + }), chain: z.string().openapi({ - example: 'eth', - }) + example: "eth", + }), }); export type ContractResponseSchema = z.infer; - - export const SupplySchema = z.object({ - address: z.string().refine((val) => ethers.isAddress(val)) + address: z + .string() + .refine((val) => ethers.isAddress(val)) .openapi({ param: { - name: 'address', - in: 'query', + name: "address", + in: "query", }, - example: 'dAC17F958D2ee523a2206206994597C13D831ec7', - }) - , - block: z.coerce.number().optional().openapi({ - param: { - name: 'block', - in: 'query', - }, - example: 1000000, - }), + example: "dAC17F958D2ee523a2206206994597C13D831ec7", + }), + block: z.coerce + .number() + .optional() + .openapi({ + param: { + name: "block", + in: "query", + }, + example: 1000000, + }), - contract: z.enum(["true", "false"]).transform((value) => value === "true").optional().openapi({ - param: { - name: 'contract', - in: 'query', - }, - example: true, - }) + contract: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .optional() + .openapi({ + param: { + name: "contract", + in: "query", + }, + example: true, + }), }); export type SupplySchema = z.infer; export const SupplyResponseSchema = z.object({ - address: z.string() - .openapi({ - example: 'dAC17F958D2ee523a2206206994597C13D831ec7', - }) - , - supply: z.string().or(z.number()) - .openapi({ - example: '10000000', - }) - , + address: z.string().openapi({ + example: "dAC17F958D2ee523a2206206994597C13D831ec7", + }), + supply: z.string().or(z.number()).openapi({ + example: "10000000", + }), block: z.number().or(z.string()).openapi({ example: 1000000, }), - chain: z.string() - .openapi({ - example: 'eth', - }) - , - contract: ContractResponseSchema.optional() + chain: z.string().openapi({ + example: "eth", + }), + contract: ContractResponseSchema.optional(), }); export type SupplyResponseSchema = z.infer; - - export const BalanceSchema = z.object({ + wallet: z + .string() + .refine((val) => ethers.isAddress(val)) + .openapi({ + param: { + name: "wallet", + in: "query", + }, + example: "a46fcc88d1e03f79e264ec48bcf05094401a6962", + }), - wallet: z.string().refine((val) => ethers.isAddress(val)).openapi({ - param: { - name: 'wallet', - in: 'query', - }, - example: 'a46fcc88d1e03f79e264ec48bcf05094401a6962', - }), - - address: z.string().refine((val) => ethers.isAddress(val)).optional() + address: z + .string() + .refine((val) => ethers.isAddress(val)) + .optional() .openapi({ param: { - name: 'address', - in: 'query', + name: "address", + in: "query", }, - example: 'd445d1c4b6d2f048b566ce6c079d20512985854e', - }) - , - block: z.coerce.number().optional().openapi({ - param: { - name: 'block', - in: 'query', - }, - example: 1000000, - }) + example: "d445d1c4b6d2f048b566ce6c079d20512985854e", + }), + block: z.coerce + .number() + .optional() + .openapi({ + param: { + name: "block", + in: "query", + }, + example: 1000000, + }), }); export type BalanceSchema = z.infer; export const BalanceResponseSchema = z.object({ - contract: z.string() - .openapi({ - example: 'd445d1c4b6d2f048b566ce6c079d20512985854e', - }) - , - balance: z.string().or(z.number()) - .openapi({ - example: '888', - }) - , + contract: z.string().openapi({ + example: "d445d1c4b6d2f048b566ce6c079d20512985854e", + }), + balance: z.string().or(z.number()).openapi({ + example: "888", + }), block: z.number().or(z.string()).openapi({ example: 1009707, }), - chain: z.string() - .openapi({ - example: 'eth', - }) - , - + chain: z.string().openapi({ + example: "eth", + }), }); export type BalanceResponseSchema = z.infer;