Skip to content

Commit

Permalink
Introduce support for custom gas in transaction request
Browse files Browse the repository at this point in the history
Implement an initial design for a flexible request action type featuring custom
decoding and encoding schemas.

Refactor request and response DTOs to accommodate BigInt.
  • Loading branch information
wcalderipe committed Jan 17, 2024
1 parent 24b0cfc commit 5e9ee9d
Show file tree
Hide file tree
Showing 26 changed files with 554 additions and 190 deletions.
7 changes: 4 additions & 3 deletions apps/orchestration/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OrchestrationModule } from '@app/orchestration/orchestration.module'
import { ApplicationExceptionFilter } from '@app/orchestration/shared/filter/application-exception.filter'
import { ZodExceptionFilter } from '@app/orchestration/shared/filter/zod-exception.filter'
import { ClassSerializerInterceptor, INestApplication, Logger, ValidationPipe } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { NestFactory, Reflector } from '@nestjs/core'
Expand Down Expand Up @@ -64,10 +65,10 @@ const withGlobalInterceptors = (app: INestApplication): INestApplication => {
* @param configService - The configuration service instance.
* @returns The modified Nest application instance.
*/
const withGlobalExceptionFilter =
const withGlobalFilters =
(configService: ConfigService<Config, true>) =>
(app: INestApplication): INestApplication => {
app.useGlobalFilters(new ApplicationExceptionFilter(configService))
app.useGlobalFilters(new ApplicationExceptionFilter(configService), new ZodExceptionFilter(configService))

return app
}
Expand All @@ -89,7 +90,7 @@ async function bootstrap(): Promise<void> {
map(withSwagger),
map(withGlobalPipes),
map(withGlobalInterceptors),
map(withGlobalExceptionFilter(configService)),
map(withGlobalFilters(configService)),
switchMap((app) => app.listen(port))
)
)
Expand Down
9 changes: 8 additions & 1 deletion apps/orchestration/src/orchestration.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PolicyEngineModule } from '@app/orchestration/policy-engine/policy-engine.module'
import { Module } from '@nestjs/common'
import { ClassSerializerInterceptor, Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_INTERCEPTOR } from '@nestjs/core'
import { load } from './orchestration.config'
import { QueueModule } from './shared/module/queue/queue.module'

