From 5c308e091495e80387c7cd68163a4e51c6c0a630 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 20 Nov 2024 13:35:39 -0500 Subject: [PATCH] feat(webhooks): add postgres persistence to clients and requests Signed-off-by: david --- .../src/entities/WebhookClient.ts | 16 +++ .../src/entities/WebhookRequest.ts | 13 +++ .../indexer-database/src/entities/index.ts | 3 + packages/webhooks/package.json | 3 +- packages/webhooks/src/database/index.ts | 2 + .../src/database/webhookClientRepository.ts | 60 +++++----- .../src/database/webhookRequestRepository.ts | 44 +++---- .../webhooks/src/eventProcessorManager.ts | 34 ++++-- .../src/eventProcessors/depositStatus.ts | 36 ++++-- packages/webhooks/src/factory.ts | 4 +- packages/webhooks/src/index.ts | 1 - packages/webhooks/src/notifier.ts | 1 - packages/webhooks/src/store.ts | 107 ------------------ packages/webhooks/src/utils.ts | 7 ++ pnpm-lock.yaml | 57 ++++++---- 15 files changed, 178 insertions(+), 210 deletions(-) create mode 100644 packages/indexer-database/src/entities/WebhookClient.ts create mode 100644 packages/indexer-database/src/entities/WebhookRequest.ts create mode 100644 packages/webhooks/src/database/index.ts delete mode 100644 packages/webhooks/src/store.ts diff --git a/packages/indexer-database/src/entities/WebhookClient.ts b/packages/indexer-database/src/entities/WebhookClient.ts new file mode 100644 index 00000000..68bfd009 --- /dev/null +++ b/packages/indexer-database/src/entities/WebhookClient.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryColumn, Column } from "typeorm"; + +@Entity() +export class WebhookClient { + @Column() + name: string; + + @PrimaryColumn() + id: string; + + @Column() + apiKey: string; + + @Column("simple-array") + domains: string[]; +} diff --git a/packages/indexer-database/src/entities/WebhookRequest.ts b/packages/indexer-database/src/entities/WebhookRequest.ts new file mode 100644 index 00000000..4514c008 --- /dev/null +++ b/packages/indexer-database/src/entities/WebhookRequest.ts @@ -0,0 +1,13 @@ +import { Entity, PrimaryColumn, Column } from "typeorm"; + +@Entity() +export class WebhookRequest { + @PrimaryColumn() + id: string; + + @Column() + url: string; + + @Column() + filter: string; +} diff --git a/packages/indexer-database/src/entities/index.ts b/packages/indexer-database/src/entities/index.ts index 58f7c660..19148c7e 100644 --- a/packages/indexer-database/src/entities/index.ts +++ b/packages/indexer-database/src/entities/index.ts @@ -19,3 +19,6 @@ export * from "./BundleEvent"; export * from "./BundleBlockRange"; export * from "./RootBundleExecutedJoinTable"; export * from "./RelayHashInfo"; + +export * from "./WebhookRequest"; +export * from "./WebhookClient"; diff --git a/packages/webhooks/package.json b/packages/webhooks/package.json index 8a0e0d03..3caaabe2 100644 --- a/packages/webhooks/package.json +++ b/packages/webhooks/package.json @@ -25,7 +25,8 @@ "express": "^4.19.2", "express-bearer-token": "^3.0.0", "redis": "^4.7.0", - "superstruct": "2.0.3-1" + "superstruct": "2.0.3-1", + "typeorm": "^0.3.20" }, "exports": { ".": "./dist/index.js" diff --git a/packages/webhooks/src/database/index.ts b/packages/webhooks/src/database/index.ts new file mode 100644 index 00000000..d2a452f6 --- /dev/null +++ b/packages/webhooks/src/database/index.ts @@ -0,0 +1,2 @@ +export * from "./webhookRequestRepository"; +export * from "./webhookClientRepository"; diff --git a/packages/webhooks/src/database/webhookClientRepository.ts b/packages/webhooks/src/database/webhookClientRepository.ts index dd40764b..41a700e7 100644 --- a/packages/webhooks/src/database/webhookClientRepository.ts +++ b/packages/webhooks/src/database/webhookClientRepository.ts @@ -1,49 +1,49 @@ -import { AsyncStore } from "../store"; - -export interface WebhookClient { - id: string; - apiKey: string; - url: string; - domains: string[]; -} +import { DataSource } from "typeorm"; +import { entities } from "@repo/indexer-database"; // This class is intended to store integration clients allowed to use the webhook service. export class WebhookClientRepository { - constructor(private store: AsyncStore) {} + private repository; + + constructor(private dataSource: DataSource) { + this.repository = this.dataSource.getRepository(entities.WebhookClient); + } - public async registerClient(client: WebhookClient): Promise { - if (await this.store.has(client.id)) { + public async registerClient(client: entities.WebhookClient): Promise { + const existingClient = await this.repository.findOne({ + where: { id: client.id }, + }); + if (existingClient) { throw new Error(`Client with id ${client.id} already exists.`); } - await this.store.set(client.id, client); + await this.repository.save(client); } public async unregisterClient(clientId: string): Promise { - if (!(await this.store.has(clientId))) { + const existingClient = await this.repository.findOne({ + where: { id: clientId }, + }); + if (!existingClient) { throw new Error(`Client with id ${clientId} does not exist.`); } - await this.store.delete(clientId); + await this.repository.delete({ id: clientId }); } - public async getClient(clientId: string): Promise { - return this.store.get(clientId); + public async getClient( + clientId: string, + ): Promise { + return ( + (await this.repository.findOne({ where: { id: clientId } })) ?? undefined + ); } - public async listClients(): Promise { - const clients: WebhookClient[] = []; - for await (const client of this.store.values()) { - clients.push(client); - } - return clients; + public async listClients(): Promise { + return this.repository.find(); } - public async findClientsByApiKey(apiKey: string): Promise { - const clients: WebhookClient[] = []; - for await (const client of this.store.values()) { - if (client.apiKey === apiKey) { - clients.push(client); - } - } - return clients; + public async findClientsByApiKey( + apiKey: string, + ): Promise { + return this.repository.find({ where: { apiKey } }); } } diff --git a/packages/webhooks/src/database/webhookRequestRepository.ts b/packages/webhooks/src/database/webhookRequestRepository.ts index fa97de6e..a1ea3231 100644 --- a/packages/webhooks/src/database/webhookRequestRepository.ts +++ b/packages/webhooks/src/database/webhookRequestRepository.ts @@ -1,48 +1,52 @@ -import { AsyncStore } from "../store"; +import { DataSource } from "typeorm"; import { WebhookRequest } from "../types"; +import { entities } from "@repo/indexer-database"; export class WebhookRequestRepository { - constructor(private store: AsyncStore) {} + private repository; + + constructor(private dataSource: DataSource) { + this.repository = this.dataSource.getRepository(entities.WebhookRequest); + } public async register(webhook: WebhookRequest): Promise { - if (await this.store.has(webhook.id)) { + const existingWebhook = await this.repository.findOne({ + where: { id: webhook.id }, + }); + if (existingWebhook) { throw new Error(`Webhook with id ${webhook.id} already exists.`); } - await this.store.set(webhook.id, webhook); + await this.repository.save(webhook); } public async unregister(webhookId: string): Promise { - if (!(await this.store.has(webhookId))) { + const existingWebhook = await this.repository.findOne({ + where: { id: webhookId }, + }); + if (!existingWebhook) { throw new Error(`Webhook with id ${webhookId} does not exist.`); } - await this.store.delete(webhookId); + await this.repository.delete({ id: webhookId }); } public async getWebhook( webhookId: string, ): Promise { - return this.store.get(webhookId); + return ( + (await this.repository.findOne({ where: { id: webhookId } })) ?? undefined + ); } public async listWebhooks(): Promise { - const webhooks: WebhookRequest[] = []; - for await (const webhook of this.store.values()) { - webhooks.push(webhook); - } - return webhooks; + return this.repository.find(); } public async filterWebhooks(filter: string): Promise { - const webhooks: WebhookRequest[] = []; - for await (const webhook of this.store.values()) { - if (webhook.filter === filter) { - webhooks.push(webhook); - } - } - return webhooks; + return this.repository.find({ where: { filter } }); } public async hasWebhook(webhookId: string): Promise { - return this.store.has(webhookId); + const count = await this.repository.count({ where: { id: webhookId } }); + return count > 0; } } diff --git a/packages/webhooks/src/eventProcessorManager.ts b/packages/webhooks/src/eventProcessorManager.ts index 99a96414..6044d719 100644 --- a/packages/webhooks/src/eventProcessorManager.ts +++ b/packages/webhooks/src/eventProcessorManager.ts @@ -1,8 +1,4 @@ -import { MemoryStore } from "./store"; -import { - WebhookClientRepository, - WebhookClient, -} from "./database/webhookClientRepository"; +import { WebhookClientRepository } from "./database/webhookClientRepository"; import { DataSource, entities } from "@repo/indexer-database"; import { Logger } from "winston"; import assert from "assert"; @@ -33,15 +29,21 @@ export class EventProcessorManager { deps: Dependencies, ) { this.logger = deps.logger; - this.clientRepository = new WebhookClientRepository(new MemoryStore()); // Initialize the client manager + this.clientRepository = new WebhookClientRepository(deps.postgres); // Initialize the client manager } // Register a new type of webhook processor able to be written to public registerEventProcessor(name: string, webhook: IEventProcessor) { + this.logger.info( + `Attempting to register event processor with name: ${name}`, + ); assert( !this.processors.has(name), `Webhook with that name already exists: ${name}`, ); this.processors.set(name, webhook); + this.logger.info( + `Successfully registered event processor with name: ${name}`, + ); } private getEventProcessor(name: string) { @@ -61,6 +63,9 @@ export class EventProcessorManager { params: { type: string; url: string; filter: JSONValue }, apiKey?: string, ) { + this.logger.info( + `Attempting to register webhook of type: ${params.type} with URL: ${params.url}`, + ); if (this.config.requireApiKey) { if (apiKey === undefined) throw new Error("Api Key required"); const clients = await this.clientRepository.findClientsByApiKey(apiKey); @@ -79,7 +84,9 @@ export class EventProcessorManager { } } const webhook = this.getEventProcessor(params.type); - return webhook.register(params.url, params.filter); + const result = await webhook.register(params.url, params.filter); + this.logger.info(`Successfully registered webhook with ID: ${result}`); + return result; } // TODO: gaurd this with api key @@ -87,12 +94,17 @@ export class EventProcessorManager { params: { type: string; id: string }, apiKey?: string, ) { - // Assuming the IWebhook interface has an unregister method + this.logger.info( + `Attempting to unregister webhook of type: ${params.type} with ID: ${params.id}`, + ); const webhook = this.getEventProcessor(params.type); - return webhook.unregister(params.id); + await webhook.unregister(params.id); + this.logger.info(`Successfully unregistered webhook with ID: ${params.id}`); } - async registerClient(client: WebhookClient) { - return this.clientRepository.registerClient(client); + async registerClient(client: entities.WebhookClient) { + this.logger.info(`Attempting to register client with ID: ${client.id}`); + await this.clientRepository.registerClient(client); + this.logger.info(`Successfully registered client with ID: ${client.id}`); } } diff --git a/packages/webhooks/src/eventProcessors/depositStatus.ts b/packages/webhooks/src/eventProcessors/depositStatus.ts index 88862c27..9c7cf4ae 100644 --- a/packages/webhooks/src/eventProcessors/depositStatus.ts +++ b/packages/webhooks/src/eventProcessors/depositStatus.ts @@ -1,14 +1,11 @@ import assert from "assert"; +import * as ss from "superstruct"; + import { DataSource, entities } from "@repo/indexer-database"; import { WebhookRequestRepository } from "../database/webhookRequestRepository"; -import { - JSONValue, - IEventProcessor, - NotificationPayload, - WebhookRequest, -} from "../types"; +import { customId } from "../utils"; -import * as ss from "superstruct"; +import { IEventProcessor, NotificationPayload } from "../types"; export const DepositStatusEvent = ss.object({ originChainId: ss.number(), @@ -25,7 +22,6 @@ export const DepositStatusFilter = ss.object({ export type DepositStatusFilter = ss.Infer; export type Dependencies = { - webhookRequests: WebhookRequestRepository; notify: (params: NotificationPayload) => void; postgres: DataSource; }; @@ -33,14 +29,21 @@ export class DepositStatusProcessor implements IEventProcessor { private webhookRequests: WebhookRequestRepository; private notify: (params: NotificationPayload) => void; private postgres: DataSource; + // Type shoudl be uniqe across all event processors, this is to avoid colliding with multiple + // processors writing to the same tables + public type = "DepositStatus"; constructor(deps: Dependencies) { - this.webhookRequests = deps.webhookRequests; + this.webhookRequests = new WebhookRequestRepository(deps.postgres); this.notify = deps.notify; this.postgres = deps.postgres; } private async _write(event: DepositStatusEvent): Promise { - const filter = [event.originChainId, event.depositTxHash].join("!"); + const filter = customId( + this.type, + event.originChainId, + event.depositTxHash, + ); const hooks = await this.webhookRequests.filterWebhooks(filter); //TODO: unregister any hooks where event has reached terminal state await Promise.all( @@ -61,8 +64,17 @@ export class DepositStatusProcessor implements IEventProcessor { url: string, params: DepositStatusFilter, ): Promise { - const id = [url, params.originChainId, params.depositTxHash].join("!"); - const filter = [params.originChainId, params.depositTxHash].join("!"); + const id = customId( + this.type, + url, + params.originChainId, + params.depositTxHash, + ); + const filter = customId( + this.type, + params.originChainId, + params.depositTxHash, + ); assert( !(await this.webhookRequests.hasWebhook(id)), "This webhook already exists", diff --git a/packages/webhooks/src/factory.ts b/packages/webhooks/src/factory.ts index d8824d1c..2231e1a0 100644 --- a/packages/webhooks/src/factory.ts +++ b/packages/webhooks/src/factory.ts @@ -1,12 +1,12 @@ import assert from "assert"; import { EventProcessorManager } from "./eventProcessorManager"; -import { MemoryStore } from "./store"; import { DataSource } from "@repo/indexer-database"; import { Logger } from "winston"; import { WebhookNotifier } from "./notifier"; import { DepositStatusProcessor } from "./eventProcessors"; import { WebhookRequestRepository } from "./database/webhookRequestRepository"; import { WebhookRouter } from "./router"; +import { entities } from "@repo/indexer-database"; export enum WebhookTypes { DepositStatus = "DepositStatus", @@ -36,7 +36,6 @@ export function WebhookFactory(config: Config, deps: Dependencies) { }, ); config.enabledWebhooks.forEach((name) => { - const hooks = new WebhookRequestRepository(new MemoryStore()); switch (name) { // add more webhook types here case "DepositStatus": { @@ -44,7 +43,6 @@ export function WebhookFactory(config: Config, deps: Dependencies) { name, new DepositStatusProcessor({ postgres, - webhookRequests: hooks, notify: notifier.notify, }), ); diff --git a/packages/webhooks/src/index.ts b/packages/webhooks/src/index.ts index a051aa6d..65b84a31 100644 --- a/packages/webhooks/src/index.ts +++ b/packages/webhooks/src/index.ts @@ -2,6 +2,5 @@ export * from "./factory"; export * as eventProcessors from "./eventProcessors"; export * as eventProcessorManager from "./eventProcessorManager"; export * as router from "./router"; -export * as store from "./store"; export * from "./types"; export * from "./utils"; diff --git a/packages/webhooks/src/notifier.ts b/packages/webhooks/src/notifier.ts index 80b1a232..810ed2b9 100644 --- a/packages/webhooks/src/notifier.ts +++ b/packages/webhooks/src/notifier.ts @@ -1,6 +1,5 @@ import { post } from "./utils"; import { NotificationPayload } from "./types"; -import { AsyncStore } from "./store"; import { Logger } from "winston"; export type Dependencies = { diff --git a/packages/webhooks/src/store.ts b/packages/webhooks/src/store.ts deleted file mode 100644 index 6bd5f03c..00000000 --- a/packages/webhooks/src/store.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { createClient, RedisClientType } from "redis"; - -export interface AsyncStore { - get(key: string): Promise; - set(key: string, value: V): Promise; - has(key: string): Promise; - delete(key: string): Promise; - values(): AsyncIterableIterator; - entries(): AsyncIterableIterator<[string, V]>; - keys(): AsyncIterableIterator; -} - -export class MemoryStore implements AsyncStore { - private map: Map; - - constructor(map?: Map) { - this.map = map ?? new Map(); - } - - async get(key: string): Promise { - return this.map.get(key); - } - - async set(key: string, value: V): Promise { - this.map.set(key, value); - } - - async has(key: string): Promise { - return this.map.has(key); - } - - async delete(key: string): Promise { - return this.map.delete(key); - } - - async *values(): AsyncIterableIterator { - for (const value of this.map.values()) { - yield value; - } - } - - async *entries(): AsyncIterableIterator<[string, V]> { - for (const entry of this.map.entries()) { - yield entry; - } - } - - async *keys(): AsyncIterableIterator { - for (const key of this.map.keys()) { - yield key; - } - } -} - -export class RedisStore implements AsyncStore { - private client: RedisClientType; - - constructor(client: RedisClientType) { - this.client = client; - } - - async get(key: string): Promise { - const value = await this.client.get(key); - return value ? JSON.parse(value) : undefined; - } - - async set(key: string, value: V): Promise { - await this.client.set(key, JSON.stringify(value)); - } - - async has(key: string): Promise { - const exists = await this.client.exists(key); - return exists === 1; - } - - async delete(key: string): Promise { - const result = await this.client.del(key); - return result === 1; - } - - async *values(): AsyncIterableIterator { - const keys = await this.client.keys("*"); - for (const key of keys) { - const value = await this.get(key); - if (value !== undefined) { - yield value; - } - } - } - - async *entries(): AsyncIterableIterator<[string, V]> { - const keys = await this.client.keys("*"); - for (const key of keys) { - const value = await this.get(key); - if (value !== undefined) { - yield [key, value]; - } - } - } - - async *keys(): AsyncIterableIterator { - const keys = await this.client.keys("*"); - for (const key of keys) { - yield key; - } - } -} diff --git a/packages/webhooks/src/utils.ts b/packages/webhooks/src/utils.ts index 8ae0ade3..dd6a0cab 100644 --- a/packages/webhooks/src/utils.ts +++ b/packages/webhooks/src/utils.ts @@ -34,3 +34,10 @@ export function asyncInterval(fn: () => Promise, delay: number) { isStopped = true; }; } + +export function exists(val: T | null | undefined): val is T { + return val !== null && val !== undefined; +} +export function customId(...args: (string | number)[]): string { + return args.join("!"); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8d7cfba..3551acd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -469,6 +469,9 @@ importers: superstruct: specifier: 2.0.3-1 version: 2.0.3-1 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4)) devDependencies: '@istanbuljs/nyc-config-typescript': specifier: ^1.0.2 @@ -8363,25 +8366,6 @@ snapshots: '@across-protocol/constants@3.1.20': {} - '@across-protocol/contracts@0.1.4(@babel/core@7.25.2)(@ethersproject/hardware-wallets@5.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(ts-generator@0.1.1)(typechain@4.0.3(typescript@5.5.4))(utf-8-validate@5.0.10)': - dependencies: - '@eth-optimism/contracts': 0.5.40(bufferutil@4.0.8)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) - '@openzeppelin/contracts': 4.1.0 - '@uma/core': 2.59.1(@babel/core@7.25.2)(@ethersproject/hardware-wallets@5.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(ts-generator@0.1.1)(typechain@4.0.3(typescript@5.5.4))(utf-8-validate@5.0.10) - transitivePeerDependencies: - - '@babel/core' - - '@codechecks/client' - - '@ethersproject/hardware-wallets' - - bufferutil - - debug - - encoding - - ethers - - hardhat - - supports-color - - ts-generator - - typechain - - utf-8-validate - '@across-protocol/contracts@0.1.4(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': dependencies: '@eth-optimism/contracts': 0.5.40(bufferutil@4.0.8)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) @@ -10855,7 +10839,7 @@ snapshots: '@uma/common@2.37.1(@babel/core@7.25.2)(@ethersproject/hardware-wallets@5.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(ts-generator@0.1.1)(typechain@4.0.3(typescript@5.5.4))(utf-8-validate@5.0.10)': dependencies: - '@across-protocol/contracts': 0.1.4(@babel/core@7.25.2)(@ethersproject/hardware-wallets@5.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(ts-generator@0.1.1)(typechain@4.0.3(typescript@5.5.4))(utf-8-validate@5.0.10) + '@across-protocol/contracts': 0.1.4(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) '@ethersproject/address': 5.7.0 '@ethersproject/bignumber': 5.7.0 '@ethersproject/bytes': 5.7.0 @@ -10964,9 +10948,9 @@ snapshots: '@ethersproject/constants': 5.7.0 '@google-cloud/kms': 3.8.0(encoding@0.1.13) '@google-cloud/storage': 6.12.0(encoding@0.1.13) - '@nomicfoundation/hardhat-verify': 1.1.1(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) - '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) - '@nomiclabs/hardhat-web3': 2.0.0(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(web3@1.10.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-verify': 1.1.1(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) + '@nomiclabs/hardhat-ethers': 2.2.3(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) + '@nomiclabs/hardhat-web3': 2.0.0(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(web3@1.10.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) '@truffle/contract': 4.6.17(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@truffle/hdwallet-provider': 1.5.1-alpha.1(@babel/core@7.25.2)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@types/ethereum-protocol': 1.0.5 @@ -10980,7 +10964,7 @@ snapshots: dotenv: 9.0.2 eth-crypto: 2.6.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat-deploy: 0.9.1(@ethersproject/hardware-wallets@5.7.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) - hardhat-gas-reporter: 1.0.10(bufferutil@4.0.8)(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@16.18.104)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + hardhat-gas-reporter: 1.0.10(bufferutil@4.0.8)(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) hardhat-typechain: 0.3.5(hardhat@2.22.12(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10))(ts-generator@0.1.1)(typechain@4.0.3(typescript@5.5.4)) lodash.uniqby: 4.7.0 minimist: 1.2.8 @@ -18222,6 +18206,31 @@ snapshots: transitivePeerDependencies: - supports-color + typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0)(redis@4.7.0)(ts-node@10.9.2(@types/node@22.7.3)(typescript@5.5.4)): + dependencies: + '@sqltools/formatter': 1.2.5 + app-root-path: 3.1.0 + buffer: 6.0.3 + chalk: 4.1.2 + cli-highlight: 2.1.11 + dayjs: 1.11.12 + debug: 4.3.6(supports-color@8.1.1) + dotenv: 16.4.5 + glob: 10.4.5 + mkdirp: 2.1.6 + reflect-metadata: 0.2.2 + sha.js: 2.4.11 + tslib: 2.6.3 + uuid: 9.0.1 + yargs: 17.7.2 + optionalDependencies: + ioredis: 5.4.1 + pg: 8.12.0 + redis: 4.7.0 + ts-node: 10.9.2(@types/node@22.7.3)(typescript@5.5.4) + transitivePeerDependencies: + - supports-color + typescript@5.5.4: {} typical@2.6.1: {}