From c5bcbd5d627286ca5e82d0aefe440485db1d1a9e Mon Sep 17 00:00:00 2001 From: fretchen Date: Sat, 31 Jan 2026 23:29:07 +0100 Subject: [PATCH 01/16] Work through the deployment scripts. --- eth/scripts/collector-nft-v1.config.json | 10 +- eth/scripts/deploy-collector-nft-v1.ts | 229 +++++++++----- eth/scripts/deploy-genimg-v4.ts | 92 +++++- .../genimg-v4-upgrade-hardhat-2026-01-31.json | 36 +++ eth/scripts/upgrade-genimg-v4.config.json | 20 +- eth/test/CollectorNFTv1_Deployment.ts | 46 ++- website/MULTICHAIN_EXPANSION_PROPOSAL.md | 291 ++++++++++++++++-- 7 files changed, 573 insertions(+), 151 deletions(-) create mode 100644 eth/scripts/deployments/genimg-v4-upgrade-hardhat-2026-01-31.json diff --git a/eth/scripts/collector-nft-v1.config.json b/eth/scripts/collector-nft-v1.config.json index 4b6f32362..578b0302c 100644 --- a/eth/scripts/collector-nft-v1.config.json +++ b/eth/scripts/collector-nft-v1.config.json @@ -1,11 +1,11 @@ { - "genImNFTAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", - "baseMintPrice": "0.00005", + "parameters": { + "genImNFTAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", + "baseMintPrice": "0.00005" + }, "options": { "validateOnly": false, - "dryRun": false, - "verify": true, - "waitConfirmations": 1 + "dryRun": false }, "metadata": { "description": "CollectorNFT v1 deployment configuration", diff --git a/eth/scripts/deploy-collector-nft-v1.ts b/eth/scripts/deploy-collector-nft-v1.ts index 562022d75..2bc575811 100644 --- a/eth/scripts/deploy-collector-nft-v1.ts +++ b/eth/scripts/deploy-collector-nft-v1.ts @@ -1,26 +1,40 @@ #!/usr/bin/env npx hardhat run import { ethers, upgrades, network } from "hardhat"; -import { getAddress } from "viem"; +import { getAddress, formatEther, parseEther } from "viem"; import { validateCollectorNFT, validateImplementation } from "./validate-contract"; import * as fs from "fs"; import * as path from "path"; - -interface CollectorNFTv1Config { - genImNFTAddress: string; - baseMintPrice: string; - options?: { - validateOnly?: boolean; - dryRun?: boolean; - // TODO: Remove verify flag - use scripts/verify-contract.ts instead (DRY principle) - verify?: boolean; - waitConfirmations?: number; - }; - metadata?: { - description?: string; - version?: string; - environment?: string; - }; -} +import { z } from "zod"; + +// Minimum ETH balance required for deployment (0.03 ETH) +const MIN_DEPLOYMENT_BALANCE = parseEther("0.03"); + +// Zod Schema für Validierung +const CollectorNFTv1ConfigSchema = z.object({ + parameters: z.object({ + genImNFTAddress: z.string().refine((addr) => { + try { + getAddress(addr); + return true; + } catch { + return false; + } + }, "Invalid genImNFTAddress format"), + baseMintPrice: z.string(), // in ETH, e.g., "0.00005" + }), + options: z.object({ + validateOnly: z.boolean(), + dryRun: z.boolean(), + }), + metadata: z.object({ + description: z.string(), + version: z.string(), + environment: z.string(), + }), +}); + +// TypeScript-Typ automatisch aus Zod-Schema generieren +type CollectorNFTv1Config = z.infer; /** * Load CollectorNFTv1 deployment configuration @@ -35,35 +49,30 @@ function loadConfig(): CollectorNFTv1Config { } const configContent = fs.readFileSync(configPath, "utf8"); - let config: CollectorNFTv1Config; + let configRaw: unknown; try { - config = JSON.parse(configContent); + configRaw = JSON.parse(configContent); } catch (error: unknown) { throw new Error(`Invalid JSON in configuration file: ${error instanceof Error ? error.message : String(error)}`); } - // Basic validation - if (!config.genImNFTAddress) { - throw new Error("genImNFTAddress is required in config"); - } - - if (!config.baseMintPrice) { - throw new Error("baseMintPrice is required in config"); - } - - // Validate address format + // Zod-Validierung + let config: CollectorNFTv1Config; try { - getAddress(config.genImNFTAddress); - } catch { - throw new Error(`Invalid genImNFTAddress format: ${config.genImNFTAddress}`); + config = CollectorNFTv1ConfigSchema.parse(configRaw); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Config validation failed: ${error.message}`); + } + throw error; } // Validate price format try { - ethers.parseEther(config.baseMintPrice); + ethers.parseEther(config.parameters.baseMintPrice); } catch { - throw new Error(`Invalid baseMintPrice format: ${config.baseMintPrice}`); + throw new Error(`Invalid baseMintPrice format: ${config.parameters.baseMintPrice}`); } console.log("✅ Configuration loaded and validated"); @@ -72,6 +81,37 @@ function loadConfig(): CollectorNFTv1Config { return config; } +/** + * Check if deployer has sufficient ETH balance for deployment + */ +async function checkDeployerBalance(deployer: { + address: string; + provider: { getBalance: (addr: string) => Promise }; +}): Promise { + const balance = await deployer.provider.getBalance(deployer.address); + const balanceFormatted = formatEther(balance); + const minFormatted = formatEther(MIN_DEPLOYMENT_BALANCE); + + console.log(`💰 Deployer Balance: ${balanceFormatted} ETH`); + console.log(`📊 Minimum Required: ${minFormatted} ETH`); + + if (balance < MIN_DEPLOYMENT_BALANCE) { + const deficit = MIN_DEPLOYMENT_BALANCE - balance; + throw new Error( + `Insufficient funds for deployment!\n` + + ` Balance: ${balanceFormatted} ETH\n` + + ` Required: ${minFormatted} ETH\n` + + ` Deficit: ${formatEther(deficit)} ETH\n\n` + + ` Please fund ${deployer.address} with at least ${formatEther(deficit)} ETH.\n` + + ` Faucets:\n` + + ` - Optimism Sepolia: https://www.alchemy.com/faucets/optimism-sepolia\n` + + ` - Base Sepolia: https://www.alchemy.com/faucets/base-sepolia`, + ); + } + + console.log("✅ Sufficient balance for deployment"); +} + /** * Deploy CollectorNFTv1 using OpenZeppelin Upgrades Plugin * @@ -83,20 +123,18 @@ function loadConfig(): CollectorNFTv1Config { */ async function deployCollectorNFT() { console.log("🚀 CollectorNFTv1 Deployment Script"); - console.log("=".repeat(50)); + console.log("=".repeat(60)); console.log(`Network: ${network.name}`); console.log(`Block: ${await ethers.provider.getBlockNumber()}`); console.log(""); // Load configuration const config = loadConfig(); + const options = config.options; + const parameters = config.parameters; - const genImNFTAddress = config.genImNFTAddress; - const baseMintPrice = ethers.parseEther(config.baseMintPrice); - const options = config.options || {}; - - console.log(`📍 GenImNFT Address: ${genImNFTAddress}`); - console.log(`💰 Base Mint Price: ${config.baseMintPrice} ETH (${baseMintPrice.toString()} wei)`); + const genImNFTAddress = parameters.genImNFTAddress; + const baseMintPrice = ethers.parseEther(parameters.baseMintPrice); // Check if validation only if (options.validateOnly) { @@ -110,6 +148,19 @@ async function deployCollectorNFT() { return await simulateDeployment(genImNFTAddress, baseMintPrice); } + // Get deployer and check balance + const [deployer] = await ethers.getSigners(); + console.log(`👤 Deployer: ${deployer.address}`); + console.log(`📍 GenImNFT Address: ${genImNFTAddress}`); + console.log(`💰 Base Mint Price: ${parameters.baseMintPrice} ETH`); + console.log(""); + + // Check deployer balance before anything else + console.log("💰 Checking Deployer Balance"); + console.log("-".repeat(40)); + await checkDeployerBalance(deployer); + console.log(""); + // Get contract factory console.log("📦 Getting CollectorNFTv1 contract factory..."); const CollectorNFTv1Factory = await ethers.getContractFactory("CollectorNFTv1"); @@ -195,14 +246,10 @@ async function deployCollectorNFT() { implementationAddress: await upgrades.erc1967.getImplementationAddress(proxyAddress), adminAddress: await upgrades.erc1967.getAdminAddress(proxyAddress), genImNFTAddress: genImNFTAddress, - baseMintPrice: config.baseMintPrice, + baseMintPrice: parameters.baseMintPrice, contractName: contractName, contractSymbol: contractSymbol, - deploymentOptions: { - verify: options.verify || false, - waitConfirmations: options.waitConfirmations || 1, - configUsed: "collector-nft-v1.config.json", - }, + version: config.metadata.version, config: config, }; @@ -210,13 +257,12 @@ async function deployCollectorNFT() { console.log(JSON.stringify(deploymentInfo, null, 2)); // Save deployment information to file - const deploymentsDir = path.join(__dirname, "deployments"); + const deploymentsDir = path.join(__dirname, "../deployments"); if (!fs.existsSync(deploymentsDir)) { fs.mkdirSync(deploymentsDir, { recursive: true }); } - const timestamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format - const deploymentFileName = `collector-nft-v1-${network.name}-${timestamp}.json`; + const deploymentFileName = `collector-nft-v1-${network.name}.json`; const deploymentFilePath = path.join(deploymentsDir, deploymentFileName); fs.writeFileSync(deploymentFilePath, JSON.stringify(deploymentInfo, null, 2)); @@ -235,20 +281,15 @@ async function deployCollectorNFT() { ); } - // Contract verification if enabled - if (options.verify) { - console.log("\n🔍 Verifying contract on block explorer..."); - try { - console.log("📋 Contract verification would be performed here"); - console.log("✅ Contract verification completed successfully!"); - } catch (error: unknown) { - console.log("⚠️ Warning: Contract verification failed:", error instanceof Error ? error.message : String(error)); - } - } + console.log("\n✅ Deployment completed successfully!"); + console.log("=".repeat(60)); + console.log("\n📝 Next Steps:"); + console.log("1. Save the proxy address for your frontend configuration"); + console.log("2. Verify contracts on Etherscan if needed"); return { - contract: deployedContract, - address: proxyAddress, + proxyAddress, + implementationAddress: deploymentInfo.implementationAddress, deploymentInfo, }; } @@ -256,6 +297,10 @@ async function deployCollectorNFT() { async function validateDeployment(genImNFTAddress: string, baseMintPrice: bigint) { console.log("🔍 Validating deployment configuration..."); + // Check deployer balance + const [deployer] = await ethers.getSigners(); + await checkDeployerBalance(deployer); + // Verify GenImNFT contract const genImNFTCode = await ethers.provider.getCode(genImNFTAddress); if (genImNFTCode === "0x") { @@ -263,10 +308,16 @@ async function validateDeployment(genImNFTAddress: string, baseMintPrice: bigint } // Get contract factory for validation - await ethers.getContractFactory("CollectorNFTv1"); + const CollectorNFTv1Factory = await ethers.getContractFactory("CollectorNFTv1"); + + // Validate OpenZeppelin upgradeable patterns + await upgrades.validateImplementation(CollectorNFTv1Factory, { + kind: "uups", + }); // Validate contract compilation console.log("✅ CollectorNFTv1 contract compiles successfully"); + console.log("✅ OpenZeppelin upgrade validation passed"); console.log("✅ GenImNFT contract exists at specified address"); console.log(`✅ Base mint price valid: ${ethers.formatEther(baseMintPrice)} ETH`); @@ -277,33 +328,41 @@ async function validateDeployment(genImNFTAddress: string, baseMintPrice: bigint async function simulateDeployment(genImNFTAddress: string, baseMintPrice: bigint) { console.log("🧪 Simulating deployment..."); - await validateDeployment(genImNFTAddress, baseMintPrice); + const [deployer] = await ethers.getSigners(); - // Get contract factory for simulation - await ethers.getContractFactory("CollectorNFTv1"); + // Check deployer balance + await checkDeployerBalance(deployer); - console.log("⛽ Estimating deployment costs..."); - console.log("📦 Contract factory created successfully"); - console.log("💡 Ready for deployment with specified parameters"); + // Verify GenImNFT contract + const genImNFTCode = await ethers.provider.getCode(genImNFTAddress); + if (genImNFTCode === "0x") { + throw new Error(`No contract found at GenImNFT address: ${genImNFTAddress}`); + } - console.log("🎉 Simulation completed successfully!"); + console.log(""); + console.log("📋 Deployment parameters:"); + console.log(` - Network: ${network.name}`); + console.log(` - GenImNFT Address: ${genImNFTAddress}`); + console.log(` - Base Mint Price: ${ethers.formatEther(baseMintPrice)} ETH`); + console.log(""); + console.log("📦 What would happen:"); + console.log(" 1. Deploy CollectorNFTv1 implementation"); + console.log(" 2. Deploy ERC1967 proxy"); + console.log(" 3. Initialize with GenImNFT address and base mint price"); + console.log(""); + console.log("✅ Simulation complete (no actual deployment)"); return true; } -// Main execution -async function main() { - try { - await deployCollectorNFT(); - } catch (error) { - console.error("❌ Deployment failed:"); - console.error(error); - process.exitCode = 1; - } -} +// Export for testing +export { deployCollectorNFT, MIN_DEPLOYMENT_BALANCE, CollectorNFTv1ConfigSchema }; -// Execute if run directly +// Execute only when run directly (not imported) if (require.main === module) { - main(); + deployCollectorNFT() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); } - -export { deployCollectorNFT, loadConfig, CollectorNFTv1Config }; diff --git a/eth/scripts/deploy-genimg-v4.ts b/eth/scripts/deploy-genimg-v4.ts index ab6b673dd..6e4ac762c 100644 --- a/eth/scripts/deploy-genimg-v4.ts +++ b/eth/scripts/deploy-genimg-v4.ts @@ -4,7 +4,10 @@ import { validateImplementation } from "./validate-contract"; import * as fs from "fs"; import * as path from "path"; import { z } from "zod"; -import { getAddress } from "viem"; +import { getAddress, formatEther, parseEther } from "viem"; + +// Minimum ETH balance required for deployment (0.03 ETH) +const MIN_DEPLOYMENT_BALANCE = parseEther("0.03"); // Zod Schema für Validierung const GenImV4DeployConfigSchema = z.object({ @@ -81,12 +84,47 @@ function loadConfig(): GenImV4DeployConfig { return config; } +/** + * Check if deployer has sufficient ETH balance for deployment + */ +async function checkDeployerBalance(deployer: { + address: string; + provider: { getBalance: (addr: string) => Promise }; +}): Promise { + const balance = await deployer.provider.getBalance(deployer.address); + const balanceFormatted = formatEther(balance); + const minFormatted = formatEther(MIN_DEPLOYMENT_BALANCE); + + console.log(`💰 Deployer Balance: ${balanceFormatted} ETH`); + console.log(`📊 Minimum Required: ${minFormatted} ETH`); + + if (balance < MIN_DEPLOYMENT_BALANCE) { + const deficit = MIN_DEPLOYMENT_BALANCE - balance; + throw new Error( + `Insufficient funds for deployment!\n` + + ` Balance: ${balanceFormatted} ETH\n` + + ` Required: ${minFormatted} ETH\n` + + ` Deficit: ${formatEther(deficit)} ETH\n\n` + + ` Please fund ${deployer.address} with at least ${formatEther(deficit)} ETH.\n` + + ` Faucets:\n` + + ` - Optimism Sepolia: https://www.alchemy.com/faucets/optimism-sepolia\n` + + ` - Base Sepolia: https://www.alchemy.com/faucets/base-sepolia`, + ); + } + + console.log("✅ Sufficient balance for deployment"); +} + /** * Validate deployment without deploying */ async function validateDeployment(): Promise { console.log("🔍 Validating GenImNFTv4 contract..."); + // Check deployer balance + const [deployer] = await ethers.getSigners(); + await checkDeployerBalance(deployer); + try { const GenImNFTv4Factory = await ethers.getContractFactory("GenImNFTv4"); @@ -116,11 +154,26 @@ async function validateDeployment(): Promise { async function simulateDeployment(config: GenImV4DeployConfig): Promise { console.log("🧪 Simulating GenImNFTv4 deployment..."); + const [deployer] = await ethers.getSigners(); + + // Check deployer balance + await checkDeployerBalance(deployer); + + console.log(""); console.log("📋 Deployment parameters:"); + console.log(` - Network: ${network.name}`); console.log(` - Mint Price: ${config.parameters.mintPrice} ETH`); console.log(` - Agent Wallet: ${config.parameters.agentWallet || "Not specified (can be set after deployment)"}`); console.log(""); - + console.log("📦 What would happen:"); + console.log(" 1. Deploy GenImNFTv4 implementation"); + console.log(" 2. Deploy ERC1967 proxy"); + console.log(" 3. Initialize contract"); + console.log(" 4. Set mint price"); + if (config.parameters.agentWallet) { + console.log(" 5. Authorize agent wallet"); + } + console.log(""); console.log("✅ Simulation complete (no actual deployment)"); } @@ -160,6 +213,17 @@ async function deployGenImV4() { return await simulateDeployment(config); } + // Get deployer + const [deployer] = await ethers.getSigners(); + console.log(`👤 Deployer: ${deployer.address}`); + console.log(""); + + // Check deployer balance before anything else + console.log("💰 Checking Deployer Balance"); + console.log("-".repeat(40)); + await checkDeployerBalance(deployer); + console.log(""); + // Get contract factory console.log("📦 Getting GenImNFTv4 contract factory..."); const GenImNFTv4Factory = await ethers.getContractFactory("GenImNFTv4"); @@ -302,13 +366,12 @@ async function deployGenImV4() { console.log(JSON.stringify(deploymentInfo, null, 2)); // Save deployment information to file - const deploymentsDir = path.join(__dirname, "deployments"); + const deploymentsDir = path.join(__dirname, "../deployments"); if (!fs.existsSync(deploymentsDir)) { fs.mkdirSync(deploymentsDir, { recursive: true }); } - const timestamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format - const deploymentFileName = `genimg-v4-${network.name}-${timestamp}.json`; + const deploymentFileName = `genimg-v4-${network.name}.json`; const deploymentFilePath = path.join(deploymentsDir, deploymentFileName); fs.writeFileSync(deploymentFilePath, JSON.stringify(deploymentInfo, null, 2)); @@ -343,10 +406,19 @@ async function deployGenImV4() { } console.log("3. Test minting: safeMint(address, uri)"); console.log("4. Verify contracts on Etherscan if needed"); + + return { proxyAddress, implementationAddress: deploymentInfo.implementationAddress }; } -// Execute deployment -deployGenImV4().catch((error) => { - console.error(error); - process.exitCode = 1; -}); +// Export for testing +export { deployGenImV4, MIN_DEPLOYMENT_BALANCE, GenImV4DeployConfigSchema }; + +// Execute only when run directly (not imported) +if (require.main === module) { + deployGenImV4() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/eth/scripts/deployments/genimg-v4-upgrade-hardhat-2026-01-31.json b/eth/scripts/deployments/genimg-v4-upgrade-hardhat-2026-01-31.json new file mode 100644 index 000000000..f2831c35e --- /dev/null +++ b/eth/scripts/deployments/genimg-v4-upgrade-hardhat-2026-01-31.json @@ -0,0 +1,36 @@ +{ + "network": "hardhat", + "timestamp": "2026-01-31T22:27:40.481Z", + "blockNumber": 153, + "upgradeType": "GenImNFTv3 → GenImNFTv4", + "proxyAddress": "0x07882Ae1ecB7429a84f1D53048d35c4bB2056877", + "implementationAddress": "0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d", + "adminAddress": "0x0000000000000000000000000000000000000000", + "securityFix": "CVE-2025-11-26", + "preUpgradeState": { + "totalSupply": "7", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "mintPrice": "0.01" + }, + "postUpgradeState": { + "totalSupply": "7", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "mintPrice": "0.01", + "authorizedAgent": null + }, + "config": { + "proxyAddress": "0x07882Ae1ecB7429a84f1D53048d35c4bB2056877", + "options": { + "validateOnly": false, + "dryRun": false, + "verify": false, + "waitConfirmations": 1 + }, + "metadata": { + "description": "Test upgrade configuration for GenImNFTv4", + "version": "4.0.0-test", + "environment": "development", + "securityFix": "CVE-2025-11-26" + } + } +} \ No newline at end of file diff --git a/eth/scripts/upgrade-genimg-v4.config.json b/eth/scripts/upgrade-genimg-v4.config.json index db9862e4f..f3c2cbb32 100644 --- a/eth/scripts/upgrade-genimg-v4.config.json +++ b/eth/scripts/upgrade-genimg-v4.config.json @@ -1,17 +1,15 @@ { - "proxyAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", "options": { - "validateOnly": true, + "validateOnly": false, "dryRun": false, - "verify": true, - "authorizeAgentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C", - "waitConfirmations": 2 + "verify": false, + "waitConfirmations": 1 }, "metadata": { - "description": "Upgrade GenImNFTv3 to v4 - CVE-2025-11-26 Security Fix", - "version": "4.0.0", - "environment": "production", - "securityFix": "CVE-2025-11-26", - "attackerAddress": "0x8B6B008A0073D34D04ff00210E7200Ab00003300" + "description": "Test upgrade configuration for GenImNFTv4", + "version": "4.0.0-test", + "environment": "development", + "securityFix": "CVE-2025-11-26" } -} +} \ No newline at end of file diff --git a/eth/test/CollectorNFTv1_Deployment.ts b/eth/test/CollectorNFTv1_Deployment.ts index 1da68abad..a311e4651 100644 --- a/eth/test/CollectorNFTv1_Deployment.ts +++ b/eth/test/CollectorNFTv1_Deployment.ts @@ -56,16 +56,16 @@ describe("CollectorNFTv1 - Deployment Tests", function () { } // Helper function to create a temporary config file for testing - async function createTempConfig(genImAddress: string, options: any = {}) { + async function createTempConfig(genImAddress: string, options: { validateOnly?: boolean; dryRun?: boolean } = {}) { const tempConfigPath = path.join(__dirname, "../scripts/collector-nft-v1.config-test.json"); const config = { - genImNFTAddress: genImAddress, - baseMintPrice: "0.001", + parameters: { + genImNFTAddress: genImAddress, + baseMintPrice: "0.001", + }, options: { validateOnly: false, dryRun: false, - verify: false, - waitConfirmations: 1, ...options, }, metadata: { @@ -80,7 +80,11 @@ describe("CollectorNFTv1 - Deployment Tests", function () { } // Helper function to backup and restore config - async function withTempConfig(genImAddress: string, options: any, testFn: () => Promise) { + async function withTempConfig( + genImAddress: string, + options: { validateOnly?: boolean; dryRun?: boolean }, + testFn: () => Promise, + ) { const originalConfigPath = path.join(__dirname, "../scripts/collector-nft-v1.config.json"); const backupConfigPath = path.join(__dirname, "../scripts/collector-nft-v1.config.json.backup"); const tempConfigPath = await createTempConfig(genImAddress, options); @@ -178,12 +182,12 @@ describe("CollectorNFTv1 - Deployment Tests", function () { expect(result).to.not.be.a("boolean"); if (typeof result === "object" && result !== null) { - expect(result).to.have.property("contract"); - expect(result).to.have.property("address"); + expect(result).to.have.property("proxyAddress"); + expect(result).to.have.property("implementationAddress"); expect(result).to.have.property("deploymentInfo"); // Verify the deployed contract using ethers - const collectorNFTv1 = await hre.ethers.getContractAt("CollectorNFTv1", result.address); + const collectorNFTv1 = await hre.ethers.getContractAt("CollectorNFTv1", result.proxyAddress); expect(await collectorNFTv1.name()).to.equal("CollectorNFTv1"); expect(await collectorNFTv1.symbol()).to.equal("COLLECTORv1"); expect(await collectorNFTv1.genImNFTContract()).to.equal(genImAddress); @@ -230,15 +234,24 @@ describe("CollectorNFTv1 - Deployment Tests", function () { }); it("Should validate config file schema", async function () { - const { genImAddress } = await deployGenImNFTv3Fixture(); + // genImAddress is intentionally unused - we're testing invalid config parsing + await deployGenImNFTv3Fixture(); // Create a config with invalid data const invalidConfigPath = path.join(__dirname, "../scripts/collector-nft-v1.config-invalid.json"); const invalidConfig = { - genImNFTAddress: "invalid-address", // Invalid address format - baseMintPrice: "invalid-price", // Invalid price format + parameters: { + genImNFTAddress: "invalid-address", // Invalid address format + baseMintPrice: "0.001", + }, options: { - validateOnly: "not-boolean", // Invalid type + validateOnly: false, + dryRun: false, + }, + metadata: { + description: "Invalid test config", + version: "1.0.0", + environment: "test", }, }; @@ -285,11 +298,10 @@ describe("CollectorNFTv1 - Deployment Tests", function () { if (typeof result === "object" && result !== null) { // Check that deployment file was created - const deploymentsDir = path.join(__dirname, "../scripts/deployments"); + const deploymentsDir = path.join(__dirname, "../deployments"); expect(fs.existsSync(deploymentsDir)).to.be.true; - const timestamp = new Date().toISOString().split("T")[0]; - const deploymentFileName = `collector-nft-v1-hardhat-${timestamp}.json`; + const deploymentFileName = `collector-nft-v1-hardhat.json`; const deploymentFilePath = path.join(deploymentsDir, deploymentFileName); expect(fs.existsSync(deploymentFilePath)).to.be.true; @@ -297,7 +309,7 @@ describe("CollectorNFTv1 - Deployment Tests", function () { // Verify deployment file content const deploymentData = JSON.parse(fs.readFileSync(deploymentFilePath, "utf8")); expect(deploymentData.network).to.equal("hardhat"); - expect(deploymentData.proxyAddress).to.equal(result.address); + expect(deploymentData.proxyAddress).to.equal(result.proxyAddress); expect(deploymentData.genImNFTAddress).to.equal(genImAddress); expect(deploymentData.baseMintPrice).to.equal("0.001"); diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md index 996ed5c7c..0c5854e73 100644 --- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md +++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md @@ -4,13 +4,13 @@ ## Aktueller Zustand -| Contract | Optimism | Base | Multi-Chain Ready | -|----------|:--------:|:----:|:-----------------:| -| **SupportV2** | ✅ | ✅ | ✅ Ja | -| **GenImNFTv4** | ✅ | ❌ | ✅ Ja (Backend ready) | -| **CollectorNFTv1** | ✅ | ❌ | ✅ Ja (Frontend ready) | -| **LLMv1** | ✅ | ❌ | ❌ (out of scope) | -| **EIP3009SplitterV1** | ✅ | ❌ | ✅ Ja | +| Contract | Optimism | Base | Multi-Chain Ready | Script Status | +|----------|:--------:|:----:|:-----------------:|:-------------:| +| **SupportV2** | ✅ | ✅ | ✅ Ja | ✅ Modern | +| **GenImNFTv4** | ✅ | ⬜ | ✅ Ja (Backend ready) | ⚠️ Veraltet | +| **CollectorNFTv1** | ✅ | ⬜ | ✅ Ja (Frontend ready) | ⚠️ Veraltet | +| **LLMv1** | ✅ | ❌ | ❌ (out of scope) | - | +| **EIP3009SplitterV1** | ✅ | ❌ | ✅ Ja | ✅ Modern | --- @@ -23,8 +23,9 @@ | **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ✅ Fertig | | **2** | GenImNFT-Komponenten migrieren | website/ | ✅ Fertig | | **3** | CollectorNFT-Komponenten migrieren | website/ | ✅ Fertig | -| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später | -| **5** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Später | +| **4.1** | deploy-genimg-v4.ts modernisieren | eth/ | ⬜ Nächster Schritt | +| **4.2** | deploy-collector-nft-v1.ts modernisieren | eth/ | ⬜ | +| **4.3** | GenImNFTv4 + CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ | --- @@ -669,30 +670,274 @@ const handleCollect = async () => { --- -## Phase 4: GenImNFTv4 auf Base deployen +## Phase 4 & 5: GenImNFTv4 + CollectorNFTv1 auf Base deployen ⬜ NEXT -1. Deploy-Script für Base erweitern -2. Deploy + Verify auf Base Mainnet -3. Agent-Wallet autorisieren -4. Adresse in `@fretchen/chain-utils/src/addresses.ts` hinzufügen: +> **Status:** Deployment-Skripte müssen auf SupportV2-Standard aktualisiert werden + +### Übersicht + +| Contract | Optimism | Base | Deployment Script Status | +|----------|:--------:|:----:|:------------------------:| +| **GenImNFTv4** | ✅ `0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb` | ⬜ | ⚠️ Veraltet | +| **CollectorNFTv1** | ✅ `0x1234...` | ⬜ | ⚠️ Veraltet | + +### Gap-Analyse: Deployment-Skripte vs. SupportV2 (Gold Standard) + +| Feature | SupportV2 ✅ | GenImNFTv4 ⚠️ | CollectorNFTv1 ⚠️ | +|---------|:------------:|:-------------:|:-----------------:| +| **Zod Schema** | ✅ | ✅ | ❌ Manuelles Interface | +| **Balance-Check** | ✅ `MIN_DEPLOYMENT_BALANCE` | ❌ | ❌ | +| **checkDeployerBalance()** | ✅ Mit Faucet-Links | ❌ | ❌ | +| **Deployment Path** | ✅ `deployments/` | ⚠️ `scripts/deployments/` | ⚠️ `scripts/deployments/` | +| **Export für Tests** | ✅ `deploySupportV2, Schema` | ❌ Kein Export | ⚠️ Ohne Schema | +| **require.main Guard** | ✅ | ❌ | ✅ | +| **Dry Run Balance Check** | ✅ | ❌ | ❌ | + +### Modernisierungsplan (3 PRs) + +#### PR 4.1: deploy-genimg-v4.ts modernisieren + +**Ziel:** GenImNFTv4 Deployment-Skript auf SupportV2-Standard bringen + +**Änderungen:** +1. `MIN_DEPLOYMENT_BALANCE` Konstante hinzufügen (0.03 ETH) +2. `checkDeployerBalance()` Funktion hinzufügen (mit Faucet-Links) +3. Balance-Check in `validateDeployment()` und `simulateDeployment()` hinzufügen +4. Deployment-Pfad von `scripts/deployments/` auf `deployments/` ändern +5. Export Pattern für Tests hinzufügen: + ```typescript + export { deployGenImV4, MIN_DEPLOYMENT_BALANCE, GenImV4DeployConfigSchema }; + + if (require.main === module) { + deployGenImV4() + .then(() => process.exit(0)) + .catch((error) => { console.error(error); process.exit(1); }); + } + ``` + +**Datei:** `eth/scripts/deploy-genimg-v4.ts` + +```typescript +// NEU: Importieren +import { formatEther, parseEther } from "viem"; + +// NEU: Balance-Konstante +const MIN_DEPLOYMENT_BALANCE = parseEther("0.03"); + +// NEU: Balance-Check Funktion (kopiert von deploy-support-v2.ts) +async function checkDeployerBalance(deployer: { + address: string; + provider: { getBalance: (addr: string) => Promise }; +}): Promise { + const balance = await deployer.provider.getBalance(deployer.address); + const balanceFormatted = formatEther(balance); + const minFormatted = formatEther(MIN_DEPLOYMENT_BALANCE); + + console.log(`💰 Deployer Balance: ${balanceFormatted} ETH`); + console.log(`📊 Minimum Required: ${minFormatted} ETH`); + + if (balance < MIN_DEPLOYMENT_BALANCE) { + const deficit = MIN_DEPLOYMENT_BALANCE - balance; + throw new Error( + `Insufficient funds for deployment!\n` + + ` Balance: ${balanceFormatted} ETH\n` + + ` Required: ${minFormatted} ETH\n` + + ` Deficit: ${formatEther(deficit)} ETH\n\n` + + ` Please fund ${deployer.address} with at least ${formatEther(deficit)} ETH.\n` + + ` Faucets:\n` + + ` - Optimism Sepolia: https://www.alchemy.com/faucets/optimism-sepolia\n` + + ` - Base Sepolia: https://www.alchemy.com/faucets/base-sepolia`, + ); + } + + console.log("✅ Sufficient balance for deployment"); +} +``` + +**Acceptance Criteria:** +- [ ] `npx hardhat run scripts/deploy-genimg-v4.ts --network hardhat` funktioniert +- [ ] Deployment-Tests bestehen (wenn erstellt) +- [ ] Deployment-File wird nach `deployments/` geschrieben + +--- + +#### PR 4.2: deploy-collector-nft-v1.ts modernisieren + +**Ziel:** CollectorNFTv1 Deployment-Skript auf SupportV2-Standard bringen + +**Änderungen:** +1. Interface durch Zod Schema ersetzen +2. `MIN_DEPLOYMENT_BALANCE` + `checkDeployerBalance()` hinzufügen +3. Deployment-Pfad auf `deployments/` ändern +4. Export Pattern für Tests anpassen + +**Datei:** `eth/scripts/deploy-collector-nft-v1.ts` + +```typescript +// NEU: Zod Schema statt Interface +import { z } from "zod"; +import { formatEther, parseEther, getAddress } from "viem"; + +const MIN_DEPLOYMENT_BALANCE = parseEther("0.03"); + +const CollectorNFTv1ConfigSchema = z.object({ + parameters: z.object({ + genImNFTAddress: z.string().refine((addr) => { + try { getAddress(addr); return true; } catch { return false; } + }, "Invalid genImNFTAddress format"), + baseMintPrice: z.string(), // in ETH, e.g., "0.00005" + }), + options: z.object({ + validateOnly: z.boolean(), + dryRun: z.boolean(), + }), + metadata: z.object({ + description: z.string(), + version: z.string(), + environment: z.string(), + }), +}); + +type CollectorNFTv1Config = z.infer; + +// NEU: Export für Tests +export { deployCollectorNFT, MIN_DEPLOYMENT_BALANCE, CollectorNFTv1ConfigSchema }; + +if (require.main === module) { + deployCollectorNFT() + .then(() => process.exit(0)) + .catch((error) => { console.error(error); process.exit(1); }); +} +``` + +**Config-Format-Änderung:** `collector-nft-v1.config.json` +```json +{ + "parameters": { + "genImNFTAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", + "baseMintPrice": "0.00005" + }, + "options": { + "validateOnly": false, + "dryRun": false + }, + "metadata": { + "description": "CollectorNFT v1 deployment configuration", + "version": "1.0.0", + "environment": "production" + } +} +``` + +--- + +#### PR 4.3: Deploy auf Base Mainnet + +**Voraussetzung:** PR 4.1 und PR 4.2 gemergt + +**Schritte:** + +1. **GenImNFTv4 auf Base Sepolia testen** + ```bash + cd eth/ + # Config für Base Sepolia anpassen + npx hardhat run scripts/deploy-genimg-v4.ts --network baseSepolia + ``` + +2. **GenImNFTv4 auf Base Mainnet deployen** + ```bash + npx hardhat run scripts/deploy-genimg-v4.ts --network base + npx hardhat run scripts/verify-genimg-v4.ts --network base + ``` + +3. **Agent-Wallet autorisieren** + ```bash + # Via Hardhat Console oder separates Script + npx hardhat console --network base + > const contract = await ethers.getContractAt("GenImNFTv4", "0x...") + > await contract.authorizeAgentWallet("0xAAEBC1441323B8ad6Bdf6793A8428166b510239C") + ``` + +4. **CollectorNFTv1 auf Base deployen** + - Config mit neuer GenImNFT Adresse anpassen + ```bash + npx hardhat run scripts/deploy-collector-nft-v1.ts --network base + ``` + +5. **Adressen in chain-utils hinzufügen** + + **Datei:** `shared/chain-utils/src/addresses.ts` ```typescript export const MAINNET_GENAI_NFT_ADDRESSES: Record = { - "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", - "eip155:8453": "0x...", // Base + "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", // Optimism + "eip155:8453": "0x...", // Base - NEU }; + + export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = { + "eip155:10": "0x...", // Optimism (bestehend) + "eip155:8453": "0x...", // Base - NEU + }; + ``` + +6. **chain-utils rebuilden und Consumer updaten** + ```bash + cd shared/chain-utils && npm run build + cd ../../website && npm install + cd ../scw_js && npm install + cd ../x402_facilitator && npm install ``` -5. `npm run build` in chain-utils -6. `npm install` in allen Projekten --- -## Phase 5: CollectorNFTv1 auf Base deployen +### Deployment Checkliste Phase 4 & 5 + +**PR 4.1: GenImNFTv4 Script modernisieren** +- [ ] `MIN_DEPLOYMENT_BALANCE` hinzufügen +- [ ] `checkDeployerBalance()` hinzufügen +- [ ] Balance-Check in validate/simulate einfügen +- [ ] Deployment-Pfad auf `deployments/` ändern +- [ ] Export Pattern für Tests hinzufügen +- [ ] Tests ausführen + +**PR 4.2: CollectorNFTv1 Script modernisieren** +- [ ] Zod Schema erstellen +- [ ] Config-Format aktualisieren +- [ ] `MIN_DEPLOYMENT_BALANCE` + `checkDeployerBalance()` hinzufügen +- [ ] Deployment-Pfad auf `deployments/` ändern +- [ ] Export Pattern für Tests hinzufügen + +**PR 4.3: Base Deployment** +- [ ] GenImNFTv4 auf Base Sepolia testen +- [ ] GenImNFTv4 auf Base Mainnet deployen +- [ ] GenImNFTv4 auf Etherscan verifizieren +- [ ] Agent-Wallet autorisieren +- [ ] CollectorNFTv1 auf Base deployen +- [ ] CollectorNFTv1 auf Etherscan verifizieren +- [ ] Adressen in chain-utils hinzufügen +- [ ] chain-utils rebuilden +- [ ] Consumer-Projekte updaten +- [ ] E2E Test auf Base -Voraussetzung: GenImNFT muss auf Base existieren. +--- -1. Deploy mit Base GenImNFT Adresse -2. Verify -3. Adresse in chain-utils hinzufügen +### Risikobewertung Phase 4 & 5 + +| Risiko | Schwere | Mitigation | +|--------|---------|------------| +| **Deployment fehlschlägt** | 🟡 Mittel | Erst auf Base Sepolia testen | +| **Agent-Wallet falsch** | 🔴 Hoch | Gleiche Wallet wie Optimism nutzen | +| **Config-Format Bruch** | 🟡 Mittel | Alte Config-Files backuppen | +| **Balance zu niedrig** | 🟢 Niedrig | Balance-Check schützt | +| **Verify fehlschlägt** | 🟢 Niedrig | Kann später nachgeholt werden | + +### Geschätzter Aufwand + +| PR | Aufwand | Komplexität | +|----|---------|-------------| +| **4.1:** GenImNFTv4 Script | 1-2 Stunden | 🟡 Mittel | +| **4.2:** CollectorNFTv1 Script | 1-2 Stunden | 🟡 Mittel | +| **4.3:** Base Deployment | 2-3 Stunden | 🔴 Hoch (echtes Geld) | +| **Gesamt** | 4-7 Stunden | | --- From 6e97a4f73793cb848d22a123d1425589bfe1deb9 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 1 Feb 2026 20:23:34 +0100 Subject: [PATCH 02/16] Base deployment --- eth/.openzeppelin/base.json | 370 +++++++++++++++++++++++ eth/deployments/genimg-v4-base.json | 34 +++ eth/scripts/deploy-genimg-v4.config.json | 4 +- eth/scripts/deploy-genimg-v4.ts | 1 - 4 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 eth/deployments/genimg-v4-base.json diff --git a/eth/.openzeppelin/base.json b/eth/.openzeppelin/base.json index a891f715d..0cdded304 100644 --- a/eth/.openzeppelin/base.json +++ b/eth/.openzeppelin/base.json @@ -5,6 +5,11 @@ "address": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694", "txHash": "0xe1ccd70219ac5083ace610abb3497b461fb38228e7f36c5672db7345da2649aa", "kind": "uups" + }, + { + "address": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", + "txHash": "0x4ef507147c3aab04871bb9375b1f6e4197530dc0f56b22031b4a0768948dec28", + "kind": "uups" } ], "impls": { @@ -132,6 +137,371 @@ ] } } + }, + "b40583a89bd8326959050c978b045f47fe830d764c5cd416c24b34e6330c5e79": { + "address": "0x1CDb5F39C51F4E3b269F557Df6734b83e19aed8C", + "txHash": "0x6ee16d89cf9085c52c237c70c0e3ff137ae9521f2e198500326ff4b3288836fb", + "layout": { + "solcVersion": "0.8.33", + "storage": [ + { + "label": "_nextTokenId", + "offset": 0, + "slot": "0", + "type": "t_uint256", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:20" + }, + { + "label": "mintPrice", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:23" + }, + { + "label": "_authorizedImageUpdaters", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_address)", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:27" + }, + { + "label": "_imageUpdated", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:30" + }, + { + "label": "_isListed", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:33" + }, + { + "label": "_whitelistedAgentWallets", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:37" + }, + { + "label": "__gap", + "offset": 0, + "slot": "6", + "type": "t_array(t_uint256)48_storage", + "contract": "GenImNFTv4", + "src": "contracts/GenImNFTv4.sol:375" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)dyn_storage": { + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "label": "mapping(uint256 => string)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(ERC721EnumerableStorage)652_storage": { + "label": "struct ERC721EnumerableUpgradeable.ERC721EnumerableStorage", + "members": [ + { + "label": "_ownedTokens", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "offset": 0, + "slot": "0" + }, + { + "label": "_ownedTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "offset": 0, + "slot": "1" + }, + { + "label": "_allTokens", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "_allTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(ERC721Storage)279_storage": { + "label": "struct ERC721Upgradeable.ERC721Storage", + "members": [ + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "2" + }, + { + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "offset": 0, + "slot": "3" + }, + { + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "4" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(ERC721URIStorageStorage)801_storage": { + "label": "struct ERC721URIStorageUpgradeable.ERC721URIStorageStorage", + "members": [ + { + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)86_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)14_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721Enumerable": [ + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_ownedTokens", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:21", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_ownedTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:22", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_allTokens", + "type": "t_array(t_uint256)dyn_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:24", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_allTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:25", + "offset": 0, + "slot": "3" + } + ], + "erc7201:openzeppelin.storage.ERC721URIStorage": [ + { + "contract": "ERC721URIStorageUpgradeable", + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol:26", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721": [ + { + "contract": "ERC721Upgradeable", + "label": "_name", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721Upgradeable", + "label": "_symbol", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:30", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721Upgradeable", + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:32", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721Upgradeable", + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34", + "offset": 0, + "slot": "3" + }, + { + "contract": "ERC721Upgradeable", + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:36", + "offset": 0, + "slot": "4" + }, + { + "contract": "ERC721Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:38", + "offset": 0, + "slot": "5" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/eth/deployments/genimg-v4-base.json b/eth/deployments/genimg-v4-base.json new file mode 100644 index 000000000..456a94da6 --- /dev/null +++ b/eth/deployments/genimg-v4-base.json @@ -0,0 +1,34 @@ +{ + "network": "base", + "timestamp": "2026-02-01T19:22:18.042Z", + "blockNumber": 41592195, + "proxyAddress": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", + "implementationAddress": "0x1CDb5F39C51F4E3b269F557Df6734b83e19aed8C", + "adminAddress": "0x0000000000000000000000000000000000000000", + "contractType": "GenImNFTv4", + "owner": "0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20", + "mintPrice": "0.00003", + "agentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C", + "deploymentOptions": { + "verify": true, + "waitConfirmations": 2, + "configUsed": "deploy-genimg-v4.config.json" + }, + "config": { + "parameters": { + "mintPrice": "0.00003", + "agentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C" + }, + "options": { + "validateOnly": false, + "dryRun": false, + "verify": true, + "waitConfirmations": 2 + }, + "metadata": { + "description": "GenImNFTv4 deployment to Base", + "version": "4.0.0", + "environment": "mainnet" + } + } +} \ No newline at end of file diff --git a/eth/scripts/deploy-genimg-v4.config.json b/eth/scripts/deploy-genimg-v4.config.json index bd056a2f4..db07b58d2 100644 --- a/eth/scripts/deploy-genimg-v4.config.json +++ b/eth/scripts/deploy-genimg-v4.config.json @@ -10,8 +10,8 @@ "waitConfirmations": 2 }, "metadata": { - "description": "GenImNFTv4 deployment to Optimism Sepolia for testing", + "description": "GenImNFTv4 deployment to Base", "version": "4.0.0", - "environment": "testnet" + "environment": "mainnet" } } diff --git a/eth/scripts/deploy-genimg-v4.ts b/eth/scripts/deploy-genimg-v4.ts index 6e4ac762c..be0e46735 100644 --- a/eth/scripts/deploy-genimg-v4.ts +++ b/eth/scripts/deploy-genimg-v4.ts @@ -323,7 +323,6 @@ async function deployGenImV4() { // Verify proxy state console.log("🔍 Verifying proxy state..."); const owner = await deployedContract.owner(); - const [deployer] = await ethers.getSigners(); console.log(`✅ Owner: ${owner}`); console.log(`✅ Deployer: ${deployer.address}`); From 215f22ab15ea394962989d76925cb609d09d0ae4 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 1 Feb 2026 20:32:41 +0100 Subject: [PATCH 03/16] Deploy genimg on Base --- shared/chain-utils/src/addresses.ts | 2 +- shared/chain-utils/test/index.test.ts | 10 +++++++--- website/MULTICHAIN_EXPANSION_PROPOSAL.md | 15 ++++++++++----- x402_facilitator/package-lock.json | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts index fee1dace5..1f423db13 100644 --- a/shared/chain-utils/src/addresses.ts +++ b/shared/chain-utils/src/addresses.ts @@ -11,7 +11,7 @@ export const MAINNET_GENAI_NFT_ADDRESSES: Record = { "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", // Optimism - // "eip155:8453": "0x...", // Base - add after Deployment + "eip155:8453": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", // Base }; export const TESTNET_GENAI_NFT_ADDRESSES: Record = { diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts index f4cdd2446..0fe9ca55c 100644 --- a/shared/chain-utils/test/index.test.ts +++ b/shared/chain-utils/test/index.test.ts @@ -131,10 +131,14 @@ describe("@fretchen/chain-utils", () => { describe("Contract Addresses", () => { describe("getGenAiNFTAddress()", () => { - test("should return Mainnet contract address", () => { + test("should return Optimism Mainnet contract address", () => { expect(getGenAiNFTAddress("eip155:10")).toBe("0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"); }); + test("should return Base Mainnet contract address", () => { + expect(getGenAiNFTAddress("eip155:8453")).toBe("0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68"); + }); + test("should return Sepolia contract address", () => { expect(getGenAiNFTAddress("eip155:11155420")).toBe( "0x10827cC42a09D0BAD2d43134C69F0e776D853D85" @@ -142,8 +146,8 @@ describe("@fretchen/chain-utils", () => { }); test("should throw for network without GenAI NFT deployment", () => { - expect(() => getGenAiNFTAddress("eip155:8453")).toThrow( - "GenAI NFT not deployed on eip155:8453" + expect(() => getGenAiNFTAddress("eip155:1")).toThrow( + "GenAI NFT not deployed on eip155:1" ); }); }); diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md index 0c5854e73..a3cd5f656 100644 --- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md +++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md @@ -7,11 +7,15 @@ | Contract | Optimism | Base | Multi-Chain Ready | Script Status | |----------|:--------:|:----:|:-----------------:|:-------------:| | **SupportV2** | ✅ | ✅ | ✅ Ja | ✅ Modern | -| **GenImNFTv4** | ✅ | ⬜ | ✅ Ja (Backend ready) | ⚠️ Veraltet | -| **CollectorNFTv1** | ✅ | ⬜ | ✅ Ja (Frontend ready) | ⚠️ Veraltet | +| **GenImNFTv4** | ✅ | ✅ | ✅ Ja | ✅ Modern | +| **CollectorNFTv1** | ✅ | ⬜ | ✅ Ja | ✅ Modern | | **LLMv1** | ✅ | ❌ | ❌ (out of scope) | - | | **EIP3009SplitterV1** | ✅ | ❌ | ✅ Ja | ✅ Modern | +**Base Addresses:** +- GenImNFTv4: `0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68` +- CollectorNFTv1: TBD (depends on GenImNFTv4) + --- ## Implementierungsplan @@ -23,9 +27,10 @@ | **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ✅ Fertig | | **2** | GenImNFT-Komponenten migrieren | website/ | ✅ Fertig | | **3** | CollectorNFT-Komponenten migrieren | website/ | ✅ Fertig | -| **4.1** | deploy-genimg-v4.ts modernisieren | eth/ | ⬜ Nächster Schritt | -| **4.2** | deploy-collector-nft-v1.ts modernisieren | eth/ | ⬜ | -| **4.3** | GenImNFTv4 + CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ | +| **4.1** | deploy-genimg-v4.ts modernisieren | eth/ | ✅ Fertig | +| **4.2** | deploy-collector-nft-v1.ts modernisieren | eth/ | ✅ Fertig | +| **4.3** | GenImNFTv4 auf Base deployen | eth/, shared/ | ✅ Fertig | +| **4.4** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Nächster Schritt | --- diff --git a/x402_facilitator/package-lock.json b/x402_facilitator/package-lock.json index 258e70952..89a5d2cb2 100644 --- a/x402_facilitator/package-lock.json +++ b/x402_facilitator/package-lock.json @@ -40,13 +40,13 @@ "@eslint/js": "^9.39.2", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "@vitest/coverage-v8": "^2.0.0", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.39.2", "prettier": "^3.8.1", "typescript": "^5.0.0", "typescript-eslint": "^8.53.1", "viem": "^2.0.0", - "vitest": "^2.0.0" + "vitest": "^4.0.18" }, "peerDependencies": { "viem": "^2.0.0" From 5c03f7eea6353136718a6521c7a96e2bcb6bc441 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 1 Feb 2026 20:36:57 +0100 Subject: [PATCH 04/16] Deployment guides --- eth/DEPLOY_COLLECTOR_V1_GUIDE.md | 245 +++++++++++++++++++ eth/GENIMG_DEPLOY_V4_GUIDE.md | 406 +++++++------------------------ 2 files changed, 327 insertions(+), 324 deletions(-) create mode 100644 eth/DEPLOY_COLLECTOR_V1_GUIDE.md diff --git a/eth/DEPLOY_COLLECTOR_V1_GUIDE.md b/eth/DEPLOY_COLLECTOR_V1_GUIDE.md new file mode 100644 index 000000000..0be3f3222 --- /dev/null +++ b/eth/DEPLOY_COLLECTOR_V1_GUIDE.md @@ -0,0 +1,245 @@ +# CollectorNFTv1 Deployment Guide + +## Overview + +This guide explains how to deploy CollectorNFTv1 to any network using `scripts/deploy-collector-nft-v1.ts`. CollectorNFTv1 is an upgradeable companion NFT that: + +- **Depends on GenImNFTv4**: Requires deployed GenImNFT contract +- **Dynamic Pricing**: Mint price increases with GenImNFT token count +- **UUPS Upgradeability**: Secure proxy pattern + +## Prerequisites + +```bash +# Set Hardhat variables +npx hardhat vars set ALCHEMY_API_KEY +npx hardhat vars set SEPOLIA_PRIVATE_KEY +``` + +**Important**: Deploy GenImNFTv4 first (see [GENIMG_DEPLOY_V4_GUIDE.md](./GENIMG_DEPLOY_V4_GUIDE.md)). + +## Current Deployments + +| Network | Proxy Address | GenImNFT Address | Status | +| ---------------- | -------------------------------------------- | -------------------------------------------- | ------ | +| Optimism Mainnet | `0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea` | `0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb` | ✅ | +| Base Mainnet | TBD | `0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68` | ⬜ | + +## Configuration + +Edit `scripts/collector-nft-v1.config.json`: + +### For Testnet + +```json +{ + "parameters": { + "genImNFTAddress": "0xYourGenImNFTProxyAddress", + "baseMintPrice": "0.00005" + }, + "options": { + "validateOnly": false, + "dryRun": false + }, + "metadata": { + "description": "CollectorNFTv1 deployment to test network", + "version": "1.0.0", + "environment": "testnet" + } +} +``` + +### For Base Mainnet + +```json +{ + "parameters": { + "genImNFTAddress": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", + "baseMintPrice": "0.00005" + }, + "options": { + "validateOnly": false, + "dryRun": false + }, + "metadata": { + "description": "CollectorNFTv1 production deployment to Base", + "version": "1.0.0", + "environment": "production" + } +} +``` + +## Deployment Process + +### Step 1: Fund Deployer + +Ensure deployer wallet has ≥0.03 ETH: + +| Network | Minimum | Faucet | +| -------- | ------- | ---------------------------------------------------- | +| Sepolia | 0.03 | https://www.alchemy.com/faucets/optimism-sepolia | +| Mainnet | 0.1 | - | + +### Step 2: Validate (Recommended) + +```bash +# Set validateOnly: true in config +npx hardhat run scripts/deploy-collector-nft-v1.ts --network optsepolia +``` + +### Step 3: Dry Run (Recommended) + +```bash +# Set dryRun: true in config +npx hardhat run scripts/deploy-collector-nft-v1.ts --network optsepolia +``` + +### Step 4: Deploy + +```bash +# Set both validateOnly: false and dryRun: false +npx hardhat run scripts/deploy-collector-nft-v1.ts --network optimisticEthereum +``` + +**For Base:** + +```bash +npx hardhat run scripts/deploy-collector-nft-v1.ts --network base +``` + +### Expected Output + +``` +🚀 CollectorNFTv1 Deployment Script +============================================================ +Network: optimisticEthereum + +💰 Checking Deployer Balance +✅ Sufficient balance for deployment + +🔍 Verifying GenImNFT contract... +✅ GenImNFT contract verified + +🚀 Deploying CollectorNFTv1... +✅ CollectorNFTv1 deployed successfully! +============================================================ +📍 Proxy Address: 0x... +📍 Implementation Address: 0x... +📍 Admin Address: 0x... + +🔍 Verifying deployment... +📄 Contract Name: CollectorNFTv1 +🏷️ Contract Symbol: COLLECTORv1 +🔗 GenImNFT Address: 0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb +💰 Base Mint Price: 0.00005 ETH +✅ All verifications passed! + +💾 Deployment info saved to: deployments/collector-nft-v1-optimisticEthereum.json +``` + +## Post-Deployment + +### 1. Verify on Block Explorer + +```bash +DEPLOYMENT_FILE=deployments/collector-nft-v1-optimisticEthereum.json \ +CONTRACT_PATH=contracts/CollectorNFTv1.sol:CollectorNFTv1 \ +npx hardhat run scripts/verify-contract.ts --network optimisticEthereum +``` + +### 2. Update Frontend + +Update chain-utils with the proxy address: + +```typescript +// shared/chain-utils/src/addresses.ts +export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = { + "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea", // Optimism + "eip155:8453": "0xYourNewBaseAddress", // Base - add after deployment +}; +``` + +Then rebuild and republish: +```bash +cd shared/chain-utils +npm run build +npm test +``` + +### 3. Test Minting + +```bash +npx hardhat console --network optimisticEthereum +> const CollectorNFT = await ethers.getContractFactory("CollectorNFTv1") +> const contract = CollectorNFT.attach("0xProxyAddress") +> const price = await contract.calculateMintPrice() +> await contract.mintWithGenImToken(tokenId, { value: price }) +``` + +## How Pricing Works + +CollectorNFTv1 pricing is dynamic: + +```solidity +mintPrice = baseMintPrice × (1 + totalSupply) +``` + +Example with `baseMintPrice = 0.00005 ETH`: +- 1st mint: 0.00005 ETH +- 10th mint: 0.00055 ETH +- 100th mint: 0.00505 ETH + +## Security Notes + +### GenImNFT Dependency + +CollectorNFTv1 validates that minters own GenImNFT tokens. The GenImNFT address is **immutable** after deployment. + +⚠️ **Verify GenImNFT address carefully** — it cannot be changed. + +### Owner Key Management + +⚠️ **For mainnet: Use hardware wallet or multisig** — owner can upgrade contracts. + +## Troubleshooting + +| Issue | Solution | +| -------------------------------- | ------------------------------------------------------- | +| "Config file not found" | Ensure `collector-nft-v1.config.json` exists | +| "No contract at GenImNFT address"| Deploy GenImNFTv4 first, use correct proxy address | +| "Insufficient funds" | Deployer needs ≥0.03 ETH | +| "Validation failed" | Check contract compiles: `npx hardhat compile` | + +## Deployment Checklist + +### Pre-Deployment + +- [ ] GenImNFTv4 deployed on target network +- [ ] GenImNFT proxy address added to config +- [ ] Deployer wallet funded with ≥0.1 ETH (mainnet) +- [ ] `ALCHEMY_API_KEY` and `SEPOLIA_PRIVATE_KEY` set +- [ ] Config file updated for correct environment +- [ ] Validation passed (`validateOnly: true`) +- [ ] Dry run completed (`dryRun: true`) + +### Deployment + +- [ ] Deploy CollectorNFTv1 +- [ ] Save deployment JSON file +- [ ] Verify contract on block explorer + +### Post-Deployment + +- [ ] Update chain-utils with proxy address +- [ ] Rebuild and test chain-utils +- [ ] Update dependent packages (scw_js, x402_facilitator, website) +- [ ] Test minting with GenImNFT token +- [ ] Verify dynamic pricing works + +## Related Documentation + +- [GENIMG_DEPLOY_V4_GUIDE.md](./GENIMG_DEPLOY_V4_GUIDE.md) — Deploy GenImNFTv4 first +- [DEPLOY_SUPPORT_V2_GUIDE.md](./DEPLOY_SUPPORT_V2_GUIDE.md) — Similar deployment pattern +- [CollectorNFTv1.sol](./contracts/CollectorNFTv1.sol) — Contract source code +- [CollectorNFTv1_Functional.ts](./test/CollectorNFTv1_Functional.ts) — Functional tests +- [CollectorNFTv1_Deployment.ts](./test/CollectorNFTv1_Deployment.ts) — Deployment script tests diff --git a/eth/GENIMG_DEPLOY_V4_GUIDE.md b/eth/GENIMG_DEPLOY_V4_GUIDE.md index 0aa916c16..a3a9fe420 100644 --- a/eth/GENIMG_DEPLOY_V4_GUIDE.md +++ b/eth/GENIMG_DEPLOY_V4_GUIDE.md @@ -2,52 +2,38 @@ ## Overview -This guide explains how to deploy a new GenImNFTv4 contract from scratch using the deployment script at `scripts/deploy-genimg-v4.ts`. Use this for **initial deployments** on new networks (e.g., Optimism Sepolia for testing). +This guide explains how to deploy GenImNFTv4 to any network using `scripts/deploy-genimg-v4.ts`. For upgrading an existing GenImNFTv3 contract, see [GENIMG_UPGRADE_TO_V4_GUIDE.md](./GENIMG_UPGRADE_TO_V4_GUIDE.md). ## What is GenImNFTv4? -GenImNFTv4 is an upgradeable NFT contract that supports: +GenImNFTv4 is an upgradeable NFT contract for AI-generated images with: -- **AI-Generated Image NFTs**: Mint NFTs that can be updated with AI-generated images -- **Agent Authorization**: EIP-8004 compatible whitelist for authorized image update services -- **Public/Private Listing**: Token owners can control visibility in public galleries -- **UUPS Upgradeability**: Secure upgradeability via UUPS proxy pattern -- **Security**: Protection against unauthorized image updates (CVE-2025-11-26 fix) +- **Agent Authorization**: EIP-8004 whitelist for authorized image update services +- **Public/Private Listing**: Token owners control gallery visibility +- **UUPS Upgradeability**: Secure proxy pattern +- **CVE-2025-11-26 Fix**: Protection against unauthorized updates ## Prerequisites -- Node.js and npm installed -- Hardhat configured with your network settings -- Access to deployer wallet with sufficient funds -- OpenZeppelin Hardhat Upgrades Plugin (`@openzeppelin/hardhat-upgrades`) -- Zod for config validation (`zod`) - -## Network Configuration - -Ensure your target network is configured in `hardhat.config.ts`: - -```typescript -networks: { - optsepolia: { - url: `https://opt-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`, - accounts: [SEPOLIA_PRIVATE_KEY], - }, - optimisticEthereum: { - url: `https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`, - accounts: [SEPOLIA_PRIVATE_KEY], - }, -} +```bash +# Set Hardhat variables +npx hardhat vars set ALCHEMY_API_KEY +npx hardhat vars set SEPOLIA_PRIVATE_KEY ``` -## Deployment Architecture +## Current Deployments -The deployment uses a **configuration-driven approach** with validation and dry-run modes for safe deployments. +| Network | Proxy Address | Status | +| ---------------- | -------------------------------------------- | ------ | +| Optimism Mainnet | `0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb` | ✅ | +| Base Mainnet | `0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68` | ✅ | +| Optimism Sepolia | `0x10827cC42a09D0BAD2d43134C69F0e776D853D85` | ✅ | -### Configuration File +## Configuration -Create or edit `scripts/deploy-genimg-v4.config.json`: +Edit `scripts/deploy-genimg-v4.config.json`: ```json { @@ -67,385 +53,157 @@ Create or edit `scripts/deploy-genimg-v4.config.json`: "environment": "testnet" } } -``` - -### Configuration Options - -| Field | Type | Required | Description | -| --------------------------- | ------- | -------- | -------------------------------------------------- | -| `parameters.mintPrice` | string | Yes | Price in ETH for minting (e.g., "0.01") | -| `parameters.agentWallet` | string | No | Agent wallet to authorize immediately after deploy | -| `options.validateOnly` | boolean | No | Only validate contract, don't deploy | -| `options.dryRun` | boolean | No | Simulate deployment without executing | -| `options.verify` | boolean | No | Verify contract on block explorer after deployment | -| `options.waitConfirmations` | number | No | Number of confirmations to wait (default: 1) | -| `metadata.description` | string | No | Deployment description | -| `metadata.version` | string | No | Version identifier | -| `metadata.environment` | string | No | Environment (testnet/mainnet) | - -## Deployment Process - -### Step 1: Configuration - -Edit `scripts/deploy-genimg-v4.config.json` with your parameters: - -**For Testnet (Optimism Sepolia):** +### For Testnet ```json { "parameters": { "mintPrice": "0.00003", - "agentWallet": "0xYourAgentAddress" + "agentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C" }, "options": { + "validateOnly": false, + "dryRun": false, + "verify": true, "waitConfirmations": 2 }, "metadata": { + "description": "GenImNFTv4 deployment to test network", + "version": "4.0.0", "environment": "testnet" } } ``` -**For Mainnet (Optimism):** +### For Mainnet ```json { "parameters": { "mintPrice": "0.01", - "agentWallet": "0xYourAgentAddress" + "agentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C" }, "options": { - "waitConfirmations": 5, - "verify": true + "validateOnly": false, + "dryRun": false, + "verify": true, + "waitConfirmations": 5 }, "metadata": { + "description": "GenImNFTv4 production deployment", + "version": "4.0.0", "environment": "mainnet" } } ``` -### Step 2: Validation (Recommended) +## Deployment Process + +### Step 1: Fund Deployer + +Ensure deployer wallet has ≥0.03 ETH: -Validate the contract before deploying: +| Network | Minimum | Faucet | +| -------- | ------- | ---------------------------------------------------- | +| Sepolia | 0.03 | https://www.alchemy.com/faucets/optimism-sepolia | +| Mainnet | 0.1 | - | + +### Step 2: Validate (Recommended) ```bash # Set validateOnly: true in config npx hardhat run scripts/deploy-genimg-v4.ts --network optsepolia ``` -**What happens:** - -- ✅ Validates contract compiles successfully -- ✅ Checks OpenZeppelin upgrade patterns -- ✅ Verifies UUPS proxy compatibility -- ⚠️ **Does NOT deploy** - ### Step 3: Dry Run (Recommended) -Preview deployment without executing: - ```bash # Set dryRun: true in config npx hardhat run scripts/deploy-genimg-v4.ts --network optsepolia ``` -**What happens:** - -- ✅ Shows deployment parameters -- ✅ Validates configuration -- ✅ Simulates deployment flow -- ⚠️ **Does NOT deploy** - ### Step 4: Deploy -Execute the actual deployment: - ```bash # Set both validateOnly: false and dryRun: false -npx hardhat run scripts/deploy-genimg-v4.ts --network optsepolia +npx hardhat run scripts/deploy-genimg-v4.ts --network optimisticEthereum ``` -**What happens:** +**For Base:** -1. ✅ Pre-deployment validation -2. ✅ Deploys UUPS proxy and implementation -3. ✅ Calls `initialize()` function -4. ✅ Sets mint price to configured value -5. ✅ Authorizes agent wallet (if provided) -6. ✅ Verifies all state is correct -7. ✅ Saves deployment info to `deployments/genimg-v4-{network}-{date}.json` -8. ✅ Runs comprehensive validation checks +```bash +npx hardhat run scripts/deploy-genimg-v4.ts --network base +``` ### Expected Output ``` 🚀 GenImNFTv4 Deployment Script ============================================================ -Network: optsepolia -Block: 18642837 - -📄 Loading configuration from: /path/to/deploy-genimg-v4.config.json -✅ Configuration loaded and validated - -📦 Getting GenImNFTv4 contract factory... +Network: optimisticEthereum -🔍 Pre-Deployment Validation ----------------------------------------- -✅ OpenZeppelin upgrade validation passed +💰 Checking Deployer Balance +✅ Sufficient balance for deployment 🚀 Deploying GenImNFTv4... -📋 Mint Price: 0.00003 ETH - ✅ GenImNFTv4 deployed successfully! ============================================================ -📍 Proxy Address: 0x1234...5678 -📍 Implementation Address: 0xabcd...ef01 -📍 Admin Address: 0x9876...5432 +📍 Proxy Address: 0x... +📍 Implementation Address: 0x... ⚙️ Post-Deployment Configuration ----------------------------------------- -✅ Mint price set to 0.00003 ETH +✅ Mint price set to 0.01 ETH ✅ Agent wallet authorized: 0xAAEB...239C -🔍 Verifying deployment... -✅ Implementation contract verified (12345 bytes) -✅ Implementation contract ABI compatible -✅ Owner: 0xYourAddress -✅ Deployer: 0xYourAddress -✅ Mint Price: 0.00003 ETH -✅ Name: GenImNFTv4 -✅ Symbol: GENIMGv4 -✅ All verifications passed! - -💾 Deployment info saved to: deployments/genimg-v4-optsepolia-2025-12-24.json -✅ Comprehensive validation completed successfully! - -✅ Deployment completed successfully! -============================================================ - -📝 Next Steps: -1. Save the proxy address for your frontend/backend configuration -2. Test minting: safeMint(address, uri) -3. Verify contracts on Etherscan if needed -``` - -## Post-Deployment Steps - -### 1. Save Contract Addresses - -The deployment info is saved to `scripts/deployments/genimg-v4-{network}-{date}.json`: - -```json -{ - "network": "optsepolia", - "timestamp": "2025-12-24T12:00:00.000Z", - "blockNumber": 18642837, - "proxyAddress": "0x1234...5678", - "implementationAddress": "0xabcd...ef01", - "adminAddress": "0x9876...5432", - "owner": "0xYourAddress", - "mintPrice": "0.00003", - "agentWallet": "0xAAEB...239C" -} -``` - -**Save the `proxyAddress`** - this is the address users will interact with. - -### 2. Authorize Additional Agents (Optional) - -If you need to authorize more agent wallets: - -```solidity -// From contract owner account -contract.authorizeAgentWallet("0xNewAgentAddress"); -``` - -Check if an agent is authorized: - -```solidity -bool isAuthorized = contract.isAuthorizedAgent("0xAgentAddress"); -``` - -Revoke an agent: - -```solidity -contract.revokeAgentWallet("0xAgentAddress"); +💾 Deployment info saved to: deployments/genimg-v4-optimisticEthereum.json ``` -### 3. Test Basic Functions +## Post-Deployment -**Mint a test NFT:** - -```javascript -const tx = await contract.safeMint("0xRecipientAddress", "ipfs://Qm.../metadata.json"); -await tx.wait(); -``` - -**Request image update (as authorized agent):** - -```javascript -const tx = await contract.requestImageUpdate(tokenId, "https://image-service.example.com/image.png", { - value: ethers.parseEther("0.00003"), -}); -await tx.wait(); -``` - -### 4. Verify on Block Explorer - -If `verify: true` in config, the script notes that verification should be done separately: +### 1. Verify on Block Explorer ```bash -npx hardhat verify --network optsepolia PROXY_ADDRESS +DEPLOYMENT_FILE=deployments/genimg-v4-optimisticEthereum.json \ +CONTRACT_PATH=contracts/GenImNFTv4.sol:GenImNFTv4 \ +npx hardhat run scripts/verify-contract.ts --network optimisticEthereum ``` -For upgradeable contracts, you may need to verify the implementation separately. - -### 5. Update Frontend Configuration +### 2. Update Frontend -Update your frontend/backend with the new proxy address: +Update chain-utils or frontend config with the proxy address from deployment file. -```javascript -const GENIMG_V4_ADDRESS = "0x1234...5678"; // Use proxy address -const GENIMG_V4_ABI = require("./abi/GenImNFTv4.json"); +### 3. Test Minting -const contract = new ethers.Contract(GENIMG_V4_ADDRESS, GENIMG_V4_ABI, signer); +```bash +npx hardhat console --network optimisticEthereum +> const GenImNFT = await ethers.getContractFactory("GenImNFTv4") +> const contract = GenImNFT.attach("0xProxyAddress") +> await contract.safeMint("0xRecipient", "ipfs://...") ``` -## Security Considerations +## Security Notes ### Agent Authorization -**Critical**: Only authorize trusted wallets as agents. Authorized agents can: - -- Call `requestImageUpdate()` and receive `mintPrice` payments -- Update token images (but NOT steal tokens) - -**Recommendation**: Use a dedicated backend service wallet with proper key management. +Only authorize trusted wallets. Authorized agents can call `requestImageUpdate()` and receive `mintPrice` payments. -### Mint Price - -Set appropriate mint prices based on: - -- Network gas costs -- Image generation costs -- Token economics - -**Testnet**: Use low prices (e.g., 0.00003 ETH) -**Mainnet**: Consider real costs (e.g., 0.01 ETH or more) +Current agent: `0xAAEBC1441323B8ad6Bdf6793A8428166b510239C` ### Owner Key Management -The deployment account becomes the contract owner with critical privileges: - -- Upgrade the contract -- Change mint price -- Authorize/revoke agents -- Pause/unpause (if implemented) - -**Use a hardware wallet or multisig for mainnet deployments.** +⚠️ **For mainnet: Use hardware wallet or multisig** — owner can upgrade contracts and change mint price. ## Troubleshooting -### "Configuration file not found" - -Ensure `deploy-genimg-v4.config.json` exists in `scripts/` directory. - -### "Invalid Ethereum address format" - -Check that addresses in config are valid checksummed Ethereum addresses. - -### "Insufficient funds" - -Ensure deployer wallet has enough ETH for: - -- Gas costs (~$5-20 depending on network) -- Multiple transactions (deploy + configuration) - -### "OpenZeppelin upgrade validation failed" - -The contract may have upgrade safety issues. Review error message and contract code. - -### Deploy fails silently - -Check you have: - -- Correct network name in `--network` flag -- Network configured in `hardhat.config.ts` -- Valid RPC URL and private key - -## Network-Specific Notes - -### Optimism Sepolia (Testnet) - -- **Chain ID**: 11155420 -- **Explorer**: https://sepolia-optimism.etherscan.io/ -- **Faucet**: https://faucet.optimism.io/ -- **Recommended mint price**: 0.0001 - 0.001 ETH - -### Optimism Mainnet - -- **Chain ID**: 10 -- **Explorer**: https://optimistic.etherscan.io/ -- **Recommended mint price**: 0.01+ ETH -- **Always use `waitConfirmations: 5` or higher** - -## Example: Complete Testnet Deployment - -```bash -# 1. Navigate to eth directory -cd eth - -# 2. Edit configuration -cat > scripts/deploy-genimg-v4.config.json << 'EOF' -{ - "parameters": { - "mintPrice": "0.00003", - "agentWallet": "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C" - }, - "options": { - "validateOnly": false, - "dryRun": false, - "verify": true, - "waitConfirmations": 2 - }, - "metadata": { - "description": "GenImNFTv4 test deployment", - "version": "4.0.0", - "environment": "testnet" - } -} -EOF - -# 3. Validate first -# Set validateOnly: true, then run: -npx hardhat run scripts/deploy-genimg-v4.ts --network optsepolia - -# 4. Deploy (after validation passes) -# Set validateOnly: false, then run: -npx hardhat run scripts/deploy-genimg-v4.ts --network optsepolia - -# 5. Save the proxy address from output -# Look for: 📍 Proxy Address: 0x... - -# 6. Test minting via Hardhat console -npx hardhat console --network optsepolia -> const GenImNFT = await ethers.getContractFactory("GenImNFTv4") -> const contract = GenImNFT.attach("0xYourProxyAddress") -> await contract.safeMint("0xRecipient", "ipfs://Qm.../metadata.json") -``` +| Issue | Solution | +| ------------------------- | -------------------------------------------------- | +| "Config file not found" | Ensure `deploy-genimg-v4.config.json` exists | +| "Insufficient funds" | Deployer needs ≥0.03 ETH | +| "Validation failed" | Check contract compiles: `npx hardhat compile` | +| Verification fails | Use `verify-contract.ts` script | ## Related Documentation -- [GENIMG_UPGRADE_TO_V4_GUIDE.md](./GENIMG_UPGRADE_TO_V4_GUIDE.md) - Upgrade existing v3 contracts -- [GenImNFTv4.sol](./contracts/GenImNFTv4.sol) - Contract source code -- [OpenZeppelin Upgrades](https://docs.openzeppelin.com/upgrades-plugins/1.x/) - Upgrades plugin docs -- [EIP-8004](https://eips.ethereum.org/EIPS/eip-8004) - Trustless Agents standard - -## Support - -For issues or questions: - -1. Check the deployment output logs -2. Review the saved deployment JSON file -3. Verify configuration matches this guide -4. Check network connectivity and RPC status +- [GENIMG_UPGRADE_TO_V4_GUIDE.md](./GENIMG_UPGRADE_TO_V4_GUIDE.md) — Upgrade guide +- [DEPLOY_COLLECTOR_V1_GUIDE.md](./DEPLOY_COLLECTOR_V1_GUIDE.md) — CollectorNFT deployment +- [DEPLOY_SUPPORT_V2_GUIDE.md](./DEPLOY_SUPPORT_V2_GUIDE.md) — SupportV2 deployment From a01e347acb54dda0a8436c72d6bb8312f85a96f0 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 1 Feb 2026 20:42:33 +0100 Subject: [PATCH 05/16] Deploy collector contract --- eth/.openzeppelin/base.json | 396 +++++++++++++++++++++ eth/deployments/collector-nft-v1-base.json | 28 ++ eth/scripts/collector-nft-v1.config.json | 4 +- 3 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 eth/deployments/collector-nft-v1-base.json diff --git a/eth/.openzeppelin/base.json b/eth/.openzeppelin/base.json index 0cdded304..e20e89484 100644 --- a/eth/.openzeppelin/base.json +++ b/eth/.openzeppelin/base.json @@ -10,6 +10,11 @@ "address": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", "txHash": "0x4ef507147c3aab04871bb9375b1f6e4197530dc0f56b22031b4a0768948dec28", "kind": "uups" + }, + { + "address": "0x5D0103393DDcD988867437233c197c6A38b23360", + "txHash": "0x26c633cbf47d04f09401081f1d78712b62a958ba49b1dfd9c3f96751a54f4c8c", + "kind": "uups" } ], "impls": { @@ -502,6 +507,397 @@ ] } } + }, + "addb660d68872c250fb9b42e6633446699ac7013e18f13a68393ccb8484e5270": { + "address": "0xe8AB747e604873292a3f7a7Be31270720EEA9e04", + "txHash": "0x349337c5e3c2ac31d996b997e28f54b98d5065e3003257a2921809ad74aa7f17", + "layout": { + "solcVersion": "0.8.33", + "storage": [ + { + "label": "_nextTokenId", + "offset": 0, + "slot": "0", + "type": "t_uint256", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:38" + }, + { + "label": "genImNFTContract", + "offset": 0, + "slot": "1", + "type": "t_contract(IGenImNFTWithListing)12816", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:41" + }, + { + "label": "baseMintPrice", + "offset": 0, + "slot": "2", + "type": "t_uint256", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:44" + }, + { + "label": "mintCountPerGenImToken", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:47" + }, + { + "label": "collectorTokensByGenImToken", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_uint256)dyn_storage)", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:50" + }, + { + "label": "collectorToGenImToken", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:53" + }, + { + "label": "__gap", + "offset": 0, + "slot": "6", + "type": "t_array(t_uint256)50_storage", + "contract": "CollectorNFTv1", + "src": "contracts/CollectorNFTv1.sol:261" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)dyn_storage": { + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "label": "mapping(uint256 => string)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(ERC721EnumerableStorage)652_storage": { + "label": "struct ERC721EnumerableUpgradeable.ERC721EnumerableStorage", + "members": [ + { + "label": "_ownedTokens", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "offset": 0, + "slot": "0" + }, + { + "label": "_ownedTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "offset": 0, + "slot": "1" + }, + { + "label": "_allTokens", + "type": "t_array(t_uint256)dyn_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "_allTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_struct(ERC721Storage)279_storage": { + "label": "struct ERC721Upgradeable.ERC721Storage", + "members": [ + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "2" + }, + { + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "offset": 0, + "slot": "3" + }, + { + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "4" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(ERC721URIStorageStorage)801_storage": { + "label": "struct ERC721URIStorageUpgradeable.ERC721URIStorageStorage", + "members": [ + { + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)86_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)14_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)894_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_contract(IGenImNFTWithListing)12816": { + "label": "contract IGenImNFTWithListing", + "numberOfBytes": "20" + }, + "t_mapping(t_uint256,t_array(t_uint256)dyn_storage)": { + "label": "mapping(uint256 => uint256[])", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:43", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721Enumerable": [ + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_ownedTokens", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:21", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_ownedTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:22", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_allTokens", + "type": "t_array(t_uint256)dyn_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:24", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721EnumerableUpgradeable", + "label": "_allTokensIndex", + "type": "t_mapping(t_uint256,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol:25", + "offset": 0, + "slot": "3" + } + ], + "erc7201:openzeppelin.storage.ERC721URIStorage": [ + { + "contract": "ERC721URIStorageUpgradeable", + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol:26", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721": [ + { + "contract": "ERC721Upgradeable", + "label": "_name", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721Upgradeable", + "label": "_symbol", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:30", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721Upgradeable", + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:32", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721Upgradeable", + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34", + "offset": 0, + "slot": "3" + }, + { + "contract": "ERC721Upgradeable", + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:36", + "offset": 0, + "slot": "4" + }, + { + "contract": "ERC721Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:38", + "offset": 0, + "slot": "5" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/eth/deployments/collector-nft-v1-base.json b/eth/deployments/collector-nft-v1-base.json new file mode 100644 index 000000000..dade947b3 --- /dev/null +++ b/eth/deployments/collector-nft-v1-base.json @@ -0,0 +1,28 @@ +{ + "network": "base", + "timestamp": "2026-02-01T19:39:52.525Z", + "blockNumber": 41592722, + "proxyAddress": "0x5D0103393DDcD988867437233c197c6A38b23360", + "implementationAddress": "0xe8AB747e604873292a3f7a7Be31270720EEA9e04", + "adminAddress": "0x0000000000000000000000000000000000000000", + "genImNFTAddress": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", + "baseMintPrice": "0.00005", + "contractName": "CollectorNFTv1", + "contractSymbol": "COLLECTORv1", + "version": "1.0.0", + "config": { + "parameters": { + "genImNFTAddress": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", + "baseMintPrice": "0.00005" + }, + "options": { + "validateOnly": false, + "dryRun": false + }, + "metadata": { + "description": "CollectorNFTv1 deployment to Base Mainnet", + "version": "1.0.0", + "environment": "production" + } + } +} \ No newline at end of file diff --git a/eth/scripts/collector-nft-v1.config.json b/eth/scripts/collector-nft-v1.config.json index 578b0302c..e03e4af3e 100644 --- a/eth/scripts/collector-nft-v1.config.json +++ b/eth/scripts/collector-nft-v1.config.json @@ -1,6 +1,6 @@ { "parameters": { - "genImNFTAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", + "genImNFTAddress": "0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68", "baseMintPrice": "0.00005" }, "options": { @@ -8,7 +8,7 @@ "dryRun": false }, "metadata": { - "description": "CollectorNFT v1 deployment configuration", + "description": "CollectorNFTv1 deployment to Base Mainnet", "version": "1.0.0", "environment": "production" } From 87bb48d8e664b55f4552035789f01ecb1253f338 Mon Sep 17 00:00:00 2001 From: fretchen Date: Sun, 1 Feb 2026 20:45:05 +0100 Subject: [PATCH 06/16] Better doc --- shared/chain-utils/src/addresses.ts | 1 + shared/chain-utils/test/index.test.ts | 10 ++++++++-- website/MULTICHAIN_EXPANSION_PROPOSAL.md | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts index 1f423db13..b3d4de290 100644 --- a/shared/chain-utils/src/addresses.ts +++ b/shared/chain-utils/src/addresses.ts @@ -30,6 +30,7 @@ export const GENAI_NFT_NETWORKS = [ export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = { "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea", // Optimism + "eip155:8453": "0x5D0103393DDcD988867437233c197c6A38b23360", // Base }; export const TESTNET_COLLECTOR_NFT_ADDRESSES: Record = { diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts index 0fe9ca55c..633469cae 100644 --- a/shared/chain-utils/test/index.test.ts +++ b/shared/chain-utils/test/index.test.ts @@ -159,9 +159,15 @@ describe("@fretchen/chain-utils", () => { ); }); + test("should return Base Mainnet address", () => { + expect(getCollectorNFTAddress("eip155:8453")).toBe( + "0x5D0103393DDcD988867437233c197c6A38b23360" + ); + }); + test("should throw for network without CollectorNFT", () => { - expect(() => getCollectorNFTAddress("eip155:8453")).toThrow( - "CollectorNFT not deployed on eip155:8453" + expect(() => getCollectorNFTAddress("eip155:1")).toThrow( + "CollectorNFT not deployed on eip155:1" ); }); }); diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md index a3cd5f656..ced624455 100644 --- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md +++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md @@ -8,13 +8,13 @@ |----------|:--------:|:----:|:-----------------:|:-------------:| | **SupportV2** | ✅ | ✅ | ✅ Ja | ✅ Modern | | **GenImNFTv4** | ✅ | ✅ | ✅ Ja | ✅ Modern | -| **CollectorNFTv1** | ✅ | ⬜ | ✅ Ja | ✅ Modern | +| **CollectorNFTv1** | ✅ | ✅ | ✅ Ja | ✅ Modern | | **LLMv1** | ✅ | ❌ | ❌ (out of scope) | - | | **EIP3009SplitterV1** | ✅ | ❌ | ✅ Ja | ✅ Modern | **Base Addresses:** -- GenImNFTv4: `0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68` -- CollectorNFTv1: TBD (depends on GenImNFTv4) +- GenImNFTv4: `0xa5d6a3eEDADc3346E22dF9556dc5B99f2777ab68` ✅ Verified +- CollectorNFTv1: `0x5D0103393DDcD988867437233c197c6A38b23360` ✅ Verified --- @@ -29,8 +29,8 @@ | **3** | CollectorNFT-Komponenten migrieren | website/ | ✅ Fertig | | **4.1** | deploy-genimg-v4.ts modernisieren | eth/ | ✅ Fertig | | **4.2** | deploy-collector-nft-v1.ts modernisieren | eth/ | ✅ Fertig | -| **4.3** | GenImNFTv4 auf Base deployen | eth/, shared/ | ✅ Fertig | -| **4.4** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Nächster Schritt | +| **4.3** | GenImNFTv4 auf Base deployen & verifizieren | eth/, shared/ | ✅ Fertig | +| **4.4** | CollectorNFTv1 auf Base deployen & verifizieren | eth/, shared/ | ✅ Fertig | --- From 06e129aaeabdf39e8cbe6348451eb2c5621f445f Mon Sep 17 00:00:00 2001 From: fretchen Date: Mon, 2 Feb 2026 07:31:37 +0100 Subject: [PATCH 07/16] Better chain management --- scw_js/genimg_x402_token.js | 13 +++++---- scw_js/getChain.js | 31 ++++++++++++++++----- scw_js/test/genimg_x402_token.test.js | 24 ++++++++-------- scw_js/test/getChain.test.js | 40 +++++++++++++++++++++++---- shared/chain-utils/src/addresses.ts | 16 +++++++++++ shared/chain-utils/src/index.ts | 1 + 6 files changed, 96 insertions(+), 29 deletions(-) diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js index 7bfde51bc..4f307a984 100644 --- a/scw_js/genimg_x402_token.js +++ b/scw_js/genimg_x402_token.js @@ -17,7 +17,7 @@ import { extractPaymentPayload, createSettlementHeaders, } from "./x402_server.js"; -import { validatePaymentNetwork } from "./getChain.js"; +import { validatePaymentNetwork, getExpectedNetworks } from "./getChain.js"; // Re-export x402 functions for backward compatibility with tests export { handle, create402Response }; @@ -425,14 +425,15 @@ async function handle(event, context, cb) { console.log("❌ No payment provided → Returning 402"); // 🎯 Network selection based on test mode - // Test mode: Only Sepolia testnet (no costs) - // Production: Only Optimism Mainnet (real payments) - const networks = sepoliaTest ? ["eip155:11155420"] : ["eip155:10"]; + // Dynamically loaded from chain-utils deployment configuration + // Test mode: All testnet deployments (currently Sepolia) + // Production: All mainnet deployments (currently Optimism + Base) + const networks = getExpectedNetworks(sepoliaTest); if (sepoliaTest) { - console.log(" Restricting to Sepolia testnet"); + console.log(` Test mode: ${networks.join(", ")}`); } else { - console.log(" Production mode: Optimism Mainnet only"); + console.log(` Production mode: ${networks.join(", ")}`); } // Create payment requirements using x402 helper diff --git a/scw_js/getChain.js b/scw_js/getChain.js index 043221958..a9949203d 100644 --- a/scw_js/getChain.js +++ b/scw_js/getChain.js @@ -1,6 +1,10 @@ // @ts-check import { sepolia, optimism, optimismSepolia } from "viem/chains"; -import { LLMv1ABI } from "@fretchen/chain-utils"; +import { + LLMv1ABI, + getGenAiNFTMainnetNetworks, + getGenAiNFTTestnetNetworks +} from "@fretchen/chain-utils"; /** * Get environment variable in both Node.js and Vite contexts @@ -70,31 +74,44 @@ export function getLLMv1ContractConfig() { } /** - * Get the expected network for a given mode + * Get the expected networks for a given mode + * Dynamically pulls from chain-utils deployment configuration + * @param {boolean} sepoliaTest - Whether test mode is enabled + * @returns {string[]} Array of CAIP-2 network IDs + */ +export function getExpectedNetworks(sepoliaTest) { + return sepoliaTest + ? getGenAiNFTTestnetNetworks() + : getGenAiNFTMainnetNetworks(); +} + +/** + * Get the expected network for a given mode (legacy, returns first network) + * @deprecated Use getExpectedNetworks() instead * @param {boolean} sepoliaTest - Whether test mode is enabled * @returns {string} CAIP-2 network ID */ export function getExpectedNetwork(sepoliaTest) { - return sepoliaTest ? "eip155:11155420" : "eip155:10"; + return getExpectedNetworks(sepoliaTest)[0]; } /** * Validate that a client-selected network matches the expected mode * @param {string|undefined} clientNetwork - Network from payment payload * @param {boolean} sepoliaTest - Whether test mode is enabled - * @returns {{ valid: boolean, reason?: string, expected?: string, received?: string }} + * @returns {{ valid: boolean, reason?: string, expected?: string[], received?: string }} */ export function validatePaymentNetwork(clientNetwork, sepoliaTest) { if (!clientNetwork) { return { valid: false, reason: "missing_network" }; } - const expectedNetwork = getExpectedNetwork(sepoliaTest); - if (clientNetwork !== expectedNetwork) { + const expectedNetworks = getExpectedNetworks(sepoliaTest); + if (!expectedNetworks.includes(clientNetwork)) { return { valid: false, reason: sepoliaTest ? "invalid_network_for_test_mode" : "invalid_network_for_production", - expected: expectedNetwork, + expected: expectedNetworks, received: clientNetwork, }; } diff --git a/scw_js/test/genimg_x402_token.test.js b/scw_js/test/genimg_x402_token.test.js index 0324b0bfa..c6e1dbfdd 100644 --- a/scw_js/test/genimg_x402_token.test.js +++ b/scw_js/test/genimg_x402_token.test.js @@ -307,13 +307,13 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { expect(body.accepts[0].network).toBe("eip155:11155420"); }); - test("should offer Mainnet only when sepoliaTest flag is false (production mode)", async () => { + test("should offer Optimism and Base when sepoliaTest flag is false (production mode)", async () => { const event = { httpMethod: "POST", headers: {}, body: JSON.stringify({ prompt: "Production image", - sepoliaTest: false, // Production: Mainnet only + sepoliaTest: false, // Production: Optimism + Base }), path: "/genimg", }; @@ -323,19 +323,20 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { expect(response.statusCode).toBe(402); const body = JSON.parse(response.body); - // Production mode: only Mainnet + // Production mode: Optimism + Base expect(body.accepts).toBeInstanceOf(Array); - expect(body.accepts.length).toBe(1); + expect(body.accepts.length).toBe(2); expect(body.accepts[0].network).toBe("eip155:10"); // Optimism Mainnet + expect(body.accepts[1].network).toBe("eip155:8453"); // Base Mainnet }); - test("should offer Mainnet only when sepoliaTest flag is omitted (default: production)", async () => { + test("should offer Optimism and Base when sepoliaTest flag is omitted (default: production)", async () => { const event = { httpMethod: "POST", headers: {}, body: JSON.stringify({ prompt: "Default behavior", - // sepoliaTest omitted - default to production (Mainnet only) + // sepoliaTest omitted - default to production (Optimism + Base) }), path: "/genimg", }; @@ -345,10 +346,11 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { expect(response.statusCode).toBe(402); const body = JSON.parse(response.body); - // Default is production mode: only Mainnet + // Default is production mode: Optimism + Base expect(body.accepts).toBeInstanceOf(Array); - expect(body.accepts.length).toBe(1); + expect(body.accepts.length).toBe(2); expect(body.accepts[0].network).toBe("eip155:10"); // Optimism Mainnet + expect(body.accepts[1].network).toBe("eip155:8453"); // Base Mainnet }); test("should handle OPTIONS request (CORS preflight)", async () => { @@ -796,7 +798,7 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { ); }); - test("should reject unsupported network (production only accepts Mainnet)", async () => { + test("should reject unsupported network (production only accepts Optimism/Base)", async () => { const unsupportedPayment = { x402Version: 2, accepted: { @@ -832,9 +834,9 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => { const body = JSON.parse(response.body); expect(body.error).toBe("Payment verification failed"); - // Production mode rejects all networks except Mainnet + // Production mode rejects all networks except Optimism and Base expect(body.reason).toBe("invalid_network_for_production"); - expect(body.expected).toBe("eip155:10"); + expect(body.expected).toEqual(["eip155:10", "eip155:8453"]); expect(body.received).toBe("eip155:1"); }); diff --git a/scw_js/test/getChain.test.js b/scw_js/test/getChain.test.js index 26a9be21a..cde2f250e 100644 --- a/scw_js/test/getChain.test.js +++ b/scw_js/test/getChain.test.js @@ -19,7 +19,8 @@ import { createPublicClient, http } from "viem"; import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains"; // Import functions under test -import { getExpectedNetwork, validatePaymentNetwork } from "../getChain.js"; +import { getExpectedNetwork, getExpectedNetworks, validatePaymentNetwork } from "../getChain.js"; +import { getGenAiNFTMainnetNetworks, getGenAiNFTTestnetNetworks } from "@fretchen/chain-utils"; // Import from chain-utils import { getViemChain, getUSDCConfig } from "@fretchen/chain-utils"; @@ -108,12 +109,36 @@ describe("getChain.js - Chain Configuration Tests", () => { // NOTE: getGenImgContractConfig() tests moved to @fretchen/chain-utils // See shared/chain-utils/test/index.test.ts - describe("getExpectedNetwork()", () => { - test("should return Sepolia for test mode", () => { + describe("getExpectedNetworks()", () => { + test("should return mainnet networks from chain-utils", () => { + const result = getExpectedNetworks(false); + const expected = getGenAiNFTMainnetNetworks(); + expect(result).toEqual(expected); + // Verify actual networks (these should match chain-utils deployment) + expect(result).toContain("eip155:10"); // Optimism + expect(result).toContain("eip155:8453"); // Base + }); + + test("should return testnet networks from chain-utils", () => { + const result = getExpectedNetworks(true); + const expected = getGenAiNFTTestnetNetworks(); + expect(result).toEqual(expected); + // Verify actual networks (these should match chain-utils deployment) + expect(result).toContain("eip155:11155420"); // Optimism Sepolia + }); + + test("mainnet should have multiple networks (multi-chain support)", () => { + const result = getExpectedNetworks(false); + expect(result.length).toBeGreaterThanOrEqual(2); // At least Optimism + Base + }); + }); + + describe("getExpectedNetwork() - legacy", () => { + test("should return first testnet for sepoliaTest=true", () => { expect(getExpectedNetwork(true)).toBe("eip155:11155420"); }); - test("should return Mainnet for production mode", () => { + test("should return first mainnet for sepoliaTest=false", () => { expect(getExpectedNetwork(false)).toBe("eip155:10"); }); }); @@ -139,10 +164,15 @@ describe("getChain.js - Chain Configuration Tests", () => { const result = validatePaymentNetwork("eip155:11155420", false); expect(result.valid).toBe(false); expect(result.reason).toBe("invalid_network_for_production"); - expect(result.expected).toBe("eip155:10"); + expect(result.expected).toEqual(["eip155:10", "eip155:8453"]); expect(result.received).toBe("eip155:11155420"); }); + test("should accept Base network for production", () => { + const result = validatePaymentNetwork("eip155:8453", false); + expect(result.valid).toBe(true); + }); + test("should reject wrong network for test mode", () => { const result = validatePaymentNetwork("eip155:10", true); expect(result.valid).toBe(false); diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts index b3d4de290..e4928d86b 100644 --- a/shared/chain-utils/src/addresses.ts +++ b/shared/chain-utils/src/addresses.ts @@ -24,6 +24,22 @@ export const GENAI_NFT_NETWORKS = [ ...Object.keys(TESTNET_GENAI_NFT_ADDRESSES), ] as const; +/** + * Get all mainnet networks where GenImNFT is deployed + * Used by backend services for dynamic network configuration + */ +export function getGenAiNFTMainnetNetworks(): readonly string[] { + return Object.keys(MAINNET_GENAI_NFT_ADDRESSES); +} + +/** + * Get all testnet networks where GenImNFT is deployed + * Used by backend services for dynamic network configuration + */ +export function getGenAiNFTTestnetNetworks(): readonly string[] { + return Object.keys(TESTNET_GENAI_NFT_ADDRESSES); +} + // ═══════════════════════════════════════════════════════════════ // CollectorNFT // ═══════════════════════════════════════════════════════════════ diff --git a/shared/chain-utils/src/index.ts b/shared/chain-utils/src/index.ts index 84c82c122..3abcde65d 100644 --- a/shared/chain-utils/src/index.ts +++ b/shared/chain-utils/src/index.ts @@ -96,6 +96,7 @@ export function isTestnet(network: string): network is TestnetNetwork { // Re-export addresses export * from "./addresses"; +export { getGenAiNFTMainnetNetworks, getGenAiNFTTestnetNetworks } from "./addresses"; // ═══════════════════════════════════════════════════════════════ // USDC Configuration From 217d061e484a16877298073bf26b658710eb8e19 Mon Sep 17 00:00:00 2001 From: fretchen Date: Mon, 2 Feb 2026 07:42:06 +0100 Subject: [PATCH 08/16] Update index.test.ts --- shared/chain-utils/test/index.test.ts | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts index 633469cae..f20ff2b3c 100644 --- a/shared/chain-utils/test/index.test.ts +++ b/shared/chain-utils/test/index.test.ts @@ -24,6 +24,8 @@ import { getEIP3009SplitterAddress, getUSDCAddress, getUSDCName, + getGenAiNFTMainnetNetworks, + getGenAiNFTTestnetNetworks, MAINNET_GENAI_NFT_ADDRESSES, TESTNET_GENAI_NFT_ADDRESSES, MAINNET_LLM_V1_ADDRESSES, @@ -130,6 +132,42 @@ describe("@fretchen/chain-utils", () => { }); describe("Contract Addresses", () => { + describe("GenAI NFT Network Helpers", () => { + test("getGenAiNFTMainnetNetworks() should return all mainnet deployments", () => { + const networks = getGenAiNFTMainnetNetworks(); + + expect(Array.isArray(networks)).toBe(true); + expect(networks).toContain("eip155:10"); // Optimism + expect(networks).toContain("eip155:8453"); // Base + expect(networks).toEqual(Object.keys(MAINNET_GENAI_NFT_ADDRESSES)); + }); + + test("getGenAiNFTTestnetNetworks() should return all testnet deployments", () => { + const networks = getGenAiNFTTestnetNetworks(); + + expect(Array.isArray(networks)).toBe(true); + expect(networks).toContain("eip155:11155420"); // Optimism Sepolia + expect(networks).toEqual(Object.keys(TESTNET_GENAI_NFT_ADDRESSES)); + }); + + test("mainnet and testnet networks should be disjoint", () => { + const mainnet = getGenAiNFTMainnetNetworks(); + const testnet = getGenAiNFTTestnetNetworks(); + + for (const network of mainnet) { + expect(testnet).not.toContain(network); + } + }); + + test("should have at least one mainnet deployment", () => { + expect(getGenAiNFTMainnetNetworks().length).toBeGreaterThanOrEqual(1); + }); + + test("should have at least one testnet deployment", () => { + expect(getGenAiNFTTestnetNetworks().length).toBeGreaterThanOrEqual(1); + }); + }); + describe("getGenAiNFTAddress()", () => { test("should return Optimism Mainnet contract address", () => { expect(getGenAiNFTAddress("eip155:10")).toBe("0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"); From 50f9d765b636fa3015a674c3c563e9116a400395 Mon Sep 17 00:00:00 2001 From: fretchen Date: Tue, 3 Feb 2026 08:03:58 +0100 Subject: [PATCH 09/16] Make the minting on Base work. --- notebooks/genimg_x402_buyer.ipynb | 212 +++++++++++++++++++++------- scw_js/genimg_x402_token.js | 49 ++++--- scw_js/getChain.js | 23 ++- shared/chain-utils/src/addresses.ts | 9 ++ 4 files changed, 214 insertions(+), 79 deletions(-) diff --git a/notebooks/genimg_x402_buyer.ipynb b/notebooks/genimg_x402_buyer.ipynb index 24bf95318..603cb4499 100644 --- a/notebooks/genimg_x402_buyer.ipynb +++ b/notebooks/genimg_x402_buyer.ipynb @@ -41,7 +41,7 @@ "4. **Running GenImg Server** - Start locally:\n", " ```bash\n", " cd scw_js\n", - " NODE_ENV=test node ./genimg_x402_token.js\n", + " NODE_ENV=test node ./dist/genimg_x402_token.js\n", " ```\n", "\n", "## 📚 How to Use This Notebook\n", @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 5, "id": "da456d3e", "metadata": {}, "outputs": [ @@ -84,7 +84,8 @@ " x402Client: function\n", " wrapFetchWithPayment: function\n", " registerExactEvmScheme: function\n", - " Chains: { optimism: \"OP Mainnet\", optimismSepolia: \"OP Sepolia\" }\n" + " createPublicClient: function\n", + " Chains: { optimism: \"OP Mainnet\", optimismSepolia: \"OP Sepolia\", base: \"Base\" }\n" ] } ], @@ -97,7 +98,8 @@ "\n", "// Viem for wallet and chains\n", "import { privateKeyToAccount } from \"npm:viem@^2.0.0/accounts\";\n", - "import { optimism, optimismSepolia } from \"npm:viem@^2.0.0/chains\";\n", + "import { createPublicClient, http } from \"npm:viem@^2.0.0\";\n", + "import { optimism, optimismSepolia, base } from \"npm:viem@^2.0.0/chains\";\n", "\n", "// Load environment variables\n", "import { load } from \"https://deno.land/std@0.224.0/dotenv/mod.ts\";\n", @@ -107,7 +109,8 @@ "console.log(\" x402Client:\", typeof x402Client);\n", "console.log(\" wrapFetchWithPayment:\", typeof wrapFetchWithPayment);\n", "console.log(\" registerExactEvmScheme:\", typeof registerExactEvmScheme);\n", - "console.log(\" Chains:\", { optimism: optimism.name, optimismSepolia: optimismSepolia.name });" + "console.log(\" createPublicClient:\", typeof createPublicClient);\n", + "console.log(\" Chains:\", { optimism: optimism.name, optimismSepolia: optimismSepolia.name, base: base.name });" ] }, { @@ -127,13 +130,19 @@ "|---------|----------|------|----------|\n", "| Optimism Sepolia | `eip155:11155420` | Test USDC | Development/Testing |\n", "| Optimism Mainnet | `eip155:10` | Real USDC | Production |\n", + "| Base Mainnet | `eip155:8453` | Real USDC | Production (Base L2) |\n", "\n", - "⚠️ **Warning**: Setting `USE_MAINNET = true` uses **real money**!" + "⚠️ **Warning**: Using mainnet networks requires **real money**!\n", + "\n", + "**To test with Base:**\n", + "1. Set `NETWORK_SELECTION = \"base-mainnet\"`\n", + "2. Ensure you have USDC on Base\n", + "3. Make sure your backend supports Base (deployed with latest code)" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 13, "id": "fd89ef25", "metadata": {}, "outputs": [ @@ -146,10 +155,11 @@ " SERVICE_URL: http://localhost:8082\n", " FACILITATOR_URL: https://facilitator.fretchen.eu\n", "\n", - "🧪 Using testnet: Optimism Sepolia\n", + "🚨 WARNING: Using REAL MONEY on Base Mainnet!\n", + "⚠️ Make sure this is intentional!\n", "\n", - "📍 Network: Optimism Sepolia (eip155:11155420)\n", - "💰 USDC Contract: 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n", + "📍 Network: Base Mainnet (eip155:8453)\n", + "💰 USDC Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\n", "🎨 Prompt: \"A cute robot painting a picture\"\n" ] } @@ -157,38 +167,51 @@ "source": [ "// ⚙️ Configuration - lädt aus .env Datei\n", "const PRIVATE_KEY = env.TEST_WALLET_PRIVATE_KEY;\n", - "// const PRIVATE_KEY = env.NFT_WALLET_PRIVATE_KEY;\n", + "//const PRIVATE_KEY = env.NFT_WALLET_PRIVATE_KEY;\n", "const SERVICE_URL = \"http://localhost:8082\";\n", "const FACILITATOR_URL = \"https://facilitator.fretchen.eu\";\n", "const PROMPT = \"A cute robot painting a picture\";\n", "\n", - "// ⚠️ NETWORK SELECTION - Change this to switch between testnet and mainnet\n", - "const USE_MAINNET = false; // Set to true for Optimism Mainnet with REAL MONEY\n", - "\n", - "// Network configuration based on selection\n", - "const NETWORK = USE_MAINNET ? {\n", - " // Optimism Mainnet - REAL MONEY! 💰\n", - " chain: optimism,\n", - " chainId: \"eip155:10\",\n", - " name: \"Optimism Mainnet\",\n", - " usdc: \"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85\",\n", - " decimals: 6,\n", - "} : {\n", - " // Optimism Sepolia - Testnet 🧪\n", - " chain: optimismSepolia,\n", - " chainId: \"eip155:11155420\",\n", - " name: \"Optimism Sepolia\",\n", - " usdc: \"0x5fd84259d66Cd46123540766Be93DFE6D43130D7\",\n", - " decimals: 6,\n", + "// ⚠️ NETWORK SELECTION - Choose your network\n", + "const NETWORK_SELECTION = \"base-mainnet\"; // Options: \"optimism-sepolia\", \"optimism-mainnet\", \"base-mainnet\"\n", + "\n", + "// Network configurations\n", + "const NETWORKS = {\n", + " \"optimism-sepolia\": {\n", + " chain: optimismSepolia,\n", + " chainId: \"eip155:11155420\",\n", + " name: \"Optimism Sepolia\",\n", + " usdc: \"0x5fd84259d66Cd46123540766Be93DFE6D43130D7\",\n", + " decimals: 6,\n", + " isTestnet: true,\n", + " },\n", + " \"optimism-mainnet\": {\n", + " chain: optimism,\n", + " chainId: \"eip155:10\",\n", + " name: \"Optimism Mainnet\",\n", + " usdc: \"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85\",\n", + " decimals: 6,\n", + " isTestnet: false,\n", + " },\n", + " \"base-mainnet\": {\n", + " chain: base,\n", + " chainId: \"eip155:8453\",\n", + " name: \"Base Mainnet\",\n", + " usdc: \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\n", + " decimals: 6,\n", + " isTestnet: false,\n", + " },\n", "};\n", "\n", + "const NETWORK = NETWORKS[NETWORK_SELECTION];\n", + "\n", "// Show configuration\n", "console.log(\"📋 Configuration:\");\n", "console.log(` PRIVATE_KEY: ${PRIVATE_KEY ? PRIVATE_KEY.slice(0, 6) + \"...\" + PRIVATE_KEY.slice(-4) : \"❌ NOT FOUND\"}`);\n", "console.log(` SERVICE_URL: ${SERVICE_URL}`);\n", "console.log(` FACILITATOR_URL: ${FACILITATOR_URL}`);\n", "\n", - "if (USE_MAINNET) {\n", + "if (!NETWORK.isTestnet) {\n", " console.log(`\\n🚨 WARNING: Using REAL MONEY on ${NETWORK.name}!`);\n", " console.log(`⚠️ Make sure this is intentional!`);\n", "} else {\n", @@ -214,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 14, "id": "951f14b9", "metadata": {}, "outputs": [ @@ -224,31 +247,118 @@ "text": [ "🔧 Setting up x402 client...\n", "✅ x402 client ready\n", - " Will use server's first offered network\n", - " Server controls via sepoliaTest flag\n" + " Configured for: Base Mainnet (eip155:8453)\n", + " USDC Address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\n" ] } ], "source": [ - "// 🔧 Setup x402 client - Exactly like Quickstart!\n", + "// 🔧 Setup x402 client - with specific chain configuration\n", "\n", "console.log(\"🔧 Setting up x402 client...\");\n", "\n", "// Create signer (wallet)\n", "const signer = privateKeyToAccount(`0x${PRIVATE_KEY}`);\n", "\n", + "// Create public client for the selected network\n", + "const publicClient = createPublicClient({\n", + " chain: NETWORK.chain,\n", + " transport: http(),\n", + "});\n", + "\n", "// Create x402 client\n", "const client = new x402Client();\n", "\n", - "// Register EVM scheme (tells client how to create EIP-3009 signatures)\n", - "registerExactEvmScheme(client, { signer });\n", + "// Register EVM scheme with specific chain configuration\n", + "registerExactEvmScheme(client, { \n", + " signer,\n", + " publicClient // This tells x402 which network to use!\n", + "});\n", "\n", "// Wrap fetch with payment handling\n", "const fetchWithPayment = wrapFetchWithPayment(fetch, client);\n", "\n", "console.log(\"✅ x402 client ready\");\n", - "console.log(\" Will use server's first offered network\");\n", - "console.log(\" Server controls via sepoliaTest flag\");" + "console.log(` Configured for: ${NETWORK.name} (${NETWORK.chainId})`);\n", + "console.log(` USDC Address: ${NETWORK.usdc}`);" + ] + }, + { + "cell_type": "markdown", + "id": "ef1f6a53", + "metadata": {}, + "source": [ + "## 💰 Step 3.5: Check USDC Balance\n", + "\n", + "Before making a payment, let's verify that your wallet has enough USDC on the selected network." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "a08ec66f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "💰 Checking USDC balance...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "📊 Balance Check:\n", + " Wallet: 0x553179556FC2A39e535D65b921e01fA995E79101\n", + " Network: Base Mainnet\n", + " USDC Balance: 1.000000 USDC\n", + " Required: 0.001000 USDC\n", + " ✅ Sufficient funds!\n" + ] + } + ], + "source": [ + "// 💰 Check USDC Balance\n", + "\n", + "const USDC_ABI = [\n", + " {\n", + " inputs: [{ name: \"account\", type: \"address\" }],\n", + " name: \"balanceOf\",\n", + " outputs: [{ name: \"\", type: \"uint256\" }],\n", + " stateMutability: \"view\",\n", + " type: \"function\",\n", + " },\n", + "] as const;\n", + "\n", + "console.log(\"💰 Checking USDC balance...\");\n", + "\n", + "const balance = await publicClient.readContract({\n", + " address: NETWORK.usdc as `0x${string}`,\n", + " abi: USDC_ABI,\n", + " functionName: \"balanceOf\",\n", + " args: [signer.address],\n", + "});\n", + "\n", + "// USDC has 6 decimals\n", + "const balanceFormatted = Number(balance) / 1_000_000;\n", + "const REQUIRED_USDC = 0.001; // $0.001 per image\n", + "\n", + "console.log(`\\n📊 Balance Check:`);\n", + "console.log(` Wallet: ${signer.address}`);\n", + "console.log(` Network: ${NETWORK.name}`);\n", + "console.log(` USDC Balance: ${balanceFormatted.toFixed(6)} USDC`);\n", + "console.log(` Required: ${REQUIRED_USDC.toFixed(6)} USDC`);\n", + "\n", + "if (balanceFormatted >= REQUIRED_USDC) {\n", + " console.log(` ✅ Sufficient funds!`);\n", + "} else {\n", + " console.log(` ❌ INSUFFICIENT FUNDS!`);\n", + " console.log(` Need: ${(REQUIRED_USDC - balanceFormatted).toFixed(6)} more USDC`);\n", + " throw new Error(`Insufficient USDC balance. Need ${REQUIRED_USDC} USDC, have ${balanceFormatted.toFixed(6)} USDC`);\n", + "}" ] }, { @@ -274,7 +384,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 17, "id": "c22113c6", "metadata": {}, "outputs": [ @@ -286,13 +396,14 @@ "📡 Response Status: 200\n", "\n", "🎉 SUCCESS!\n", - "🖼️ Image URL: undefined\n", - "🎫 NFT Token ID: 7\n", - "📍 Contract: undefined\n", + "🖼️ Image URL: https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1770101331378_097c9848eafe.jpg\n", + "🎫 NFT Token ID: 0\n", + "📍 Network: eip155:8453\n", + "💰 Mint Price: 30000000000000 wei\n", "\n", "✅ Payment settled:\n", " Transaction: undefined\n", - " Network: eip155:11155420\n", + " Network: eip155:8453\n", "\n", "💡 fetchWithPayment automatically:\n", " 1. Received 402 Payment Required\n", @@ -317,7 +428,7 @@ " },\n", " body: JSON.stringify({ \n", " prompt: PROMPT,\n", - " sepoliaTest: !USE_MAINNET // Server will restrict to Sepolia if true\n", + " network: NETWORK.chainId // 🎯 Server derives testnet/mainnet from this!\n", " }),\n", "});\n", "\n", @@ -326,9 +437,10 @@ "if (response.ok) {\n", " const result = await response.json();\n", " console.log(\"\\n🎉 SUCCESS!\");\n", - " console.log(`🖼️ Image URL: ${result.imageUrl}`);\n", + " console.log(`🖼️ Image URL: ${result.image_url}`);\n", " console.log(`🎫 NFT Token ID: ${result.tokenId}`);\n", - " console.log(`📍 Contract: ${result.contractAddress}`);\n", + " console.log(`📍 Network: ${result.network}`);\n", + " console.log(`💰 Mint Price: ${result.mintPrice} wei`);\n", " \n", " // Get payment receipt from response headers (exactly like Quickstart!)\n", " const httpClient = new x402HTTPClient(client);\n", @@ -367,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 18, "id": "59c78c27", "metadata": {}, "outputs": [ @@ -377,8 +489,8 @@ "\n", "
\n", "

🎨 Generated Image

\n", - " \"Generated\n", - "

URL: https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1766753789601_9cdf583207d1.jpg

\n", + " \"Generated\n", + "

URL: https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1770101331378_097c9848eafe.jpg

\n", "
\n", " " ] @@ -389,7 +501,7 @@ ], "source": [ "// Zeige das generierte Bild an\n", - "const imageUrl = \"https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1766753789601_9cdf583207d1.jpg\";\n", + "const imageUrl = \"https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1770101331378_097c9848eafe.jpg\";\n", "\n", "if (imageUrl) {\n", " // Deno Jupyter API für HTML-Anzeige\n", diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js index 4f307a984..5d6b3e1c5 100644 --- a/scw_js/genimg_x402_token.js +++ b/scw_js/genimg_x402_token.js @@ -6,6 +6,7 @@ import { getViemChain, getGenAiNFTAddress, getUSDCConfig, + isTestnet, } from "@fretchen/chain-utils"; import { getContract, createWalletClient, createPublicClient, http, parseEther } from "viem"; import { generateAndUploadImage, JSON_BASE_PATH } from "./image_service.js"; @@ -378,6 +379,7 @@ async function handle(event, context, cb) { // Get optional parameters const mode = body.mode || "generate"; const size = body.size || "1024x1024"; + const requestedNetwork = body.network || null; // Optional: Client can request specific network const isListed = body.isListed === true; // Default: false (not listed in public gallery) // Validate size parameter @@ -412,10 +414,14 @@ async function handle(event, context, cb) { console.log("🖼️ Reference image provided for editing"); } - // Extract test mode flag from body - const sepoliaTest = body.sepoliaTest === true; - if (sepoliaTest) { - console.log("🧪 Test mode enabled: Sepolia only, mock image generation"); + // Determine network and test mode from network parameter + // If no network specified, default to mainnet networks + if (requestedNetwork) { + const isTestnetMode = isTestnet(requestedNetwork); + console.log(`🌐 Network: ${requestedNetwork} (${isTestnetMode ? "testnet" : "mainnet"})`); + if (isTestnetMode) { + console.log("🧪 Test mode: Using mock image generation"); + } } // ====== x402 v2 TOKEN PAYMENT FLOW ====== @@ -424,16 +430,24 @@ async function handle(event, context, cb) { if (!paymentPayload) { console.log("❌ No payment provided → Returning 402"); - // 🎯 Network selection based on test mode - // Dynamically loaded from chain-utils deployment configuration - // Test mode: All testnet deployments (currently Sepolia) - // Production: All mainnet deployments (currently Optimism + Base) - const networks = getExpectedNetworks(sepoliaTest); - - if (sepoliaTest) { - console.log(` Test mode: ${networks.join(", ")}`); + // 🎯 Network selection + // If client specified network, use only that one (if supported) + // Otherwise, return all mainnet networks by default + let networks; + if (requestedNetwork) { + const allNetworks = [...getExpectedNetworks(false), ...getExpectedNetworks(true)]; + if (allNetworks.includes(requestedNetwork)) { + networks = [requestedNetwork]; + console.log(` Client requested network: ${requestedNetwork}`); + } else { + console.log(` ⚠️ Client requested unsupported network: ${requestedNetwork}`); + console.log(` Falling back to all mainnet networks`); + networks = getExpectedNetworks(false); + } } else { - console.log(` Production mode: ${networks.join(", ")}`); + // Default: offer all mainnet networks + networks = getExpectedNetworks(false); + console.log(` Default mode: ${networks.join(", ")}`); } // Create payment requirements using x402 helper @@ -455,7 +469,7 @@ async function handle(event, context, cb) { const clientNetwork = paymentPayload?.accepted?.network; console.log(`🌐 Payment payload network: ${clientNetwork}`); - const networkValidation = validatePaymentNetwork(clientNetwork, sepoliaTest); + const networkValidation = validatePaymentNetwork(clientNetwork); if (!networkValidation.valid) { console.error(`❌ Network validation failed: ${networkValidation.reason}`); return { @@ -601,7 +615,7 @@ async function handle(event, context, cb) { size, mode, referenceImageBase64, - sepoliaTest, // useMockImage in test mode + isTestnet(clientNetwork), // useMockImage in test mode mintPrice, isListed, ); @@ -760,8 +774,9 @@ if (process.env.NODE_ENV === "test" && !process.env.CI) { ` - Optimism Sepolia: 0x10827cC42a09D0BAD2d43134C69F0e776D853D85 (Test)`, ); console.log(` Network Policy:`); - console.log(` - Production: Optimism Mainnet only`); - console.log(` - Test mode (sepoliaTest=true): Sepolia only + mock images`); + console.log(` - Default: All mainnet networks (Optimism + Base)`); + console.log(` - With network parameter: Requested network only`); + console.log(` - Testnet networks: Mock images + Sepolia`); }); }); }); diff --git a/scw_js/getChain.js b/scw_js/getChain.js index a9949203d..b238713ea 100644 --- a/scw_js/getChain.js +++ b/scw_js/getChain.js @@ -1,9 +1,10 @@ // @ts-check import { sepolia, optimism, optimismSepolia } from "viem/chains"; -import { +import { LLMv1ABI, getGenAiNFTMainnetNetworks, - getGenAiNFTTestnetNetworks + getGenAiNFTTestnetNetworks, + isTestnet, } from "@fretchen/chain-utils"; /** @@ -80,9 +81,7 @@ export function getLLMv1ContractConfig() { * @returns {string[]} Array of CAIP-2 network IDs */ export function getExpectedNetworks(sepoliaTest) { - return sepoliaTest - ? getGenAiNFTTestnetNetworks() - : getGenAiNFTMainnetNetworks(); + return sepoliaTest ? getGenAiNFTTestnetNetworks() : getGenAiNFTMainnetNetworks(); } /** @@ -96,22 +95,22 @@ export function getExpectedNetwork(sepoliaTest) { } /** - * Validate that a client-selected network matches the expected mode + * Validate that a client-selected network is supported * @param {string|undefined} clientNetwork - Network from payment payload - * @param {boolean} sepoliaTest - Whether test mode is enabled * @returns {{ valid: boolean, reason?: string, expected?: string[], received?: string }} */ -export function validatePaymentNetwork(clientNetwork, sepoliaTest) { +export function validatePaymentNetwork(clientNetwork) { if (!clientNetwork) { return { valid: false, reason: "missing_network" }; } - const expectedNetworks = getExpectedNetworks(sepoliaTest); - if (!expectedNetworks.includes(clientNetwork)) { + // Check against all supported networks (mainnet + testnet) + const allNetworks = [...getExpectedNetworks(false), ...getExpectedNetworks(true)]; + if (!allNetworks.includes(clientNetwork)) { return { valid: false, - reason: sepoliaTest ? "invalid_network_for_test_mode" : "invalid_network_for_production", - expected: expectedNetworks, + reason: "unsupported_network", + expected: allNetworks, received: clientNetwork, }; } diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts index e4928d86b..327163fec 100644 --- a/shared/chain-utils/src/addresses.ts +++ b/shared/chain-utils/src/addresses.ts @@ -40,6 +40,15 @@ export function getGenAiNFTTestnetNetworks(): readonly string[] { return Object.keys(TESTNET_GENAI_NFT_ADDRESSES); } +/** + * Check if a given network is a testnet + * @param network CAIP-2 network identifier (e.g., "eip155:8453") + * @returns true if the network is a testnet, false otherwise + */ +export function isTestnetNetwork(network: string): boolean { + return Object.keys(TESTNET_GENAI_NFT_ADDRESSES).includes(network); +} + // ═══════════════════════════════════════════════════════════════ // CollectorNFT // ═══════════════════════════════════════════════════════════════ From cf8723ab04ed7f27027842ed424761695d0488f0 Mon Sep 17 00:00:00 2001 From: fretchen Date: Tue, 3 Feb 2026 20:39:35 +0100 Subject: [PATCH 10/16] Ability to generate images on Base ist implemented --- notebooks/genimg_x402_buyer.ipynb | 6 +- website/MULTICHAIN_EXPANSION_PROPOSAL.md | 84 +++++++++++++++++++++ website/components/EntryNftImage.tsx | 16 ++-- website/components/ImageGenerator.tsx | 5 +- website/hooks/useX402ImageGeneration.ts | 12 +-- website/test/useX402ImageGeneration.test.ts | 59 ++++++++++++++- website/types/x402.ts | 3 +- 7 files changed, 165 insertions(+), 20 deletions(-) diff --git a/notebooks/genimg_x402_buyer.ipynb b/notebooks/genimg_x402_buyer.ipynb index 603cb4499..e011854e3 100644 --- a/notebooks/genimg_x402_buyer.ipynb +++ b/notebooks/genimg_x402_buyer.ipynb @@ -384,7 +384,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, "id": "c22113c6", "metadata": {}, "outputs": [ @@ -396,8 +396,8 @@ "📡 Response Status: 200\n", "\n", "🎉 SUCCESS!\n", - "🖼️ Image URL: https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1770101331378_097c9848eafe.jpg\n", - "🎫 NFT Token ID: 0\n", + "🖼️ Image URL: https://my-imagestore.s3.nl-ams.scw.cloud/images/image_1770102163172_61b90e96c972.jpg\n", + "🎫 NFT Token ID: 1\n", "📍 Network: eip155:8453\n", "💰 Mint Price: 30000000000000 wei\n", "\n", diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md index ced624455..5dd66c2bc 100644 --- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md +++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md @@ -965,3 +965,87 @@ if (require.main === module) { - chain-utils: 46 Tests, 98.75% Coverage - scw_js: 175 Tests - CI Pipelines für alle Packages + +--- + +## Phase 5: Website Notebook-Pattern Migration 🔄 IN PROGRESS + +### Änderungen + +Backend erwartet jetzt `network` Parameter (CAIP-2) statt `sepoliaTest` Boolean. Website muss angepasst werden. + +**Breaking Change:** `X402GenImgRequest.sepoliaTest` → `X402GenImgRequest.network` + +### Code-Änderungen + +**1. Type Definition** (`website/types/x402.ts`): +```typescript +export interface X402GenImgRequest { + prompt: string; + size?: "1024x1024" | "1792x1024"; + mode?: "generate" | "edit"; + referenceImage?: string; + sepoliaTest?: boolean; // ❌ Remove + network: string; // ✅ Add (CAIP-2, z.B. "eip155:8453") + expectedChainId?: number; + isListed?: boolean; +} +``` + +**2. ImageGenerator** (`website/components/ImageGenerator.tsx`): +```tsx +// VORHER (Line ~308) +const result = await generateImage({ + prompt, + size, + mode, + referenceImage: isEditMode ? referenceImageBase64 : undefined, + sepoliaTest: useTestnetFlag, // ❌ + expectedChainId: targetChainId, + isListed, +}); + +// NACHHER +const result = await generateImage({ + prompt, + size, + mode, + referenceImage: isEditMode ? referenceImageBase64 : undefined, + network, // ✅ Von useAutoNetwork + expectedChainId: targetChainId, + isListed, +}); +``` + +**3. useAutoNetwork Hook** - Bleibt unverändert: +- Verwendet bereits wallet-connected chain +- Fällt zurück auf erste supported chain wenn wallet nicht verbunden +- Keine UI für manuelle Network-Auswahl (bereits verworfen) + +**4. x402 Hook Validation** (`website/hooks/useX402ImageGeneration.ts`): +```typescript +// Update: Validate gegen network statt expectedChainId +const { expectedChainId, network, ...requestBody } = request; + +if (response.status === 402 && network) { + const decoded = JSON.parse(atob(response.headers.get("Payment-Required"))); + if (decoded.accepts?.[0]?.network !== network) { + throw new Error(`Network mismatch: expected ${network}`); + } +} +``` + +### Deployment + +**Backend First:** +- [x] Facilitator deployed (Base support) +- [ ] scw_js deployment: `cd scw_js && npm run deploy` + +**Website:** +- [ ] Remove `sepoliaTest` references +- [ ] Add `network` parameter +- [ ] Update tests +- [ ] Deploy + +**Status:** Ready for Implementation +**Blocking:** scw_js deployment diff --git a/website/components/EntryNftImage.tsx b/website/components/EntryNftImage.tsx index c2bd32622..b34ad1ef3 100644 --- a/website/components/EntryNftImage.tsx +++ b/website/components/EntryNftImage.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { entryList } from "../layouts/styles"; -import { useAutoNetwork } from "../hooks/useAutoNetwork"; -import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils"; +import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, isTestnet } from "@fretchen/chain-utils"; import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient"; import { NFTMetadata } from "../types/components"; @@ -11,15 +10,22 @@ interface EntryNftImageProps { nftName?: string; } +// Filter to mainnet networks only for blog NFTs (they're minted on mainnet) +const MAINNET_NETWORKS = GENAI_NFT_NETWORKS.filter((n) => !isTestnet(n)); + /** * Larger NFT image that spans both date and title rows + * + * Blog NFTs may be minted on any mainnet (Optimism, Base). + * This component tries each mainnet until it finds the token. */ export const EntryNftImage: React.FC = ({ tokenId, fallbackImageUrl, nftName }) => { const [imageUrl, setImageUrl] = React.useState(null); const [isLoading, setIsLoading] = React.useState(true); - const { network } = useAutoNetwork(GENAI_NFT_NETWORKS); - const contractAddress = getGenAiNFTAddress(network); - const publicClient = useConfiguredPublicClient(network); + + // Default to first mainnet for public client (we'll try all networks anyway) + const defaultNetwork = MAINNET_NETWORKS[0]; + const publicClient = useConfiguredPublicClient(defaultNetwork); // Fetch metadata from tokenURI const fetchNFTMetadata = async (tokenURI: string): Promise => { diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx index 5da70c247..27f28941b 100644 --- a/website/components/ImageGenerator.tsx +++ b/website/components/ImageGenerator.tsx @@ -126,7 +126,6 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) { const { network, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS); const targetChainId = fromCAIP2(network); const targetChain = getViemChain(network); - const useTestnetFlag = isTestnet(network); // Preview area state machine type PreviewState = "empty" | "reference" | "generated"; @@ -306,8 +305,8 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) { size, mode, referenceImage: isEditMode ? referenceImageBase64 : undefined, - // Use testnet: derived from useAutoNetwork - sepoliaTest: useTestnetFlag, + // CAIP-2 network from useAutoNetwork (wallet-connected or default) + network, // Pass expected chain ID for validation in hook expectedChainId: targetChainId, // Whether to list in public gallery diff --git a/website/hooks/useX402ImageGeneration.ts b/website/hooks/useX402ImageGeneration.ts index 269e33210..9a8c6af0f 100644 --- a/website/hooks/useX402ImageGeneration.ts +++ b/website/hooks/useX402ImageGeneration.ts @@ -65,6 +65,7 @@ export function useX402ImageGeneration(): UseX402ImageGenerationResult { // === Make the paid request === // Remove expectedChainId from request body (it's only for client-side validation) + // Keep network in the body - backend uses it to filter 402 response const { expectedChainId, ...requestBody } = request; // === Create a validating fetch wrapper === @@ -74,22 +75,21 @@ export function useX402ImageGeneration(): UseX402ImageGenerationResult { const response = await fetch(input, init); // Validate chain from 402 response before x402 SDK triggers signing - if (response.status === 402 && expectedChainId) { + if (response.status === 402 && request.network) { const paymentRequiredHeader = response.headers.get("Payment-Required"); if (paymentRequiredHeader) { try { const decoded = JSON.parse(atob(paymentRequiredHeader)); const serverNetwork = decoded.accepts?.[0]?.network; - const expectedNetwork = `eip155:${expectedChainId}`; - if (serverNetwork && serverNetwork !== expectedNetwork) { + if (serverNetwork && serverNetwork !== request.network) { throw new Error( - `Chain mismatch! Expected ${expectedNetwork} but server requires ${serverNetwork}. ` + - `This could indicate a configuration error.`, + `Network mismatch! Selected ${request.network} but server requires ${serverNetwork}. ` + + `This could indicate a backend configuration error.`, ); } } catch (parseError) { - if (parseError instanceof Error && parseError.message.includes("Chain mismatch")) { + if (parseError instanceof Error && parseError.message.includes("Network mismatch")) { throw parseError; } // Silently continue if header parsing fails - the request will proceed diff --git a/website/test/useX402ImageGeneration.test.ts b/website/test/useX402ImageGeneration.test.ts index 1d568ed26..979ace4e8 100644 --- a/website/test/useX402ImageGeneration.test.ts +++ b/website/test/useX402ImageGeneration.test.ts @@ -112,7 +112,7 @@ describe("useX402ImageGeneration", () => { size: "1792x1024", mode: "edit", referenceImage: "base64encodedimage", - sepoliaTest: true, + network: "eip155:11155420", expectedChainId: 11155420, isListed: true, }; @@ -121,7 +121,7 @@ describe("useX402ImageGeneration", () => { expect(request.size).toBe("1792x1024"); expect(request.mode).toBe("edit"); expect(request.referenceImage).toBeDefined(); - expect(request.sepoliaTest).toBe(true); + expect(request.network).toBe("eip155:11155420"); expect(request.expectedChainId).toBe(11155420); expect(request.isListed).toBe(true); }); @@ -129,9 +129,64 @@ describe("useX402ImageGeneration", () => { it("should have isListed default to undefined when not specified", () => { const request: X402GenImgRequest = { prompt: "Test image", + network: "eip155:10", }; expect(request.isListed).toBeUndefined(); }); }); + + describe("Request Body Transformation", () => { + it("should keep network in requestBody but remove expectedChainId", () => { + // This test verifies the transformation logic: + // const { expectedChainId, ...requestBody } = request; + // network must stay in requestBody, expectedChainId must be removed + + const request: X402GenImgRequest = { + prompt: "Test image", + network: "eip155:8453", + expectedChainId: 8453, + size: "1024x1024", + }; + + // Simulate the transformation from the hook + const { expectedChainId, ...requestBody } = request; + + // network MUST be in requestBody (sent to server) + expect(requestBody.network).toBe("eip155:8453"); + expect(requestBody.prompt).toBe("Test image"); + expect(requestBody.size).toBe("1024x1024"); + + // expectedChainId MUST NOT be in requestBody (client-side only) + expect("expectedChainId" in requestBody).toBe(false); + expect(expectedChainId).toBe(8453); // still available for validation + }); + + it("should work with Base network", () => { + const request: X402GenImgRequest = { + prompt: "A dog on Base", + network: "eip155:8453", + expectedChainId: 8453, + }; + + const { expectedChainId, ...requestBody } = request; + + expect(requestBody.network).toBe("eip155:8453"); + expect(JSON.stringify(requestBody)).toContain("eip155:8453"); + expect(JSON.stringify(requestBody)).not.toContain("expectedChainId"); + }); + + it("should work with Optimism network", () => { + const request: X402GenImgRequest = { + prompt: "A dog on Optimism", + network: "eip155:10", + expectedChainId: 10, + }; + + const { expectedChainId, ...requestBody } = request; + + expect(requestBody.network).toBe("eip155:10"); + expect(JSON.stringify(requestBody)).toContain("eip155:10"); + }); + }); }); diff --git a/website/types/x402.ts b/website/types/x402.ts index 242e98453..be1183aa6 100644 --- a/website/types/x402.ts +++ b/website/types/x402.ts @@ -7,7 +7,8 @@ export interface X402GenImgRequest { size?: "1024x1024" | "1792x1024"; mode?: "generate" | "edit"; referenceImage?: string; // base64 encoded - sepoliaTest?: boolean; + /** CAIP-2 network identifier (e.g., "eip155:8453" for Base) */ + network: string; /** Expected chain ID for validation - prevents signing on wrong network */ expectedChainId?: number; /** Whether to list the NFT in the public gallery (default: false) */ From 1c7cfaf0d6b8b41d678cbd7d385b477f71c712ca Mon Sep 17 00:00:00 2001 From: fretchen Date: Tue, 3 Feb 2026 21:11:34 +0100 Subject: [PATCH 11/16] Implement the minting on Base too. --- website/components/ChainBadge.tsx | 171 +++++++++++++++ website/components/ImageGenerator.tsx | 4 +- website/components/ImageModal.tsx | 4 +- website/components/MyNFTList.tsx | 180 +++++----------- website/components/NFTCard.tsx | 47 +++- website/components/NFTList.tsx | 39 ++-- website/components/PublicNFTList.tsx | 71 ++---- website/hooks/useMultiChainNFTs.ts | 285 +++++++++++++++++++++++++ website/pages/imagegen/+Page.tsx | 6 +- website/test/ChainBadge.test.tsx | 99 +++++++++ website/test/MyNFTList.test.tsx | 251 ++++++++++++++-------- website/test/NFTList.test.tsx | 26 +-- website/test/PublicNFTList.test.tsx | 210 +++++++++--------- website/test/useMultiChainNFTs.test.ts | 239 +++++++++++++++++++++ website/types/components.ts | 14 +- 15 files changed, 1206 insertions(+), 440 deletions(-) create mode 100644 website/components/ChainBadge.tsx create mode 100644 website/hooks/useMultiChainNFTs.ts create mode 100644 website/test/ChainBadge.test.tsx create mode 100644 website/test/useMultiChainNFTs.test.ts diff --git a/website/components/ChainBadge.tsx b/website/components/ChainBadge.tsx new file mode 100644 index 000000000..7ca14b034 --- /dev/null +++ b/website/components/ChainBadge.tsx @@ -0,0 +1,171 @@ +import React from "react"; +import { css } from "../styled-system/css"; + +/** + * Chain metadata for display purposes + */ +interface ChainInfo { + name: string; + shortName: string; + color: string; + bgColor: string; +} + +const CHAIN_INFO: Record = { + "eip155:10": { + name: "Optimism", + shortName: "OP", + color: "#FF0420", + bgColor: "rgba(255, 4, 32, 0.9)", + }, + "eip155:8453": { + name: "Base", + shortName: "Base", + color: "#0052FF", + bgColor: "rgba(0, 82, 255, 0.9)", + }, + "eip155:11155420": { + name: "OP Sepolia", + shortName: "OPS", + color: "#FF0420", + bgColor: "rgba(255, 4, 32, 0.7)", + }, + "eip155:84532": { + name: "Base Sepolia", + shortName: "BaseS", + color: "#0052FF", + bgColor: "rgba(0, 82, 255, 0.7)", + }, +}; + +/** + * Get human-readable chain name from CAIP-2 identifier + */ +export function getChainName(network: string): string { + return CHAIN_INFO[network]?.name ?? network; +} + +/** + * Get short chain name (2-4 chars) from CAIP-2 identifier + */ +export function getChainShortName(network: string): string { + return CHAIN_INFO[network]?.shortName ?? "?"; +} + +interface ChainBadgeProps { + /** CAIP-2 network identifier (e.g., "eip155:10") */ + network: string; + /** Size variant */ + size?: "sm" | "md"; + /** Position variant for absolute positioning on cards */ + position?: "bottom-right" | "top-left" | "inline"; +} + +const badgeBase = css({ + display: "inline-flex", + alignItems: "center", + gap: "4px", + fontWeight: "600", + borderRadius: "full", + backdropFilter: "blur(4px)", + border: "1px solid rgba(255,255,255,0.2)", + color: "white", + whiteSpace: "nowrap", +}); + +const badgeSm = css({ + fontSize: "10px", + padding: "2px 6px", +}); + +const badgeMd = css({ + fontSize: "12px", + padding: "4px 8px", +}); + +const positionBottomRight = css({ + position: "absolute", + bottom: "8px", + right: "8px", + zIndex: 10, +}); + +const positionTopLeft = css({ + position: "absolute", + top: "8px", + left: "8px", + zIndex: 10, +}); + +/** + * Small chain indicator badge + * + * Shows the chain name (OP/Base) with appropriate brand colors. + * Used on NFT cards to indicate which chain the NFT is on. + */ +export function ChainBadge({ network, size = "sm", position = "bottom-right" }: ChainBadgeProps) { + const chainInfo = CHAIN_INFO[network]; + + if (!chainInfo) { + return null; + } + + const sizeClass = size === "sm" ? badgeSm : badgeMd; + const positionClass = position === "bottom-right" + ? positionBottomRight + : position === "top-left" + ? positionTopLeft + : ""; + + return ( + + {chainInfo.shortName} + + ); +} + +/** + * Inline chain info for modals and detail views + */ +interface ChainInfoDisplayProps { + network: string; + tokenId?: bigint; +} + +const infoRow = css({ + display: "flex", + alignItems: "center", + gap: "8px", + fontSize: "14px", + color: "gray.600", + marginTop: "8px", +}); + +const infoLabel = css({ + fontWeight: "500", +}); + +export function ChainInfoDisplay({ network, tokenId }: ChainInfoDisplayProps) { + const chainInfo = CHAIN_INFO[network]; + + if (!chainInfo) { + return null; + } + + return ( +
+ Network: + + {tokenId !== undefined && ( + <> + Token: + #{tokenId.toString()} + + )} +
+ ); +} diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx index 27f28941b..c5da8d3e2 100644 --- a/website/components/ImageGenerator.tsx +++ b/website/components/ImageGenerator.tsx @@ -351,8 +351,8 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) { ], }; - // Call success callback - onSuccess?.(newTokenId, imageUrl, metadata); + // Call success callback with network info + onSuccess?.(newTokenId, imageUrl, metadata, network); // Track success with analytics trackEvent("x402-image-generated", { diff --git a/website/components/ImageModal.tsx b/website/components/ImageModal.tsx index ea669f686..9f99e176d 100644 --- a/website/components/ImageModal.tsx +++ b/website/components/ImageModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from "react"; import { ImageModalProps } from "../types/components"; import { useToast } from "./Toast"; +import { ChainInfoDisplay } from "./ChainBadge"; import * as styles from "../layouts/styles"; // Bildvergrößerungs-Modal Komponente @@ -47,10 +48,11 @@ export function ImageModal({ image, onClose }: ImageModalProps) { ✕ {image.alt} - {(image.title || image.description) && ( + {(image.title || image.description || image.network) && (
{image.title &&

{image.title}

} {image.description &&

{image.description}

} + {image.network && }