diff --git a/apps/armory/src/main.ts b/apps/armory/src/main.ts index a5d7b49dd..1fa31cfa1 100644 --- a/apps/armory/src/main.ts +++ b/apps/armory/src/main.ts @@ -37,7 +37,7 @@ const withSwagger = (app: INestApplication): INestApplication => { * @returns The modified INestApplication instance. */ const withGlobalPipes = (app: INestApplication): INestApplication => { - app.useGlobalPipes(new ValidationPipe()) + app.useGlobalPipes(new ValidationPipe({ transform: true })) return app } diff --git a/apps/policy-engine/src/engine/evaluation-request.dto.ts b/apps/policy-engine/src/engine/evaluation-request.dto.ts index 7660f50a6..4676668cb 100644 --- a/apps/policy-engine/src/engine/evaluation-request.dto.ts +++ b/apps/policy-engine/src/engine/evaluation-request.dto.ts @@ -1,112 +1,13 @@ -import { AccessList, AccountId, Action, Address, BaseActionDto, FiatCurrency, Hex } from '@narval/policy-engine-shared' +import { + AccountId, + Action, + FiatCurrency, + SignMessageRequestDataDto, + SignTransactionRequestDataDto +} from '@narval/policy-engine-shared' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' -import { Transform, Type } from 'class-transformer' -import { IsDefined, IsEthereumAddress, IsIn, IsOptional, IsString, ValidateNested } from 'class-validator' - -export class TransactionRequestDto { - @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 - - @IsOptional() - @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - format: 'bigint', - required: false, - type: 'string' - }) - gas?: bigint - @IsOptional() - @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - format: 'bigint', - required: false, - type: 'string' - }) - maxFeePerGas?: bigint - @IsOptional() - @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - format: 'bigint', - required: false, - type: 'string' - }) - maxPriorityFeePerGas?: bigint - - @ApiProperty() - nonce?: number - - value?: Hex - - chainId: number - - accessList?: AccessList - - type?: '2' -} - -export class SignTransactionRequestDataDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.SIGN_TRANSACTION - }) - action: typeof Action.SIGN_TRANSACTION - - @IsString() - @IsDefined() - @ApiProperty() - resourceId: string - - @ValidateNested() - @IsDefined() - @ApiProperty({ - type: TransactionRequestDto - }) - transactionRequest: TransactionRequestDto -} - -export class SignMessageRequestDataDto extends BaseActionDto { - @IsIn(Object.values(Action)) - @IsDefined() - @ApiProperty({ - enum: Object.values(Action), - default: Action.SIGN_MESSAGE - }) - action: typeof Action.SIGN_MESSAGE - - @IsString() - @IsDefined() - @ApiProperty() - resourceId: string - - @IsString() - @IsDefined() - @ApiProperty() - message: string // TODO: Is this string hex or raw? -} +import { Type } from 'class-transformer' +import { IsDefined, IsOptional, ValidateNested } from 'class-validator' export class HistoricalTransferDto { amount: string diff --git a/apps/policy-engine/src/main.ts b/apps/policy-engine/src/main.ts index 66ca0a8fc..724cf85c6 100644 --- a/apps/policy-engine/src/main.ts +++ b/apps/policy-engine/src/main.ts @@ -32,7 +32,7 @@ const withSwagger = (app: INestApplication): INestApplication => { * @returns The modified INestApplication instance. */ const withGlobalPipes = (app: INestApplication): INestApplication => { - app.useGlobalPipes(new ValidationPipe()) + app.useGlobalPipes(new ValidationPipe({ transform: true })) return app } diff --git a/apps/vault/jest.config.ts b/apps/vault/jest.config.ts index b3d7101ff..d19762a82 100644 --- a/apps/vault/jest.config.ts +++ b/apps/vault/jest.config.ts @@ -13,7 +13,8 @@ const config: Config = { tsconfig: '/tsconfig.spec.json' } ] - } + }, + workerThreads: true // EXPERIMENTAL; lets BigInt serialization work } export default config diff --git a/apps/vault/src/main.ts b/apps/vault/src/main.ts index 605643b5e..1e86aa6b6 100644 --- a/apps/vault/src/main.ts +++ b/apps/vault/src/main.ts @@ -32,7 +32,7 @@ const withSwagger = (app: INestApplication): INestApplication => { * @returns The modified INestApplication instance. */ const withGlobalPipes = (app: INestApplication): INestApplication => { - app.useGlobalPipes(new ValidationPipe()) + app.useGlobalPipes(new ValidationPipe({ transform: true })) return app } diff --git a/apps/vault/src/shared/schema/wallet.schema.ts b/apps/vault/src/shared/schema/wallet.schema.ts index b426171bd..8618dd2b8 100644 --- a/apps/vault/src/shared/schema/wallet.schema.ts +++ b/apps/vault/src/shared/schema/wallet.schema.ts @@ -1,7 +1,14 @@ +import { Hex } from '@narval/policy-engine-shared' import { z } from 'zod' export const walletSchema = z.object({ id: z.string().min(1), - privateKey: z.string().regex(/^(0x)?([A-Fa-f0-9]{64})$/), - address: z.string().regex(/^0x([A-Fa-f0-9]{40})$/) + privateKey: z + .string() + .regex(/^(0x)?([A-Fa-f0-9]{64})$/) + .transform((val: string): Hex => val as Hex), + address: z + .string() + .regex(/^0x([A-Fa-f0-9]{40})$/) + .transform((val: string): Hex => val as Hex) }) diff --git a/apps/vault/src/vault/__test__/e2e/sign.spec.ts b/apps/vault/src/vault/__test__/e2e/sign.spec.ts new file mode 100644 index 000000000..160598323 --- /dev/null +++ b/apps/vault/src/vault/__test__/e2e/sign.spec.ts @@ -0,0 +1,162 @@ +import { EncryptionModuleOptionProvider } from '@narval/encryption-module' +import { HttpStatus, INestApplication, ValidationPipe } from '@nestjs/common' +import { ConfigModule } from '@nestjs/config' +import { Test, TestingModule } from '@nestjs/testing' +import request from 'supertest' +import { v4 as uuid } from 'uuid' +import { load } from '../../../main.config' +import { REQUEST_HEADER_API_KEY, REQUEST_HEADER_CLIENT_ID } from '../../../main.constant' +import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository' +import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository' +import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing' +import { Tenant, Wallet } from '../../../shared/type/domain.type' +import { TenantService } from '../../../tenant/core/service/tenant.service' +import { TenantModule } from '../../../tenant/tenant.module' +import { WalletRepository } from '../../persistence/repository/wallet.repository' + +describe('Sign', () => { + let app: INestApplication + let module: TestingModule + + const adminApiKey = 'test-admin-api-key' + const clientId = uuid() + const tenant: Tenant = { + clientId, + clientSecret: adminApiKey, + createdAt: new Date(), + updatedAt: new Date() + } + + const wallet: Wallet = { + id: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157', + address: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' + } + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: [load], + isGlobal: true + }), + TenantModule + ] + }) + .overrideProvider(KeyValueRepository) + .useValue(new InMemoryKeyValueRepository()) + .overrideProvider(EncryptionModuleOptionProvider) + .useValue({ + keyring: getTestRawAesKeyring() + }) + .overrideProvider(TenantService) + .useValue({ + findAll: jest.fn().mockResolvedValue([tenant]), + findByClientId: jest.fn().mockResolvedValue(tenant) + }) + .overrideProvider(WalletRepository) + .useValue({ + findById: jest.fn().mockResolvedValue(wallet) + }) + .compile() + + app = module.createNestApplication() + + // Use global pipes + // THIS IS NEEDED to make sure it parses/transforms properly. + app.useGlobalPipes(new ValidationPipe({ transform: true })) + + await app.init() + }) + + afterAll(async () => { + // await testPrismaService.truncateAll() + await module.close() + await app.close() + }) + + describe('POST /sign', () => { + it('has client secret guard', async () => { + const { status } = await request(app.getHttpServer()) + .post('/sign') + .set(REQUEST_HEADER_API_KEY, adminApiKey) + // .set(REQUEST_HEADER_CLIENT_ID, clientId) NO CLIENT SECRET + .send({}) + + expect(status).toEqual(HttpStatus.UNAUTHORIZED) + }) + + it('validates nested txn data', async () => { + // ValidationPipe & Transforms can easily be implemented incorrectly, so make sure this is running. + + const payload = { + request: { + action: 'signTransaction', + nonce: 'random-nonce-111', + transactionRequest: { + from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + to: '04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B', // INVALID + chainId: 137, + value: '0x5af3107a4000', + data: '0x', + nonce: 317, + type: '2', + gas: '21004', + maxFeePerGas: '291175227375', + maxPriorityFeePerGas: '81000000000' + }, + resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157' + } + } + + const { status, body } = await request(app.getHttpServer()) + .post('/sign') + .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .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 + }) + }) + + it('signs', async () => { + const payload = { + request: { + action: 'signTransaction', + nonce: 'random-nonce-111', + transactionRequest: { + from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B', + chainId: 137, + value: '0x5af3107a4000', + data: '0x', + nonce: 317, + type: '2', + gas: '21004', + maxFeePerGas: '291175227375', + maxPriorityFeePerGas: '81000000000' + }, + resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157' + } + } + + const { status, body } = await request(app.getHttpServer()) + .post('/sign') + .set(REQUEST_HEADER_API_KEY, adminApiKey) + .set(REQUEST_HEADER_CLIENT_ID, clientId) + .send(payload) + + expect(status).toEqual(HttpStatus.CREATED) + + expect(body).toEqual({ + signature: + '0x02f875818982013d8512dbf9ea008543cb655fef82520c9404b12f0863b83c7162429f0ebb0dfda20e1aa97b865af3107a400080c080a00de78cbb96f83ef1b8d6be4d55b4046b2706c7d63ce0a815bae2b1ea4f891e6ba06f7648a9c9710b171d55e056c4abca268857f607a8a4a257d945fc44ace9f076' + }) + }) + }) +}) diff --git a/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts b/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts new file mode 100644 index 000000000..8c0c9ab6f --- /dev/null +++ b/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts @@ -0,0 +1,81 @@ +import { Request } from '@narval/policy-engine-shared' +import { Test } from '@nestjs/testing' +import { Hex, TransactionSerializable, hexToBigInt, parseTransaction, serializeTransaction } from 'viem' +import { Wallet } from '../../../../../shared/type/domain.type' +import { WalletRepository } from '../../../../persistence/repository/wallet.repository' +import { SigningService } from '../../signing.service' + +describe('SigningService', () => { + let signingService: SigningService + + const wallet: Wallet = { + id: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157', + address: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' + } + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + SigningService, + { + provide: WalletRepository, + useValue: { + findById: jest.fn().mockResolvedValue(wallet) + } + } + ] + }).compile() + + signingService = moduleRef.get(SigningService) + }) + + describe('sign', () => { + it('should sign the request and return a string', async () => { + // Mock the dependencies and setup the test data + const tenantId = 'tenantId' + const request: Request = { + action: 'signTransaction', + nonce: 'random-nonce-111', + resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157', + transactionRequest: { + from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B', + chainId: 137, + value: '0x5af3107a4000', + data: '0x', + nonce: 317, + type: '2', + gas: 21004n, + maxFeePerGas: 291175227375n, + maxPriorityFeePerGas: 81000000000n + } + } + + const expectedSignature = + '0x02f875818982013d8512dbf9ea008543cb655fef82520c9404b12f0863b83c7162429f0ebb0dfda20e1aa97b865af3107a400080c080a00de78cbb96f83ef1b8d6be4d55b4046b2706c7d63ce0a815bae2b1ea4f891e6ba06f7648a9c9710b171d55e056c4abca268857f607a8a4a257d945fc44ace9f076' + + // Call the sign method + const result = await signingService.sign(tenantId, request) + + // Assert the result + expect(result).toEqual(expectedSignature) + }) + + // Just for testing formatting & stuff + it('should serialize/deserialize', async () => { + const txRequest: TransactionSerializable = { + // from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157', + to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B'.toLowerCase() as Hex, + chainId: 137, + value: hexToBigInt('0x5af3107a4000'), + type: 'eip1559' + } + + const serialized = serializeTransaction(txRequest) + const deserialized = parseTransaction(serialized) + + expect(deserialized).toEqual(txRequest) + }) + }) +}) diff --git a/apps/vault/src/vault/core/service/signing.service.ts b/apps/vault/src/vault/core/service/signing.service.ts index 8728ac5c5..982eb09be 100644 --- a/apps/vault/src/vault/core/service/signing.service.ts +++ b/apps/vault/src/vault/core/service/signing.service.ts @@ -1,76 +1,81 @@ -import { JsonWebKey, toHex } from '@narval/policy-engine-shared' +import { Action, Hex, Request, SignTransactionAction } from '@narval/policy-engine-shared' +import { HttpStatus, Injectable } from '@nestjs/common' import { - Alg, - Payload, - SigningAlg, - buildSignerEip191, - buildSignerEs256k, - privateKeyToJwk, - signJwt -} from '@narval/signature' -import { Injectable } from '@nestjs/common' -import { secp256k1 } from '@noble/curves/secp256k1' - -// Optional additional configs, such as for MPC-based DKG. -type KeyGenerationOptions = { - keyId: string -} - -type KeyGenerationResponse = { - publicKey: JsonWebKey - privateKey?: JsonWebKey -} - -type SignOptions = { - alg?: SigningAlg -} + TransactionRequest, + checksumAddress, + createWalletClient, + extractChain, + hexToBigInt, + http, + transactionType +} from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import * as chains from 'viem/chains' +import { ApplicationException } from '../../../shared/exception/application.exception' +import { WalletRepository } from '../../persistence/repository/wallet.repository' @Injectable() export class SigningService { - constructor() {} + constructor(private walletRepository: WalletRepository) {} - async generateSigningKey(alg: Alg, options?: KeyGenerationOptions): Promise { - if (alg === Alg.ES256K) { - const privateKey = toHex(secp256k1.utils.randomPrivateKey()) - const privateJwk = privateKeyToJwk(privateKey, options?.keyId) - - // Remove the privateKey from the public jwk - const publicJwk = { - ...privateJwk, - d: undefined - } - - return { - publicKey: publicJwk, - privateKey: privateJwk - } + async sign(tenantId: string, request: Request): Promise { + if (request.action === Action.SIGN_TRANSACTION) { + return this.signTransaction(tenantId, request) } - throw new Error('Unsupported algorithm') + throw new Error('Action not supported') } - async sign(payload: Payload, jwk: JsonWebKey, opts: SignOptions = {}): Promise { - const alg: SigningAlg = opts.alg || jwk.alg - if (alg === SigningAlg.ES256K) { - if (!jwk.d) { - throw new Error('Missing private key') - } - const pk = jwk.d - - const jwt = await signJwt(payload, jwk, opts, buildSignerEs256k(pk)) + async signTransaction(tenantId: string, action: SignTransactionAction): Promise { + const { transactionRequest, resourceId } = action + const wallet = await this.walletRepository.findById(tenantId, resourceId) + if (!wallet) { + throw new ApplicationException({ + message: 'Wallet not found', + suggestedHttpStatusCode: HttpStatus.BAD_REQUEST, + context: { clientId: tenantId, resourceId } + }) + } - return jwt - } else if (alg === SigningAlg.EIP191) { - if (!jwk.d) { - throw new Error('Missing private key') - } - const pk = jwk.d + const account = privateKeyToAccount(wallet.privateKey) + const chain = extractChain({ + chains: Object.values(chains), + id: transactionRequest.chainId + }) - const jwt = await signJwt(payload, jwk, opts, buildSignerEip191(pk)) + const client = createWalletClient({ + account, + chain, + transport: http('') // clear the RPC so we don't call any chain stuff here. + }) - return jwt + const txRequest: TransactionRequest = { + from: checksumAddress(account.address), + to: transactionRequest.to, + nonce: transactionRequest.nonce, + data: transactionRequest.data, + gas: transactionRequest.gas, + maxFeePerGas: transactionRequest.maxFeePerGas, + maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, + type: transactionType['0x2'], + value: transactionRequest.value ? hexToBigInt(transactionRequest.value) : undefined } - throw new Error('Unsupported algorithm') + const signature = await client.signTransaction(txRequest) + // /* + // TEMPORARY + // for testing, uncomment the below lines to actually SEND the tx to the chain. + // */ + + // const c2 = createWalletClient({ + // account, + // chain, + // transport: http('https://polygon-mainnet.g.alchemy.com/v2/zBfj-qB2fQVXyTlbD8DRitsNn_ukCJAp') // clear the RPC so we don't call any chain stuff here. + // }) + // console.log('sending transaction') + // const hash = await c2.sendRawTransaction({ serializedTransaction: signature }) + // console.log('sent transaction', hash) + + return signature } } diff --git a/apps/vault/src/vault/http/rest/controller/sign.controller.ts b/apps/vault/src/vault/http/rest/controller/sign.controller.ts new file mode 100644 index 000000000..28429622a --- /dev/null +++ b/apps/vault/src/vault/http/rest/controller/sign.controller.ts @@ -0,0 +1,20 @@ +import { Request } from '@narval/policy-engine-shared' +import { Body, Controller, Post, UseGuards } from '@nestjs/common' +import { ClientId } from '../../../../shared/decorator/client-id.decorator' +import { ClientSecretGuard } from '../../../../shared/guard/client-secret.guard' +import { SigningService } from '../../../core/service/signing.service' +import { SignRequestDto } from '../dto/sign-request.dto' + +@Controller('/sign') +@UseGuards(ClientSecretGuard) +export class SignController { + constructor(private signingService: SigningService) {} + + @Post() + async sign(@ClientId() clientId: string, @Body() body: SignRequestDto) { + const request: Request = body.request + const result = await this.signingService.sign(clientId, request) + + return { signature: result } + } +} diff --git a/apps/vault/src/vault/http/rest/dto/sign-request.dto.ts b/apps/vault/src/vault/http/rest/dto/sign-request.dto.ts new file mode 100644 index 000000000..36e90e342 --- /dev/null +++ b/apps/vault/src/vault/http/rest/dto/sign-request.dto.ts @@ -0,0 +1,19 @@ +import { Action, SignMessageRequestDataDto, SignTransactionRequestDataDto } from '@narval/policy-engine-shared' +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { IsDefined, ValidateNested } from 'class-validator' + +@ApiExtraModels(SignTransactionRequestDataDto, SignMessageRequestDataDto) +export class SignRequestDto { + @Type((opts) => { + return opts?.object.request.action === Action.SIGN_TRANSACTION + ? SignTransactionRequestDataDto + : SignMessageRequestDataDto + }) + @IsDefined() + @ApiProperty({ + oneOf: [{ $ref: getSchemaPath(SignTransactionRequestDataDto) }, { $ref: getSchemaPath(SignMessageRequestDataDto) }] + }) + @ValidateNested() + request: SignTransactionRequestDataDto | SignMessageRequestDataDto +} diff --git a/apps/vault/src/vault/persistence/repository/wallet.repository.ts b/apps/vault/src/vault/persistence/repository/wallet.repository.ts index ca3fb0214..a5645f883 100644 --- a/apps/vault/src/vault/persistence/repository/wallet.repository.ts +++ b/apps/vault/src/vault/persistence/repository/wallet.repository.ts @@ -14,7 +14,7 @@ export class WalletRepository { } async findById(tenantId: string, id: string): Promise { - const value = await this.keyValueService.get(this.getKey(id, tenantId)) + const value = await this.keyValueService.get(this.getKey(tenantId, id)) if (value) { return this.decode(value) diff --git a/apps/vault/src/vault/vault.module.ts b/apps/vault/src/vault/vault.module.ts index 9f4d7b842..4b93842a6 100644 --- a/apps/vault/src/vault/vault.module.ts +++ b/apps/vault/src/vault/vault.module.ts @@ -13,6 +13,7 @@ import { ImportService } from './core/service/import.service' import { ProvisionService } from './core/service/provision.service' import { SigningService } from './core/service/signing.service' import { ImportController } from './http/rest/controller/import.controller' +import { SignController } from './http/rest/controller/sign.controller' import { AppRepository } from './persistence/repository/app.repository' import { WalletRepository } from './persistence/repository/wallet.repository' import { VaultController } from './vault.controller' @@ -33,7 +34,7 @@ import { VaultService } from './vault.service' }), forwardRef(() => TenantModule) ], - controllers: [VaultController, ImportController], + controllers: [VaultController, ImportController, SignController], providers: [ AppService, AppRepository, diff --git a/packages/policy-engine-shared/src/lib/dto/index.ts b/packages/policy-engine-shared/src/lib/dto/index.ts index 5d6b07444..6e69c84b0 100644 --- a/packages/policy-engine-shared/src/lib/dto/index.ts +++ b/packages/policy-engine-shared/src/lib/dto/index.ts @@ -1,3 +1,5 @@ export * from './base-action-request.dto' export * from './base-action.dto' +export * from './sign-message-request-data-dto' +export * from './sign-transaction-request-data.dto' export * from './signature.dto' diff --git a/packages/policy-engine-shared/src/lib/dto/sign-message-request-data-dto.ts b/packages/policy-engine-shared/src/lib/dto/sign-message-request-data-dto.ts new file mode 100644 index 000000000..3cc83fa68 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/dto/sign-message-request-data-dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsDefined, IsIn, IsString } from 'class-validator' +import { Action } from '../type/action.type' +import { BaseActionDto } from './' + +export class SignMessageRequestDataDto extends BaseActionDto { + @IsIn(Object.values(Action)) + @IsDefined() + @ApiProperty({ + enum: Object.values(Action), + default: Action.SIGN_MESSAGE + }) + action: typeof Action.SIGN_MESSAGE + + @IsString() + @IsDefined() + @ApiProperty() + resourceId: string + + @IsString() + @IsDefined() + @ApiProperty() + message: string // TODO: Is this string hex or raw? +} diff --git a/packages/policy-engine-shared/src/lib/dto/sign-transaction-request-data.dto.ts b/packages/policy-engine-shared/src/lib/dto/sign-transaction-request-data.dto.ts new file mode 100644 index 000000000..7cd2f29c8 --- /dev/null +++ b/packages/policy-engine-shared/src/lib/dto/sign-transaction-request-data.dto.ts @@ -0,0 +1,116 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' +import { IsDefined, IsEthereumAddress, IsIn, IsInt, IsOptional, IsString, Min, ValidateNested } from 'class-validator' +import { IsHexString } from '../decorators/is-hex-string.decorator' +import { Action } from '../type/action.type' +import { Address, Hex } from '../type/domain.type' +import { BaseActionDto } from './' + +class AccessListDto { + @IsString() + @IsDefined() + @IsEthereumAddress() + @Transform(({ value }) => value.toLowerCase()) + address: Address + + @IsString() + @IsHexString() + storageKeys: Hex[] +} + +export class TransactionRequestDto { + @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 + + @IsOptional() + @IsString() + @ApiProperty({ + type: 'string', + format: 'Hexadecimal' + }) + data?: Hex + + @IsOptional() + @ApiProperty({ + format: 'bigint', + required: false, + type: 'string' + }) + @Transform(({ value }) => BigInt(value)) + gas?: bigint + + @IsOptional() + @Transform(({ value }) => BigInt(value)) + @ApiProperty({ + format: 'bigint', + required: false, + type: 'string' + }) + maxFeePerGas?: bigint + + @IsOptional() + @Transform(({ value }) => BigInt(value)) + @ApiProperty({ + format: 'bigint', + required: false, + type: 'string' + }) + maxPriorityFeePerGas?: bigint + + @ApiProperty() + nonce?: number + + @IsHexString() + @IsOptional() + value?: Hex + + @IsInt() + @Min(1) + chainId: number + + @Type(() => AccessListDto) + @ValidateNested({ each: true }) + accessList?: AccessListDto[] + + @IsString() + @IsOptional() + type?: '2' +} + +export class SignTransactionRequestDataDto extends BaseActionDto { + @IsIn(Object.values(Action)) + @IsDefined() + @ApiProperty({ + enum: Object.values(Action), + default: Action.SIGN_TRANSACTION + }) + action: typeof Action.SIGN_TRANSACTION + + @IsString() + @IsDefined() + @ApiProperty() + resourceId: string + + @ApiProperty({ + type: TransactionRequestDto + }) + @IsDefined() + @Type(() => TransactionRequestDto) + @ValidateNested() + transactionRequest: TransactionRequestDto +}