diff --git a/config/config.ts b/config/config.ts index 04f9fb7..e3a745f 100644 --- a/config/config.ts +++ b/config/config.ts @@ -17,6 +17,11 @@ export interface TimeoutConfig { waitTimeout: number; } +export interface AztecScanConfig { + apiUrl: string; + apiKey: string; +} + export interface EnvironmentConfig { name: string; environment: 'local' | 'testnet' | 'devnet' | 'mainnet'; @@ -26,6 +31,7 @@ export interface EnvironmentConfig { version: string; }; timeouts?: TimeoutConfig; + aztecscan?: AztecScanConfig; } export class ConfigManager { @@ -138,5 +144,12 @@ export function getTimeouts(): TimeoutConfig { return configManager.getTimeouts(); } +/** + * Gets AztecScan configuration, or undefined if not configured for this environment. + */ +export function getAztecScanConfig(): AztecScanConfig | undefined { + return configManager.getConfig().aztecscan; +} + // Export the singleton instance for direct access if needed export default configManager; \ No newline at end of file diff --git a/config/devnet.json b/config/devnet.json index 0ca16e7..c2af2e7 100644 --- a/config/devnet.json +++ b/config/devnet.json @@ -14,5 +14,9 @@ "deployTimeout": 1200000, "txTimeout": 180000, "waitTimeout": 60000 + }, + "aztecscan": { + "apiUrl": "https://api.devnet.aztecscan.xyz", + "apiKey": "temporary-api-key" } } diff --git a/scripts/deploy_contract.ts b/scripts/deploy_contract.ts index 8d59281..e47c2d7 100644 --- a/scripts/deploy_contract.ts +++ b/scripts/deploy_contract.ts @@ -1,3 +1,6 @@ +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; import { PodRacingContract } from "../src/artifacts/PodRacing.js" import { type Logger, createLogger } from "@aztec/foundation/log"; import { SponsoredFeePaymentMethod } from "@aztec/aztec.js/fee"; @@ -5,7 +8,10 @@ import { setupWallet } from "../src/utils/setup_wallet.js"; import { getSponsoredFPCInstance } from "../src/utils/sponsored_fpc.js"; import { SponsoredFPCContractArtifact } from "@aztec/noir-contracts.js/SponsoredFPC"; import { deploySchnorrAccount } from "../src/utils/deploy_account.js"; -import { getTimeouts } from "../config/config.js"; +import { getTimeouts, getAztecScanConfig } from "../config/config.js"; +import { verifyArtifactOnAztecScan, verifyInstanceOnAztecScan } from "../src/utils/verify_on_aztecscan.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); async function main() { let logger: Logger; @@ -51,10 +57,6 @@ async function main() { logger.info(`📍 Contract address: ${podRacingContract.address}`); logger.info(`👤 Admin address: ${address}`); - // Verify deployment - logger.info('🔍 Verifying contract deployment...'); - logger.info('✅ Contract deployed and ready for game creation'); - // Log contract instantiation data if (instance) { logger.info('đŸ“Ļ Contract instantiation data:'); @@ -68,6 +70,65 @@ async function main() { } logger.info(`Constructor args: ${JSON.stringify([address.toString()])}`); } + + // Verify on AztecScan (best-effort — deploy succeeds regardless) + const aztecscanConfig = getAztecScanConfig(); + if (aztecscanConfig && instance) { + logger.info('🔍 Verifying contract on AztecScan...'); + + const contractClassId = instance.currentContractClassId.toString(); + const publicKeysString = instance.publicKeys + ? instance.publicKeys.toString() + : "0x" + "0".repeat(512); + const constructorArgs = [address.toString()]; + + // Load the raw JSON artifact (Noir compiler output) — NOT the codegen'd + // ContractArtifact. The server expects the original JSON with base64 bytecode + // strings, snake_case keys, etc. The codegen'd artifact has Buffer objects, + // camelCase keys, and transformed function structure that the server can't parse. + const rawArtifactPath = join(__dirname, "../target/pod_racing_contract-PodRacing.json"); + const rawArtifact = JSON.parse(readFileSync(rawArtifactPath, "utf8")); + + try { + // Wait for the AztecScan indexer to pick up the on-chain data + logger.info('âŗ Waiting 15s for AztecScan indexer...'); + await new Promise(resolve => setTimeout(resolve, 15_000)); + + // 1. Verify artifact (contract class) + const artifactResult = await verifyArtifactOnAztecScan( + aztecscanConfig, + contractClassId, + 1, + rawArtifact, + ); + logger.info(`📋 Artifact verification: ${artifactResult.status} ${artifactResult.statusText}`); + + // 2. Verify instance (deployment) + const instanceResult = await verifyInstanceOnAztecScan( + aztecscanConfig, + podRacingContract.address.toString(), + { + publicKeysString, + deployer: instance.deployer.toString(), + salt: instance.salt.toString(), + constructorArgs, + }, + rawArtifact, + ); + logger.info(`📋 Instance verification: ${instanceResult.status} ${instanceResult.statusText}`); + + if (artifactResult.ok && instanceResult.ok) { + logger.info('✅ Contract verified on AztecScan'); + } else { + logger.warn('âš ī¸ AztecScan verification returned non-OK status (contract still deployed successfully)'); + } + } catch (err) { + logger.warn(`âš ī¸ AztecScan verification failed (contract still deployed successfully): ${err}`); + } + } else if (!aztecscanConfig) { + logger.info('â„šī¸ AztecScan not configured for this environment, skipping verification'); + } + logger.info('🏁 Deployment process completed successfully!'); logger.info(`📋 Summary:`); logger.info(` - Contract Address: ${podRacingContract.address}`); diff --git a/src/utils/deploy_account.ts b/src/utils/deploy_account.ts index 2601c4f..3279427 100644 --- a/src/utils/deploy_account.ts +++ b/src/utils/deploy_account.ts @@ -8,6 +8,7 @@ import { setupWallet } from "./setup_wallet.js"; import { AztecAddress } from "@aztec/aztec.js/addresses"; import { AccountManager } from "@aztec/aztec.js/wallet"; import { EmbeddedWallet } from "@aztec/wallets/embedded"; +import { getTimeouts } from "../../config/config.js"; export async function deploySchnorrAccount(wallet?: EmbeddedWallet): Promise { let logger: Logger; @@ -41,7 +42,8 @@ export async function deploySchnorrAccount(wallet?: EmbeddedWallet): Promise { const nodeUrl = getAztecNodeUrl(); const node = createAztecNodeClient(nodeUrl); - const wallet = await EmbeddedWallet.create(node, { ephemeral: true }); + // Real proofs required on devnet/testnet (fake proofs are rejected). + // Disabled on local network for faster iteration. + const proverEnabled = !configManager.isLocalNetwork(); + const wallet = await EmbeddedWallet.create(node, { + ephemeral: true, + pxeConfig: { proverEnabled }, + }); return wallet; } diff --git a/src/utils/verify_on_aztecscan.ts b/src/utils/verify_on_aztecscan.ts new file mode 100644 index 0000000..00bc940 --- /dev/null +++ b/src/utils/verify_on_aztecscan.ts @@ -0,0 +1,134 @@ +/** + * Verify contracts on AztecScan after deployment. + * + * Two-step verification: + * 1. Artifact verification — uploads the contract artifact JSON so AztecScan can + * match it byte-for-byte against the on-chain bytecode. + * 2. Instance verification — sends the deployment parameters (salt, deployer, + * publicKeysString, constructorArgs) so AztecScan can recompute and confirm + * the contract address. + * + * Uses raw fetch — no SDK dependency required. + */ + +import type { AztecScanConfig } from "../../config/config.js"; + +// ── Types ─────────────────────────────────────────────────────────── + +export interface VerificationResult { + ok: boolean; + status: number; + statusText: string; +} + +export interface VerifyInstanceArgs { + publicKeysString: string; + deployer: string; + salt: string; + constructorArgs: string[]; +} + +// ── Artifact verification ─────────────────────────────────────────── + +/** + * Verify a contract artifact (contract class) on AztecScan. + * + * POST /v1/{apiKey}/l2/contract-classes/{classId}/versions/{version} + * Body: { stringifiedArtifactJson: string } + * + * Returns 200 if already verified, 201 if newly verified. + */ +export async function verifyArtifactOnAztecScan( + config: AztecScanConfig, + contractClassId: string, + version: number, + artifact: Record, +): Promise { + const url = `${config.apiUrl}/v1/${config.apiKey}/l2/contract-classes/${contractClassId}/versions/${version}`; + + // Handle { default: artifact } module-style exports + const raw = "default" in artifact && typeof artifact.default === "object" + ? artifact.default + : artifact; + + const body = JSON.stringify({ stringifiedArtifactJson: JSON.stringify(raw) }); + const sizeMB = (new TextEncoder().encode(body).length / 1_000_000).toFixed(2); + + console.log(`[aztecscan] Verifying artifact -> POST ${url} (${sizeMB} MB)`); + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + + console.log(`[aztecscan] Artifact verification: ${response.status} ${response.statusText}`); + if (!response.ok) { + const text = await response.text().catch(() => "(no body)"); + console.log(`[aztecscan] Artifact verification response: ${text.slice(0, 500)}`); + } + return { ok: response.ok, status: response.status, statusText: response.statusText }; +} + +// ── Instance verification ─────────────────────────────────────────── + +/** + * Verify a contract instance deployment on AztecScan. + * + * POST /v1/{apiKey}/l2/contract-instances/{address} + * Body: { verifiedDeploymentArguments: { salt, deployer, publicKeysString, constructorArgs, stringifiedArtifactJson? } } + * + * The server recomputes the contract address from the provided parameters. + */ +export async function verifyInstanceOnAztecScan( + config: AztecScanConfig, + contractAddress: string, + args: VerifyInstanceArgs, + artifact?: Record, +): Promise { + // Validate field lengths (matching server-side Zod schema) + if (args.publicKeysString.length !== 514) { + throw new Error(`Invalid publicKeysString length: expected 514, got ${args.publicKeysString.length}`); + } + if (args.deployer.length !== 66) { + throw new Error(`Invalid deployer length: expected 66, got ${args.deployer.length}`); + } + if (args.salt.length !== 66) { + throw new Error(`Invalid salt length: expected 66, got ${args.salt.length}`); + } + + const url = `${config.apiUrl}/v1/${config.apiKey}/l2/contract-instances/${contractAddress}`; + + const verifiedDeploymentArguments: Record = { + salt: args.salt, + deployer: args.deployer, + publicKeysString: args.publicKeysString, + constructorArgs: args.constructorArgs, + }; + + // Optionally include the artifact for combined verification + if (artifact) { + const raw = "default" in artifact && typeof artifact.default === "object" + ? artifact.default + : artifact; + verifiedDeploymentArguments.stringifiedArtifactJson = JSON.stringify(raw); + } + + const body = JSON.stringify({ verifiedDeploymentArguments }); + const sizeMB = (new TextEncoder().encode(body).length / 1_000_000).toFixed(2); + + console.log(`[aztecscan] Verifying instance -> POST ${url} (${sizeMB} MB)`); + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + + console.log(`[aztecscan] Instance verification: ${response.status} ${response.statusText}`); + if (!response.ok) { + const text = await response.text().catch(() => "(no body)"); + console.log(`[aztecscan] Instance verification response: ${text.slice(0, 500)}`); + } + return { ok: response.ok, status: response.status, statusText: response.statusText }; +}