diff --git a/src/app.ts b/src/app.ts index 3eb82234..128ab0a3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,6 @@ -import fastify from 'fastify'; +import fastify, { type FastifySchemaCompiler } from 'fastify'; import { FastifyInstance } from 'fastify'; -import sensible from '@fastify/sensible'; +import sensible, { httpErrors } from '@fastify/sensible'; import compress from '@fastify/compress'; import bitcoinRoutes from './routes/bitcoin'; import tokenRoutes from './routes/token'; @@ -12,7 +12,7 @@ import { getSafeEnvs } from './env'; import container from './container'; import { asValue } from 'awilix'; import options from './options'; -import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod'; +import { serializerCompiler, ZodTypeProvider } from 'fastify-type-provider-zod'; import cors from './plugins/cors'; import { NetworkType } from './constants'; import rgbppRoutes from './routes/rgbpp'; @@ -23,6 +23,7 @@ import internalRoutes from './routes/internal'; import healthcheck from './plugins/healthcheck'; import sentry from './plugins/sentry'; import cron from './plugins/cron'; +import { ZodAny } from 'zod'; async function routes(fastify: FastifyInstance) { fastify.log.info(`Process env: ${JSON.stringify(getSafeEnvs(), null, 2)}`); @@ -59,6 +60,26 @@ async function routes(fastify: FastifyInstance) { } } +export const validatorCompiler: FastifySchemaCompiler = + ({ schema }) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data) => { + const result = schema.safeParse(data); + if (result.success) { + return { value: result.data }; + } + + const error = result.error; + if (error.errors.length) { + const firstError = error.errors[0]; + const propName = firstError.path.length ? firstError.path.join('.') : 'param'; + return { + error: httpErrors.badRequest(`Invalid ${propName}: ${error.errors[0].message}`), + }; + } + return { error }; + }; + export function buildFastify() { const app = fastify(options).withTypeProvider(); app.setValidatorCompiler(validatorCompiler); diff --git a/src/routes/bitcoin/block.ts b/src/routes/bitcoin/block.ts index fd02c844..eca96f74 100644 --- a/src/routes/bitcoin/block.ts +++ b/src/routes/bitcoin/block.ts @@ -13,7 +13,7 @@ const blockRoutes: FastifyPluginCallback, Server, ZodTypePr description: 'Get a block by its hash', tags: ['Bitcoin'], params: z.object({ - hash: z.string().describe('The Bitcoin block hash'), + hash: z.string().length(64, 'should be a 64-character hex string').describe('The Bitcoin block hash'), }), response: { 200: Block, @@ -35,7 +35,7 @@ const blockRoutes: FastifyPluginCallback, Server, ZodTypePr description: 'Get block transaction ids by its hash', tags: ['Bitcoin'], params: z.object({ - hash: z.string().describe('The Bitcoin block hash'), + hash: z.string().length(64, 'should be a 64-character hex string').describe('The Bitcoin block hash'), }), response: { 200: z.object({ @@ -59,7 +59,7 @@ const blockRoutes: FastifyPluginCallback, Server, ZodTypePr description: 'Get a block header by its hash', tags: ['Bitcoin'], params: z.object({ - hash: z.string().describe('The Bitcoin block hash'), + hash: z.string().length(64, 'should be a 64-character hex string').describe('The Bitcoin block hash'), }), response: { 200: z.object({ @@ -85,7 +85,11 @@ const blockRoutes: FastifyPluginCallback, Server, ZodTypePr description: 'Get a block hash by its height', tags: ['Bitcoin'], params: z.object({ - height: z.coerce.number().describe('The Bitcoin block height'), + height: z + .string() + .min(1, 'cannot be empty') + .pipe(z.coerce.number().min(0, 'cannot be negative')) + .describe('The Bitcoin block height'), }), response: { 200: z.object({ diff --git a/src/routes/bitcoin/transaction.ts b/src/routes/bitcoin/transaction.ts index 3e514238..fbd81886 100644 --- a/src/routes/bitcoin/transaction.ts +++ b/src/routes/bitcoin/transaction.ts @@ -38,7 +38,7 @@ const transactionRoutes: FastifyPluginCallback, Server, Zod description: 'Get a transaction by its txid', tags: ['Bitcoin'], params: z.object({ - txid: z.string().describe('The Bitcoin transaction id'), + txid: z.string().length(64, 'should be a 64-character hex string').describe('The Bitcoin transaction id'), }), response: { 200: Transaction, @@ -62,7 +62,7 @@ const transactionRoutes: FastifyPluginCallback, Server, Zod description: 'Get a transaction hex by its txid', tags: ['Bitcoin'], params: z.object({ - txid: z.string().describe('The Bitcoin transaction id'), + txid: z.string().length(64, 'should be a 64-character hex string').describe('The Bitcoin transaction id'), }), response: { 200: z.object({ diff --git a/src/routes/rgbpp/assets.ts b/src/routes/rgbpp/assets.ts index c4106aba..e4e4a651 100644 --- a/src/routes/rgbpp/assets.ts +++ b/src/routes/rgbpp/assets.ts @@ -19,7 +19,7 @@ const assetsRoute: FastifyPluginCallback, Server, ZodTypePr description: `Get RGB++ assets by BTC txid.`, tags: ['RGB++'], params: z.object({ - btc_txid: z.string(), + btc_txid: z.string().length(64, 'Should be a 64-character hex string'), }), response: { 200: z.array( @@ -65,8 +65,8 @@ const assetsRoute: FastifyPluginCallback, Server, ZodTypePr description: 'Get RGB++ assets by btc txid and vout', tags: ['RGB++'], params: z.object({ - btc_txid: z.string(), - vout: z.coerce.number(), + btc_txid: z.string().length(64, 'should be a 64-character hex string'), + vout: z.string().min(1, 'cannot be empty').pipe(z.coerce.number().min(0, 'cannot be negative')), }), response: { 200: z.array( diff --git a/src/routes/rgbpp/transaction.ts b/src/routes/rgbpp/transaction.ts index e849f8de..2ef92f7d 100644 --- a/src/routes/rgbpp/transaction.ts +++ b/src/routes/rgbpp/transaction.ts @@ -22,7 +22,9 @@ const transactionRoute: FastifyPluginCallback, Server, ZodT } const parsed = CKBVirtualResult.safeParse(value); if (!parsed.success) { - throw new Error(`Invalid CKB virtual result: ${JSON.stringify(parsed.error.flatten())}`); + throw fastify.httpErrors.badRequest( + `Invalid CKB virtual result: ${JSON.stringify(parsed.error.flatten())}`, + ); } return parsed.data; }), @@ -54,7 +56,7 @@ const transactionRoute: FastifyPluginCallback, Server, ZodT description: `Get the CKB transaction hash by BTC txid.`, tags: ['RGB++'], params: z.object({ - btc_txid: z.string(), + btc_txid: z.string().length(64, 'should be a 64-character hex string'), }), response: { 200: z.object({ @@ -116,7 +118,7 @@ const transactionRoute: FastifyPluginCallback, Server, ZodT `, tags: ['RGB++'], params: z.object({ - btc_txid: z.string(), + btc_txid: z.string().length(64, 'should be a 64-character hex string'), }), querystring: z.object({ with_data: z.enum(['true', 'false']).default('false'), diff --git a/test/routes/__snapshots__/token.test.ts.snap b/test/routes/__snapshots__/token.test.ts.snap index 526a3d58..77d8c975 100644 --- a/test/routes/__snapshots__/token.test.ts.snap +++ b/test/routes/__snapshots__/token.test.ts.snap @@ -1,13 +1,3 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`\`/token/generate\` - without params 1`] = ` -"[ - { - "code": "invalid_type", - "expected": "object", - "received": "null", - "path": [], - "message": "Expected object, received null" - } -]" -`; +exports[`\`/token/generate\` - without params 1`] = `"Invalid param: Expected object, received null"`;