Expand All @@ -12,6 +13,12 @@ import { QueueModule } from './shared/module/queue/queue.module'
}),
QueueModule.forRoot(),
PolicyEngineModule
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ClassSerializerInterceptor
}
]
})
export class OrchestrationModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ describe('Policy Engine Cluster Facade', () => {
const signTransactionRequest = {
from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43',
to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23',
data: '0x'
data: '0x',
gas: '5000'
}
const payload = {
action: Action.SIGN_TRANSACTION,
Expand All @@ -143,7 +144,6 @@ describe('Policy Engine Cluster Facade', () => {
.set(REQUEST_HEADER_ORG_ID, org.id)
.send(payload)

expect(status).toEqual(HttpStatus.OK)
expect(body).toMatchObject({
id: expect.any(String),
status: AuthorizationRequestStatus.CREATED,
Expand All @@ -154,6 +154,7 @@ describe('Policy Engine Cluster Facade', () => {
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
expect(status).toEqual(HttpStatus.OK)
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ export class AuthorizationRequestService {
) {}

async create(input: CreateAuthorizationRequest): Promise<AuthorizationRequest> {
const authzRequest = await this.authzRequestRepository.create(input)
const now = new Date()

const authzRequest = await this.authzRequestRepository.create({
id: input.id || uuid(),
createdAt: input.createdAt || now,
updatedAt: input.updatedAt || now,
...input
})

await this.authzRequestProcessingProducer.add(authzRequest)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type TransactionRequest = {
data?: Hex
from: Address
to?: Address | null
gas: bigint
}

export type SignTransactionAuthorizationRequest = SharedAuthorizationRequest & {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,10 @@
import { AuthorizationRequestService } from '@app/orchestration/policy-engine/core/service/authorization-request.service'
import { Action, CreateAuthorizationRequest } from '@app/orchestration/policy-engine/core/type/domain.type'
import { AuthorizationRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto'
import { AuthorizationResponseDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-response.dto'
import { toCreateAuthorizationRequest } from '@app/orchestration/policy-engine/http/rest/util'
import { OrgId } from '@app/orchestration/shared/decorator/org-id.decorator'
import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common'
import { ApiResponse, ApiTags } from '@nestjs/swagger'
import { plainToInstance } from 'class-transformer'

// Not in love with the gymnastics required to bend a DTO to a domain object.
// Most of the complexity came from the discriminated union type.
// It's fine for now to keep it ugly here but I'll look at the problem later
const toDomainType = (orgId: string, body: AuthorizationRequestDto): CreateAuthorizationRequest => {
const dto = plainToInstance(AuthorizationRequestDto, body)
const shared = {
orgId,
initiatorId: '97389cac-20f0-4d02-a3a9-b27c564ffd18',
hash: dto.hash,
evaluations: []
}

if (dto.isSignMessage(dto.request)) {
return {
...shared,
action: Action.SIGN_MESSAGE,
request: {
message: dto.request.message
}
}
}

return {
...shared,
action: Action.SIGN_TRANSACTION,
request: {
from: dto.request.from,
to: dto.request.to,
data: dto.request.data
}
}
}

@Controller('/policy-engine')
@ApiTags('Policy Engine')
Expand All @@ -53,7 +19,7 @@ export class FacadeController {
type: AuthorizationResponseDto
})
async evaluation(@OrgId() orgId: string, @Body() body: AuthorizationRequestDto): Promise<AuthorizationResponseDto> {
const authzRequest = await this.authorizationRequestService.create(toDomainType(orgId, body))
const authzRequest = await this.authorizationRequestService.create(toCreateAuthorizationRequest(orgId, body))

return new AuthorizationResponseDto(authzRequest)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { Action, Address, Hex } from '@app/orchestration/policy-engine/core/type/domain.type'
import { SignatureDto } from '@app/orchestration/policy-engine/http/rest/dto/validator/signature.dto'
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { Transform, Type } from 'class-transformer'
import { IsDefined, IsEnum, IsEthereumAddress, IsString, Validate, ValidateNested } from 'class-validator'
import { RequestHash } from './validator/request-hash.validator'

export class SignatureDto {
@IsDefined()
@IsString()
@ApiProperty()
hash: string

@ApiProperty({
enum: ['ECDSA']
})
type?: string = 'ECDSA'
}

export class AuthenticationDto {
class AuthenticationDto {
@ApiProperty()
signature: SignatureDto
}

export class ApprovalDto {
class ApprovalDto {
@ApiProperty({
type: () => SignatureDto,
isArray: true
Expand All @@ -30,26 +19,36 @@ export class ApprovalDto {
}

export class SignTransactionRequestDto {
@IsString()
@IsDefined()
@IsEthereumAddress()
@Transform(({ value }) => value.toLowerCase())
@ApiProperty({
required: true,
format: 'EthereumAddress'
})
from: Address

@IsString()
@IsEthereumAddress()
@Transform(({ value }) => value.toLowerCase())
@ApiProperty({
format: 'EthereumAddress'
})
to: Address
to?: Address | null

@IsString()
@ApiProperty({
type: 'string',
format: 'Hexadecimal'
})
data: Hex
data?: Hex

@Transform(({ value }) => BigInt(value))
@ApiProperty({
type: 'string'
})
gas: bigint
}

export class SignMessageRequestDto {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { Action, AuthorizationRequestStatus } from '@app/orchestration/policy-engine/core/type/domain.type'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
SignMessageRequestDto,
SignTransactionRequestDto
} from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto'
import { EvaluationDto } from '@app/orchestration/policy-engine/http/rest/dto/validator/evaluation.dto'
import { ApiProperty, getSchemaPath } from '@nestjs/swagger'
import { Transform, Type } from 'class-transformer'
import { IsString } from 'class-validator'

export class EvaluationDto {
@ApiProperty()
id: string

@ApiProperty()
decision: string

/**
* The transformer function in the "@Transformer" decorator for bigint
* properties differs between the request and response. This variation is due to
* the limitations of JS' built-in functions, such as JSON, when handling
* bigints.
*
* - Request: The transformer converts from a string to bigint.
* - Response: The transformer converts from bigint to a string.
*/
class SignTransactionResponseDto extends SignTransactionRequestDto {
@IsString()
@Transform(({ value }) => value.toString())
@ApiProperty({
type: String
type: 'string'
})
signature?: string | null

@ApiProperty()
createdAt: Date
gas: bigint
}

// Just for keeping consistency on the naming.
class SignMessageResponseDto extends SignMessageRequestDto {}

export class AuthorizationResponseDto {
@ApiProperty()
id: string
Expand Down Expand Up @@ -53,11 +63,13 @@ export class AuthorizationResponseDto {
@Type(() => EvaluationDto)
evaluations: EvaluationDto[]

// TODO: Figure out the request discrimination. It's been too painful.
// @ApiProperty({
// oneOf: [{ $ref: getSchemaPath(SignTransactionRequestDto) }, { $ref: getSchemaPath(SignMessageRequestDto) }]
// })
// request: SignTransactionRequestDto | SignMessageRequestDto
@Type((opts) => {
return opts?.object.action === Action.SIGN_TRANSACTION ? SignTransactionResponseDto : SignMessageResponseDto
})
@ApiProperty({
oneOf: [{ $ref: getSchemaPath(SignMessageResponseDto) }, { $ref: getSchemaPath(SignTransactionResponseDto) }]
})
request: SignTransactionResponseDto | SignMessageResponseDto

constructor(partial: Partial<AuthorizationResponseDto>) {
Object.assign(this, partial)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger'

export class EvaluationDto {
@ApiProperty()
id: string

@ApiProperty()
decision: string

@ApiProperty({
type: String
})
signature?: string | null

@ApiProperty()
createdAt: Date
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { stringify } from '@app/orchestration/shared/lib/json'
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'
import { hashMessage } from 'viem'

Expand All @@ -18,6 +19,6 @@ export class RequestHash implements ValidatorConstraintInterface {
}

static hash(request: unknown): string {
return hashMessage(JSON.stringify(request))
return hashMessage(stringify(request))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsDefined, IsString } from 'class-validator'

export class SignatureDto {
@IsDefined()
@IsString()
@ApiProperty()
hash: string

@ApiProperty({
enum: ['ECDSA']
})
type?: string = 'ECDSA'
}
40 changes: 40 additions & 0 deletions apps/orchestration/src/policy-engine/http/rest/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Action, CreateAuthorizationRequest } from '@app/orchestration/policy-engine/core/type/domain.type'
import { AuthorizationRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto'
import { plainToInstance } from 'class-transformer'

// Not in love with the gymnastics required to bend a DTO to a domain object.
// Most of the complexity came from the discriminated union type.
// It's fine for now to keep it ugly here but I'll look at the problem later
export const toCreateAuthorizationRequest = (
orgId: string,
body: AuthorizationRequestDto
): CreateAuthorizationRequest => {
const dto = plainToInstance(AuthorizationRequestDto, body)
const shared = {
orgId,
initiatorId: '97389cac-20f0-4d02-a3a9-b27c564ffd18',
hash: dto.hash,
evaluations: []
}

if (dto.isSignMessage(dto.request)) {
return {
...shared,
action: Action.SIGN_MESSAGE,
request: {
message: dto.request.message
}
}
}

return {
...shared,
action: Action.SIGN_TRANSACTION,
request: {
from: dto.request.from,
to: dto.request.to,
data: dto.request.data,
gas: dto.request.gas
}
}
}
Loading

0 comments on commit 5e9ee9d

Please sign in to comment.