diff --git a/README.md b/README.md index 59e39f4ac..141b7ae5a 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,19 @@ make format/check make lint/check ``` -## Creating a NestJS library +## Generating a new project -Run the command below and follow the instructions to create a new NestJS library. +NX provides two types of projects: applications and libraries. Run the commands +below to generate a project of your choice. ```bash +# Generate an standard JavaScript library. + npx nx g @nrwl/workspace:lib + # Generate an NestJS library. npx nx g @nx/nest:library + # Generate an NestJS application. + npx nx g @nx/nest:application ``` + +For more information about code generation, please refer to the [NX +documentation](https://nx.dev/nx-api/nx). diff --git a/apps/authz/Makefile b/apps/authz/Makefile index a5ea5f2e3..7978cc5b8 100644 --- a/apps/authz/Makefile +++ b/apps/authz/Makefile @@ -1,16 +1,22 @@ AUTHZ_PROJECT_NAME := authz AUTHZ_PROJECT_DIR := ./apps/authz -authz/setup: - make authz/copy-default-env +# === Start === authz/start/dev: npx nx serve ${AUTHZ_PROJECT_NAME} +# === Setup === + +authz/setup: + make authz/copy-default-env + authz/copy-default-env: cp ${AUTHZ_PROJECT_DIR}/.env.default ${AUTHZ_PROJECT_DIR}/.env cp ${AUTHZ_PROJECT_DIR}/.env.test.default ${AUTHZ_PROJECT_DIR}/.env.test +# === Testing === + authz/test/type: npx nx test:type ${AUTHZ_PROJECT_NAME} diff --git a/apps/documentation/docs/tutorial-basics/debugging-redis-with-redis-insight.md b/apps/documentation/docs/tutorial-basics/debugging-redis-with-redis-insight.md new file mode 100644 index 000000000..e3fea4773 --- /dev/null +++ b/apps/documentation/docs/tutorial-basics/debugging-redis-with-redis-insight.md @@ -0,0 +1,17 @@ +# Debugging Redis with Redis Insight + +Redis Insight is a graphical user interface for Redis. It is available as a +Docker image, and can be used to inspect the contents of your Redis database. + +```bash + docker run -v redisinsight:/db \ + --publish 8001:8001 \ + redislabs/redisinsight:latest +``` + +You can then access Redis Insight at http://localhost:8001. + +> [!IMPORTANT] +> When adding a new connection, use the hostname of the host machine, not +> `localhost`. +> If you're on macOS, use `host.docker.internal`. diff --git a/apps/orchestration/.env.default b/apps/orchestration/.env.default index 56b015d22..787ffc815 100644 --- a/apps/orchestration/.env.default +++ b/apps/orchestration/.env.default @@ -1,3 +1,8 @@ +NODE_ENV=development + PORT=3005 ORCHESTRATION_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/orchestration?schema=public" + +REDIS_HOST=localhost +REDIS_PORT=6379 diff --git a/apps/orchestration/Makefile b/apps/orchestration/Makefile index ecd8afc94..70411a5da 100644 --- a/apps/orchestration/Makefile +++ b/apps/orchestration/Makefile @@ -1,19 +1,25 @@ ORCHESTRATION_PROJECT_NAME := orchestration ORCHESTRATION_PROJECT_DIR := ./apps/orchestration -ORCHESTRATION_DATABASE_SCHEMA := ${ORCHESTRATION_PROJECT_DIR}/src/persistence/schema/schema.prisma +ORCHESTRATION_DATABASE_SCHEMA := ${ORCHESTRATION_PROJECT_DIR}/src/shared/module/persistence/schema/schema.prisma + +# === Start === orchestration/start/dev: npx nx serve ${ORCHESTRATION_PROJECT_NAME} +# === Setup === + orchestration/setup: make orchestration/copy-default-env - make orchestration/db/migrate + make orchestration/db/setup make orchestration/test/db/setup orchestration/copy-default-env: cp ${ORCHESTRATION_PROJECT_DIR}/.env.default ${ORCHESTRATION_PROJECT_DIR}/.env cp ${ORCHESTRATION_PROJECT_DIR}/.env.test.default ${ORCHESTRATION_PROJECT_DIR}/.env.test +# === Database === + orchestration/db/generate-types: npx prisma generate \ --schema ${ORCHESTRATION_DATABASE_SCHEMA} @@ -23,12 +29,27 @@ orchestration/db/migrate: prisma migrate dev \ --schema ${ORCHESTRATION_DATABASE_SCHEMA} +orchestration/db/setup: + npx dotenv -e ${ORCHESTRATION_PROJECT_DIR}/.env -- \ + prisma migrate reset \ + --schema ${ORCHESTRATION_DATABASE_SCHEMA} \ + --force + make orchestration/db/seed + +orchestration/db/create-migration: + npx dotenv -e ${ORCHESTRATION_PROJECT_DIR}/.env -- \ + prisma migrate dev \ + --schema ${ORCHESTRATION_DATABASE_SCHEMA} \ + --name ${NAME} + # Reference: https://www.prisma.io/docs/orm/prisma-migrate/workflows/seeding#seeding-your-database-with-typescript-or-javascript orchestration/db/seed: npx dotenv -e ${ORCHESTRATION_PROJECT_DIR}/.env -- \ ts-node \ --compiler-options "{\"module\":\"CommonJS\"}" \ - ${ORCHESTRATION_PROJECT_DIR}/src/persistence/seed.ts + ${ORCHESTRATION_PROJECT_DIR}/src/shared/module/persistence/seed.ts + +# === Testing === orchestration/test/db/setup: npx dotenv -e ${ORCHESTRATION_PROJECT_DIR}/.env.test --override -- \ diff --git a/apps/orchestration/README.md b/apps/orchestration/README.md index 17b24d261..9486f0dd2 100644 --- a/apps/orchestration/README.md +++ b/apps/orchestration/README.md @@ -32,3 +32,10 @@ make orchestration/test/unit make orchestration/test/integration make orchestration/test/e2e ``` + +## Database + +```bash +make orchestration/db/migrate +make orchestration/db/create-migration NAME=your-migration-name +``` diff --git a/apps/orchestration/src/main.ts b/apps/orchestration/src/main.ts index a7616167a..8ef58191d 100644 --- a/apps/orchestration/src/main.ts +++ b/apps/orchestration/src/main.ts @@ -3,22 +3,23 @@ import { INestApplication, Logger, ValidationPipe } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { NestFactory } from '@nestjs/core' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' -import { lastValueFrom, map, of, switchMap } from 'rxjs' +import { lastValueFrom, map, of, switchMap, tap } from 'rxjs' import { Config } from './orchestration.config' /** * Sets up Swagger documentation for the application. * * @param app - The INestApplication instance. - * @param logger - The logger instance. * @returns The modified INestApplication instance. */ -const setupSwagger = (app: INestApplication, logger: Logger): INestApplication => { - logger.log('Setting up Swagger') - +const withSwagger = (app: INestApplication): INestApplication => { const document = SwaggerModule.createDocument( app, - new DocumentBuilder().setTitle('Orchestration').setVersion('1.0').addTag('Orchestration').build() + new DocumentBuilder() + .setTitle('Orchestration') + .setDescription('Orchestration is the most secure infrastructure to run authorization for web3.') + .setVersion('1.0') + .build() ) SwaggerModule.setup('docs', app, document, { swaggerOptions: { @@ -35,12 +36,9 @@ const setupSwagger = (app: INestApplication, logger: Logger): INestApplication = * Sets up REST global validation for the application. * * @param app - The INestApplication instance. - * @param logger - The logger instance. * @returns The modified INestApplication instance. */ -const setupRestValidation = (app: INestApplication, logger: Logger): INestApplication => { - logger.log('Setting up REST global validation') - +const withRestValidation = (app: INestApplication): INestApplication => { app.useGlobalPipes(new ValidationPipe()) return app @@ -52,7 +50,7 @@ const setupRestValidation = (app: INestApplication, logger: Logger): INestApplic * @returns {Promise} A promise that resolves when the application is * successfully bootstrapped. */ -async function bootstrap() { +async function bootstrap(): Promise { const logger = new Logger('OrchestrationBootstrap') const application = await NestFactory.create(OrchestrationModule) const configService = application.get>(ConfigService) @@ -60,8 +58,10 @@ async function bootstrap() { await lastValueFrom( of(application).pipe( - map((app) => setupSwagger(app, logger)), - map((app) => setupRestValidation(app, logger)), + map((app) => withSwagger(app)), + tap(() => logger.log('Added Swagger')), + map((app) => withRestValidation(app)), + tap(() => logger.log('Added REST global validation')), switchMap((app) => app.listen(port)) ) ) diff --git a/apps/orchestration/src/orchestration.config.ts b/apps/orchestration/src/orchestration.config.ts index cb08d8781..6766fdb7f 100644 --- a/apps/orchestration/src/orchestration.config.ts +++ b/apps/orchestration/src/orchestration.config.ts @@ -1,19 +1,34 @@ import { z } from 'zod' -const ConfigSchema = z.object({ +export enum Env { + DEVELOPMENT = 'development', + TEST = 'test' +} + +const configSchema = z.object({ + env: z.nativeEnum(Env), port: z.coerce.number(), database: z.object({ url: z.string().startsWith('postgresql://') + }), + redis: z.object({ + host: z.string().min(0), + port: z.coerce.number() }) }) -export type Config = z.infer +export type Config = z.infer export const load = (): Config => { - const result = ConfigSchema.safeParse({ + const result = configSchema.safeParse({ + env: process.env.NODE_ENV, port: process.env.PORT, database: { url: process.env.ORCHESTRATION_DATABASE_URL + }, + redis: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT } }) @@ -21,5 +36,5 @@ export const load = (): Config => { return result.data } - throw new Error(`Invalid application configuration: ${result.error.message}`) + throw new Error(`Invalid Orchestration configuration: ${result.error.message}`) } diff --git a/apps/orchestration/src/orchestration.constant.ts b/apps/orchestration/src/orchestration.constant.ts new file mode 100644 index 000000000..e5597ac53 --- /dev/null +++ b/apps/orchestration/src/orchestration.constant.ts @@ -0,0 +1,4 @@ +export const QUEUE_PREFIX = 'orchestration' +export const AUTHORIZATION_REQUEST_PROCESSING_QUEUE = 'authorization-request:processing' + +export const REQUEST_HEADER_ORG_ID = 'x-org-id' diff --git a/apps/orchestration/src/orchestration.module.ts b/apps/orchestration/src/orchestration.module.ts index 337b7cdd9..a3a30b074 100644 --- a/apps/orchestration/src/orchestration.module.ts +++ b/apps/orchestration/src/orchestration.module.ts @@ -1,8 +1,8 @@ import { PolicyEngineModule } from '@app/orchestration/policy-engine/policy-engine.module' -import { TransactionEngineModule } from '@narval/transaction-engine-module' import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { load } from './orchestration.config' +import { QueueModule } from './shared/module/queue/queue.module' @Module({ imports: [ @@ -10,8 +10,8 @@ import { load } from './orchestration.config' load: [load], isGlobal: true }), - PolicyEngineModule, - TransactionEngineModule + QueueModule.forRoot(), + PolicyEngineModule ] }) export class OrchestrationModule {} diff --git a/apps/orchestration/src/persistence/persistence.module.ts b/apps/orchestration/src/persistence/persistence.module.ts deleted file mode 100644 index ddbf93b4a..000000000 --- a/apps/orchestration/src/persistence/persistence.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PrismaService } from '@app/orchestration/persistence/service/prisma.service' -import { TestPrismaService } from '@app/orchestration/persistence/service/test-prisma.service' -import { Module } from '@nestjs/common' - -@Module({ - exports: [PrismaService, TestPrismaService], - providers: [PrismaService, TestPrismaService] -}) -export class PersistenceModule {} diff --git a/apps/orchestration/src/persistence/schema/migrations/20240103105012_init/migration.sql b/apps/orchestration/src/persistence/schema/migrations/20240103105012_init/migration.sql deleted file mode 100644 index b6a8924a8..000000000 --- a/apps/orchestration/src/persistence/schema/migrations/20240103105012_init/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "name" TEXT, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/apps/orchestration/src/persistence/schema/schema.prisma b/apps/orchestration/src/persistence/schema/schema.prisma deleted file mode 100644 index e42ad3b7e..000000000 --- a/apps/orchestration/src/persistence/schema/schema.prisma +++ /dev/null @@ -1,19 +0,0 @@ -generator client { - provider = "prisma-client-js" - // Output into a separate subdirectory so multiple schemas can be used in a - // monorepo. - // - // Reference: https://github.com/nrwl/nx-recipes/tree/main/nestjs-prisma - output = "../../../../../node_modules/@prisma/client/orchestration" -} - -datasource db { - provider = "postgresql" - url = env("ORCHESTRATION_DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -} diff --git a/apps/orchestration/src/persistence/seed.ts b/apps/orchestration/src/persistence/seed.ts deleted file mode 100644 index a7580d759..000000000 --- a/apps/orchestration/src/persistence/seed.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrismaClient } from '@prisma/client/orchestration' - -const prisma = new PrismaClient() - -async function main() { - const alice = await prisma.user.upsert({ - where: { email: 'alice@prisma.io' }, - update: {}, - create: { - email: 'alice@prisma.io', - name: 'Alice' - } - }) - - const bob = await prisma.user.upsert({ - where: { email: 'bob@prisma.io' }, - update: {}, - create: { - email: 'bob@prisma.io', - name: 'Bob' - } - }) - - console.log({ alice, bob }) -} - -main() - .then(async () => { - await prisma.$disconnect() - }) - .catch(async (e) => { - console.error(e) - await prisma.$disconnect() - process.exit(1) - }) 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 8ff2711f8..ab410a3f5 100644 --- a/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts +++ b/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts @@ -1,17 +1,39 @@ import { load } from '@app/orchestration/orchestration.config' -import { PersistenceModule } from '@app/orchestration/persistence/persistence.module' -import { TestPrismaService } from '@app/orchestration/persistence/service/test-prisma.service' -import { Action, Decision } from '@app/orchestration/policy-engine/core/type/domain.type' +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 { 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' +import { TestPrismaService } from '@app/orchestration/shared/module/persistence/service/test-prisma.service' +import { QueueModule } from '@app/orchestration/shared/module/queue/queue.module' +import { getQueueToken } from '@nestjs/bull' import { HttpStatus, INestApplication } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' 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' + +const EVALUATIONS_ENDPOINT = '/policy-engine/evaluations' describe('Policy Engine Cluster Facade', () => { let app: INestApplication let module: TestingModule let testPrismaService: TestPrismaService + let authzRequestRepository: AuthorizationRequestRepository + let authzRequestProcessingQueue: Queue + + // TODO: Create domain type + const org: Organization = { + id: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc', + name: 'Test Evaluation', + createdAt: new Date(), + updatedAt: new Date() + } beforeAll(async () => { module = await Test.createTestingModule({ @@ -20,12 +42,15 @@ describe('Policy Engine Cluster Facade', () => { load: [load], isGlobal: true }), - PolicyEngineModule, - PersistenceModule + QueueModule.forRoot(), + PersistenceModule, + PolicyEngineModule ] }).compile() testPrismaService = module.get(TestPrismaService) + authzRequestRepository = module.get(AuthorizationRequestRepository) + authzRequestProcessingQueue = module.get(getQueueToken(AUTHORIZATION_REQUEST_PROCESSING_QUEUE)) app = module.createNestApplication() @@ -33,78 +58,95 @@ describe('Policy Engine Cluster Facade', () => { }) beforeEach(async () => { + await testPrismaService.getClient().organization.create({ data: org }) + }) + + afterEach(async () => { await testPrismaService.truncateAll() + await authzRequestProcessingQueue.empty() }) afterAll(async () => { await testPrismaService.truncateAll() + await authzRequestProcessingQueue.empty() + module.close() }) - describe('POST /evaluation', () => { - const EVALUATION_ENDPOINT = '/policy-engine/evaluation' - + describe('POST /evaluations', () => { it('evaluates a sign message authorization request', async () => { - const { status, body } = await request(app.getHttpServer()) - .post(EVALUATION_ENDPOINT) - .send({ - action: Action.SIGN_MESSAGE, - authentication: { - signature: { - hash: 'fake-signature-hash-one', - type: 'ECDSA' - } - }, - approval: { - signatures: [{ hash: 'fake-signature-hash-two', type: 'ECDSA' }] - }, - request: { - message: 'Sign me, please!' + const signMessageRequest = { + message: 'Sign me, please' + } + const payload = { + action: Action.SIGN_MESSAGE, + request: signMessageRequest, + hash: hashMessage(JSON.stringify(signMessageRequest)), + 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(status).toEqual(HttpStatus.OK) - expect(body).toEqual({ - decision: Decision.CONFIRM, - reasons: [ - { - code: 'require_approval', - message: 'Missing one or more approval(s)' - } - ] + 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) }) }) }) - // Temporary test to ensure the connectivity with a test database. - describe('GET /users', () => { - const USERS_ENDPOINT = '/policy-engine/users' - - const bob = { - id: 1, - email: 'bob@test.com', - name: 'Bob' + describe('GET /evaluations/:id', () => { + const signMessageRequest = { + message: 'Testing sign message request' } - - const alice = { - id: 2, - email: 'alice@test.com', - name: 'Alice' + const authzRequest: AuthorizationRequest = { + id: '986ae19d-c30c-40c6-b873-1fb6c49011de', + orgId: org.id, + initiatorId: 'ac792884-be18-4361-9323-8f711c3f070e', + status: AuthorizationRequestStatus.PERMITTED, + action: Action.SIGN_MESSAGE, + request: signMessageRequest, + hash: hashMessage(JSON.stringify(signMessageRequest)), + idempotencyKey: '8dcbb7ad-82a2-4eca-b2f0-b1415c1d4a17', + createdAt: new Date(), + updatedAt: new Date() } - const sortByEmail = (a: { email: string }, b: { email: string }) => a.email.localeCompare(b.email) - beforeEach(async () => { - await testPrismaService.getPrismaClient().user.createMany({ - data: [alice, bob] - }) + await authzRequestRepository.create(authzRequest) }) - it('responds with users', async () => { - const { status, body } = await request(app.getHttpServer()).get(USERS_ENDPOINT) + it('responds with authorization request', async () => { + const { status, body } = await request(app.getHttpServer()) + .get(`${EVALUATIONS_ENDPOINT}/${authzRequest.id}`) + .set(REQUEST_HEADER_ORG_ID, org.id) expect(status).toEqual(HttpStatus.OK) - expect(body.sort(sortByEmail)).toEqual([alice, bob].sort(sortByEmail)) + expect(body).toEqual({ + ...authzRequest, + createdAt: authzRequest.createdAt.toISOString(), + updatedAt: authzRequest.createdAt.toISOString() + }) }) }) }) diff --git a/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts b/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts new file mode 100644 index 000000000..84a2d7e35 --- /dev/null +++ b/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts @@ -0,0 +1,42 @@ +import { + AuthorizationRequest, + AuthorizationRequestStatus, + CreateAuthorizationRequest +} from '@app/orchestration/policy-engine/core/type/domain.type' +import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository' +import { AuthorizationRequestProcessingProducer } from '@app/orchestration/policy-engine/queue/producer/authorization-request-processing.producer' +import { Injectable } from '@nestjs/common' + +@Injectable() +export class AuthorizationRequestService { + constructor( + private authzRequestRepository: AuthorizationRequestRepository, + private authzRequestProcessingProducer: AuthorizationRequestProcessingProducer + ) {} + + async create(input: CreateAuthorizationRequest): Promise { + const authzRequest = await this.authzRequestRepository.create(input) + + await this.authzRequestProcessingProducer.add(authzRequest) + + return authzRequest + } + + async findById(id: string): Promise { + return this.authzRequestRepository.findById(id) + } + + async process(id: string) { + await this.authzRequestRepository.findById(id) + + await this.authzRequestRepository.changeStatus(id, AuthorizationRequestStatus.PROCESSING) + + await new Promise((resolve) => { + setTimeout(() => resolve(true), 3000) + }) + } + + async complete(id: string) { + await this.authzRequestRepository.changeStatus(id, AuthorizationRequestStatus.APPROVING) + } +} 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 8fab7fb4d..73e93ecd0 100644 --- a/apps/orchestration/src/policy-engine/core/type/domain.type.ts +++ b/apps/orchestration/src/policy-engine/core/type/domain.type.ts @@ -1,18 +1,84 @@ +import { SetOptional } from 'type-fest' + export enum Action { SIGN_TRANSACTION = 'signTransaction', - SIGN_MESSAGE = 'signMessage', - SIGN_TYPED_DATA = 'signTypedData' + SIGN_MESSAGE = 'signMessage' +} + +export enum AuthorizationRequestStatus { + CREATED = 'CREATED', + CANCELED = 'CANCELED', + PROCESSING = 'PROCESSING', + APPROVING = 'APPROVING', + PERMITTED = 'PERMITTED', + FORBIDDEN = 'FORBIDDEN' +} + +export type SharedAuthorizationRequest = { + id: string + orgId: string + initiatorId: string + status: `${AuthorizationRequestStatus}` + /** + * The hash of the request in EIP-191 format. + * + * @see https://eips.ethereum.org/EIPS/eip-191 + * @see https://viem.sh/docs/utilities/hashMessage.html + * @see https://docs.ethers.org/v5/api/utils/hashing/ + */ + hash: string + idempotencyKey?: string | null + createdAt: Date + updatedAt: Date +} + +export type Hex = `0x${string}` +export type Address = `0x${string}` +export type AccessList = { address: Address; storageKeys: Hex[] }[] + +// 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 + from: Address + to?: Address | null } -export enum Intent { - TRANSFER_NATIVE = 'transferNative', - TRANSFER_TOKEN = 'transferToken', - TRANSFER_NFT = 'transferNft', - CALL_CONTRACT = 'callContract' +export type SignTransactionAuthorizationRequest = SharedAuthorizationRequest & { + action: `${Action.SIGN_TRANSACTION}` + request: TransactionRequest +} + +export type MessageRequest = { + message: string +} + +export type SignMessageAuthorizationRequest = SharedAuthorizationRequest & { + action: `${Action.SIGN_MESSAGE}` + request: MessageRequest +} + +export type AuthorizationRequest = SignTransactionAuthorizationRequest | SignMessageAuthorizationRequest + +export type CreateAuthorizationRequest = SetOptional + +export function isSignTransaction(request: AuthorizationRequest): request is SignTransactionAuthorizationRequest { + return (request as SignTransactionAuthorizationRequest).action === Action.SIGN_TRANSACTION } -export enum Decision { - PERMIT = 'PERMIT', - FORBID = 'FORBID', - CONFIRM = 'CONFIRM' +export function isSignMessage(request: AuthorizationRequest): request is SignMessageAuthorizationRequest { + return (request as SignMessageAuthorizationRequest).action === Action.SIGN_MESSAGE } diff --git a/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts b/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts index 4901ca31e..73bd8cf1a 100644 --- a/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts +++ b/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts @@ -1,81 +1,75 @@ -import { PrismaService } from '@app/orchestration/persistence/service/prisma.service' -import { Decision } from '@app/orchestration/policy-engine/core/type/domain.type' +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 { HttpService } from '@nestjs/axios' -import { Body, Controller, Get, HttpCode, HttpStatus, Logger, Post } from '@nestjs/common' -import { ApiOkResponse } from '@nestjs/swagger' -import { lastValueFrom, map, tap } from 'rxjs' +import { OrgId } from '@app/orchestration/shared/decorator/org-id.decorator' +import { Body, Controller, Get, HttpCode, HttpStatus, 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 + } + + 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') export class FacadeController { - private logger = new Logger(FacadeController.name) + constructor(private authorizationRequestService: AuthorizationRequestService) {} - constructor(private prisma: PrismaService, private http: HttpService) {} - - @Post('/evaluation') + @Post('/evaluations') @HttpCode(HttpStatus.OK) - @ApiOkResponse({ - description: 'The authorization evaluation has been successfully processed.', + @ApiResponse({ + status: HttpStatus.OK, + description: 'The authorization evaluation has been successfully submit', type: AuthorizationResponseDto }) - evaluate(@Body() evaluation: AuthorizationRequestDto): AuthorizationResponseDto { - this.logger.log(evaluation) + async evaluation(@OrgId() orgId: string, @Body() body: AuthorizationRequestDto): Promise { + const authzRequest = await this.authorizationRequestService.create(toDomainType(orgId, body)) - return { - decision: Decision.CONFIRM, - reasons: [ - { - code: 'require_approval', - message: 'Missing one or more approval(s)' - } - ] - } + return new AuthorizationResponseDto(authzRequest) } - @Get('/ping') - async ping() { - const cluster = [ - { - host: 'localhost', - port: 3010, - protocol: 'http' - }, - { - host: 'localhost', - port: 3010, - protocol: 'http' - } - ] - - const responses: string[] = [] - - for (const { protocol, host, port } of cluster) { - const url = `${protocol}://${host}:${port}/ping` - - const response = await lastValueFrom( - this.http.get(url).pipe( - map((response) => response.data), - tap((data) => - this.logger.log({ - message: 'Received response from node', - response: data, - url - }) - ), - map((pong) => `${pong} from ${url}`) - ) - ) + @Get('/evaluations/:id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'The authorization evaluation request', + type: AuthorizationResponseDto + }) + async getBydId(@Param('id') id: string): Promise { + const authzRequest = await this.authorizationRequestService.findById(id) - responses.push(response) + if (authzRequest) { + return new AuthorizationResponseDto(authzRequest) } - return responses - } - - // Temporary endpoint to end-to-end test the connectivity with the database. - @Get('/users') - justCheckingTheDatabase() { - return this.prisma.user.findMany() + return null } } 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 0f1981352..08d269414 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,20 @@ -import { Action, Intent } from '@app/orchestration/policy-engine/core/type/domain.type' +import { Action, Address, Hex } from '@app/orchestration/policy-engine/core/type/domain.type' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' -import { Equals, IsEnum, IsString } from 'class-validator' +import { Type } from 'class-transformer' +import { + IsDefined, + IsEnum, + IsEthereumAddress, + IsHexadecimal, + IsString, + Validate, + ValidateNested +} from 'class-validator' +import { RequestHash } from './validator/request-hash.validator' -class SignatureDto { +export class SignatureDto { + @IsDefined() + @IsString() @ApiProperty() hash: string @@ -12,12 +24,12 @@ class SignatureDto { type?: string = 'ECDSA' } -class AuthenticationDto { +export class AuthenticationDto { @ApiProperty() signature: SignatureDto } -class ApprovalDto { +export class ApprovalDto { @ApiProperty({ type: () => SignatureDto, isArray: true @@ -25,53 +37,46 @@ class ApprovalDto { signatures: SignatureDto[] } -class TransferNativeIntentDto { +export class SignTransactionRequestDto { + @IsDefined() + @IsEthereumAddress() @ApiProperty({ - default: Intent.TRANSFER_NATIVE + required: true, + format: 'EthereumAddress(0x[Aa-fF0-9]{40})' }) - @IsString() - @Equals(Intent.TRANSFER_NATIVE) - type: string = Intent.TRANSFER_NATIVE -} + from: Address -class TransferTokenIntentDto { + @IsEthereumAddress() @ApiProperty({ - default: Intent.TRANSFER_TOKEN + format: 'EthereumAddress(0x[Aa-fF0-9]{40})' }) - @IsString() - @Equals(Intent.TRANSFER_TOKEN) - type: string = Intent.TRANSFER_TOKEN -} - -@ApiExtraModels(TransferNativeIntentDto, TransferTokenIntentDto) -class SignTransactionRequestDto { - @ApiProperty() - @IsString() - from: string - - @ApiProperty() - @IsString() - to: string + to: Address + @IsHexadecimal() @ApiProperty({ - oneOf: [{ $ref: getSchemaPath(TransferNativeIntentDto) }, { $ref: getSchemaPath(TransferTokenIntentDto) }] + type: 'string', + format: 'Hexadecimal' }) - intent: TransferNativeIntentDto | TransferTokenIntentDto + data: Hex } -class SignMessageRequestDto { - @ApiProperty() +export class SignMessageRequestDto { @IsString() + @IsDefined() + @ApiProperty({ + required: true + }) message: string } @ApiExtraModels(SignTransactionRequestDto, SignMessageRequestDto) export class AuthorizationRequestDto { + @IsEnum(Action) + @IsDefined() @ApiProperty({ enum: Action }) - @IsEnum(Action) - action: Action + action: `${Action}` @ApiProperty() authentication: AuthenticationDto @@ -79,8 +84,29 @@ export class AuthorizationRequestDto { @ApiProperty() approval: ApprovalDto + @ValidateNested() + @Type((opts) => { + return opts?.object.action === Action.SIGN_TRANSACTION ? SignTransactionRequestDto : SignMessageRequestDto + }) @ApiProperty({ oneOf: [{ $ref: getSchemaPath(SignTransactionRequestDto) }, { $ref: getSchemaPath(SignMessageRequestDto) }] }) request: SignTransactionRequestDto | SignMessageRequestDto + + @IsString() + @IsDefined() + @Validate(RequestHash) + @ApiProperty({ + description: 'The hash of the request in EIP-191 format.', + required: true + }) + hash: string + + isSignTransaction(request: SignTransactionRequestDto | SignMessageRequestDto): request is SignTransactionRequestDto { + return this.action === Action.SIGN_TRANSACTION + } + + isSignMessage(request: SignTransactionRequestDto | SignMessageRequestDto): request is SignMessageRequestDto { + return this.action === Action.SIGN_MESSAGE + } } 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 cff07405c..00453d27d 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,27 +1,51 @@ -import { Decision } from '@app/orchestration/policy-engine/core/type/domain.type' +import { Action, AuthorizationRequestStatus } from '@app/orchestration/policy-engine/core/type/domain.type' import { ApiProperty } from '@nestjs/swagger' -import { IsEnum, IsString } from 'class-validator' +import { IsString } from 'class-validator' -class ReasonDto { - @ApiProperty() - @IsString() - code: string +export class AuthorizationResponseDto { + @ApiProperty({ + required: true + }) + id: string + + @ApiProperty({ + required: true + }) + orgId: string + + @ApiProperty({ + required: true + }) + initiatorId: string - @ApiProperty() @IsString() - message: string -} + idempotencyKey?: string | null + + @ApiProperty({ + required: true, + enum: Action + }) + action: `${Action}` -export class AuthorizationResponseDto { @ApiProperty({ - enum: Decision + required: true, + enum: AuthorizationRequestStatus }) - @IsEnum(Decision) - decision: Decision + status: `${AuthorizationRequestStatus}` @ApiProperty({ - type: () => ReasonDto, - isArray: true + description: 'The hash of the request in EIP-191 format.', + required: true }) - reasons: ReasonDto[] + hash: string + + // TODO: Figure out the request discrimination. It's been too painful. + // @ApiProperty({ + // oneOf: [{ $ref: getSchemaPath(SignTransactionRequestDto) }, { $ref: getSchemaPath(SignMessageRequestDto) }] + // }) + // request: SignTransactionRequestDto | SignMessageRequestDto + + constructor(partial: Partial) { + Object.assign(this, partial) + } } diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/validator/__test__/unit/request-hash.validator.spec.ts b/apps/orchestration/src/policy-engine/http/rest/dto/validator/__test__/unit/request-hash.validator.spec.ts new file mode 100644 index 000000000..ba1f9ff27 --- /dev/null +++ b/apps/orchestration/src/policy-engine/http/rest/dto/validator/__test__/unit/request-hash.validator.spec.ts @@ -0,0 +1,76 @@ +import { RequestHash } from '@app/orchestration/policy-engine/http/rest/dto/validator/request-hash.validator' +import { ValidationArguments } from 'class-validator' + +describe('RequestHash Validator', () => { + let validator: RequestHash + + beforeEach(() => { + validator = new RequestHash() + }) + + describe('validate', () => { + it('returns true if the given hash matches the hash of the request object', () => { + const request = { foo: 'bar' } + const hash = RequestHash.hash(request) + const args: ValidationArguments = { + targetName: 'AuthorizationRequestDto', + object: { request }, + property: 'hash', + constraints: [], + value: hash + } + + const result = validator.validate(hash, args) + + expect(result).toBe(true) + }) + + it('returns false if the given hash does not match the hash of the request object', () => { + const hash = '123456' + const request = { foo: 'baz' } + const args: ValidationArguments = { + targetName: 'AuthorizationRequestDto', + object: { request }, + property: 'hash', + constraints: [], + value: hash + } + + const result = validator.validate(hash, args) + + expect(result).toBe(false) + }) + + it('returns false if the request object is not present in the arguments', () => { + const hash = '123456' + const args: ValidationArguments = { + targetName: 'AuthorizationRequestDto', + object: {}, + property: 'hash', + constraints: [], + value: hash + } + + const result = validator.validate(hash, args) + + expect(result).toBe(false) + }) + }) + + describe('defaultMessage', () => { + it('returns error message for the given property', () => { + const property = 'hash' + const args: ValidationArguments = { + targetName: 'AuthorizationRequestDto', + object: {}, + property, + constraints: [], + value: '' + } + + const message = validator.defaultMessage(args) + + expect(message).toBe(`${property} is not a valid EIP-191 hash format`) + }) + }) +}) diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/validator/request-hash.validator.ts b/apps/orchestration/src/policy-engine/http/rest/dto/validator/request-hash.validator.ts new file mode 100644 index 000000000..bb7f51273 --- /dev/null +++ b/apps/orchestration/src/policy-engine/http/rest/dto/validator/request-hash.validator.ts @@ -0,0 +1,23 @@ +import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator' +import { hashMessage } from 'viem' + +@ValidatorConstraint({ async: false }) +export class RequestHash implements ValidatorConstraintInterface { + validate(givenHash: string, args: ValidationArguments) { + if ('request' in args.object) { + const hash = RequestHash.hash(args.object.request) + + return givenHash === hash + } + + return false + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} is not a valid EIP-191 hash format` + } + + static hash(request: unknown): string { + return hashMessage(JSON.stringify(request)) + } +} diff --git a/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts b/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts new file mode 100644 index 000000000..77d659a47 --- /dev/null +++ b/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts @@ -0,0 +1,112 @@ +import { + Action, + AuthorizationRequest, + AuthorizationRequestStatus, + CreateAuthorizationRequest, + MessageRequest, + TransactionRequest +} from '@app/orchestration/policy-engine/core/type/domain.type' +import { PrismaService } from '@app/orchestration/shared/module/persistence/service/prisma.service' +import { Injectable } from '@nestjs/common' +import { AuthorizationRequest as AuthorizationRequestModel } from '@prisma/client/orchestration' + +const toDomainType = (model: AuthorizationRequestModel): AuthorizationRequest => { + const shared = { + id: model.id, + orgId: model.orgId, + initiatorId: model.initiatorId, + status: model.status, + hash: model.hash, + idempotencyKey: model.idempotencyKey, + createdAt: model.createdAt, + updatedAt: model.updatedAt + } + + if (model.action === Action.SIGN_MESSAGE) { + return { + ...shared, + action: model.action, + request: { + // IMPORTANT: Get rid of this force cast. + message: (model.request as MessageRequest).message + } + } + } + + // IMPORTANT: Get rid of this force cast. + const request = model.request as TransactionRequest + + return { + ...shared, + action: model.action, + request + } +} + +@Injectable() +export class AuthorizationRequestRepository { + constructor(private prismaService: PrismaService) {} + + async create({ + id, + action, + request, + orgId, + initiatorId, + hash, + status, + idempotencyKey, + createdAt, + updatedAt + }: CreateAuthorizationRequest): Promise { + const model = await this.prismaService.authorizationRequest.create({ + data: { + status: status || AuthorizationRequestStatus.CREATED, + id, + orgId, + initiatorId, + action, + request, + hash, + idempotencyKey, + createdAt, + updatedAt + } + }) + + return toDomainType(model) + } + + async findById(id: string): Promise { + const model = await this.prismaService.authorizationRequest.findUnique({ + where: { id } + }) + + if (model) { + return toDomainType(model) + } + + return null + } + + async findByStatus( + statusOrStatuses: AuthorizationRequestStatus | AuthorizationRequestStatus[] + ): Promise { + const models = await this.prismaService.authorizationRequest.findMany({ + where: { + status: Array.isArray(statusOrStatuses) ? { in: statusOrStatuses } : statusOrStatuses + } + }) + + return models.map(toDomainType) + } + + async changeStatus(id: string, status: AuthorizationRequestStatus): Promise { + const model = await this.prismaService.authorizationRequest.update({ + where: { id }, + data: { status } + }) + + return toDomainType(model) + } +} diff --git a/apps/orchestration/src/policy-engine/policy-engine.module.ts b/apps/orchestration/src/policy-engine/policy-engine.module.ts index 1a383398e..5a4b6a9e9 100644 --- a/apps/orchestration/src/policy-engine/policy-engine.module.ts +++ b/apps/orchestration/src/policy-engine/policy-engine.module.ts @@ -1,11 +1,50 @@ -import { PersistenceModule } from '@app/orchestration/persistence/persistence.module' +import { AUTHORIZATION_REQUEST_PROCESSING_QUEUE } from '@app/orchestration/orchestration.constant' +import { PersistenceModule } from '@app/orchestration/shared/module/persistence/persistence.module' +import { BullAdapter } from '@bull-board/api/bullAdapter' +import { BullBoardModule } from '@bull-board/nestjs' import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { BullModule } from '@nestjs/bull' +import { Logger, Module, OnApplicationBootstrap } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' +import { AuthorizationRequestService } from './core/service/authorization-request.service' import { FacadeController } from './http/rest/controller/facade.controller' +import { AuthorizationRequestRepository } from './persistence/repository/authorization-request.repository' +import { AuthorizationRequestProcessingConsumer } from './queue/consumer/authorization-request-processing.consumer' +import { AuthorizationRequestProcessingProducer } from './queue/producer/authorization-request-processing.producer' @Module({ - imports: [ConfigModule.forRoot(), PersistenceModule, HttpModule], - controllers: [FacadeController] + imports: [ + ConfigModule.forRoot(), + HttpModule, + PersistenceModule, + // TODO (@wcalderipe, 11/01/24): Figure out why can I have a wrapper to + // register both queue and board at the same time. + // + // Desired DevX: QueueModule.registerQueue({ name: "my-queue" }) + BullModule.registerQueue({ + name: AUTHORIZATION_REQUEST_PROCESSING_QUEUE + }), + BullBoardModule.forFeature({ + name: AUTHORIZATION_REQUEST_PROCESSING_QUEUE, + adapter: BullAdapter + }) + ], + controllers: [FacadeController], + providers: [ + AuthorizationRequestService, + AuthorizationRequestRepository, + AuthorizationRequestProcessingConsumer, + AuthorizationRequestProcessingProducer + ] }) -export class PolicyEngineModule {} +export class PolicyEngineModule implements OnApplicationBootstrap { + private logger = new Logger(PolicyEngineModule.name) + + constructor(private authzRequestProcessingProducer: AuthorizationRequestProcessingProducer) {} + + async onApplicationBootstrap() { + this.logger.log('Policy Engine module boot') + + await this.authzRequestProcessingProducer.onApplicationBootstrap() + } +} diff --git a/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts b/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts new file mode 100644 index 000000000..8aee10cbf --- /dev/null +++ b/apps/orchestration/src/policy-engine/queue/consumer/authorization-request-processing.consumer.ts @@ -0,0 +1,43 @@ +import { AUTHORIZATION_REQUEST_PROCESSING_QUEUE } from '@app/orchestration/orchestration.constant' +import { AuthorizationRequestService } from '@app/orchestration/policy-engine/core/service/authorization-request.service' +import { OnQueueActive, OnQueueCompleted, Process, Processor } from '@nestjs/bull' +import { Logger } from '@nestjs/common' +import { Job } from 'bull' + +@Processor(AUTHORIZATION_REQUEST_PROCESSING_QUEUE) +export class AuthorizationRequestProcessingConsumer { + private logger = new Logger(AuthorizationRequestProcessingConsumer.name) + + constructor(private authzService: AuthorizationRequestService) {} + + @Process() + async process(job: Job) { + this.logger.log('Processing authorization request job', { + id: job.id, + data: job.data + }) + + await this.authzService.process(job.id.toString()) + + return true + } + + @OnQueueActive() + onActive(job: Job) { + this.logger.log('Consuming authorization request job', { + id: job.id, + data: job.data + }) + } + + @OnQueueCompleted() + onCompleted(job: Job, result: unknown) { + this.logger.log('Completed authorization request job', { + id: job.id, + data: job.data, + result + }) + + this.authzService.complete(job.id.toString()) + } +} diff --git a/apps/orchestration/src/policy-engine/queue/producer/authorization-request-processing.producer.ts b/apps/orchestration/src/policy-engine/queue/producer/authorization-request-processing.producer.ts new file mode 100644 index 000000000..de340d344 --- /dev/null +++ b/apps/orchestration/src/policy-engine/queue/producer/authorization-request-processing.producer.ts @@ -0,0 +1,53 @@ +import { AUTHORIZATION_REQUEST_PROCESSING_QUEUE } from '@app/orchestration/orchestration.constant' +import { + AuthorizationRequest, + AuthorizationRequestStatus +} from '@app/orchestration/policy-engine/core/type/domain.type' +import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository' +import { InjectQueue } from '@nestjs/bull' +import { Injectable, Logger } from '@nestjs/common' +import { Queue } from 'bull' + +@Injectable() +export class AuthorizationRequestProcessingProducer { + private logger = new Logger(AuthorizationRequestProcessingProducer.name) + + constructor( + @InjectQueue(AUTHORIZATION_REQUEST_PROCESSING_QUEUE) private processingQueue: Queue, + private authzRequestRepository: AuthorizationRequestRepository + ) {} + + async add(authzRequest: AuthorizationRequest) { + await this.processingQueue.add( + { + id: authzRequest.id + }, + { + jobId: authzRequest.id + } + ) + } + + async bulkAdd(requests: AuthorizationRequest[]) { + const jobs = requests.map(({ id }) => ({ + data: { id }, + opts: { + jobId: id + } + })) + + await this.processingQueue.addBulk(jobs) + } + + async onApplicationBootstrap() { + const requests = await this.authzRequestRepository.findByStatus(AuthorizationRequestStatus.CREATED) + + if (requests.length) { + this.logger.log('Bulk add created authorization requests to the processing queue', { + ids: requests.map(({ id }) => id) + }) + + await this.bulkAdd(requests) + } + } +} diff --git a/apps/orchestration/src/shared/decorator/__test__/unit/org-id.decorator.spec.ts b/apps/orchestration/src/shared/decorator/__test__/unit/org-id.decorator.spec.ts new file mode 100644 index 000000000..d719efc8d --- /dev/null +++ b/apps/orchestration/src/shared/decorator/__test__/unit/org-id.decorator.spec.ts @@ -0,0 +1,34 @@ +import { REQUEST_HEADER_ORG_ID } from '@app/orchestration/orchestration.constant' +import { factory } from '@app/orchestration/shared/decorator/org-id.decorator' +import { ExecutionContext } from '@nestjs/common' + +describe('OrgId Decorator', () => { + it(`returns ${REQUEST_HEADER_ORG_ID} if it exists in the headers`, () => { + const orgId = '123456' + const headers = { + [REQUEST_HEADER_ORG_ID]: orgId + } + const request = { headers } + const context = { + switchToHttp: () => ({ + getRequest: () => request + }) + } as ExecutionContext + + const result = factory(null, context) + + expect(result).toBe(orgId) + }) + + it(`throws BadRequestException if ${REQUEST_HEADER_ORG_ID} is missing in the headers`, () => { + const headers = {} + const request = { headers } + const context = { + switchToHttp: () => ({ + getRequest: () => request + }) + } as ExecutionContext + + expect(() => factory(null, context)).toThrow(`Missing ${REQUEST_HEADER_ORG_ID} header`) + }) +}) diff --git a/apps/orchestration/src/shared/decorator/org-id.decorator.ts b/apps/orchestration/src/shared/decorator/org-id.decorator.ts new file mode 100644 index 000000000..7a1e38a2c --- /dev/null +++ b/apps/orchestration/src/shared/decorator/org-id.decorator.ts @@ -0,0 +1,21 @@ +import { REQUEST_HEADER_ORG_ID } from '@app/orchestration/orchestration.constant' +import { BadRequestException, ExecutionContext, createParamDecorator } from '@nestjs/common' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const factory = (value: any, ctx: ExecutionContext) => { + const headers = ctx.switchToHttp().getRequest().headers + const orgId = headers[REQUEST_HEADER_ORG_ID] + + if (orgId) { + return orgId + } + + throw new BadRequestException(`Missing ${REQUEST_HEADER_ORG_ID} header`) +} + +/** + * Decorator that extracts the organization ID from the request object. + * + * @throw BadRequestException + */ +export const OrgId = createParamDecorator(factory) diff --git a/apps/orchestration/src/shared/module/persistence/persistence.module.ts b/apps/orchestration/src/shared/module/persistence/persistence.module.ts new file mode 100644 index 000000000..bd34e89f7 --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/persistence.module.ts @@ -0,0 +1,9 @@ +import { PrismaService } from '@app/orchestration/shared/module/persistence/service/prisma.service' +import { TestPrismaService } from '@app/orchestration/shared/module/persistence/service/test-prisma.service' +import { Module } from '@nestjs/common' + +@Module({ + exports: [PrismaService, TestPrismaService], + providers: [PrismaService, TestPrismaService] +}) +export class PersistenceModule {} diff --git a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240110173739_init/migration.sql b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240110173739_init/migration.sql new file mode 100644 index 000000000..912115165 --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240110173739_init/migration.sql @@ -0,0 +1,49 @@ +-- CreateEnum +CREATE TYPE "AuthorizationRequestStatus" AS ENUM ('CREATED', 'CANCELED', 'PROCESSING', 'APPROVING', 'PERMITTED', 'FORBIDDEN'); + +-- CreateEnum +CREATE TYPE "AuthorizationRequestAction" AS ENUM ('signTransaction', 'signMessage'); + +-- CreateTable +CREATE TABLE "organization" ( + "id" VARCHAR(255) NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "authorization_request" ( + "id" VARCHAR(255) NOT NULL, + "orgId" TEXT NOT NULL, + "initiator_id" TEXT NOT NULL, + "status" "AuthorizationRequestStatus" NOT NULL DEFAULT 'CREATED', + "action" "AuthorizationRequestAction" NOT NULL, + "hash" TEXT NOT NULL, + "request" JSONB NOT NULL, + "idempotency_key" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "authorization_request_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "authorization_request_approval" ( + "id" VARCHAR(255) NOT NULL, + "signer_id" TEXT NOT NULL, + "signature" TEXT NOT NULL, + "alg" TEXT NOT NULL DEFAULT 'ECDSA', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "authorization_request_approval_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "authorization_request_idempotency_key_key" ON "authorization_request"("idempotency_key"); + +-- AddForeignKey +ALTER TABLE "authorization_request" ADD CONSTRAINT "authorization_request_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/orchestration/src/persistence/schema/migrations/migration_lock.toml b/apps/orchestration/src/shared/module/persistence/schema/migrations/migration_lock.toml similarity index 100% rename from apps/orchestration/src/persistence/schema/migrations/migration_lock.toml rename to apps/orchestration/src/shared/module/persistence/schema/migrations/migration_lock.toml diff --git a/apps/orchestration/src/shared/module/persistence/schema/schema.prisma b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma new file mode 100644 index 000000000..3ca73448a --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma @@ -0,0 +1,69 @@ +generator client { + provider = "prisma-client-js" + // Output into a separate subdirectory so multiple schemas can be used in a + // monorepo. + // + // Reference: https://github.com/nrwl/nx-recipes/tree/main/nestjs-prisma + output = "../../../../../../../node_modules/@prisma/client/orchestration" +} + +datasource db { + provider = "postgresql" + url = env("ORCHESTRATION_DATABASE_URL") +} + +model Organization { + id String @id @default(uuid()) @db.VarChar(255) + name String + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + + authorizationRequests AuthorizationRequest[] + + @@map("organization") +} + +enum AuthorizationRequestStatus { + CREATED + CANCELED + PROCESSING + APPROVING + PERMITTED + FORBIDDEN +} + +enum AuthorizationRequestAction { + signTransaction + signMessage +} + +model AuthorizationRequest { + id String @id @default(uuid()) @db.VarChar(255) + orgId String + initiatorId String @map("initiator_id") + status AuthorizationRequestStatus @default(CREATED) + action AuthorizationRequestAction + hash String + request Json + idempotencyKey String? @unique @map("idempotency_key") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + + org Organization @relation(fields: [orgId], references: [id]) + // TODO (@wcalderipe, 10/01/24): Add relationship with the User model via initiatorId. + + @@map("authorization_request") +} + +model AuthorizationRequestApproval { + id String @id @default(uuid()) @db.VarChar(255) + signerId String @map("signer_id") + signature String + alg String @default("ECDSA") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + + // signer User @relation(fields: [signerId], references: [id]) + + @@map("authorization_request_approval") +} diff --git a/apps/orchestration/src/shared/module/persistence/seed.ts b/apps/orchestration/src/shared/module/persistence/seed.ts new file mode 100644 index 000000000..7319a716a --- /dev/null +++ b/apps/orchestration/src/shared/module/persistence/seed.ts @@ -0,0 +1,33 @@ +import { Logger } from '@nestjs/common' +import { Organization, PrismaClient } from '@prisma/client/orchestration' + +const logger = new Logger('OrchestrationSeed') +const prisma = new PrismaClient() +const now = new Date() + +const orgs: Organization[] = [ + { + id: '7d704a62-d15e-4382-a826-1eb41563043b', + name: 'Dev', + createdAt: now, + updatedAt: now + } +] + +async function main() { + await prisma.organization.createMany({ + data: orgs + }) + + logger.log('Orchestration database germinated 🌱') +} + +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/apps/orchestration/src/persistence/service/__test__/unit/prisma.service.spec.ts b/apps/orchestration/src/shared/module/persistence/service/__test__/unit/prisma.service.spec.ts similarity index 83% rename from apps/orchestration/src/persistence/service/__test__/unit/prisma.service.spec.ts rename to apps/orchestration/src/shared/module/persistence/service/__test__/unit/prisma.service.spec.ts index b6c8a588b..cb520f0fb 100644 --- a/apps/orchestration/src/persistence/service/__test__/unit/prisma.service.spec.ts +++ b/apps/orchestration/src/shared/module/persistence/service/__test__/unit/prisma.service.spec.ts @@ -1,4 +1,4 @@ -import { PrismaService } from '@app/orchestration/persistence/service/prisma.service' +import { PrismaService } from '@app/orchestration/shared/module/persistence/service/prisma.service' import { createMock } from '@golevelup/ts-jest' import { ConfigService } from '@nestjs/config' diff --git a/apps/orchestration/src/persistence/service/prisma.service.ts b/apps/orchestration/src/shared/module/persistence/service/prisma.service.ts similarity index 100% rename from apps/orchestration/src/persistence/service/prisma.service.ts rename to apps/orchestration/src/shared/module/persistence/service/prisma.service.ts diff --git a/apps/orchestration/src/persistence/service/test-prisma.service.ts b/apps/orchestration/src/shared/module/persistence/service/test-prisma.service.ts similarity index 96% rename from apps/orchestration/src/persistence/service/test-prisma.service.ts rename to apps/orchestration/src/shared/module/persistence/service/test-prisma.service.ts index 2623fa356..bea1784dc 100644 --- a/apps/orchestration/src/persistence/service/test-prisma.service.ts +++ b/apps/orchestration/src/shared/module/persistence/service/test-prisma.service.ts @@ -6,7 +6,7 @@ import { PrismaService } from './prisma.service' export class TestPrismaService { constructor(private prisma: PrismaService) {} - getPrismaClient(): PrismaClient { + getClient(): PrismaClient { return this.prisma } diff --git a/apps/orchestration/src/shared/module/queue/queue.module.ts b/apps/orchestration/src/shared/module/queue/queue.module.ts new file mode 100644 index 000000000..82f315b34 --- /dev/null +++ b/apps/orchestration/src/shared/module/queue/queue.module.ts @@ -0,0 +1,38 @@ +import { Config, Env } from '@app/orchestration/orchestration.config' +import { QUEUE_PREFIX } from '@app/orchestration/orchestration.constant' +import { ExpressAdapter } from '@bull-board/express' +import { BullBoardModule } from '@bull-board/nestjs' +import { BullModule } from '@nestjs/bull' +import { DynamicModule } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' + +export type RegisterQueueOption = { + name: string +} + +const getQueuePrefix = (env: Env) => (env === Env.TEST ? `test:${QUEUE_PREFIX}` : QUEUE_PREFIX) + +export class QueueModule { + static forRoot(): DynamicModule { + return { + module: QueueModule, + imports: [ + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + const env = configService.get('env', { infer: true }) + + return { + prefix: getQueuePrefix(env), + redis: configService.get('redis', { infer: true }) + } + } + }), + BullBoardModule.forRoot({ + route: '/admin/queues', + adapter: ExpressAdapter + }) + ] + } + } +} diff --git a/package-lock.json b/package-lock.json index f9dac3ddc..dd93a2267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,14 @@ "apps/documentation" ], "dependencies": { + "@bull-board/api": "^5.11.0", + "@bull-board/express": "^5.11.0", + "@bull-board/nestjs": "^5.11.0", "@docusaurus/core": "3.0.0", "@docusaurus/preset-classic": "3.0.0", "@mdx-js/react": "^3.0.0", "@nestjs/axios": "^3.0.1", + "@nestjs/bull": "^10.0.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.2", @@ -23,6 +27,7 @@ "@nestjs/swagger": "^7.1.16", "@prisma/client": "^5.7.1", "axios": "^1.6.3", + "bull": "^4.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "clsx": "^1.2.1", @@ -32,6 +37,8 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "tslib": "^2.3.0", + "type-fest": "^4.9.0", + "viem": "^2.0.5", "zod": "^3.22.4" }, "devDependencies": { @@ -148,6 +155,11 @@ "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", "dev": true }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "node_modules/@algolia/autocomplete-core": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", @@ -2408,6 +2420,52 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bull-board/api": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-5.11.0.tgz", + "integrity": "sha512-i5nPoTodSQhpYsr93wKLiiWSoZdbYK6zJx0Urf0djKjkzXxh8Ubh9cmDQqavunu3E/bV2Fe5E8eIFvxuTafbig==", + "dependencies": { + "redis-info": "^3.0.8" + }, + "peerDependencies": { + "@bull-board/ui": "5.11.0" + } + }, + "node_modules/@bull-board/express": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-5.11.0.tgz", + "integrity": "sha512-Y+avqRHWpI8xu0mAD7ICZeFZekNkMcK/Kt5yGhgxoa5/I4e4bj19VLrU3qvxmXF1Af1lYqlVIYqOeoyF4lpfYg==", + "dependencies": { + "@bull-board/api": "5.11.0", + "@bull-board/ui": "5.11.0", + "ejs": "^3.1.7", + "express": "^4.17.3" + } + }, + "node_modules/@bull-board/nestjs": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-5.11.0.tgz", + "integrity": "sha512-+cSNmzPTjYIeG3X1kqELACOznwSWNOYChHZZWlupYoKFwznRiNN3kViJ2titXyHpMYqemYgUb0jBZ+4pnvZfzQ==", + "dependencies": { + "@nestjs/bull-shared": "^10.0.0" + }, + "peerDependencies": { + "@bull-board/api": "^5.11.0", + "@bull-board/express": "^5.11.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1" + } + }, + "node_modules/@bull-board/ui": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-5.11.0.tgz", + "integrity": "sha512-+1FVMUwgtI/lwiEs8NuUOP4BAbiJR3gFfqUK4ii3AKtt7Bc57PTvYw4q41FKbmnD3J8oTZHER8zRv8qe7keJ+Q==", + "dependencies": { + "@bull-board/api": "5.11.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4427,6 +4485,11 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "devOptional": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -4909,6 +4972,78 @@ "react": "^15.0.2 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", + "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", + "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", + "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", + "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", + "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", + "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@nestjs/axios": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", @@ -4920,6 +5055,42 @@ "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/bull": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-10.0.1.tgz", + "integrity": "sha512-1GcJ8BkHDgQdBMZ7SqAqgUHiFnISXmpGvewFeTc8wf87JLk2PweiKv9j9/KQKU+NI237pCe82XB0bXzTnsdxSw==", + "dependencies": { + "@nestjs/bull-shared": "^10.0.1", + "tslib": "2.6.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "bull": "^3.3 || ^4.0.0" + } + }, + "node_modules/@nestjs/bull-shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.0.1.tgz", + "integrity": "sha512-8Td36l2i5x9+iQWjPB5Bd5+6u5Eangb5DclNcwrdwKqvd28xE92MSW97P4JV52C2kxrTjZwx8ck/wObAwtpQPw==", + "dependencies": { + "tslib": "2.6.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/bull-shared/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, + "node_modules/@nestjs/bull/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, "node_modules/@nestjs/common": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", @@ -5121,6 +5292,28 @@ } } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7560,6 +7753,39 @@ "@prisma/debug": "5.7.1" } }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -9686,6 +9912,26 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "node_modules/abitype": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.0.tgz", + "integrity": "sha512-QvMHEUzgI9nPj9TWtUGnS2scas80/qaL5PBxGdwWhhvzqXfOph+IEiiiWrzuisu3U3JgDQVruW9oLbJoQ3oZ3A==", + "funding": { + "url": "https://github.com/sponsors/wagmi-dev" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -9881,6 +10127,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -10094,8 +10352,7 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -10758,6 +11015,23 @@ "semver": "^7.0.0" } }, + "node_modules/bull": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bull/-/bull-4.12.0.tgz", + "integrity": "sha512-+GCM3KayIZvgiwAq5YC1qDcuncQbRLusLULOBZYRky7a7ttf4tlKWaFxTFtOfRrcb0erzFw6aWy73waorvR5pw==", + "dependencies": { + "cron-parser": "^4.2.1", + "get-port": "^5.1.1", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.5.2", + "semver": "^7.5.2", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -11178,6 +11452,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11702,6 +11984,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -12877,6 +13170,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -13208,7 +13509,6 @@ "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", - "dev": true, "dependencies": { "jake": "^10.8.5" }, @@ -14647,7 +14947,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, "dependencies": { "minimatch": "^5.0.1" } @@ -14656,7 +14955,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -14665,7 +14963,6 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -15113,6 +15410,17 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -16228,6 +16536,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -16796,6 +17127,20 @@ "node": ">=0.10.0" } }, + "node_modules/isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -16907,7 +17252,6 @@ "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", - "dev": true, "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -16925,7 +17269,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -18074,11 +18417,21 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.flow": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -18164,6 +18517,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -20462,6 +20823,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msgpackr": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz", + "integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", + "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, "node_modules/multer": { "version": "1.4.4-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", @@ -20657,6 +21047,17 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", + "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -23073,6 +23474,33 @@ "node": ">=6.0.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "dependencies": { + "lodash": "^4.17.11" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -24415,6 +24843,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -25374,12 +25807,11 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.9.0.tgz", + "integrity": "sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -26174,6 +26606,55 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/viem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.0.5.tgz", + "integrity": "sha512-bModld3fP6e7dJmI2fROkI1JpcVqyUYLJ/TuK1HUBd61PazwDpm+QMmfJM9+yMdIsOVpbDbDGlUPWUupOqU1Bg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "0.10.0", + "isows": "1.0.3", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 406aa8ee5..42dc42578 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,14 @@ "typescript": "^5.3.3" }, "dependencies": { + "@bull-board/api": "^5.11.0", + "@bull-board/express": "^5.11.0", + "@bull-board/nestjs": "^5.11.0", "@docusaurus/core": "3.0.0", "@docusaurus/preset-classic": "3.0.0", "@mdx-js/react": "^3.0.0", "@nestjs/axios": "^3.0.1", + "@nestjs/bull": "^10.0.1", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.2", @@ -72,6 +76,7 @@ "@nestjs/swagger": "^7.1.16", "@prisma/client": "^5.7.1", "axios": "^1.6.3", + "bull": "^4.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "clsx": "^1.2.1", @@ -81,6 +86,8 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "tslib": "^2.3.0", + "type-fest": "^4.9.0", + "viem": "^2.0.5", "zod": "^3.22.4" } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 82e358b6c..6c9d7c296 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,26 +1,31 @@ { "compileOnSave": false, "compilerOptions": { - "rootDir": ".", - "sourceMap": true, + "baseUrl": ".", "declaration": false, - "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, - "target": "es2015", - "module": "esnext", "lib": ["es2020", "dom"], - "skipLibCheck": true, - "strictNullChecks": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitThis": true, "paths": { "@app/authz/*": ["apps/authz/src/*"], "@app/orchestration/*": ["apps/orchestration/src/*"], "@narval/transaction-engine-module": ["packages/transaction-engine-module/src/index.ts"], "@narval/transaction-request-intent": ["packages/transaction-request-intent/src/index.ts"] - } + }, + "removeComments": true, + "rootDir": ".", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "target": "es2015" }, "exclude": ["node_modules", "tmp"] }