Skip to content

Commit

Permalink
Adding zod dto to vault, initial zod-based scheam for Action types (#196
Browse files Browse the repository at this point in the history
)
  • Loading branch information
mattschoch authored Mar 27, 2024
1 parent cfbd9f4 commit dbf85b0
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 36 deletions.
16 changes: 10 additions & 6 deletions apps/vault/src/shared/filter/zod-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Response } from 'express'
import { ZodValidationException } from 'nestjs-zod'
import { ZodError } from 'zod'
import { Config, Env } from '../../main.config'

@Catch(ZodError)
// Catch both types, because the zodToDto function will throw a wrapped ZodValidationError that otherwise isn't picked up here
@Catch(ZodError, ZodValidationException)
export class ZodExceptionFilter implements ExceptionFilter {
private logger = new Logger(ZodExceptionFilter.name)

constructor(private configService: ConfigService<Config, true>) {}

catch(exception: ZodError, host: ArgumentsHost) {
catch(exception: ZodError | ZodValidationException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const status = HttpStatus.UNPROCESSABLE_ENTITY
const isProduction = this.configService.get('env') === Env.PRODUCTION

const zodError = exception instanceof ZodValidationException ? exception.getZodError() : exception

// Log as error level because Zod issues should be handled by the caller.
this.logger.error('Uncaught ZodError', {
exception
exception: zodError
})

response.status(status).json(
isProduction
? {
statusCode: status,
message: 'Internal validation error',
context: exception.errors
context: zodError.flatten()
}
: {
statusCode: status,
message: 'Internal validation error',
context: exception.errors,
stacktrace: exception.stack
context: zodError.flatten(),
stacktrace: zodError.stack
}
)
}
Expand Down
15 changes: 8 additions & 7 deletions apps/vault/src/vault/__test__/e2e/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,14 @@ describe('Sign', () => {
.set('authorization', `GNAP ${accessToken}`)
.send(payload)

expect(status).toEqual(HttpStatus.BAD_REQUEST)

expect(body).toEqual({
error: 'Bad Request',
message: ['request.transactionRequest.to must be an Ethereum address'],
statusCode: HttpStatus.BAD_REQUEST
})
expect(body).toEqual(
expect.objectContaining({
context: expect.any(Object),
message: 'Internal validation error',
statusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
expect(status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY)
})

it('signs', async () => {
Expand Down
13 changes: 12 additions & 1 deletion apps/vault/src/vault/http/rest/controller/sign.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { Request } from '@narval/policy-engine-shared'
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
import { createZodDto } from 'nestjs-zod'
import {
SignMessageActionSchema,
SignTransactionActionSchema,
SignTypedDataActionSchema
} from 'packages/policy-engine-shared/src/lib/schema/action.schema'
import { z } from 'zod'
import { ClientId } from '../../../../shared/decorator/client-id.decorator'
import { AuthorizationGuard } from '../../../../shared/guard/authorization.guard'
import { SigningService } from '../../../core/service/signing.service'
import { SignRequestDto } from '../dto/sign-request.dto'

const SignRequestSchema = z.object({
request: z.union([SignTransactionActionSchema, SignMessageActionSchema, SignTypedDataActionSchema])
})

class SignRequestDto extends createZodDto(SignRequestSchema) {}
@Controller('/sign')
@UseGuards(AuthorizationGuard)
export class SignController {
Expand Down
20 changes: 0 additions & 20 deletions apps/vault/src/vault/http/rest/dto/sign-request.dto.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/policy-engine-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './lib/schema/action.schema'
export * from './lib/schema/address.schema'
export * from './lib/schema/data-store.schema'
export * from './lib/schema/domain.schema'
Expand Down
74 changes: 74 additions & 0 deletions packages/policy-engine-shared/src/lib/schema/action.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { z } from 'zod'
import { Action } from '../type/action.type'
import { addressSchema } from './address.schema'
import { hexSchema } from './hex.schema'

export const AccessListSchema = z.array(
z.object({
address: addressSchema,
storageKeys: z.array(hexSchema)
})
)
// export type AccessList = z.infer<typeof AccessList>

export const ActionSchema = z.nativeEnum(Action)

export const BaseActionSchema = z.object({
action: ActionSchema,
nonce: z.string()
})
// export type BaseActionSchema = z.infer<typeof BaseActionSchema>

export const TransactionRequestSchema = z.object({
chainId: z.number(),
from: addressSchema,
nonce: z.number().optional(),
accessList: AccessListSchema.optional(),
data: hexSchema.optional(),
gas: z.coerce.bigint().optional(),
maxFeePerGas: z.coerce.bigint().optional(),
maxPriorityFeePerGas: z.coerce.bigint().optional(),
to: addressSchema.nullable().optional(),
type: z.literal('2').optional(),
value: hexSchema.optional()
})
// export type TransactionRequest = z.infer<typeof TransactionRequest>

export const SignTransactionActionSchema = z.intersection(
BaseActionSchema,
z.object({
action: z.literal(Action.SIGN_TRANSACTION),
resourceId: z.string(),
transactionRequest: TransactionRequestSchema
})
)
// export type SignTransactionAction = z.infer<typeof SignTransactionAction>

// Matching viem's SignableMessage options https://viem.sh/docs/actions/wallet/signMessage#message
export const SignableMessageSchema = z.union([
z.string(),
z.object({
raw: hexSchema
})
])
// export type SignableMessage = z.infer<typeof SignableMessage>

export const SignMessageActionSchema = z.intersection(
BaseActionSchema,
z.object({
action: z.literal(Action.SIGN_MESSAGE),
resourceId: z.string(),
message: SignableMessageSchema
})
)
// export type SignMessageAction = z.infer<typeof SignMessageAction>

export const SignTypedDataActionSchema = z.intersection(
BaseActionSchema,
z.object({
action: z.literal(Action.SIGN_TYPED_DATA),
resourceId: z.string(),
typedData: z.string()
})
)
// export type SignTypedDataAction = z.infer<typeof SignTypedDataAction>
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ describe('evm', () => {
expect(isAddress('foo')).toEqual(false)
expect(isAddress(address.toUpperCase())).toEqual(false)
})

it('requires 0x prefix', () => {
expect(isAddress(address.slice(2))).toEqual(false)
})
})

describe('getAddress', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/policy-engine-shared/src/lib/util/evm.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { Address } from '../type/domain.type'
* returns false.
*/
export const isAddress = (address: string): boolean => {
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(address)) {
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
return false
} else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
} else if (/^0x[0-9a-f]{40}$/.test(address) || /^0x[0-9A-F]{40}$/.test(address)) {
return true
} else {
return viemIsAddress(address)
Expand Down

0 comments on commit dbf85b0

Please sign in to comment.