From 5af28842275de09f99b3e6737352acc7df749731 Mon Sep 17 00:00:00 2001 From: William Calderipe Date: Wed, 17 Jan 2024 18:56:54 +0100 Subject: [PATCH] Full support of sign transaction schema --- .../policy-engine/__test__/e2e/facade.spec.ts | 60 ++++++++- .../policy-engine/core/type/domain.type.ts | 35 +++--- .../rest/dto/authorization-request.dto.ts | 50 +------- .../rest/dto/authorization-response.dto.ts | 17 ++- .../http/rest/dto/sign-message-request.dto.ts | 12 ++ .../rest/dto/sign-transaction-request.dto.ts | 117 ++++++++++++++++++ .../src/policy-engine/http/rest/util.ts | 9 +- .../schema/sign-transaction-request.schema.ts | 15 ++- 8 files changed, 240 insertions(+), 75 deletions(-) create mode 100644 apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts create mode 100644 apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts diff --git a/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts b/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts index 8ed5f7631..0762d60e9 100644 --- a/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts +++ b/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts @@ -3,7 +3,7 @@ import { AUTHORIZATION_REQUEST_PROCESSING_QUEUE, REQUEST_HEADER_ORG_ID } from '@app/orchestration/orchestration.constant' -import { Action, AuthorizationRequest } from '@app/orchestration/policy-engine/core/type/domain.type' +import { Action, AuthorizationRequest, TransactionType } from '@app/orchestration/policy-engine/core/type/domain.type' import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository' import { PolicyEngineModule } from '@app/orchestration/policy-engine/policy-engine.module' import { PersistenceModule } from '@app/orchestration/shared/module/persistence/persistence.module' @@ -16,7 +16,7 @@ import { Test, TestingModule } from '@nestjs/testing' import { AuthorizationRequestStatus, Organization } from '@prisma/client/orchestration' import { Queue } from 'bull' import request from 'supertest' -import { hashMessage } from 'viem' +import { hashMessage, stringToHex } from 'viem' const EVALUATIONS_ENDPOINT = '/policy-engine/evaluations' @@ -116,10 +116,62 @@ describe('Policy Engine Cluster Facade', () => { it('evaluates a sign transaction authorization request', async () => { const signTransactionRequest = { + chainId: 1, + data: '0x', from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + gas: '5000', + nonce: 0, to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', - data: '0x', - gas: '5000' + type: TransactionType.EIP1559, + value: '0x', + accessList: [ + { + address: '0xccc1472fce4ec74a1e3f9653776acfc790cd0743', + storageKeys: [stringToHex('storage-key-one'), stringToHex('storage-key-two')] + } + ] + } + const payload = { + action: Action.SIGN_TRANSACTION, + hash: hashMessage(JSON.stringify(signTransactionRequest)), + request: signTransactionRequest, + authentication: { + signature: { + hash: 'string' + } + }, + approval: { + signatures: [ + { + hash: 'string' + } + ] + } + } + + const { status, body } = await request(app.getHttpServer()) + .post(EVALUATIONS_ENDPOINT) + .set(REQUEST_HEADER_ORG_ID, org.id) + .send(payload) + + expect(body).toMatchObject({ + id: expect.any(String), + status: AuthorizationRequestStatus.CREATED, + idempotencyKey: null, + action: payload.action, + hash: payload.hash, + request: payload.request, + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + expect(status).toEqual(HttpStatus.OK) + }) + + it('evaluates a partial sign transaction authorization request', async () => { + const signTransactionRequest = { + from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + nonce: 0, + chainId: 1 } const payload = { action: Action.SIGN_TRANSACTION, diff --git a/apps/orchestration/src/policy-engine/core/type/domain.type.ts b/apps/orchestration/src/policy-engine/core/type/domain.type.ts index 8c382244b..22633223b 100644 --- a/apps/orchestration/src/policy-engine/core/type/domain.type.ts +++ b/apps/orchestration/src/policy-engine/core/type/domain.type.ts @@ -43,27 +43,30 @@ export type SharedAuthorizationRequest = { export type Hex = `0x${string}` export type Address = `0x${string}` -export type AccessList = { address: Address; storageKeys: Hex[] }[] +export type AccessList = { + address: Address + storageKeys: Hex[] +}[] + +/** + * @see https://viem.sh/docs/glossary/types#transactiontype + */ +export enum TransactionType { + LEGACY = 'legacy', + EIP2930 = 'eip2930', + EIP1559 = 'eip1559' +} -// Original transaction request -// -// export type TransactionRequest = { -// data?: Hex -// from: Address -// to?: Address | null -// gas?: TQuantity -// nonce: TIndex -// value?: TQuantity -// chainId: string | null -// accessList?: AccessList -// type?: TTransactionType -// } -// Temporary lite version export type TransactionRequest = { - data?: Hex + chainId: number from: Address + nonce: number + accessList?: AccessList + data?: Hex gas?: bigint to?: Address | null + type?: `${TransactionType}` + value?: Hex } export type SignTransactionAuthorizationRequest = SharedAuthorizationRequest & { diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts index 85df6260b..c615005a1 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts @@ -1,8 +1,10 @@ -import { Action, Address, Hex } from '@app/orchestration/policy-engine/core/type/domain.type' +import { Action } from '@app/orchestration/policy-engine/core/type/domain.type' +import { SignMessageRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-message-request.dto' +import { SignTransactionRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-transaction-request.dto' import { SignatureDto } from '@app/orchestration/policy-engine/http/rest/dto/signature.dto' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' -import { Transform, Type } from 'class-transformer' -import { IsDefined, IsEnum, IsEthereumAddress, IsString, Validate, ValidateNested } from 'class-validator' +import { Type } from 'class-transformer' +import { IsDefined, IsEnum, IsString, Validate, ValidateNested } from 'class-validator' import { RequestHash } from './validator/request-hash.validator' class AuthenticationDto { @@ -18,48 +20,6 @@ class ApprovalDto { signatures: SignatureDto[] } -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 | null - - @IsString() - @ApiProperty({ - type: 'string', - format: 'Hexadecimal' - }) - data?: Hex - - @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - type: 'string' - }) - gas?: bigint -} - -export class SignMessageRequestDto { - @IsString() - @IsDefined() - @ApiProperty({ - required: true - }) - message: string -} - @ApiExtraModels(SignTransactionRequestDto, SignMessageRequestDto) export class AuthorizationRequestDto { @IsEnum(Action) diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts index 5d30ff19b..b3d1f2d8c 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts @@ -1,12 +1,10 @@ import { Action, AuthorizationRequestStatus } from '@app/orchestration/policy-engine/core/type/domain.type' -import { - SignMessageRequestDto, - SignTransactionRequestDto -} from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto' import { EvaluationDto } from '@app/orchestration/policy-engine/http/rest/dto/evaluation.dto' -import { ApiProperty, getSchemaPath } from '@nestjs/swagger' +import { SignMessageRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-message-request.dto' +import { SignTransactionRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-transaction-request.dto' +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' import { Transform, Type } from 'class-transformer' -import { IsString } from 'class-validator' +import { IsOptional, IsString } from 'class-validator' /** * The transformer function in the "@Transformer" decorator for bigint @@ -19,6 +17,7 @@ import { IsString } from 'class-validator' */ class SignTransactionResponseDto extends SignTransactionRequestDto { @IsString() + @IsOptional() @Transform(({ value }) => value.toString()) @ApiProperty({ type: 'string' @@ -29,6 +28,7 @@ class SignTransactionResponseDto extends SignTransactionRequestDto { // Just for keeping consistency on the naming. class SignMessageResponseDto extends SignMessageRequestDto {} +@ApiExtraModels(SignTransactionResponseDto, SignMessageResponseDto) export class AuthorizationResponseDto { @ApiProperty() id: string @@ -40,6 +40,11 @@ export class AuthorizationResponseDto { initiatorId: string @IsString() + @ApiProperty({ + required: false, + type: 'string', + nullable: true + }) idempotencyKey?: string | null @ApiProperty({ diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts new file mode 100644 index 000000000..7f3a59f15 --- /dev/null +++ b/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsDefined, IsString } from 'class-validator' + +export class SignMessageRequestDto { + @IsString() + @IsDefined() + @ApiProperty({ + required: true, + type: 'string' + }) + message: string +} diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts new file mode 100644 index 000000000..21288cae3 --- /dev/null +++ b/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts @@ -0,0 +1,117 @@ +import { Address, Hex, TransactionType } from '@app/orchestration/policy-engine/core/type/domain.type' +import { ApiProperty } from '@nestjs/swagger' +import { Transform } from 'class-transformer' +import { + IsDefined, + IsEnum, + IsEthereumAddress, + IsInt, + IsNumber, + IsOptional, + IsString, + Min, + ValidateNested +} from 'class-validator' + +class AccessListDto { + @IsString() + @IsDefined() + @IsEthereumAddress() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'Address', + required: true, + type: 'string' + }) + address: Address + + @IsString() + @ApiProperty({ + format: 'Hexadecimal', + isArray: true, + required: true, + type: 'string' + }) + storageKeys: Hex[] +} + +export class SignTransactionRequestDto { + @IsInt() + @Min(1) + @ApiProperty({ + minimum: 1 + }) + chainId: number + + @IsString() + @IsDefined() + @IsEthereumAddress() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'address', + type: 'string' + }) + from: Address + + @IsNumber() + @Min(0) + @ApiProperty({ + minimum: 0 + }) + nonce: number + + @IsOptional() + @ValidateNested() + @ApiProperty({ + isArray: true, + required: false, + type: AccessListDto + }) + accessList?: AccessListDto[] + + @IsString() + @IsOptional() + @ApiProperty({ + format: 'hexadecimal', + required: false, + type: 'string' + }) + data?: Hex + + @IsOptional() + @Transform(({ value }) => BigInt(value)) + @ApiProperty({ + format: 'bigint', + required: false, + type: 'string' + }) + gas?: bigint + + @IsString() + @IsEthereumAddress() + @IsOptional() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'address', + required: false, + type: 'string' + }) + to?: Address | null + + @IsEnum(TransactionType) + @IsOptional() + @ApiProperty({ + enum: TransactionType, + required: false + }) + type?: `${TransactionType}` + + @IsString() + @IsOptional() + @ApiProperty({ + format: 'hexadecimal', + required: false, + type: 'string' + }) + value?: Hex +} diff --git a/apps/orchestration/src/policy-engine/http/rest/util.ts b/apps/orchestration/src/policy-engine/http/rest/util.ts index b15c69fa0..de628a97c 100644 --- a/apps/orchestration/src/policy-engine/http/rest/util.ts +++ b/apps/orchestration/src/policy-engine/http/rest/util.ts @@ -31,10 +31,15 @@ export const toCreateAuthorizationRequest = ( ...shared, action: Action.SIGN_TRANSACTION, request: { + accessList: dto.request.accessList, + chainId: dto.request.chainId, + data: dto.request.data, from: dto.request.from, + gas: dto.request.gas, + nonce: dto.request.nonce, to: dto.request.to, - data: dto.request.data, - gas: dto.request.gas + type: dto.request.type, + value: dto.request.value } } } diff --git a/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts index 7932acdc4..d771410c0 100644 --- a/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts +++ b/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts @@ -1,3 +1,4 @@ +import { TransactionType } from '@app/orchestration/policy-engine/core/type/domain.type' import { isAddress, isHex } from 'viem' import { z } from 'zod' @@ -41,11 +42,21 @@ const hexSchema = z.custom<`0x${string}`>( } ) +const accessListSchema = z.object({ + address: addressSchema, + storageKeys: z.array(hexSchema) +}) + export const readSignTransactionRequestSchema = z.object({ + chainId: z.coerce.number(), from: addressSchema, - to: addressSchema.optional(), + nonce: z.coerce.number(), + accessList: z.array(accessListSchema).optional(), data: hexSchema.optional(), - gas: z.coerce.bigint().optional() + gas: z.coerce.bigint().optional(), + to: addressSchema.optional(), + type: z.nativeEnum(TransactionType).optional(), + value: hexSchema.optional() }) export const createSignTransactionRequestSchema = readSignTransactionRequestSchema.extend({