diff --git a/k8s/local/aztec-devnet-node/deployment.yaml b/k8s/local/aztec-devnet-node/deployment.yaml index d7464f88..8c6c5020 100644 --- a/k8s/local/aztec-devnet-node/deployment.yaml +++ b/k8s/local/aztec-devnet-node/deployment.yaml @@ -38,7 +38,10 @@ spec: - name: DEBUG value: "aztec:*" - name: ETHEREUM_HOST - value: "https://devnet-mainnet-fork.aztec.network:8545/$(CHICMOZ_AZTEC_API_KEY)" # TODO: WARNING! We need to have this as env-var + valueFrom: + secretKeyRef: + name: global + key: CHICMOZ_AZTEC_ETHEREUM_HOST - name: L1_CHAIN_ID value: "677692" - name: ARCHIVER_POLLING_INTERVAL_MS @@ -58,7 +61,10 @@ spec: - name: AZTEC_PORT value: "8080" - name: FAUCET_ENDPOINT - value: "https://api.aztec.network/devnet/aztec-faucet/$(CHICMOZ_AZTEC_API_KEY)" + valueFrom: + secretKeyRef: + name: global + key: CHICMOZ_AZTEC_FAUCET_ENDPOINT - name: PRIVATE_KEY valueFrom: secretKeyRef: diff --git a/k8s/local/aztec-listener/deployment.yaml b/k8s/local/aztec-listener/deployment.yaml index 3a47d90b..223ed0cf 100644 --- a/k8s/local/aztec-listener/deployment.yaml +++ b/k8s/local/aztec-listener/deployment.yaml @@ -44,7 +44,7 @@ spec: - name: NODE_ENV value: "development" - name: BLOCK_INTERVAL_MS - value: "2000" + value: "5000" - name: BATCH_SIZE value: "50" - name: CHAIN_NAME @@ -72,6 +72,10 @@ spec: - name: DISABLE_AZTEC value: "false" - name: AZTEC_RPC - # value: "http://aztec-devnet-node:8080" # DEVNET - value: "http://aztec-sandbox-node:8081" # SANDBOX + #value: "http://aztec-devnet-node:8080" # DEVNET + #value: "http://aztec-sandbox-node:8081" # SANDBOX + valueFrom: + secretKeyRef: + name: global + key: CHICMOZ_AZTEC_RPC status: {} diff --git a/k8s/local/explorer-api/deployment.yaml b/k8s/local/explorer-api/deployment.yaml index 5dd4c8f9..c0b17773 100644 --- a/k8s/local/explorer-api/deployment.yaml +++ b/k8s/local/explorer-api/deployment.yaml @@ -61,8 +61,6 @@ spec: value: "admin" - name: POSTGRES_PASSWORD value: "secret-local-password" - - name: BLOCK_INTERVAL_MS - value: "2000" - name: BLOCK_DB_VALIDATION_ENABLED value: "false" - name: BLOCK_DB_VALIDATION_INTERVAL diff --git a/k8s/local/skaffold.local.yaml b/k8s/local/skaffold.local.yaml index 1f860289..d2b42887 100644 --- a/k8s/local/skaffold.local.yaml +++ b/k8s/local/skaffold.local.yaml @@ -49,17 +49,17 @@ build: manifests: rawYaml: - k8s/local/common/namespace.yaml - # - k8s/local/explorer-ui/ingress.yaml - # - k8s/local/explorer-ui/deployment.yaml - # - k8s/local/explorer-ui/service.yaml + #- k8s/local/explorer-ui/ingress.yaml + #- k8s/local/explorer-ui/deployment.yaml + #- k8s/local/explorer-ui/service.yaml - k8s/local/explorer-api/ingress.yaml - k8s/local/explorer-api/deployment.yaml - k8s/local/explorer-api/service.yaml - k8s/local/auth/deployment.yaml - k8s/local/auth/service.yaml - - k8s/local/dummy-node/ingress.yaml - - k8s/local/dummy-node/deployment.yaml - - k8s/local/dummy-node/service.yaml + # - k8s/local/dummy-node/ingress.yaml + # - k8s/local/dummy-node/deployment.yaml + # - k8s/local/dummy-node/service.yaml - k8s/local/aztec-listener/deployment.yaml #- k8s/local/aztec-devnet-node/deployment.yaml # DEVNET #- k8s/local/aztec-devnet-node/service.yaml # DEVNET @@ -70,9 +70,9 @@ manifests: - k8s/local/anvil-ethereum-node/ingress.yaml # SANDBOX - k8s/local/anvil-ethereum-node/deployment.yaml # SANDBOX - k8s/local/anvil-ethereum-node/service.yaml # SANDBOX - - k8s/local/kafka-ui/configmap.yaml - - k8s/local/kafka-ui/configmap-env.yaml - - k8s/local/kafka-ui/ingress.yaml + # - k8s/local/kafka-ui/configmap.yaml + # - k8s/local/kafka-ui/configmap-env.yaml + # - k8s/local/kafka-ui/ingress.yaml helm: releases: - name: ingress-nginx @@ -137,17 +137,17 @@ manifests: skipBuildDependencies: false useHelmSecrets: false wait: false - - name: kafka-ui - version: 0.7.6 - remoteChart: kafka-ui - repo: https://provectus.github.io/kafka-ui-charts - namespace: chicmoz - setValues: - yamlApplicationConfigConfigMap.name: kafka-ui-configmap - yamlApplicationConfigConfigMap.keyName: config.yml - existingConfigMap: kafka-ui-helm-values - createNamespace: false - recreatePods: false - skipBuildDependencies: false - useHelmSecrets: false - wait: false + #- name: kafka-ui + # version: 0.7.6 + # remoteChart: kafka-ui + # repo: https://provectus.github.io/kafka-ui-charts + # namespace: chicmoz + # setValues: + # yamlApplicationConfigConfigMap.name: kafka-ui-configmap + # yamlApplicationConfigConfigMap.keyName: config.yml + # existingConfigMap: kafka-ui-helm-values + # createNamespace: false + # recreatePods: false + # skipBuildDependencies: false + # useHelmSecrets: false + # wait: false diff --git a/packages/message-registry/src/aztec.ts b/packages/message-registry/src/aztec.ts index c14e647e..2e26f51f 100644 --- a/packages/message-registry/src/aztec.ts +++ b/packages/message-registry/src/aztec.ts @@ -1,7 +1,7 @@ -import { NodeInfoAlias } from '@chicmoz-pkg/types'; +import { NodeInfo } from '@chicmoz-pkg/types'; export type NewBlockEvent = { - nodeInfo: NodeInfoAlias; + nodeInfo: NodeInfo; block?: string; }; diff --git a/packages/types/src/aztec/index.ts b/packages/types/src/aztec/index.ts index d6804c7b..7f5f3689 100644 --- a/packages/types/src/aztec/index.ts +++ b/packages/types/src/aztec/index.ts @@ -1,13 +1,4 @@ export * from "./l2Block.js"; +export * from "./l2Contract.js"; -// TODO: the import should work, but instead we're using a type alias -// import { type NodeInfo } from "@aztec/aztec.js"; -export type NodeInfoAlias = { - nodeVersion: string; - l1ChainId: number; - protocolVersion: number; - enr: string | undefined; - l1ContractAddresses: object; - protocolContractAddresses: object; -}; - +export { type NodeInfo } from "@aztec/aztec.js"; diff --git a/packages/types/src/aztec/l2Block.ts b/packages/types/src/aztec/l2Block.ts index 95c58c5b..f6e0717a 100644 --- a/packages/types/src/aztec/l2Block.ts +++ b/packages/types/src/aztec/l2Block.ts @@ -1,49 +1,9 @@ import { z } from "zod"; import { deepPartial } from "../utils.js"; +import { bufferSchema, frSchema } from "./utils.js"; // TODO: separate type for transaction? -type AztecFr = { - toString(): string; -}; - -type StringifiedAztecFr = { - type: "Fr"; - value: string; -}; - -const frSchema = z.preprocess( - (val) => { - if ((val as StringifiedAztecFr).value) - return (val as StringifiedAztecFr).value; - else if ((val as AztecFr).toString) return (val as AztecFr).toString(); - else return val; - }, - z - .string() - .length(66) - .regex(/^0x[0-9a-fA-F]+$/) -); - -type StringifiedBuffer = { - type: "Buffer"; - data: number[]; -}; - -const bufferSchema = z.preprocess( - (val) => { - if ((val as StringifiedBuffer).data) - return Buffer.from((val as StringifiedBuffer).data); - return val; - }, - z.custom( - (value) => { - return value instanceof Buffer; - }, - { message: "Expected a Buffer" } - ) -); - export const noteEncryptedLogEntrySchema = z.object({ data: z.string(), }); diff --git a/packages/types/src/aztec/l2Contract.ts b/packages/types/src/aztec/l2Contract.ts new file mode 100644 index 00000000..6afa74b7 --- /dev/null +++ b/packages/types/src/aztec/l2Contract.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import { aztecAddressSchema, bufferSchema, frSchema } from "./utils.js"; +import { chicmozL2BlockSchema } from "./l2Block.js"; + +export const chicmozL2ContractInstanceDeployedEventSchema = z.object({ + address: aztecAddressSchema, + blockHash: chicmozL2BlockSchema.shape.hash, + version: z.number(), + salt: frSchema, + contractClassId: frSchema, + initializationHash: frSchema, + publicKeysHash: frSchema, + deployer: aztecAddressSchema, +}); + +export type ChicmozL2ContractInstanceDeployedEvent = z.infer< + typeof chicmozL2ContractInstanceDeployedEventSchema +>; + +export const chicmozL2ContractClassRegisteredEventSchema = z.object({ + blockHash: chicmozL2BlockSchema.shape.hash, + contractClassId: frSchema, + version: z.number(), + artifactHash: frSchema, + privateFunctionsRoot: frSchema, + packedPublicBytecode: bufferSchema, +}); + +export type ChicmozL2ContractClassRegisteredEvent = z.infer< + typeof chicmozL2ContractClassRegisteredEventSchema +>; + +export const chicmozL2ContractInstanceDeluxeSchema = z.object({ + ...chicmozL2ContractInstanceDeployedEventSchema.shape, + ...chicmozL2ContractClassRegisteredEventSchema.shape, +}); + +// TODO: come up with a better name for this type +export type ChicmozL2ContractInstanceDeluxe = + ChicmozL2ContractInstanceDeployedEvent & + ChicmozL2ContractClassRegisteredEvent; diff --git a/packages/types/src/aztec/utils.ts b/packages/types/src/aztec/utils.ts new file mode 100644 index 00000000..b4f0f6cc --- /dev/null +++ b/packages/types/src/aztec/utils.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +export type AztecFr = { + toString(): string; +}; + +export type AztecAddress = { + toString(): string; +}; + +export type StringifiedAztecFr = { + type: "Fr"; + value: `0x${string}`; +}; + +export type StringifiedAztecAddress = { + type: "AztecAddress"; + value: `0x${string}`; +}; + +export const frSchema = z.preprocess( + (val) => { + if ((val as StringifiedAztecFr).value) + return (val as StringifiedAztecFr).value; + else if ((val as AztecFr).toString) return (val as AztecFr).toString(); + else return val; + }, + z + .string() + .length(66) + .regex(/^0x[0-9a-fA-F]+$/) +); + +// NOTE: it's technically not the same as Fr but practically it is +export const aztecAddressSchema = frSchema; + +export type StringifiedBuffer = { + type: "Buffer"; + data: number[]; +}; + +export const bufferSchema = z.preprocess( + (val) => { + if ((val as StringifiedBuffer).data) + return Buffer.from((val as StringifiedBuffer).data); + return val; + }, + z.custom( + (value) => { + return value instanceof Buffer; + }, + { message: "Expected a Buffer" } + ) +); diff --git a/packages/types/src/general.ts b/packages/types/src/general.ts new file mode 100644 index 00000000..936a2ba7 --- /dev/null +++ b/packages/types/src/general.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const hexStringSchema = z.custom<`0x${string}`>( + (value) => { + return (value as string).startsWith("0x"); + }, + { message: "Expected a hex string" } +); + +export type HexString = z.infer; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d7adfcae..8a5da3cd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,2 +1,3 @@ export * from "./events.js"; export * from "./aztec/index.js"; +export * from "./general.js"; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index e80f33c0..6266bfdc 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -59,8 +59,8 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "ESNext" /* Specify what module code is generated. */, - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "module": "NodeNext" /* Specify what module code is generated. */, + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./build" /* Specify an output folder for all emitted files. */, "rootDir": "./src" /* Specify the root folder within your source files. */, diff --git a/services/aztec-listener/package.json b/services/aztec-listener/package.json index 14a2968b..cb3b9056 100644 --- a/services/aztec-listener/package.json +++ b/services/aztec-listener/package.json @@ -5,6 +5,7 @@ "license": "UNLICENSED", "dependencies": { "@aztec/aztec.js": "0.53.0", + "@aztec/circuits.js": "0.53.0", "@chicmoz-pkg/logger-server": "workspace:^", "@chicmoz-pkg/message-bus": "workspace:^", "@chicmoz-pkg/message-registry": "workspace:^", diff --git a/services/aztec-listener/scripts/migrate.ts b/services/aztec-listener/scripts/migrate.ts index d2f7e569..95dec192 100644 --- a/services/aztec-listener/scripts/migrate.ts +++ b/services/aztec-listener/scripts/migrate.ts @@ -29,14 +29,17 @@ async function runMigrations() { console.log(`Retrying attempt ${attemptNumber} of ${retries}...`); return true; } - console.error(e.stack); + console.error(e); return false; }, }); - console.log("🤩 Migrations complete!"); - await pool.end(); + console.log("🤩 Migrations complete!"); } -runMigrations().catch(console.error); +runMigrations().catch(async (e) => { + console.error(e); + await pool.end(); + process.exit(1); +}); diff --git a/services/aztec-listener/src/aztec/index.ts b/services/aztec-listener/src/aztec/index.ts index 53b6dc59..28dcf8f5 100644 --- a/services/aztec-listener/src/aztec/index.ts +++ b/services/aztec-listener/src/aztec/index.ts @@ -1,5 +1,5 @@ import { IBackOffOptions, backOff } from "exponential-backoff"; -import { NodeInfoAlias } from "@chicmoz-pkg/types"; +import { NodeInfo } from "@chicmoz-pkg/types"; import { logger } from "../logger.js"; import { getLatestHeight, @@ -16,11 +16,7 @@ const backOffOptions: Partial = { numOfAttempts: 10, maxDelay: 10000, retry: (e, attemptNumber: number) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (e.cause) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - logger.warn(e.cause); - else logger.warn(e); + logger.warn(e); logger.info( `🤡 We'll allow some errors during start-up, retrying attempt ${attemptNumber}...` @@ -29,7 +25,7 @@ const backOffOptions: Partial = { }, }; -let nodeInfo: NodeInfoAlias; +let nodeInfo: NodeInfo; export const init = async () => { if (DISABLE_AZTEC) { @@ -51,7 +47,7 @@ export const init = async () => { // startCatchup({ untilHeight: currentHeight }); // Should it be blocking? - if (LISTEN_FOR_BLOCKS) await startPolling({ fromHeight: currentHeight }); + if (LISTEN_FOR_BLOCKS) startPolling({ fromHeight: currentHeight }); return { shutdownAztec: () => { diff --git a/services/aztec-listener/src/aztec/network-client.ts b/services/aztec-listener/src/aztec/network-client.ts index a433c389..c289fedf 100644 --- a/services/aztec-listener/src/aztec/network-client.ts +++ b/services/aztec-listener/src/aztec/network-client.ts @@ -1,31 +1,69 @@ -import { PXE, createPXEClient } from "@aztec/aztec.js"; +import { + createAztecNodeClient, + AztecNode, + NodeInfo, +} from "@aztec/aztec.js"; import { AZTEC_RPC } from "../constants.js"; +import {logger} from "../logger.js"; -let pxe: PXE; +let aztecNode: AztecNode; -export const init = () => { - pxe = createPXEClient(AZTEC_RPC); - return pxe.getNodeInfo(); +const node = () => { + if (!aztecNode) throw new Error("Node not initialized"); + return aztecNode; }; -export const getBlock = (height: number) => { - if (!pxe) throw new Error("PXE client not initialized"); +export const logFetchFailedCause = (e: Error) => { + if (e.cause) logger.warn(`Aztec failed to fetch: ${JSON.stringify(e.cause)}`); + throw e; +} + +export const init = async () => { + aztecNode = createAztecNodeClient(AZTEC_RPC); + return getNodeInfo().catch(logFetchFailedCause); +}; + +export const getNodeInfo = async (): Promise => { + const n = node(); + const [ + nodeVersion, + protocolVersion, + chainId, + enr, + contractAddresses, + protocolContractAddresses, + ] = await Promise.all([ + n.getNodeVersion().catch(logFetchFailedCause), + n.getVersion().catch(logFetchFailedCause), + n.getChainId().catch(logFetchFailedCause), + n.getEncodedEnr().catch(logFetchFailedCause), + n.getL1ContractAddresses().catch(logFetchFailedCause), + n.getProtocolContractAddresses().catch(logFetchFailedCause), + ]); + + const nodeInfo: NodeInfo = { + nodeVersion, + l1ChainId: chainId, + protocolVersion, + enr, + l1ContractAddresses: contractAddresses, + protocolContractAddresses: protocolContractAddresses, + }; - return pxe.getBlock(height); + // TODO: why unsafe? + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return nodeInfo; }; +export const getBlock = async (height: number) => node().getBlock(height).catch(logFetchFailedCause); + export const getBlocks = async (fromHeight: number, toHeight: number) => { - if (!pxe) throw new Error("PXE client not initialized"); const blocks = []; for (let i = fromHeight; i < toHeight; i++) { - const block = await pxe.getBlock(i); + const block = await node().getBlock(i).catch(logFetchFailedCause); blocks.push(block); } return blocks; -} - -export const getLatestHeight = () => { - if (!pxe) throw new Error("PXE client not initialized"); - - return pxe.getBlockNumber(); }; + +export const getLatestHeight = () => node().getBlockNumber().catch(logFetchFailedCause); diff --git a/services/aztec-listener/src/aztec/poller.ts b/services/aztec-listener/src/aztec/poller.ts index cade7ccc..9c918211 100644 --- a/services/aztec-listener/src/aztec/poller.ts +++ b/services/aztec-listener/src/aztec/poller.ts @@ -1,7 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { IBackOffOptions, backOff } from "exponential-backoff"; import { BLOCK_INTERVAL_MS, @@ -12,6 +8,7 @@ import { logger } from "../logger.js"; import { storeHeight } from "../database/latestProcessedHeight.controller.js"; import { getBlock, getBlocks, getLatestHeight } from "./network-client.js"; import { onBlock } from "../event-handler/index.js"; +import {L2Block} from "@aztec/aztec.js"; const backOffOptions: Partial = { numOfAttempts: 3, @@ -27,8 +24,8 @@ const backOffOptions: Partial = { let pollInterval: NodeJS.Timeout; let latestProcessedHeight = -1; -export const startPolling = async ({ fromHeight }: { fromHeight: number }) => { - await setLatestProcessedHeight(fromHeight - 1); +export const startPolling = ({ fromHeight }: { fromHeight: number }) => { + latestProcessedHeight = fromHeight - 100; pollInterval = setInterval(() => { void fetchAndPublishLatestBlockReoccurring(); }, BLOCK_INTERVAL_MS); @@ -38,7 +35,7 @@ export const stopPolling = () => { if (pollInterval) clearInterval(pollInterval); }; -const setLatestProcessedHeight = async (height: number) => { +const storeLatestProcessedHeight = async (height: number) => { const isLater = height > latestProcessedHeight; if (isLater) { latestProcessedHeight = height; @@ -47,11 +44,23 @@ const setLatestProcessedHeight = async (height: number) => { return isLater; }; +const internalOnBlock = async (blockRes: L2Block | undefined) => { + if (!blockRes) { + throw new Error( + "FATAL: Poller received no block." + ); + } + await onBlock(blockRes); + await storeLatestProcessedHeight( + Number(blockRes.header.globalVariables.blockNumber) + ); +}; + const fetchAndPublishLatestBlockReoccurring = async () => { const networkLatestHeight = await getLatestHeight(); - const alreadyProcessed = networkLatestHeight <= latestProcessedHeight; - const missedBlocks = networkLatestHeight - latestProcessedHeight > 1; + if (networkLatestHeight === 0) throw new Error("FATAL: network returned height 0"); + const alreadyProcessed = networkLatestHeight <= latestProcessedHeight; if (alreadyProcessed && !IGNORE_PROCESSED_HEIGHT) { logger.info( `🐻 block ${networkLatestHeight} has already been processed, skipping...` @@ -59,23 +68,14 @@ const fetchAndPublishLatestBlockReoccurring = async () => { return; } - const blockRes = await backOff(async () => { - return await getBlock(networkLatestHeight); - }, backOffOptions); - - if (!blockRes) { - throw new Error( - "FATAL: Poller received no block, eventhough receiveing height from network." - ); - } - await onBlock(blockRes); - + const missedBlocks = networkLatestHeight - latestProcessedHeight > 1; if (missedBlocks) await catchUpOnMissedBlocks(latestProcessedHeight + 1, networkLatestHeight); - await setLatestProcessedHeight( - Number(blockRes.header.globalVariables.blockNumber) - ); + const blockRes = await backOff(async () => { + return await getBlock(networkLatestHeight); + }, backOffOptions); + await internalOnBlock(blockRes); }; const catchUpOnMissedBlocks = async (start: number, end: number) => { @@ -91,11 +91,12 @@ const catchUpOnMissedBlocks = async (start: number, end: number) => { const missedBlocks = await getBlocks(start, end); const processBlocks = missedBlocks.map((block) => { - if (block) return onBlock(block); + if (block) return internalOnBlock(block); }); await Promise.allSettled(processBlocks); const durationMs = new Date().getTime() - startTimeMs; logger.info(`🐯 missed blocks ${start}-${end} published (${durationMs}ms)`); + await new Promise((resolve) => setTimeout(resolve, 500)); } }; diff --git a/services/aztec-listener/tsconfig.json b/services/aztec-listener/tsconfig.json index 1424b3ec..a13b3b82 100644 --- a/services/aztec-listener/tsconfig.json +++ b/services/aztec-listener/tsconfig.json @@ -59,8 +59,8 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "ESNext" /* Specify what module code is generated. */, - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "module": "NodeNext" /* Specify what module code is generated. */, + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./build" /* Specify an output folder for all emitted files. */, "rootDirs": ["./"] /* Specify the root folder within your source files. */, diff --git a/services/explorer-api/migrations/0000_cloudy_cardiac.sql b/services/explorer-api/migrations/0000_elite_jackpot.sql similarity index 84% rename from services/explorer-api/migrations/0000_cloudy_cardiac.sql rename to services/explorer-api/migrations/0000_elite_jackpot.sql index c514a816..56685a8e 100644 --- a/services/explorer-api/migrations/0000_cloudy_cardiac.sql +++ b/services/explorer-api/migrations/0000_elite_jackpot.sql @@ -143,6 +143,29 @@ CREATE TABLE IF NOT EXISTS "state" ( "partial_id" uuid NOT NULL ); --> statement-breakpoint +CREATE TABLE IF NOT EXISTS "l2_contract_class_registered" ( + "block_hash" varchar NOT NULL, + "contract_class_id" varchar(66) NOT NULL, + "version" bigint NOT NULL, + "artifact_hash" varchar(66) NOT NULL, + "private_functions_root" varchar(66) NOT NULL, + "packed_public_bytecode" "bytea" NOT NULL, + CONSTRAINT "contract_class_id_version" PRIMARY KEY("contract_class_id","version") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "l2_contract_instance_deployed" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "block_hash" varchar NOT NULL, + "address" varchar(66) NOT NULL, + "version" integer NOT NULL, + "salt" varchar(66) NOT NULL, + "contract_class_id" varchar(66) NOT NULL, + "initialization_hash" varchar(66) NOT NULL, + "public_keys_hash" varchar(66) NOT NULL, + "deployer" varchar(66) NOT NULL, + CONSTRAINT "l2_contract_instance_deployed_contract_class_id_address_version_unique" UNIQUE("contract_class_id","address","version") +); +--> statement-breakpoint DO $$ BEGIN ALTER TABLE "l2Block" ADD CONSTRAINT "l2Block_archive_id_archive_id_fk" FOREIGN KEY ("archive_id") REFERENCES "public"."archive"("id") ON DELETE no action ON UPDATE no action; EXCEPTION @@ -262,3 +285,21 @@ DO $$ BEGIN EXCEPTION WHEN duplicate_object THEN null; END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "l2_contract_class_registered" ADD CONSTRAINT "l2_contract_class_registered_block_hash_l2Block_hash_fk" FOREIGN KEY ("block_hash") REFERENCES "public"."l2Block"("hash") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "l2_contract_instance_deployed" ADD CONSTRAINT "l2_contract_instance_deployed_block_hash_l2Block_hash_fk" FOREIGN KEY ("block_hash") REFERENCES "public"."l2Block"("hash") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "l2_contract_instance_deployed" ADD CONSTRAINT "contract_class" FOREIGN KEY ("contract_class_id","version") REFERENCES "public"."l2_contract_class_registered"("contract_class_id","version") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/services/explorer-api/migrations/meta/0000_snapshot.json b/services/explorer-api/migrations/meta/0000_snapshot.json index c9aefe7a..9b3536c9 100644 --- a/services/explorer-api/migrations/meta/0000_snapshot.json +++ b/services/explorer-api/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "67d1e04e-0624-4b34-9729-d0541ad0bada", + "id": "a851266f-c47d-4039-b868-b4da25991a3a", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -910,6 +910,157 @@ }, "compositePrimaryKeys": {}, "uniqueConstraints": {} + }, + "public.l2_contract_class_registered": { + "name": "l2_contract_class_registered", + "schema": "", + "columns": { + "block_hash": { + "name": "block_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "contract_class_id": { + "name": "contract_class_id", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "artifact_hash": { + "name": "artifact_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "private_functions_root": { + "name": "private_functions_root", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "packed_public_bytecode": { + "name": "packed_public_bytecode", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "l2_contract_class_registered_block_hash_l2Block_hash_fk": { + "name": "l2_contract_class_registered_block_hash_l2Block_hash_fk", + "tableFrom": "l2_contract_class_registered", + "tableTo": "l2Block", + "columnsFrom": ["block_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contract_class_id_version": { + "name": "contract_class_id_version", + "columns": ["contract_class_id", "version"] + } + }, + "uniqueConstraints": {} + }, + "public.l2_contract_instance_deployed": { + "name": "l2_contract_instance_deployed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "block_hash": { + "name": "block_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "salt": { + "name": "salt", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "contract_class_id": { + "name": "contract_class_id", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "initialization_hash": { + "name": "initialization_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "public_keys_hash": { + "name": "public_keys_hash", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + }, + "deployer": { + "name": "deployer", + "type": "varchar(66)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "l2_contract_instance_deployed_block_hash_l2Block_hash_fk": { + "name": "l2_contract_instance_deployed_block_hash_l2Block_hash_fk", + "tableFrom": "l2_contract_instance_deployed", + "tableTo": "l2Block", + "columnsFrom": ["block_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contract_class": { + "name": "contract_class", + "tableFrom": "l2_contract_instance_deployed", + "tableTo": "l2_contract_class_registered", + "columnsFrom": ["contract_class_id", "version"], + "columnsTo": ["contract_class_id", "version"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "l2_contract_instance_deployed_contract_class_id_address_version_unique": { + "name": "l2_contract_instance_deployed_contract_class_id_address_version_unique", + "nullsNotDistinct": false, + "columns": ["contract_class_id", "address", "version"] + } + } } }, "enums": {}, diff --git a/services/explorer-api/migrations/meta/_journal.json b/services/explorer-api/migrations/meta/_journal.json index b319293b..5e6e1f1a 100644 --- a/services/explorer-api/migrations/meta/_journal.json +++ b/services/explorer-api/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1726502081824, - "tag": "0000_cloudy_cardiac", + "when": 1726560632652, + "tag": "0000_elite_jackpot", "breakpoints": true } ] diff --git a/services/explorer-api/package.json b/services/explorer-api/package.json index cde40a51..cd8d0f20 100644 --- a/services/explorer-api/package.json +++ b/services/explorer-api/package.json @@ -8,6 +8,7 @@ "@aws-sdk/credential-providers": "3.525.0", "@aztec/aztec.js": "0.53.0", "@aztec/circuits.js": "0.53.0", + "@aztec/protocol-contracts": "0.53.0", "@chicmoz-pkg/error-middleware": "workspace:^", "@chicmoz-pkg/logger-server": "workspace:^", "@chicmoz-pkg/message-bus": "workspace:^", @@ -70,7 +71,6 @@ "generate": "yarn build && drizzle-kit generate", "migrate": "yarn node build/scripts/migrate.js", "start": "yarn node --unhandled-rejections=strict --enable-source-maps build/src/index.js", - "aztec-playground": "yarn node build/scripts/aztec_playground.js", "lint": "yarn run lint-base --ext .ts .", "lint-base": "yarn run g:lint", "test": "yarn node --experimental-vm-modules $(yarn bin jest)" diff --git a/services/explorer-api/scripts/aztec_playground.ts b/services/explorer-api/scripts/aztec_playground.ts deleted file mode 100644 index 8c1f5e2b..00000000 --- a/services/explorer-api/scripts/aztec_playground.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createPXEClient } from "@aztec/aztec.js"; -import { logger } from "../src/logger.js"; -import { reconstructedL2BlockSchema } from "../src/types/block.js"; - -const AZTEC_NODE_URL = "http://localhost:8080"; - -const testType = (dbBlock: unknown) => { const b = reconstructedL2BlockSchema.parse(dbBlock); const nbr = parseInt(b.header.globalVariables.blockNumber.toString()); - logger.info(nbr); -}; - -const main = async () => { - const pxe = createPXEClient(AZTEC_NODE_URL); - const latestHeight = await pxe.getBlockNumber(); - const block = await pxe.getBlock(latestHeight); - - if (!block) { - logger.error(`Failed to get block ${latestHeight}`); - return; - } - - const hash = block?.hash()?.toString() ; - const sentToDb = { - number: block.number, - hash, - timestamp: block.getStats().blockTimestamp, - archive: block.archive, - header: block.header, - body: block.body, - }; - testType(JSON.parse(JSON.stringify(sentToDb))); - // logger.info(JSON.stringify(dbBlock, null, 2)); -}; - -main().catch((e) => { - logger.error(`Failed to investigate block: ${e}`); -}); diff --git a/services/explorer-api/scripts/migrate.ts b/services/explorer-api/scripts/migrate.ts index df58fdaa..bc07a0bb 100644 --- a/services/explorer-api/scripts/migrate.ts +++ b/services/explorer-api/scripts/migrate.ts @@ -29,14 +29,16 @@ async function runMigrations() { console.log(`Retrying attempt ${attemptNumber} of ${retries}...`); return true; } - console.error(e.stack); return false; }, }); - console.log("🤩 Migrations complete!"); - await pool.end(); + console.log("🤩 Migrations complete!"); } -runMigrations().catch(console.error); +runMigrations().catch(async (e) => { + console.error(e); + await pool.end(); + process.exit(1); +}); diff --git a/services/explorer-api/src/database/controllers/index.ts b/services/explorer-api/src/database/controllers/index.ts index 3d3539fe..34f142e9 100644 --- a/services/explorer-api/src/database/controllers/index.ts +++ b/services/explorer-api/src/database/controllers/index.ts @@ -1 +1,2 @@ export * as l2Block from "./l2block/index.js"; +export * as l2Contract from "./l2contract/index.js"; diff --git a/services/explorer-api/src/database/controllers/l2block/store.ts b/services/explorer-api/src/database/controllers/l2block/store.ts index 8d80e8dd..28641547 100644 --- a/services/explorer-api/src/database/controllers/l2block/store.ts +++ b/services/explorer-api/src/database/controllers/l2block/store.ts @@ -1,4 +1,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ +import { + HexString, + type ChicmozL2Block, + type EncryptedLogEntry, + type NoteEncryptedLogEntry, + type UnencryptedLogEntry, +} from "@chicmoz-pkg/types"; import { v4 as uuidv4 } from "uuid"; import { getDb as db } from "../../../database/index.js"; import { @@ -6,31 +13,24 @@ import { body, bodyToTxEffects, contentCommitment, + functionLogs, + gasFees, globalVariables, header, + l1ToL2MessageTree, l2Block, - state, - txEffect, lastArchive, - l1ToL2MessageTree, - partial, + logs, noteHashTree, nullifierTree, + partial, publicDataTree, - gasFees, publicDataWrite, - logs, - functionLogs, + state, + txEffect, txEffectToLogs, txEffectToPublicDataWrite, } from "../../../database/schema/l2block/index.js"; -import { - type ChicmozL2Block, - type EncryptedLogEntry, - type NoteEncryptedLogEntry, - type UnencryptedLogEntry, -} from "@chicmoz-pkg/types"; -import { HexString } from "../../schema/utils.js"; export const store = async (block: ChicmozL2Block): Promise => { return await db().transaction(async (tx) => { @@ -246,9 +246,7 @@ export const store = async (block: ChicmozL2Block): Promise => { encrypted: txEff.encryptedLogs.functionLogs, unencrypted: txEff.unencryptedLogs.functionLogs, })) { - for (const [functionLogIndex, functionLog] of Object.entries( - fLogs - )) { + for (const [functionLogIndex, functionLog] of Object.entries(fLogs)) { // Insert logs const functionLogId = uuidv4(); await tx diff --git a/services/explorer-api/src/database/controllers/l2contract/get-contract-instance.ts b/services/explorer-api/src/database/controllers/l2contract/get-contract-instance.ts new file mode 100644 index 00000000..71cf26bf --- /dev/null +++ b/services/explorer-api/src/database/controllers/l2contract/get-contract-instance.ts @@ -0,0 +1,62 @@ +import { + ChicmozL2ContractInstanceDeluxe, + HexString, +} from "@chicmoz-pkg/types"; +import { and, desc, eq } from "drizzle-orm"; +import { getDb as db } from "../../../database/index.js"; +import { logger } from "../../../logger.js"; +import { + l2ContractClassRegistered, + l2ContractInstanceDeployed, +} from "../../schema/l2contract/index.js"; + +export const getL2DeployedContractInstanceByAddress = async ( + address: HexString +): Promise< + | ChicmozL2ContractInstanceDeluxe + | null +> => { + const result = await db() + .select({ + address: l2ContractInstanceDeployed.address, + blockHash: l2ContractInstanceDeployed.blockHash, + version: l2ContractInstanceDeployed.version, + salt: l2ContractInstanceDeployed.salt, + contractClassId: l2ContractInstanceDeployed.contractClassId, + initializationHash: l2ContractInstanceDeployed.initializationHash, + publicKeysHash: l2ContractInstanceDeployed.publicKeysHash, + deployer: l2ContractInstanceDeployed.deployer, + artifactHash: l2ContractClassRegistered.artifactHash, + privateFunctionsRoot: l2ContractClassRegistered.privateFunctionsRoot, + packedPublicBytecode: l2ContractClassRegistered.packedPublicBytecode, + }) + .from(l2ContractInstanceDeployed) + .innerJoin( + l2ContractClassRegistered, + and( + eq( + l2ContractInstanceDeployed.contractClassId, + l2ContractClassRegistered.contractClassId + ), + eq( + l2ContractInstanceDeployed.version, + l2ContractClassRegistered.version + ) + ) + ) + .where(eq(l2ContractInstanceDeployed.address, address)) + .orderBy(desc(l2ContractInstanceDeployed.version)) + .limit(1); + + if (result.length === 0) { + logger.info(`No contract instance found for address: ${address}`); + return null; + } + + const contractInstance = result[0]; + + return { + ...contractInstance, + packedPublicBytecode: Buffer.from(contractInstance.packedPublicBytecode), + }; +}; diff --git a/services/explorer-api/src/database/controllers/l2contract/get-contract-instances.ts b/services/explorer-api/src/database/controllers/l2contract/get-contract-instances.ts new file mode 100644 index 00000000..9e726bd7 --- /dev/null +++ b/services/explorer-api/src/database/controllers/l2contract/get-contract-instances.ts @@ -0,0 +1,48 @@ +import { + ChicmozL2ContractInstanceDeluxe +} from "@chicmoz-pkg/types"; +import { and, desc, eq } from "drizzle-orm"; +import { getDb as db } from "../../../database/index.js"; +import { + l2ContractClassRegistered, + l2ContractInstanceDeployed, +} from "../../schema/l2contract/index.js"; + +export const getL2DeployedContractInstancesByBlockHash = async ( + blockHash: string +): Promise< + ChicmozL2ContractInstanceDeluxe[] +> => { + const result = await db() + .select({ + address: l2ContractInstanceDeployed.address, + blockHash: l2ContractInstanceDeployed.blockHash, + version: l2ContractInstanceDeployed.version, + salt: l2ContractInstanceDeployed.salt, + contractClassId: l2ContractInstanceDeployed.contractClassId, + initializationHash: l2ContractInstanceDeployed.initializationHash, + publicKeysHash: l2ContractInstanceDeployed.publicKeysHash, + deployer: l2ContractInstanceDeployed.deployer, + artifactHash: l2ContractClassRegistered.artifactHash, + privateFunctionsRoot: l2ContractClassRegistered.privateFunctionsRoot, + packedPublicBytecode: l2ContractClassRegistered.packedPublicBytecode, + }) + .from(l2ContractInstanceDeployed) + .innerJoin( + l2ContractClassRegistered, + and( + eq( + l2ContractInstanceDeployed.contractClassId, + l2ContractClassRegistered.contractClassId + ), + eq( + l2ContractInstanceDeployed.version, + l2ContractClassRegistered.version + ) + ) + ) + .where(eq(l2ContractInstanceDeployed.blockHash, blockHash)) + .orderBy(desc(l2ContractInstanceDeployed.version)); + + return result; +}; diff --git a/services/explorer-api/src/database/controllers/l2contract/index.ts b/services/explorer-api/src/database/controllers/l2contract/index.ts new file mode 100644 index 00000000..6b16059f --- /dev/null +++ b/services/explorer-api/src/database/controllers/l2contract/index.ts @@ -0,0 +1,3 @@ +export * from "./get-contract-instance.js"; +export * from "./get-contract-instances.js"; +export * from "./store.js"; diff --git a/services/explorer-api/src/database/controllers/l2contract/store.ts b/services/explorer-api/src/database/controllers/l2contract/store.ts new file mode 100644 index 00000000..b42f4e2c --- /dev/null +++ b/services/explorer-api/src/database/controllers/l2contract/store.ts @@ -0,0 +1,34 @@ +/* eslint-disable no-console, @typescript-eslint/no-unsafe-member-access */ + +import { + type ChicmozL2ContractClassRegisteredEvent, + type ChicmozL2ContractInstanceDeployedEvent, +} from "@chicmoz-pkg/types"; +import { getDb as db } from "../../../database/index.js"; +import { + l2ContractInstanceDeployed, + l2ContractClassRegistered, +} from "../../../database/schema/l2contract/index.js"; + +export const storeContractInstance = async ( + instance: ChicmozL2ContractInstanceDeployedEvent, +): Promise => { + + await db() + .insert(l2ContractInstanceDeployed) + .values({ + ...instance, + }) + .onConflictDoNothing(); +}; +export const storeContractClass = async ( + contractClass: ChicmozL2ContractClassRegisteredEvent, +): Promise => { + + await db() + .insert(l2ContractClassRegistered) + .values({ + ...contractClass, + }) + .onConflictDoNothing(); +}; diff --git a/services/explorer-api/src/database/schema/index.ts b/services/explorer-api/src/database/schema/index.ts index cf09725e..96f852f1 100644 --- a/services/explorer-api/src/database/schema/index.ts +++ b/services/explorer-api/src/database/schema/index.ts @@ -1 +1,2 @@ export * from "./l2block/index.js"; +export * from "./l2contract/index.js"; diff --git a/services/explorer-api/src/database/schema/l2block/body.ts b/services/explorer-api/src/database/schema/l2block/body.ts index 0cc51c32..9954be7e 100644 --- a/services/explorer-api/src/database/schema/l2block/body.ts +++ b/services/explorer-api/src/database/schema/l2block/body.ts @@ -1,3 +1,4 @@ +import { HexString } from "@chicmoz-pkg/types"; import { integer, jsonb, @@ -6,7 +7,7 @@ import { uuid, varchar, } from "drizzle-orm/pg-core"; -import { HexString, generateFrColumn } from "../utils.js"; +import { generateFrColumn } from "../utils.js"; export const body = pgTable("body", { id: uuid("id").primaryKey().defaultRandom(), @@ -31,7 +32,9 @@ export const txEffect = pgTable("tx_effect", { noteHashes: jsonb("note_hashes").notNull(), nullifiers: jsonb("nullifiers").notNull(), l2ToL1Msgs: jsonb("l2_to_l1_msgs").notNull().$type(), - noteEncryptedLogsLength: generateFrColumn("note_encrypted_logs_length").notNull(), + noteEncryptedLogsLength: generateFrColumn( + "note_encrypted_logs_length" + ).notNull(), encryptedLogsLength: generateFrColumn("encrypted_logs_length").notNull(), unencryptedLogsLength: generateFrColumn("unencrypted_logs_length").notNull(), }); diff --git a/services/explorer-api/src/database/schema/l2block/header.ts b/services/explorer-api/src/database/schema/l2block/header.ts index 1f7464d4..7f5c661e 100644 --- a/services/explorer-api/src/database/schema/l2block/header.ts +++ b/services/explorer-api/src/database/schema/l2block/header.ts @@ -1,5 +1,5 @@ import { pgTable, uuid, varchar } from "drizzle-orm/pg-core"; -import { bufferType, generateFrColumn, generateTreeTable } from "../utils.js"; +import { bufferType, generateAztecAddressColumn, generateFrColumn, generateTreeTable } from "../utils.js"; export const header = pgTable("header", { id: uuid("id").primaryKey().defaultRandom(), @@ -68,8 +68,7 @@ export const globalVariables = pgTable("global_variables", { slotNumber: generateFrColumn("slot_number"), timestamp: generateFrColumn("timestamp"), coinbase: varchar("coinbase", { length: 42 }).notNull(), - // NOTE: feeRecipient is referred to as "AztecAddress" and not Fr (although it is(?) a Fr) - feeRecipient: varchar("fee_recipient", { length: 66 }).notNull(), + feeRecipient: generateAztecAddressColumn("fee_recipient").notNull(), gasFeesId: uuid("gas_fees_id") .notNull() .references(() => gasFees.id), diff --git a/services/explorer-api/src/database/schema/l2block/relations.ts b/services/explorer-api/src/database/schema/l2block/relations.ts index 975d0f49..5313a3a6 100644 --- a/services/explorer-api/src/database/schema/l2block/relations.ts +++ b/services/explorer-api/src/database/schema/l2block/relations.ts @@ -176,12 +176,9 @@ export const logsRelations = relations(logs, ({ many }) => ({ txEffectToLogs: many(txEffectToLogs), })); -export const functionLogsRelations = relations( - functionLogs, - ({ many }) => ({ - txEffectToLogs: many(txEffectToLogs), - }) -); +export const functionLogsRelations = relations(functionLogs, ({ many }) => ({ + txEffectToLogs: many(txEffectToLogs), +})); export const txEffectToLogsRelations = relations(txEffectToLogs, ({ one }) => ({ txEffect: one(txEffect, { diff --git a/services/explorer-api/src/database/schema/l2block/root.ts b/services/explorer-api/src/database/schema/l2block/root.ts index aac3bbb2..eda91683 100644 --- a/services/explorer-api/src/database/schema/l2block/root.ts +++ b/services/explorer-api/src/database/schema/l2block/root.ts @@ -2,7 +2,7 @@ import { bigint, pgTable, uuid, varchar } from "drizzle-orm/pg-core"; import { body } from "./body.js"; import { header } from "./header.js"; -import {generateTreeTable} from "../utils.js"; +import { generateTreeTable } from "../utils.js"; export const l2Block = pgTable("l2Block", { hash: varchar("hash").primaryKey().notNull(), diff --git a/services/explorer-api/src/database/schema/l2contract/index.ts b/services/explorer-api/src/database/schema/l2contract/index.ts new file mode 100644 index 00000000..63c3e03c --- /dev/null +++ b/services/explorer-api/src/database/schema/l2contract/index.ts @@ -0,0 +1,86 @@ +import { relations } from "drizzle-orm"; +import { + bigint, + foreignKey, + integer, + pgTable, + primaryKey, + unique, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { l2Block } from "../index.js"; +import { + bufferType, + generateAztecAddressColumn, + generateFrColumn, +} from "../utils.js"; + +export const l2ContractInstanceDeployed = pgTable( + "l2_contract_instance_deployed", + { + // TODO: perhaps a different name for this column? + id: uuid("id").primaryKey().defaultRandom(), + blockHash: varchar("block_hash") + .notNull() + .references(() => l2Block.hash), + address: generateAztecAddressColumn("address").notNull(), + version: integer("version").notNull(), + salt: generateFrColumn("salt").notNull(), + contractClassId: generateFrColumn("contract_class_id").notNull(), + initializationHash: generateFrColumn("initialization_hash").notNull(), + publicKeysHash: generateFrColumn("public_keys_hash").notNull(), + deployer: generateAztecAddressColumn("deployer").notNull(), + }, + (t) => ({ + unq: unique().on(t.contractClassId, t.address, t.version), + contractClass: foreignKey({ + name: "contract_class", + columns: [t.contractClassId, t.version], + foreignColumns: [l2ContractClassRegistered.contractClassId, l2ContractClassRegistered.version], + }), + }) +); + +export const l2ContractClassRegistered = pgTable( + "l2_contract_class_registered", + { + blockHash: varchar("block_hash") + .notNull() + .references(() => l2Block.hash), + contractClassId: generateFrColumn("contract_class_id").notNull(), + version: bigint("version", { mode: "number" }).notNull(), + artifactHash: generateFrColumn("artifact_hash").notNull(), + privateFunctionsRoot: generateFrColumn("private_functions_root").notNull(), + packedPublicBytecode: bufferType("packed_public_bytecode").notNull(), + }, + (t) => ({ + primaryKey: primaryKey({ + name: "contract_class_id_version", + columns: [t.contractClassId, t.version], + }), + }) +); + +export const l2ContractInstanceDeployedRelations = relations( + l2ContractInstanceDeployed, + ({ one }) => ({ + contractClass: one(l2ContractClassRegistered, { + fields: [ + l2ContractInstanceDeployed.contractClassId, + l2ContractInstanceDeployed.version, + ], + references: [ + l2ContractClassRegistered.contractClassId, + l2ContractClassRegistered.version, + ], + }), + }) +); + +export const l2ContractClassRegisteredRelations = relations( + l2ContractClassRegistered, + ({ many }) => ({ + instances: many(l2ContractInstanceDeployed), + }) +); diff --git a/services/explorer-api/src/database/schema/utils.ts b/services/explorer-api/src/database/schema/utils.ts index e261f66a..74c9c085 100644 --- a/services/explorer-api/src/database/schema/utils.ts +++ b/services/explorer-api/src/database/schema/utils.ts @@ -9,6 +9,9 @@ import { export const generateFrColumn = (name: string) => varchar(name, { length: 66 }); +export const generateAztecAddressColumn = (name: string) => + varchar(name, { length: 66 }); + export const generateTreeTable = (name: string) => pgTable(name, { id: uuid("id").primaryKey().defaultRandom(), root: generateFrColumn("root"), @@ -23,5 +26,3 @@ export const bufferType = customType<{ return 'bytea' }, }); - -export type HexString = `${"0x"}${string}`; diff --git a/services/explorer-api/src/event-handler/on-block.ts b/services/explorer-api/src/event-handler/on-block.ts index 6f33228f..f99c91a0 100644 --- a/services/explorer-api/src/event-handler/on-block.ts +++ b/services/explorer-api/src/event-handler/on-block.ts @@ -1,6 +1,18 @@ import { L2Block } from "@aztec/aztec.js"; +import { + ContractClassRegisteredEvent, + ContractInstanceDeployedEvent, +} from "@aztec/circuits.js"; +import { ClassRegistererAddress } from "@aztec/protocol-contracts/class-registerer"; import { NewBlockEvent } from "@chicmoz-pkg/message-registry"; -import { chicmozL2BlockSchema, type ChicmozL2Block } from "@chicmoz-pkg/types"; +import { + chicmozL2BlockSchema, + chicmozL2ContractClassRegisteredEventSchema, + chicmozL2ContractInstanceDeployedEventSchema, + type ChicmozL2Block, + type ChicmozL2ContractClassRegisteredEvent, + type ChicmozL2ContractInstanceDeployedEvent, +} from "@chicmoz-pkg/types"; import { controllers } from "../database/index.js"; import { logger } from "../logger.js"; @@ -11,11 +23,17 @@ export const onBlock = async ({ block }: NewBlockEvent) => { return; } const b = L2Block.fromString(block); + const hash = b.hash().toString(); + await storeBlock(b, hash); + await storeContracts(b, hash); +}; + +const storeBlock = async (b: L2Block, hash: string) => { let parsedBlock: ChicmozL2Block; try { logger.info(`👓 Parsing block ${b.number}`); parsedBlock = chicmozL2BlockSchema.parse({ - hash: b.hash().toString(), + hash, height: b.number, ...JSON.parse(JSON.stringify(b)), }); @@ -37,3 +55,80 @@ export const onBlock = async ({ block }: NewBlockEvent) => { ); } }; + +const storeContracts = async (b: L2Block, blockHash: string) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + const blockLogs = b.body.txEffects + .flatMap((txEffect) => (txEffect ? [txEffect.unencryptedLogs] : [])) + .flatMap((txLog) => txLog.unrollLogs()); + const contractClasses = ContractClassRegisteredEvent.fromLogs( + blockLogs, + ClassRegistererAddress + ); + const contractInstances = ContractInstanceDeployedEvent.fromLogs(blockLogs); + + const parsedContractClasses: ChicmozL2ContractClassRegisteredEvent[] = []; + const parsedContractInstances: ChicmozL2ContractInstanceDeployedEvent[] = []; + for (const contractClass of contractClasses) { + try { + const parsed = chicmozL2ContractClassRegisteredEventSchema.parse( + JSON.parse( + JSON.stringify({ + blockHash, + ...contractClass, + }) + ) + ); + parsedContractClasses.push(parsed); + } catch (e) { + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Failed to parse contractClass ${contractClass.contractClassId}: ${e}` + ); + } + } + for (const contractInstance of contractInstances) { + try { + const parsed: ChicmozL2ContractInstanceDeployedEvent = + chicmozL2ContractInstanceDeployedEventSchema.parse( + JSON.parse( + JSON.stringify({ + blockHash, + ...contractInstance, + }) + ) + ); + parsedContractInstances.push(parsed); + } catch (e) { + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Failed to parse contractInstance ${contractInstance.address}: ${e}` + ); + } + } + + for (const contractClass of parsedContractClasses) { + try { + await controllers.l2Contract.storeContractClass(contractClass); + } catch (e) { + logger.error( + `Failed to store contractClass (${ + contractClass.contractClassId + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }) in block ${blockHash}): ${(e as Error)?.stack ?? e}` + ); + } + } + for (const contractInstance of parsedContractInstances) { + try { + await controllers.l2Contract.storeContractInstance(contractInstance); + } catch (e) { + logger.error( + `Failed to store contractInstance (${ + contractInstance.address + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }) in block ${blockHash}): ${(e as Error)?.stack ?? e}` + ); + } + } +}; diff --git a/services/explorer-api/src/http-server/routes/controller.ts b/services/explorer-api/src/http-server/routes/controller.ts index e12b78e1..556d1824 100644 --- a/services/explorer-api/src/http-server/routes/controller.ts +++ b/services/explorer-api/src/http-server/routes/controller.ts @@ -1,5 +1,6 @@ import asyncHandler from "express-async-handler"; import { controllers as db } from "../../database/index.js"; +import {getContractInstanceSchema, getContractInstancesByBlockHashSchema} from "./validation-schemas.js"; export const GET_LATEST_HEIGHT = asyncHandler(async (_req, res) => { const latestBlock = await db.l2Block.getLatestBlock(); @@ -27,62 +28,16 @@ export const GET_HEALTH = asyncHandler((_req, res) => { res.sendStatus(200); }); -//GET_BLOCK: RequestHandler = async (req, res) => { -// const { -// params: { heightOrHash }, -// } = getBlockSchema.parse(req); -// let block; -// if (isNaN(Number(heightOrHash))) { -// if (useIndex) block = await this.db.block.getByHash(heightOrHash); -// else block = await this.db.block.getByHashWithoutIndex(heightOrHash); -// } else { -// block = await this.db.block.getByHeight(Number(heightOrHash)); -// } - -// if (!block) throw new NotFoundError("Block not found"); -// res.status(200).send(JSONbig.stringify(block)); -// }; - -// GET_BLOCKS: RequestHandler = async (req, res) => { -// try { -// const { start, end } = getBlocksSchema.parse(req).query; - -// if (end < start) throw new Error("Something went wrong: Invalid block range"); -// if (start === end) { -// res.status(200).send([]); -// return; -// } - -// const blockHeight = this.db.block.getBlockHeight(); -// if (blockHeight === -1 || blockHeight < end - 1) -// throw new Error(`Something went wrong: Block ${blockHeight < start ? start : blockHeight + 1} does not exist in storage`); - -// // TODO: check if this logic can be simplified -// let blocks; -// const dbQueryParams = getBlockRangeQueryParams(start, end - 1); - -// if (dbQueryParams.length === 1) { -// const { start, end, batch } = dbQueryParams[0]; -// blocks = await this.db.block.getBlocksByRange(start, end, batch); -// } else { -// const [params1, params2] = dbQueryParams; -// const blocks1 = await this.db.block.getBlocksByRange(params1.start, params1.end, params1.batch); -// const blocks2 = await this.db.block.getBlocksByRange(params2.start, params2.end, params2.batch); - -// blocks = blocks1 && blocks2 ? blocks1.concat(blocks2) : undefined; -// } - -// if (!blocks || blocks.length != end - start) throw new Error("Blocks not found"); +export const GET_L2_CONTRACT_INSTANCE = asyncHandler(async (req, res) => { + const { address } = getContractInstanceSchema.parse(req).params; + const instance = await db.l2Contract.getL2DeployedContractInstanceByAddress(address); + if (!instance) throw new Error("Contract instance not found"); + res.status(200).send(JSON.stringify(instance)); +}); -// res.status(200).send(JSONbig.stringify(blocks)); -// } catch (err) { -// if (err instanceof ZodError) { -// res.status(400).send({ message: "Schema validation error", errors: err.issues }); -// } else { -// res -// .status(500) -// .type("text/plain") -// .send(err instanceof Error && err.message ? err.message : "An internal error occurred"); -// } -// } -// }; +export const GET_L2_CONTRACT_INSTANCES_BY_BLOCK_HASH = asyncHandler(async (req, res) => { + const { blockHash } = getContractInstancesByBlockHashSchema.parse(req).params; + const instances = await db.l2Contract.getL2DeployedContractInstancesByBlockHash(blockHash); + if (!instances) throw new Error("Contract instances not found"); + res.status(200).send(JSON.stringify(instances)); +}); diff --git a/services/explorer-api/src/http-server/routes/index.ts b/services/explorer-api/src/http-server/routes/index.ts index 447c23a9..658caaee 100644 --- a/services/explorer-api/src/http-server/routes/index.ts +++ b/services/explorer-api/src/http-server/routes/index.ts @@ -8,9 +8,12 @@ export const init = ({ }) => { router.get(`/health`, controller.GET_HEALTH); - router.get(`/latest-height`, controller.GET_LATEST_HEIGHT); - router.get(`/blocks/latest`, controller.GET_LATEST_BLOCK); - router.get(`/blocks/:heightOrHash`, controller.GET_BLOCK); + router.get(`/l2/latest-height`, controller.GET_LATEST_HEIGHT); + router.get(`/l2/blocks/latest`, controller.GET_LATEST_BLOCK); + router.get(`/l2/blocks/:heightOrHash`, controller.GET_BLOCK); // router.get(`/blocks`, controller.GET_BLOCKS); + + router.get(`/l2/contract-instance/:address`, controller.GET_L2_CONTRACT_INSTANCE); + router.get(`/l2/blocks/:blockHash/contract-instances`, controller.GET_L2_CONTRACT_INSTANCES_BY_BLOCK_HASH); return router; }; diff --git a/services/explorer-api/src/http-server/routes/validation-schemas.ts b/services/explorer-api/src/http-server/routes/validation-schemas.ts index 1cfe3092..1c5509d3 100644 --- a/services/explorer-api/src/http-server/routes/validation-schemas.ts +++ b/services/explorer-api/src/http-server/routes/validation-schemas.ts @@ -1,7 +1,14 @@ +import { hexStringSchema } from "@chicmoz-pkg/types"; import { z } from "zod"; -export const getBlockSchema = z.object({ +export const getContractInstanceSchema = z.object({ params: z.object({ - heightOrHash: z.string(), - }) + address: hexStringSchema, + }), +}); + +export const getContractInstancesByBlockHashSchema = z.object({ + params: z.object({ + blockHash: hexStringSchema, + }), }); diff --git a/services/explorer-api/src/types/block.ts b/services/explorer-api/src/types/block.ts deleted file mode 100644 index 1a145155..00000000 --- a/services/explorer-api/src/types/block.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Fr as FrType } from "@aztec/aztec.js"; -import { z } from "zod"; - -declare module "@aztec/aztec.js" { - const Fr: { - fromString(val: string): FrType; - }; - interface Fr { - toString(): string; - } - interface L2Block { - hash(): Fr; - } -} - -const FrSchema = z - .preprocess( - (val) => { - if (typeof val === "string") return { type: "Fr", value: val }; - return val; - }, - z.object({ - type: z.literal("Fr"), - value: z.string(), - }) - ) - .transform((val, ctx): FrType => { - try { - return FrType.fromString(val.value); - } catch (e) { - let errorMessage = (e as string) || "Failed to do something exceptional"; - if (e instanceof Error) errorMessage = e.message; - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: errorMessage, - }); - return z.NEVER; - } - }); - -const bufferSchema = z.custom((value) => { - return value instanceof Buffer; -}, { message: "Expected a Buffer" }); - -export const reconstructedL2BlockSchema = z.object({ - archive: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - header: z.object({ - lastArchive: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - contentCommitment: z.object({ - numTxs: FrSchema, - txsEffectsHash: bufferSchema, - inHash: bufferSchema, - outHash: bufferSchema, - }), - state: z.object({ - l1ToL2MessageTree: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - partial: z.object({ - noteHashTree: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - nullifierTree: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - publicDataTree: z.object({ - root: FrSchema, - nextAvailableLeafIndex: z.number(), - }), - }), - }), - globalVariables: z.object({ - chainId: z.string(), - version: z.string(), - blockNumber: FrSchema, - slotNumber: FrSchema, - timestamp: FrSchema, - coinbase: z.string(), - feeRecipient: z.string(), - gasFees: z.object({ - feePerDaGas: z.string(), - feePerL2Gas: z.string(), - }), - }), - totalFees: FrSchema, - }), - body: z.object({ - txEffects: z.array( - z.object({ - revertCode: z.object({ code: z.number() }), - transactionFee: FrSchema, - noteHashes: z.array(FrSchema), - nullifiers: z.array(FrSchema), - l2ToL1Msgs: z.array(FrSchema), - publicDataWrites: z.array(FrSchema), - noteEncryptedLogsLength: FrSchema, - encryptedLogsLength: FrSchema, - unencryptedLogsLength: FrSchema, - noteEncryptedLogs: z.object({ - functionLogs: z.array( - z.object({ - logs: z.array( - z.object({ - data: z.string(), - }) - ), - }) - ), - }), - encryptedLogs: z.object({ - functionLogs: z.array( - z.object({ - logs: z.array( - z.object({ - data: z.string(), - maskedContractAddress: FrSchema, - }) - ), - }) - ), - }), - unencryptedLogs: z.object({ - functionLogs: z.array( - z.object({ - logs: z.array( - z.object({ - data: z.string(), - contractAddress: z.string(), - }) - ), - }) - ), - }), - }) - ), - }), -}); - -export type ReconstructedL2Block = z.infer; diff --git a/services/explorer-api/tsconfig.json b/services/explorer-api/tsconfig.json index 1424b3ec..ab60ecc4 100644 --- a/services/explorer-api/tsconfig.json +++ b/services/explorer-api/tsconfig.json @@ -59,8 +59,8 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "ESNext" /* Specify what module code is generated. */, - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "module": "NodeNext" /* Specify what module code is generated. */, + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./build" /* Specify an output folder for all emitted files. */, "rootDirs": ["./"] /* Specify the root folder within your source files. */, @@ -98,5 +98,5 @@ /* Language and Environment */ "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ }, - "include": ["src/**/*", "drizzle.config.js", "scripts/**/*"] + "include": ["src/**/*", "drizzle.config.ts", "scripts/**/*"] } diff --git a/services/explorer-ui/src/hooks/useLatestBlock.ts b/services/explorer-ui/src/hooks/useLatestBlock.ts index 4dc13b30..d66b4a9e 100644 --- a/services/explorer-ui/src/hooks/useLatestBlock.ts +++ b/services/explorer-ui/src/hooks/useLatestBlock.ts @@ -1,6 +1,8 @@ import { type ChicmozL2Block } from "@chicmoz-pkg/types"; import { useCallback, useEffect, useState } from "react"; -import { getLatestBlock } from "~/service/api"; +import { + getLatestBlock, +} from "~/service/api"; const formatTimeSince = (seconds: number) => { const intervals = [ @@ -20,6 +22,32 @@ const formatTimeSince = (seconds: number) => { return "just now"; }; +// const logLogs = (txEffects: ChicmozL2Block["body"]["txEffects"]) => { +// for (const [txI, tx] of Object.entries(txEffects)) { +// for (const [fLogI, fLog] of Object.entries( +// tx.unencryptedLogs.functionLogs +// )) { +// for (const [logI, log] of Object.entries(fLog.logs)) { +// console.log(`tx ${txI} fLog ${fLogI} log ${logI}: `, log); +// } +// } +// } +// }; +// +// const logPublicWrites = (txEffects: ChicmozL2Block["body"]["txEffects"]) => { +// for (const [txI, tx] of Object.entries(txEffects)) { +// for (const [pubWriteI, pubWrite] of Object.entries(tx.publicDataWrites)) { +// console.log(`tx ${txI} pubWrite ${pubWriteI}: `, pubWrite); +// for (const [logI, log] of Object.entries( +// tx.unencryptedLogs.functionLogs[Number(pubWriteI)].logs +// )) { +// console.log(`\tlog ${logI}: `, log); +// } +// } +// } +// }; +// + export const useLatestBlock = () => { const [latestBlockData, setLatestBlockData] = useState( null @@ -30,7 +58,7 @@ export const useLatestBlock = () => { const fetchLatestBlock = useCallback(async () => { try { const block = await getLatestBlock(); - console.log("block", block); + //console.log("block", block); if ( !block || latestBlockData?.header?.globalVariables?.blockNumber === @@ -38,6 +66,8 @@ export const useLatestBlock = () => { ) return; + console.log(block); + //logPublicWrites(block.body.txEffects); setLatestBlockData(block); setError(null); } catch (err) { @@ -70,4 +100,4 @@ export const useLatestBlock = () => { error, timeSince, }; -} +}; diff --git a/services/explorer-ui/src/routes/blocks.$blockNumber.lazy.tsx b/services/explorer-ui/src/routes/blocks.$blockNumber.lazy.tsx index c225b9d3..7dc4bdf8 100644 --- a/services/explorer-ui/src/routes/blocks.$blockNumber.lazy.tsx +++ b/services/explorer-ui/src/routes/blocks.$blockNumber.lazy.tsx @@ -7,7 +7,6 @@ export const Route = createLazyFileRoute("/blocks/$blockNumber")({ function Block() { const { latestBlockData, loading, error, timeSince } = useLatestBlock(); const { blockNumber } = Route.useParams(); - console.log("blockNumber", blockNumber); // TODO: these messages should perhaps be diplayed? // console.log("Extracted messages:"); diff --git a/services/explorer-ui/src/routes/blocks.lazy.tsx b/services/explorer-ui/src/routes/blocks.lazy.tsx index 4ce1183a..3b4bfddd 100644 --- a/services/explorer-ui/src/routes/blocks.lazy.tsx +++ b/services/explorer-ui/src/routes/blocks.lazy.tsx @@ -6,8 +6,14 @@ export const Route = createLazyFileRoute("/blocks")({ }); function Blocks() { - const params = useParams({ from: "/blocks/$blockNumber" }); - const isIndex = !params.blockNumber; + let isIndex = true; + try { + const params = useParams({ from: "/blocks/$blockNumber" }); + isIndex = !params.blockNumber; + } catch (e) { + console.error(e); + isIndex = true; + } const text = { title: isIndex ? "All blocks" : "Block Details", diff --git a/services/explorer-ui/src/service/api.ts b/services/explorer-ui/src/service/api.ts index 5ba4da12..0bb8204e 100644 --- a/services/explorer-ui/src/service/api.ts +++ b/services/explorer-ui/src/service/api.ts @@ -1,5 +1,6 @@ -import { type ChicmozL2Block, chicmozL2BlockSchema } from "@chicmoz-pkg/types"; +import { type ChicmozL2Block, chicmozL2BlockSchema, chicmozL2ContractInstanceDeluxeSchema, type ChicmozL2ContractInstanceDeluxe } from "@chicmoz-pkg/types"; import { API_URL, aztecExplorer } from "./constants"; +import {z} from "zod"; const defaultHeaders = { "Content-Type": "application/json", @@ -7,7 +8,7 @@ const defaultHeaders = { }; export const getLatestHeight = async () => { - const url = `${API_URL}/${aztecExplorer.getLatestHeight}`; + const url = `${API_URL}/${aztecExplorer.getL2LatestHeight}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -21,24 +22,23 @@ export const getLatestHeight = async () => { }; export const getLatestBlock = async (): Promise => { - const url = `${API_URL}/${aztecExplorer.getLatestBlock}`; + const url = `${API_URL}/${aztecExplorer.getL2LatestBlock}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, }); const result = await response.json(); - console.log(result); + if (response.status !== 200) throw new Error(`An error occurred while fetching latest height: ${result}`); const res = chicmozL2BlockSchema.parse(result); - if (response.status !== 200) throw new Error(`An error occurred while fetching latest height: ${result}`); return res; }; export const getBlockByHeight = async (height: number): Promise => { - const url = `${API_URL}/${aztecExplorer.getBlockByHeight}${height}`; + const url = `${API_URL}/${aztecExplorer.getL2BlockByHeight}${height}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -52,7 +52,7 @@ export const getBlockByHeight = async (height: number): Promise }; export const getBlocksByHeightRange = async (start: number, end: number) => { - const url = `${API_URL}/${aztecExplorer.getBlocksByHeightRange}`; + const url = `${API_URL}/${aztecExplorer.getL2BlocksByHeightRange}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -67,7 +67,7 @@ export const getBlocksByHeightRange = async (start: number, end: number) => { }; export const getBlocksByHash = async (hash: string): Promise => { - const url = `${API_URL}/${aztecExplorer.getBlockByHash}${hash}`; + const url = `${API_URL}/${aztecExplorer.getL2BlockByHash}${hash}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -81,7 +81,7 @@ export const getBlocksByHash = async (hash: string): Promise => }; export const getTransactionById = async (id: string) => { - const url = `${API_URL}/${aztecExplorer.getTransactionById}`; + const url = `${API_URL}/${aztecExplorer.getL2TransactionById}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -96,7 +96,7 @@ export const getTransactionById = async (id: string) => { }; export const getTransactionsByHeight = async (height: number) => { - const url = `${API_URL}/${aztecExplorer.getTransactionsByHeight}`; + const url = `${API_URL}/${aztecExplorer.getL2TransactionsByHeight}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -111,7 +111,7 @@ export const getTransactionsByHeight = async (height: number) => { }; export const getTransactionsByHeightRange = async (start: number, end: number) => { - const url = `${API_URL}/${aztecExplorer.getTransactionsByHeightRange}`; + const url = `${API_URL}/${aztecExplorer.getL2TransactionsByHeightRange}`; const response = await fetch(url, { method: "GET", headers: defaultHeaders, @@ -124,3 +124,35 @@ export const getTransactionsByHeightRange = async (start: number, end: number) = return result; }; + +export const getL2ContractInstance = async (address: string): Promise => { + const url = `${API_URL}/${aztecExplorer.getL2ContractInstance}${address}`; + const response = await fetch(url, { + method: "GET", + headers: defaultHeaders, + }); + const result = await response.json(); + + console.info(`GET ${url}: `, response.status); + if (response.status !== 200) throw new Error(`An error occurred while fetching contract instance: ${result}`); + + const res = chicmozL2ContractInstanceDeluxeSchema.parse(result); + + return res; +} + +export const getL2ContractInstancesByBlockHash = async (hash: string): Promise => { + const url = `${API_URL}/${aztecExplorer.getL2ContractInstancesByBlockHash(hash)}`; + const response = await fetch(url, { + method: "GET", + headers: defaultHeaders, + }); + const result = await response.json(); + + console.info(`GET ${url}: `, response.status); + if (response.status !== 200) throw new Error(`An error occurred while fetching contract instances by block hash: ${result}`); + + const res = z.array(chicmozL2ContractInstanceDeluxeSchema).parse(result); + + return res; +} diff --git a/services/explorer-ui/src/service/constants.ts b/services/explorer-ui/src/service/constants.ts index b0617ac6..a0322762 100644 --- a/services/explorer-ui/src/service/constants.ts +++ b/services/explorer-ui/src/service/constants.ts @@ -1,13 +1,15 @@ // TODO: update url values export const aztecExplorer = { - getLatestHeight: "latest-height", - getLatestBlock: "blocks/latest", - getBlockByHash: "blocks/", - getBlockByHeight: "blocks/", - getBlocksByHeightRange: "", - getTransactionById: "", - getTransactionsByHeight: "", - getTransactionsByHeightRange: "", + getL2LatestHeight: "l2/latest-height", + getL2LatestBlock: "l2/blocks/latest", + getL2BlockByHash: "l2/blocks/", + getL2BlockByHeight: "l2/blocks/", + getL2BlocksByHeightRange: "", + getL2TransactionById: "", + getL2TransactionsByHeight: "", + getL2TransactionsByHeightRange: "", + getL2ContractInstance: "l2/contract-instance/", + getL2ContractInstancesByBlockHash: (hash: string) => `l2/blocks/${hash}/contract-instances`, }; export const API_URL = import.meta.env.VITE_API_URL; diff --git a/yarn.lock b/yarn.lock index d14821c7..a83ab2c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3003,6 +3003,7 @@ __metadata: resolution: "@chicmoz/aztec-listener@workspace:services/aztec-listener" dependencies: "@aztec/aztec.js": "npm:0.53.0" + "@aztec/circuits.js": "npm:0.53.0" "@chicmoz-pkg/logger-server": "workspace:^" "@chicmoz-pkg/message-bus": "workspace:^" "@chicmoz-pkg/message-registry": "workspace:^" @@ -3056,6 +3057,7 @@ __metadata: "@aws-sdk/credential-providers": "npm:3.525.0" "@aztec/aztec.js": "npm:0.53.0" "@aztec/circuits.js": "npm:0.53.0" + "@aztec/protocol-contracts": "npm:0.53.0" "@chicmoz-pkg/error-middleware": "workspace:^" "@chicmoz-pkg/logger-server": "workspace:^" "@chicmoz-pkg/message-bus": "workspace:^"