From d5306e90669300e4015ee5b89547290f2616c241 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 18:19:40 +0100
Subject: [PATCH 01/46] Create MULTICHAIN_EXPANSION_PROPOSAL.md
---
website/MULTICHAIN_EXPANSION_PROPOSAL.md | 159 +++++++++++++++++++++++
1 file changed, 159 insertions(+)
create mode 100644 website/MULTICHAIN_EXPANSION_PROPOSAL.md
diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
new file mode 100644
index 000000000..155d3c3be
--- /dev/null
+++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
@@ -0,0 +1,159 @@
+# Multi-Chain Expansion Proposal
+
+> Migration von GenImNFT, CollectorNFT auf CAIP-2 Pattern
+
+## Aktueller Zustand
+
+| Contract | Optimism | Base | Multi-Chain Ready |
+|----------|:--------:|:----:|:-----------------:|
+| **SupportV2** | ✅ | ✅ | ✅ Ja |
+| **GenImNFTv4** | ✅ `0x80f95d33...` | ❌ | ❌ Nein |
+| **CollectorNFTv1** | ✅ `0x584c40d8...` | ❌ | ❌ Nein |
+| **LLMv1** | ✅ | ❌ | ❌ (out of scope) |
+
+**Problem:** GenImNFT/CollectorNFT nutzen `PUBLIC_ENV__CHAIN_NAME` (Build-Zeit). SupportV2 nutzt CAIP-2 Address-Maps (Runtime).
+
+---
+
+## Implementierungsplan
+
+| Phase | Was | Status |
+|-------|-----|--------|
+| **1** | getChain.ts → CAIP-2 Pattern für alle Contracts | 🔜 Next |
+| **2** | GenImNFT-Komponenten auf neues Pattern migrieren | ⬜ Geplant |
+| **3** | CollectorNFT-Komponenten migrieren | ⬜ Geplant |
+| **4** | GenImNFTv4 auf Base deployen | ⬜ Später |
+| **5** | CollectorNFTv1 auf Base deployen (nach GenImNFT) | ⬜ Später |
+
+> **Out of Scope:** LLMv1 Migration (Assistent-Seite) - kommt später.
+
+---
+
+## Phase 1: getChain.ts Refactoring
+
+**Ziel:** CAIP-2 Strings als primärer Key überall (konsistent mit scw_js).
+
+**Prinzip:**
+- CAIP-2 `"eip155:10"` ist die Wahrheit
+- wagmi/viem `chainId: number` wird nur an den Grenzen übersetzt
+
+```typescript
+// ═══════════════════════════════════════════════════════════════
+// CAIP-2 Utilities
+// ═══════════════════════════════════════════════════════════════
+
+/** wagmi chainId → CAIP-2 */
+export const toCAIP2 = (chainId: number): string => `eip155:${chainId}`;
+
+/** CAIP-2 → wagmi chainId */
+export const fromCAIP2 = (network: string): number => {
+ const match = network.match(/^eip155:(\d+)$/);
+ if (!match) throw new Error(`Invalid CAIP-2: ${network}`);
+ return parseInt(match[1], 10);
+};
+
+/** CAIP-2 → viem Chain object */
+export function getViemChain(network: string): Chain {
+ switch (network) {
+ case "eip155:10": return optimism;
+ case "eip155:11155420": return optimismSepolia;
+ case "eip155:8453": return base;
+ case "eip155:84532": return baseSepolia;
+ default: throw new Error(`Unsupported network: ${network}`);
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════
+// Contract Adressen (CAIP-2 als Key - wie scw_js)
+// ═══════════════════════════════════════════════════════════════
+
+const GENAI_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
+ "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
+ // "eip155:8453": "0x...", // Base - nach Deployment
+};
+
+const COLLECTOR_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea",
+ // Nur Optimism (benötigt GenImNFT auf gleicher Chain)
+};
+
+const SUPPORT_V2_ADDRESSES: Record = {
+ "eip155:10": "0x4ca63f8A4Cd56287E854f53E18ca482D74391316",
+ "eip155:8453": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694",
+ "eip155:11155420": "0x9859431b682e861b19e87Db14a04944BC747AB6d",
+ "eip155:84532": "0xaB44BE78499721b593a0f4BE2099b246e9C53B57",
+};
+
+// ═══════════════════════════════════════════════════════════════
+// Config-Funktionen (nehmen CAIP-2, wie scw_js)
+// ═══════════════════════════════════════════════════════════════
+
+export function getGenAiNFTConfig(network: string) {
+ const address = GENAI_NFT_ADDRESSES[network];
+ if (!address) return null;
+ return { address, abi: GenImNFTv3ABI, chainId: fromCAIP2(network) };
+}
+
+export function getCollectorNFTConfig(network: string) {
+ const address = COLLECTOR_NFT_ADDRESSES[network];
+ if (!address) return null;
+ return { address, abi: CollectorNFTv1ABI, chainId: fromCAIP2(network) };
+}
+
+// ═══════════════════════════════════════════════════════════════
+// Unterstützte Networks (CAIP-2 Arrays)
+// ═══════════════════════════════════════════════════════════════
+
+export const GENAI_NFT_NETWORKS = Object.keys(GENAI_NFT_ADDRESSES);
+export const COLLECTOR_NFT_NETWORKS = Object.keys(COLLECTOR_NFT_ADDRESSES);
+```
+
+---
+
+## Automatische Network-Wahl
+
+**Kein Chain-Selector** - Network wird automatisch basierend auf User-Wallet gewählt:
+
+```typescript
+// hooks/useAutoNetwork.ts
+export function useAutoNetwork(supportedNetworks: string[]): string {
+ const { chain } = useAccount();
+
+ if (chain) {
+ const userNetwork = toCAIP2(chain.id);
+ if (supportedNetworks.includes(userNetwork)) {
+ return userNetwork;
+ }
+ }
+
+ // Fallback: Erstes unterstütztes Network (Optimism)
+ return supportedNetworks[0];
+}
+
+// Verwendung
+const network = useAutoNetwork(GENAI_NFT_NETWORKS); // "eip155:10"
+const config = getGenAiNFTConfig(network);
+```
+
+---
+
+## Betroffene Dateien
+
+**Phase 1 (getChain.ts):**
+- [utils/getChain.ts](utils/getChain.ts)
+- [utils/nodeChainUtils.ts](utils/nodeChainUtils.ts)
+
+**Phase 2-3 (Komponenten):**
+- [components/NFTCard.tsx](components/NFTCard.tsx)
+- [components/MyNFTList.tsx](components/MyNFTList.tsx)
+- [components/SimpleCollectButton.tsx](components/SimpleCollectButton.tsx)
+- [utils/nftLoader.ts](utils/nftLoader.ts)
+
+---
+
+## Nächste Schritte
+
+1. ✅ Review Proposal
+2. **Phase 1 starten**: getChain.ts auf CAIP-2 Pattern umstellen
+3. GenImNFT-Komponenten migrieren
From cf4733bc6603fdb8bb01342f4803a0444b96f4ac Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 18:48:26 +0100
Subject: [PATCH 02/46] Update MULTICHAIN_EXPANSION_PROPOSAL.md
---
website/MULTICHAIN_EXPANSION_PROPOSAL.md | 569 +++++++++++++++++++++--
1 file changed, 540 insertions(+), 29 deletions(-)
diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
index 155d3c3be..09a640a54 100644
--- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md
+++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
@@ -33,9 +33,10 @@
**Ziel:** CAIP-2 Strings als primärer Key überall (konsistent mit scw_js).
-**Prinzip:**
+**Prinzipien:**
- CAIP-2 `"eip155:10"` ist die Wahrheit
- wagmi/viem `chainId: number` wird nur an den Grenzen übersetzt
+- **Getrennte Maps für Mainnet/Testnet** (Option A) - kein Risiko Mainnet/Testnet zu mischen
```typescript
// ═══════════════════════════════════════════════════════════════
@@ -64,51 +65,229 @@ export function getViemChain(network: string): Chain {
}
// ═══════════════════════════════════════════════════════════════
-// Contract Adressen (CAIP-2 als Key - wie scw_js)
+// Contract Adressen (CAIP-2 als Key, getrennt nach Mainnet/Testnet)
// ═══════════════════════════════════════════════════════════════
-const GENAI_NFT_ADDRESSES: Record = {
+/** GenImNFT - Mainnet */
+const MAINNET_GENAI_NFT_ADDRESSES: Record = {
"eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
// "eip155:8453": "0x...", // Base - nach Deployment
};
-const COLLECTOR_NFT_ADDRESSES: Record = {
- "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea",
- // Nur Optimism (benötigt GenImNFT auf gleicher Chain)
+/** GenImNFT - Testnet */
+const TESTNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
+ // "eip155:84532": "0x...", // Base Sepolia - nach Deployment
};
-const SUPPORT_V2_ADDRESSES: Record = {
- "eip155:10": "0x4ca63f8A4Cd56287E854f53E18ca482D74391316",
- "eip155:8453": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694",
- "eip155:11155420": "0x9859431b682e861b19e87Db14a04944BC747AB6d",
- "eip155:84532": "0xaB44BE78499721b593a0f4BE2099b246e9C53B57",
-};
+/** Aktive Adressen basierend auf VITE_USE_TESTNET */
+const GENAI_NFT_ADDRESSES = USE_TESTNET
+ ? TESTNET_GENAI_NFT_ADDRESSES
+ : MAINNET_GENAI_NFT_ADDRESSES;
-// ═══════════════════════════════════════════════════════════════
-// Config-Funktionen (nehmen CAIP-2, wie scw_js)
-// ═══════════════════════════════════════════════════════════════
+// Analog für CollectorNFT, SupportV2...
+```
-export function getGenAiNFTConfig(network: string) {
- const address = GENAI_NFT_ADDRESSES[network];
- if (!address) return null;
- return { address, abi: GenImNFTv3ABI, chainId: fromCAIP2(network) };
+---
+
+## Offene Frage: Shared Package
+
+Die CAIP-2 Utilities (`toCAIP2`, `fromCAIP2`, `getViemChain`, Address-Maps) werden in mehreren Projekten benötigt:
+- `website/` (Vite + TypeScript)
+- `scw_js/` (Node.js + JavaScript)
+- `x402_facilitator/` (Node.js + JavaScript)
+
+### Empfehlung: TypeScript Package mit file: Link
+
+**Struktur:**
+```
+fretchen.github.io/
+├── shared/
+│ └── chain-utils/
+│ ├── package.json
+│ ├── tsconfig.json
+│ ├── src/
+│ │ ├── index.ts # CAIP-2 utilities
+│ │ └── addresses.ts # Contract-Adressen (Mainnet/Testnet Maps)
+│ └── dist/ # Generiert bei npm install
+│ ├── index.js
+│ ├── index.d.ts
+│ └── ...
+├── website/
+├── scw_js/
+└── x402_facilitator/
+```
+
+**shared/chain-utils/package.json:**
+```json
+{
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./addresses": {
+ "types": "./dist/addresses.d.ts",
+ "default": "./dist/addresses.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc",
+ "prepare": "npm run build"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0"
+ }
}
+```
-export function getCollectorNFTConfig(network: string) {
- const address = COLLECTOR_NFT_ADDRESSES[network];
- if (!address) return null;
- return { address, abi: CollectorNFTv1ABI, chainId: fromCAIP2(network) };
+**shared/chain-utils/tsconfig.json:**
+```json
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "declaration": true,
+ "outDir": "dist",
+ "strict": true
+ },
+ "include": ["src"]
}
+```
-// ═══════════════════════════════════════════════════════════════
-// Unterstützte Networks (CAIP-2 Arrays)
-// ═══════════════════════════════════════════════════════════════
+**Konsumenten (scw_js, x402_facilitator, website):**
+```json
+{
+ "dependencies": {
+ "@fretchen/chain-utils": "file:../shared/chain-utils"
+ }
+}
+```
+
+**Workflow:**
+1. `npm install` in `shared/chain-utils/` → `prepare` Script → `tsc` → `dist/`
+2. `npm install` in Konsumenten → kopiert fertiges `dist/` nach `node_modules/`
+3. Import: `import { toCAIP2, fromCAIP2 } from "@fretchen/chain-utils"`
+
+### Monorepo: Nötig oder nicht?
+
+**Aktuell reicht `file:` Link** - kein formales Monorepo-Setup nötig.
+
+| Ansatz | Wann sinnvoll |
+|--------|---------------|
+| **file: Links (aktuell)** | 1-2 shared packages, wenige Abhängigkeiten |
+| **npm workspaces** | 3+ packages, gemeinsame devDependencies |
+| **pnpm workspaces** | Viele packages, Disk-Space-Optimierung |
+| **Turborepo/Nx** | Build-Caching, komplexe CI/CD |
+
+**Empfehlung:** Mit `file:` Links starten. Falls später mehr shared packages entstehen oder `npm install` zu langsam wird → npm workspaces nachrüsten:
+
+```json
+// Root package.json (optional, für später)
+{
+ "workspaces": [
+ "shared/*",
+ "website",
+ "scw_js",
+ "x402_facilitator"
+ ]
+}
+```
+
+Dann reicht ein `npm install` im Root für alle Projekte.
+
+### Kompatibilitätsanalyse: scw_js & x402_facilitator
+
+**Aktuelle Implementierungen:**
+
+| Funktion | scw_js | x402_facilitator | Shared? |
+|----------|--------|------------------|---------|
+| `toCAIP2(chainId)` | ❌ fehlt | ❌ fehlt | ✅ Neu |
+| `fromCAIP2(network)` | ❌ implizit | `getChainId()` | ✅ |
+| `getViemChain(network)` | ✅ vorhanden | `getChain()` | ✅ |
+| Contract Adressen | `getGenImgContractConfig()` | `getChainConfig()` | ⚠️ |
+| USDC Config | `getUSDCConfig()` | `TOKEN_INFO` | ⚠️ |
+| RPC URLs | ❌ fehlt | `getRpcUrl()` | ❌ Lokal |
+
+**Unterschiedliche Strukturen (später vereinheitlichen):**
+
+scw_js:
+```javascript
+getUSDCConfig("eip155:10") → { address, decimals, usdcName, ... }
+```
+
+x402_facilitator:
+```javascript
+getChainConfig("eip155:10") → { chain, rpcUrl, USDC_ADDRESS, ... }
+TOKEN_INFO["eip155:10"]["0x..."] → { address, symbol, ... }
+```
+
+**Layered Shared Package:**
+
+```
+@fretchen/chain-utils/src/
+├── index.ts # Core: toCAIP2, fromCAIP2, getViemChain
+├── addresses.ts # Maps: GENAI_NFT_ADDRESSES, USDC_ADDRESSES, etc.
+└── usdc.ts # USDC_NAMES per network
+```
+
+**Was shared wird:**
+```typescript
+// Core utilities
+export const toCAIP2 = (chainId: number) => `eip155:${chainId}`;
+export const fromCAIP2 = (network: string) => parseInt(network.split(":")[1], 10);
+export function getViemChain(network: string): Chain { ... }
-export const GENAI_NFT_NETWORKS = Object.keys(GENAI_NFT_ADDRESSES);
-export const COLLECTOR_NFT_NETWORKS = Object.keys(COLLECTOR_NFT_ADDRESSES);
+// Address Maps
+export const GENAI_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
+ "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
+};
+export const USDC_ADDRESSES: Record = {
+ "eip155:10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
+ "eip155:11155420": "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
+ "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
+ "eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+};
+export const USDC_NAMES: Record = {
+ "eip155:10": "USD Coin",
+ "eip155:11155420": "USDC",
+ "eip155:8453": "USD Coin",
+ "eip155:84532": "USDC",
+};
+```
+
+**Was lokal bleibt (projekt-spezifisch):**
+- `getRpcUrl()` - x402 env-var Logik
+- `getChainConfig()` Struktur - x402 bündelt mehr
+- ABIs - unterschiedliche Imports pro Projekt
+
+**Migration in scw_js/x402_facilitator:**
+```javascript
+// Vorher
+export function getViemChain(network) { switch... }
+
+// Nachher
+import { getViemChain, fromCAIP2, USDC_ADDRESSES } from "@fretchen/chain-utils";
+
+// Lokale Funktionen nutzen shared imports
+export function getChainConfig(network) {
+ return {
+ chain: getViemChain(network), // ← shared
+ rpcUrl: getRpcUrl(network), // ← lokal
+ USDC_ADDRESS: USDC_ADDRESSES[network], // ← shared
+ };
+}
```
+> **Später vereinheitlichen:** Die unterschiedlichen Config-Strukturen (`getUSDCConfig` vs `TOKEN_INFO`) können in einem späteren Schritt konsolidiert werden.
+
---
## Automatische Network-Wahl
@@ -138,6 +317,338 @@ const config = getGenAiNFTConfig(network);
---
+## Phase 2: GenImNFT-Komponenten migrieren
+
+**Betroffene Komponenten:**
+- `MyNFTList.tsx` - 4 Stellen mit `genAiNFTContractConfig`
+- `NFTCard.tsx` - 8 Stellen mit `genAiNFTContractConfig`
+- `NFTList.tsx` - 1 Stelle
+- `EntryNftImage.tsx` - 2 Stellen
+- `PublicNFTList.tsx` - 1 Stelle
+- `nftLoader.ts` - 2 Stellen
+
+**Aktuelles Pattern:**
+```tsx
+// MyNFTList.tsx - VORHER
+import { getChain, genAiNFTContractConfig } from "../utils/getChain";
+
+const chain = getChain(); // Statisch, Build-Zeit
+
+const { data: userBalance } = useReadContract({
+ ...genAiNFTContractConfig, // Statische Config
+ functionName: "balanceOf",
+ args: address ? [address] : undefined,
+ chainId: chain.id, // Manuell hinzugefügt
+});
+```
+
+**Neues Pattern mit chain-utils:**
+```tsx
+// MyNFTList.tsx - NACHHER
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTConfig, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+
+// Network automatisch basierend auf User-Wallet
+const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+const config = getGenAiNFTConfig(network); // Enthält address, abi, chainId
+
+const { data: userBalance } = useReadContract({
+ ...config, // chainId ist bereits enthalten!
+ functionName: "balanceOf",
+ args: address ? [address] : undefined,
+ // chainId: nicht mehr nötig, kommt aus config
+});
+```
+
+**Migration pro Komponente:**
+
+| Komponente | Änderungen | Aufwand |
+|------------|------------|---------|
+| `MyNFTList.tsx` | Import ändern, `useAutoNetwork` hinzufügen, 4x Config ersetzen | 30min |
+| `NFTCard.tsx` | Import ändern, `useAutoNetwork` hinzufügen, 8x Config ersetzen | 45min |
+| `NFTList.tsx` | Import ändern, 1x Config ersetzen | 15min |
+| `EntryNftImage.tsx` | Import ändern, 2x Config ersetzen | 15min |
+| `PublicNFTList.tsx` | Import ändern, 1x Config ersetzen | 15min |
+| `nftLoader.ts` | Parameter `network` hinzufügen, Config dynamisch | 30min |
+
+**nftLoader.ts Sonderfall:**
+```typescript
+// VORHER - statisch
+export async function loadNFTMetadata(tokenID: number, publicClient: PublicClient) {
+ const tokenURIResult = await publicClient.readContract({
+ address: genAiNFTContractConfig.address,
+ abi: genAiNFTContractConfig.abi,
+ functionName: "tokenURI",
+ args: [BigInt(tokenID)],
+ });
+}
+
+// NACHHER - network als Parameter
+export async function loadNFTMetadata(
+ tokenID: number,
+ publicClient: PublicClient,
+ network: string // NEU: CAIP-2 network
+) {
+ const config = getGenAiNFTConfig(network);
+ if (!config) throw new Error(`GenImNFT not available on ${network}`);
+
+ const tokenURIResult = await publicClient.readContract({
+ address: config.address,
+ abi: config.abi,
+ functionName: "tokenURI",
+ args: [BigInt(tokenID)],
+ });
+}
+```
+
+---
+
+## Phase 3: CollectorNFT-Komponenten migrieren
+
+**Betroffene Komponenten:**
+- `SimpleCollectButton.tsx` - 2 Stellen mit `collectorNFTContractConfig`
+
+**Aktuelles Pattern:**
+```tsx
+// VORHER
+import { collectorNFTContractConfig, getChain } from "../utils/getChain";
+
+const chain = getChain();
+const isCorrectNetwork = chainId === chain.id;
+
+const { data: mintStats } = useReadContract({
+ ...collectorNFTContractConfig,
+ functionName: "getMintStats",
+ chainId: chain.id,
+});
+```
+
+**Neues Pattern:**
+```tsx
+// NACHHER
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getCollectorNFTConfig, COLLECTOR_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
+
+const network = useAutoNetwork(COLLECTOR_NFT_NETWORKS);
+const config = getCollectorNFTConfig(network);
+
+// CollectorNFT nur auf Optimism → Network-Check anpassen
+const isCorrectNetwork = config && chainId === config.chainId;
+
+const { data: mintStats } = useReadContract({
+ ...config,
+ functionName: "getMintStats",
+ // chainId bereits in config
+});
+```
+
+**Besonderheit CollectorNFT:**
+- Nur auf Optimism deployed (referenziert GenImNFT)
+- `COLLECTOR_NFT_NETWORKS` = `["eip155:10"]` (nur Mainnet)
+- Wenn User auf Base → Button disabled oder versteckt
+
+---
+
+## Phase 4: GenImNFTv4 auf Base deployen
+
+**Voraussetzungen:**
+- Phase 1-2 abgeschlossen (chain-utils + Frontend-Migration)
+- Deploy-Script für Base anpassen
+
+**Schritte:**
+1. `eth/scripts/deploy-genimg-v4.ts` für Base erweitern
+2. Deploy auf Base Mainnet
+3. Contract verifizieren
+4. Agent-Wallet autorisieren: `authorizeAgentWallet(0xAAEBC1441323...)`
+5. Address in `@fretchen/chain-utils` hinzufügen:
+ ```typescript
+ const MAINNET_GENAI_NFT_ADDRESSES = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
+ "eip155:8453": "0x...", // NEU
+ };
+ ```
+6. `scw_js/getChain.js` aktualisieren (nutzt dann shared package)
+7. Test: Image-Generation auf Base
+
+---
+
+## Phase 5: CollectorNFTv1 auf Base deployen
+
+**Voraussetzungen:**
+- Phase 4 abgeschlossen (GenImNFT auf Base)
+- CollectorNFT referenziert GenImNFT → muss zuerst existieren
+
+**Schritte:**
+1. `eth/scripts/deploy-collector-nft.ts` für Base anpassen
+2. Deploy mit `genImNFTAddress` = Base GenImNFT Adresse
+3. Contract verifizieren
+4. Address in `@fretchen/chain-utils` hinzufügen
+5. Test: Collect-Button funktioniert auf Base
+
+---
+
+## Zusammenfassung: Wie chain-utils unterstützt
+
+| Phase | Ohne chain-utils | Mit chain-utils |
+|-------|------------------|-----------------|
+| **2** | Jede Komponente hat eigene Chain-Logik | `useAutoNetwork()` + `getGenAiNFTConfig()` |
+| **3** | Duplizierte Logik für CollectorNFT | Gleiches Pattern wie Phase 2 |
+| **4** | Adressen in 3 Projekten manuell pflegen | Eine Stelle in shared package |
+| **5** | Nochmal manuelle Pflege | Nur `COLLECTOR_NFT_ADDRESSES` erweitern |
+
+**Kritischer Vorteil:** Wenn Base-Adressen hinzugefügt werden, reicht ein Update in `@fretchen/chain-utils` → alle Projekte (website, scw_js, x402) profitieren automatisch nach `npm install`.
+
+---
+
+## Migration: scw_js
+
+**Betroffene Dateien:**
+- `scw_js/getChain.js` - Hauptdatei mit Chain-Logik
+
+**Aktuell dupliziert:**
+```javascript
+// scw_js/getChain.js - VORHER
+export function getGenImgContractConfig(network) {
+ switch (network) {
+ case "eip155:11155420":
+ return { address: "0x10827cC42a09D0BAD2d43134C69F0e776D853D85" };
+ case "eip155:10":
+ return { address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb" };
+ }
+}
+
+export function getViemChain(network) {
+ switch (network) { ... } // Dupliziert
+}
+
+export function getUSDCConfig(network) {
+ switch (network) { ... } // Dupliziert
+}
+```
+
+**Nach Migration:**
+```javascript
+// scw_js/getChain.js - NACHHER
+import {
+ getViemChain,
+ fromCAIP2,
+ GENAI_NFT_ADDRESSES,
+ USDC_ADDRESSES,
+ USDC_NAMES
+} from "@fretchen/chain-utils";
+
+// Re-export für Abwärtskompatibilität
+export { getViemChain, fromCAIP2 };
+
+// Lokale Wrapper (falls nötig)
+export function getGenImgContractConfig(network) {
+ const address = GENAI_NFT_ADDRESSES[network];
+ if (!address) throw new Error(`GenImg not deployed on ${network}`);
+ return { address };
+}
+
+export function getUSDCConfig(network) {
+ return {
+ address: USDC_ADDRESSES[network],
+ name: USDC_NAMES[network],
+ chainId: fromCAIP2(network),
+ decimals: 6,
+ version: "2",
+ };
+}
+```
+
+**Gelöschter Code:** ~80 Zeilen (switch statements für Adressen)
+
+---
+
+## Migration: x402_facilitator
+
+**Betroffene Dateien:**
+- `x402_facilitator/chain_utils.js` - Hauptdatei mit Chain-Logik
+
+**Aktuell dupliziert:**
+```javascript
+// x402_facilitator/chain_utils.js - VORHER
+export function getChainConfig(network) {
+ if (network === "eip155:10") {
+ return {
+ chain: optimism,
+ GENIMG_V4_ADDRESS: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", // Dupliziert!
+ USDC_ADDRESS: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Dupliziert!
+ ...
+ };
+ }
+}
+
+export const TOKEN_INFO = {
+ "eip155:10": {
+ "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85": { ... } // Dupliziert!
+ },
+};
+```
+
+**Nach Migration:**
+```javascript
+// x402_facilitator/chain_utils.js - NACHHER
+import {
+ getViemChain,
+ GENAI_NFT_ADDRESSES,
+ LLMV1_ADDRESSES,
+ USDC_ADDRESSES,
+ USDC_NAMES
+} from "@fretchen/chain-utils";
+
+export function getChainConfig(network) {
+ return {
+ chain: getViemChain(network), // ← aus shared
+ rpcUrl: getRpcUrl(network), // ← bleibt lokal (env vars)
+ GENIMG_V4_ADDRESS: GENAI_NFT_ADDRESSES[network] || null, // ← aus shared
+ LLMV1_ADDRESS: LLMV1_ADDRESSES[network] || null, // ← aus shared
+ USDC_ADDRESS: USDC_ADDRESSES[network], // ← aus shared
+ USDC_NAME: USDC_NAMES[network], // ← aus shared
+ };
+}
+
+// TOKEN_INFO kann dynamisch generiert werden
+export function getTokenInfo(network, tokenAddress) {
+ if (tokenAddress === USDC_ADDRESSES[network]) {
+ return {
+ address: tokenAddress,
+ symbol: "USDC",
+ name: USDC_NAMES[network],
+ decimals: 6,
+ version: "2",
+ };
+ }
+ throw new Error(`Unsupported token: ${tokenAddress}`);
+}
+```
+
+**Gelöschter Code:** ~60 Zeilen (hardcoded Adressen, TOKEN_INFO Objekt)
+
+---
+
+## Aktualisierter Implementierungsplan
+
+| Phase | Was | Projekte | Status |
+|-------|-----|----------|--------|
+| **1a** | `@fretchen/chain-utils` erstellen | shared/ | 🔜 Next |
+| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ⬜ Geplant |
+| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ Geplant |
+| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ Geplant |
+| **3** | CollectorNFT-Komponenten migrieren | website/ | ⬜ Geplant |
+| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später |
+| **5** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Später |
+
+**Neue Reihenfolge-Begründung:**
+- 1a → Shared package muss zuerst existieren
+- 1b/1c → Backend-Projekte migrieren (einfacher, weniger UI-Abhängigkeiten)
+- 2/3 → Frontend migrieren (komplexer, Tests nötig)
+- 4/5 → Neue Deployments (profitieren von shared package)
+
+---
+
## Betroffene Dateien
**Phase 1 (getChain.ts):**
From 0eef461666d8cf7e598c5ec635575a79e64d87ea Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 18:53:07 +0100
Subject: [PATCH 03/46] Update MULTICHAIN_EXPANSION_PROPOSAL.md
---
website/MULTICHAIN_EXPANSION_PROPOSAL.md | 699 ++++++-----------------
1 file changed, 168 insertions(+), 531 deletions(-)
diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
index 09a640a54..398ffc4ae 100644
--- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md
+++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
@@ -1,103 +1,33 @@
-# Multi-Chain Expansion Proposal
+# Multi-Chain Expansion Plan
-> Migration von GenImNFT, CollectorNFT auf CAIP-2 Pattern
+> Migration von GenImNFT, CollectorNFT auf CAIP-2 Pattern mit shared `@fretchen/chain-utils` Package
## Aktueller Zustand
| Contract | Optimism | Base | Multi-Chain Ready |
|----------|:--------:|:----:|:-----------------:|
| **SupportV2** | ✅ | ✅ | ✅ Ja |
-| **GenImNFTv4** | ✅ `0x80f95d33...` | ❌ | ❌ Nein |
-| **CollectorNFTv1** | ✅ `0x584c40d8...` | ❌ | ❌ Nein |
+| **GenImNFTv4** | ✅ | ❌ | ❌ Nein |
+| **CollectorNFTv1** | ✅ | ❌ | ❌ Nein |
| **LLMv1** | ✅ | ❌ | ❌ (out of scope) |
-**Problem:** GenImNFT/CollectorNFT nutzen `PUBLIC_ENV__CHAIN_NAME` (Build-Zeit). SupportV2 nutzt CAIP-2 Address-Maps (Runtime).
-
---
## Implementierungsplan
-| Phase | Was | Status |
-|-------|-----|--------|
-| **1** | getChain.ts → CAIP-2 Pattern für alle Contracts | 🔜 Next |
-| **2** | GenImNFT-Komponenten auf neues Pattern migrieren | ⬜ Geplant |
-| **3** | CollectorNFT-Komponenten migrieren | ⬜ Geplant |
-| **4** | GenImNFTv4 auf Base deployen | ⬜ Später |
-| **5** | CollectorNFTv1 auf Base deployen (nach GenImNFT) | ⬜ Später |
-
-> **Out of Scope:** LLMv1 Migration (Assistent-Seite) - kommt später.
-
----
-
-## Phase 1: getChain.ts Refactoring
-
-**Ziel:** CAIP-2 Strings als primärer Key überall (konsistent mit scw_js).
-
-**Prinzipien:**
-- CAIP-2 `"eip155:10"` ist die Wahrheit
-- wagmi/viem `chainId: number` wird nur an den Grenzen übersetzt
-- **Getrennte Maps für Mainnet/Testnet** (Option A) - kein Risiko Mainnet/Testnet zu mischen
-
-```typescript
-// ═══════════════════════════════════════════════════════════════
-// CAIP-2 Utilities
-// ═══════════════════════════════════════════════════════════════
-
-/** wagmi chainId → CAIP-2 */
-export const toCAIP2 = (chainId: number): string => `eip155:${chainId}`;
-
-/** CAIP-2 → wagmi chainId */
-export const fromCAIP2 = (network: string): number => {
- const match = network.match(/^eip155:(\d+)$/);
- if (!match) throw new Error(`Invalid CAIP-2: ${network}`);
- return parseInt(match[1], 10);
-};
-
-/** CAIP-2 → viem Chain object */
-export function getViemChain(network: string): Chain {
- switch (network) {
- case "eip155:10": return optimism;
- case "eip155:11155420": return optimismSepolia;
- case "eip155:8453": return base;
- case "eip155:84532": return baseSepolia;
- default: throw new Error(`Unsupported network: ${network}`);
- }
-}
-
-// ═══════════════════════════════════════════════════════════════
-// Contract Adressen (CAIP-2 als Key, getrennt nach Mainnet/Testnet)
-// ═══════════════════════════════════════════════════════════════
-
-/** GenImNFT - Mainnet */
-const MAINNET_GENAI_NFT_ADDRESSES: Record = {
- "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- // "eip155:8453": "0x...", // Base - nach Deployment
-};
-
-/** GenImNFT - Testnet */
-const TESTNET_GENAI_NFT_ADDRESSES: Record = {
- "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
- // "eip155:84532": "0x...", // Base Sepolia - nach Deployment
-};
-
-/** Aktive Adressen basierend auf VITE_USE_TESTNET */
-const GENAI_NFT_ADDRESSES = USE_TESTNET
- ? TESTNET_GENAI_NFT_ADDRESSES
- : MAINNET_GENAI_NFT_ADDRESSES;
-
-// Analog für CollectorNFT, SupportV2...
-```
+| Phase | Was | Projekte | Status |
+|-------|-----|----------|--------|
+| **1a** | `@fretchen/chain-utils` erstellen | shared/ | 🔜 Next |
+| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ⬜ |
+| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ |
+| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ |
+| **3** | CollectorNFT-Komponenten migrieren | website/ | ⬜ |
+| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später |
+| **5** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Später |
---
-## Offene Frage: Shared Package
-
-Die CAIP-2 Utilities (`toCAIP2`, `fromCAIP2`, `getViemChain`, Address-Maps) werden in mehreren Projekten benötigt:
-- `website/` (Vite + TypeScript)
-- `scw_js/` (Node.js + JavaScript)
-- `x402_facilitator/` (Node.js + JavaScript)
-
-### Empfehlung: TypeScript Package mit file: Link
+## Phase 1a: @fretchen/chain-utils erstellen
**Struktur:**
```
@@ -106,19 +36,12 @@ fretchen.github.io/
│ └── chain-utils/
│ ├── package.json
│ ├── tsconfig.json
-│ ├── src/
-│ │ ├── index.ts # CAIP-2 utilities
-│ │ └── addresses.ts # Contract-Adressen (Mainnet/Testnet Maps)
-│ └── dist/ # Generiert bei npm install
-│ ├── index.js
-│ ├── index.d.ts
-│ └── ...
-├── website/
-├── scw_js/
-└── x402_facilitator/
+│ └── src/
+│ ├── index.ts # Core utilities
+│ └── addresses.ts # Contract address maps
```
-**shared/chain-utils/package.json:**
+**package.json:**
```json
{
"name": "@fretchen/chain-utils",
@@ -127,134 +50,92 @@ fretchen.github.io/
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
- ".": {
- "types": "./dist/index.d.ts",
- "default": "./dist/index.js"
- },
- "./addresses": {
- "types": "./dist/addresses.d.ts",
- "default": "./dist/addresses.js"
- }
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
+ "./addresses": { "types": "./dist/addresses.d.ts", "default": "./dist/addresses.js" }
},
"scripts": {
"build": "tsc",
"prepare": "npm run build"
},
"devDependencies": {
- "typescript": "^5.0.0"
+ "typescript": "^5.0.0",
+ "viem": "^2.0.0"
}
}
```
-**shared/chain-utils/tsconfig.json:**
-```json
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "declaration": true,
- "outDir": "dist",
- "strict": true
- },
- "include": ["src"]
-}
-```
-
-**Konsumenten (scw_js, x402_facilitator, website):**
-```json
-{
- "dependencies": {
- "@fretchen/chain-utils": "file:../shared/chain-utils"
- }
-}
-```
-
-**Workflow:**
-1. `npm install` in `shared/chain-utils/` → `prepare` Script → `tsc` → `dist/`
-2. `npm install` in Konsumenten → kopiert fertiges `dist/` nach `node_modules/`
-3. Import: `import { toCAIP2, fromCAIP2 } from "@fretchen/chain-utils"`
-
-### Monorepo: Nötig oder nicht?
+**src/index.ts:**
+```typescript
+import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
+import type { Chain } from "viem";
-**Aktuell reicht `file:` Link** - kein formales Monorepo-Setup nötig.
+// ═══════════════════════════════════════════════════════════════
+// CAIP-2 Utilities
+// ═══════════════════════════════════════════════════════════════
-| Ansatz | Wann sinnvoll |
-|--------|---------------|
-| **file: Links (aktuell)** | 1-2 shared packages, wenige Abhängigkeiten |
-| **npm workspaces** | 3+ packages, gemeinsame devDependencies |
-| **pnpm workspaces** | Viele packages, Disk-Space-Optimierung |
-| **Turborepo/Nx** | Build-Caching, komplexe CI/CD |
+export const toCAIP2 = (chainId: number): string => `eip155:${chainId}`;
-**Empfehlung:** Mit `file:` Links starten. Falls später mehr shared packages entstehen oder `npm install` zu langsam wird → npm workspaces nachrüsten:
+export const fromCAIP2 = (network: string): number => {
+ const match = network.match(/^eip155:(\d+)$/);
+ if (!match) throw new Error(`Invalid CAIP-2: ${network}`);
+ return parseInt(match[1], 10);
+};
-```json
-// Root package.json (optional, für später)
-{
- "workspaces": [
- "shared/*",
- "website",
- "scw_js",
- "x402_facilitator"
- ]
+export function getViemChain(network: string): Chain {
+ switch (network) {
+ case "eip155:10": return optimism;
+ case "eip155:11155420": return optimismSepolia;
+ case "eip155:8453": return base;
+ case "eip155:84532": return baseSepolia;
+ default: throw new Error(`Unsupported network: ${network}`);
+ }
}
-```
-
-Dann reicht ein `npm install` im Root für alle Projekte.
-### Kompatibilitätsanalyse: scw_js & x402_facilitator
-
-**Aktuelle Implementierungen:**
+export * from "./addresses";
+```
-| Funktion | scw_js | x402_facilitator | Shared? |
-|----------|--------|------------------|---------|
-| `toCAIP2(chainId)` | ❌ fehlt | ❌ fehlt | ✅ Neu |
-| `fromCAIP2(network)` | ❌ implizit | `getChainId()` | ✅ |
-| `getViemChain(network)` | ✅ vorhanden | `getChain()` | ✅ |
-| Contract Adressen | `getGenImgContractConfig()` | `getChainConfig()` | ⚠️ |
-| USDC Config | `getUSDCConfig()` | `TOKEN_INFO` | ⚠️ |
-| RPC URLs | ❌ fehlt | `getRpcUrl()` | ❌ Lokal |
+**src/addresses.ts:**
+```typescript
+// ═══════════════════════════════════════════════════════════════
+// Contract Adressen (CAIP-2 als Key, getrennt nach Mainnet/Testnet)
+// ═══════════════════════════════════════════════════════════════
-**Unterschiedliche Strukturen (später vereinheitlichen):**
+// GenImNFT
+export const MAINNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
+ // "eip155:8453": "0x...", // Base - nach Deployment
+};
-scw_js:
-```javascript
-getUSDCConfig("eip155:10") → { address, decimals, usdcName, ... }
-```
+export const TESTNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
+};
-x402_facilitator:
-```javascript
-getChainConfig("eip155:10") → { chain, rpcUrl, USDC_ADDRESS, ... }
-TOKEN_INFO["eip155:10"]["0x..."] → { address, symbol, ... }
-```
+// CollectorNFT
+export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea",
+};
-**Layered Shared Package:**
+export const TESTNET_COLLECTOR_NFT_ADDRESSES: Record = {};
-```
-@fretchen/chain-utils/src/
-├── index.ts # Core: toCAIP2, fromCAIP2, getViemChain
-├── addresses.ts # Maps: GENAI_NFT_ADDRESSES, USDC_ADDRESSES, etc.
-└── usdc.ts # USDC_NAMES per network
-```
-
-**Was shared wird:**
-```typescript
-// Core utilities
-export const toCAIP2 = (chainId: number) => `eip155:${chainId}`;
-export const fromCAIP2 = (network: string) => parseInt(network.split(":")[1], 10);
-export function getViemChain(network: string): Chain { ... }
+// SupportV2
+export const MAINNET_SUPPORT_V2_ADDRESSES: Record = {
+ "eip155:10": "0x4ca63f8A4Cd56287E854f53E18ca482D74391316",
+ "eip155:8453": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694",
+};
-// Address Maps
-export const GENAI_NFT_ADDRESSES: Record = {
- "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
+export const TESTNET_SUPPORT_V2_ADDRESSES: Record = {
+ "eip155:11155420": "0x9859431b682e861b19e87Db14a04944BC747AB6d",
+ "eip155:84532": "0xaB44BE78499721b593a0f4BE2099b246e9C53B57",
};
+
+// USDC
export const USDC_ADDRESSES: Record = {
"eip155:10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
"eip155:11155420": "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
"eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
};
+
export const USDC_NAMES: Record = {
"eip155:10": "USD Coin",
"eip155:11155420": "USDC",
@@ -263,286 +144,37 @@ export const USDC_NAMES: Record = {
};
```
-**Was lokal bleibt (projekt-spezifisch):**
-- `getRpcUrl()` - x402 env-var Logik
-- `getChainConfig()` Struktur - x402 bündelt mehr
-- ABIs - unterschiedliche Imports pro Projekt
-
-**Migration in scw_js/x402_facilitator:**
-```javascript
-// Vorher
-export function getViemChain(network) { switch... }
-
-// Nachher
-import { getViemChain, fromCAIP2, USDC_ADDRESSES } from "@fretchen/chain-utils";
-
-// Lokale Funktionen nutzen shared imports
-export function getChainConfig(network) {
- return {
- chain: getViemChain(network), // ← shared
- rpcUrl: getRpcUrl(network), // ← lokal
- USDC_ADDRESS: USDC_ADDRESSES[network], // ← shared
- };
-}
-```
-
-> **Später vereinheitlichen:** Die unterschiedlichen Config-Strukturen (`getUSDCConfig` vs `TOKEN_INFO`) können in einem späteren Schritt konsolidiert werden.
-
----
-
-## Automatische Network-Wahl
-
-**Kein Chain-Selector** - Network wird automatisch basierend auf User-Wallet gewählt:
-
-```typescript
-// hooks/useAutoNetwork.ts
-export function useAutoNetwork(supportedNetworks: string[]): string {
- const { chain } = useAccount();
-
- if (chain) {
- const userNetwork = toCAIP2(chain.id);
- if (supportedNetworks.includes(userNetwork)) {
- return userNetwork;
- }
+**Konsumenten installieren via file: Link:**
+```json
+{
+ "dependencies": {
+ "@fretchen/chain-utils": "file:../shared/chain-utils"
}
-
- // Fallback: Erstes unterstütztes Network (Optimism)
- return supportedNetworks[0];
-}
-
-// Verwendung
-const network = useAutoNetwork(GENAI_NFT_NETWORKS); // "eip155:10"
-const config = getGenAiNFTConfig(network);
-```
-
----
-
-## Phase 2: GenImNFT-Komponenten migrieren
-
-**Betroffene Komponenten:**
-- `MyNFTList.tsx` - 4 Stellen mit `genAiNFTContractConfig`
-- `NFTCard.tsx` - 8 Stellen mit `genAiNFTContractConfig`
-- `NFTList.tsx` - 1 Stelle
-- `EntryNftImage.tsx` - 2 Stellen
-- `PublicNFTList.tsx` - 1 Stelle
-- `nftLoader.ts` - 2 Stellen
-
-**Aktuelles Pattern:**
-```tsx
-// MyNFTList.tsx - VORHER
-import { getChain, genAiNFTContractConfig } from "../utils/getChain";
-
-const chain = getChain(); // Statisch, Build-Zeit
-
-const { data: userBalance } = useReadContract({
- ...genAiNFTContractConfig, // Statische Config
- functionName: "balanceOf",
- args: address ? [address] : undefined,
- chainId: chain.id, // Manuell hinzugefügt
-});
-```
-
-**Neues Pattern mit chain-utils:**
-```tsx
-// MyNFTList.tsx - NACHHER
-import { useAutoNetwork } from "../hooks/useAutoNetwork";
-import { getGenAiNFTConfig, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
-
-// Network automatisch basierend auf User-Wallet
-const network = useAutoNetwork(GENAI_NFT_NETWORKS);
-const config = getGenAiNFTConfig(network); // Enthält address, abi, chainId
-
-const { data: userBalance } = useReadContract({
- ...config, // chainId ist bereits enthalten!
- functionName: "balanceOf",
- args: address ? [address] : undefined,
- // chainId: nicht mehr nötig, kommt aus config
-});
-```
-
-**Migration pro Komponente:**
-
-| Komponente | Änderungen | Aufwand |
-|------------|------------|---------|
-| `MyNFTList.tsx` | Import ändern, `useAutoNetwork` hinzufügen, 4x Config ersetzen | 30min |
-| `NFTCard.tsx` | Import ändern, `useAutoNetwork` hinzufügen, 8x Config ersetzen | 45min |
-| `NFTList.tsx` | Import ändern, 1x Config ersetzen | 15min |
-| `EntryNftImage.tsx` | Import ändern, 2x Config ersetzen | 15min |
-| `PublicNFTList.tsx` | Import ändern, 1x Config ersetzen | 15min |
-| `nftLoader.ts` | Parameter `network` hinzufügen, Config dynamisch | 30min |
-
-**nftLoader.ts Sonderfall:**
-```typescript
-// VORHER - statisch
-export async function loadNFTMetadata(tokenID: number, publicClient: PublicClient) {
- const tokenURIResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
- functionName: "tokenURI",
- args: [BigInt(tokenID)],
- });
}
-
-// NACHHER - network als Parameter
-export async function loadNFTMetadata(
- tokenID: number,
- publicClient: PublicClient,
- network: string // NEU: CAIP-2 network
-) {
- const config = getGenAiNFTConfig(network);
- if (!config) throw new Error(`GenImNFT not available on ${network}`);
-
- const tokenURIResult = await publicClient.readContract({
- address: config.address,
- abi: config.abi,
- functionName: "tokenURI",
- args: [BigInt(tokenID)],
- });
-}
-```
-
----
-
-## Phase 3: CollectorNFT-Komponenten migrieren
-
-**Betroffene Komponenten:**
-- `SimpleCollectButton.tsx` - 2 Stellen mit `collectorNFTContractConfig`
-
-**Aktuelles Pattern:**
-```tsx
-// VORHER
-import { collectorNFTContractConfig, getChain } from "../utils/getChain";
-
-const chain = getChain();
-const isCorrectNetwork = chainId === chain.id;
-
-const { data: mintStats } = useReadContract({
- ...collectorNFTContractConfig,
- functionName: "getMintStats",
- chainId: chain.id,
-});
-```
-
-**Neues Pattern:**
-```tsx
-// NACHHER
-import { useAutoNetwork } from "../hooks/useAutoNetwork";
-import { getCollectorNFTConfig, COLLECTOR_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
-
-const network = useAutoNetwork(COLLECTOR_NFT_NETWORKS);
-const config = getCollectorNFTConfig(network);
-
-// CollectorNFT nur auf Optimism → Network-Check anpassen
-const isCorrectNetwork = config && chainId === config.chainId;
-
-const { data: mintStats } = useReadContract({
- ...config,
- functionName: "getMintStats",
- // chainId bereits in config
-});
```
-**Besonderheit CollectorNFT:**
-- Nur auf Optimism deployed (referenziert GenImNFT)
-- `COLLECTOR_NFT_NETWORKS` = `["eip155:10"]` (nur Mainnet)
-- Wenn User auf Base → Button disabled oder versteckt
-
---
-## Phase 4: GenImNFTv4 auf Base deployen
-
-**Voraussetzungen:**
-- Phase 1-2 abgeschlossen (chain-utils + Frontend-Migration)
-- Deploy-Script für Base anpassen
-
-**Schritte:**
-1. `eth/scripts/deploy-genimg-v4.ts` für Base erweitern
-2. Deploy auf Base Mainnet
-3. Contract verifizieren
-4. Agent-Wallet autorisieren: `authorizeAgentWallet(0xAAEBC1441323...)`
-5. Address in `@fretchen/chain-utils` hinzufügen:
- ```typescript
- const MAINNET_GENAI_NFT_ADDRESSES = {
- "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- "eip155:8453": "0x...", // NEU
- };
- ```
-6. `scw_js/getChain.js` aktualisieren (nutzt dann shared package)
-7. Test: Image-Generation auf Base
+## Phase 1b: scw_js Migration
----
-
-## Phase 5: CollectorNFTv1 auf Base deployen
-
-**Voraussetzungen:**
-- Phase 4 abgeschlossen (GenImNFT auf Base)
-- CollectorNFT referenziert GenImNFT → muss zuerst existieren
-
-**Schritte:**
-1. `eth/scripts/deploy-collector-nft.ts` für Base anpassen
-2. Deploy mit `genImNFTAddress` = Base GenImNFT Adresse
-3. Contract verifizieren
-4. Address in `@fretchen/chain-utils` hinzufügen
-5. Test: Collect-Button funktioniert auf Base
-
----
-
-## Zusammenfassung: Wie chain-utils unterstützt
-
-| Phase | Ohne chain-utils | Mit chain-utils |
-|-------|------------------|-----------------|
-| **2** | Jede Komponente hat eigene Chain-Logik | `useAutoNetwork()` + `getGenAiNFTConfig()` |
-| **3** | Duplizierte Logik für CollectorNFT | Gleiches Pattern wie Phase 2 |
-| **4** | Adressen in 3 Projekten manuell pflegen | Eine Stelle in shared package |
-| **5** | Nochmal manuelle Pflege | Nur `COLLECTOR_NFT_ADDRESSES` erweitern |
-
-**Kritischer Vorteil:** Wenn Base-Adressen hinzugefügt werden, reicht ein Update in `@fretchen/chain-utils` → alle Projekte (website, scw_js, x402) profitieren automatisch nach `npm install`.
-
----
-
-## Migration: scw_js
-
-**Betroffene Dateien:**
-- `scw_js/getChain.js` - Hauptdatei mit Chain-Logik
+**Datei:** `scw_js/getChain.js`
-**Aktuell dupliziert:**
```javascript
-// scw_js/getChain.js - VORHER
-export function getGenImgContractConfig(network) {
- switch (network) {
- case "eip155:11155420":
- return { address: "0x10827cC42a09D0BAD2d43134C69F0e776D853D85" };
- case "eip155:10":
- return { address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb" };
- }
-}
-
-export function getViemChain(network) {
- switch (network) { ... } // Dupliziert
-}
-
-export function getUSDCConfig(network) {
- switch (network) { ... } // Dupliziert
-}
-```
-
-**Nach Migration:**
-```javascript
-// scw_js/getChain.js - NACHHER
+// NACHHER
import {
getViemChain,
fromCAIP2,
- GENAI_NFT_ADDRESSES,
+ MAINNET_GENAI_NFT_ADDRESSES,
+ TESTNET_GENAI_NFT_ADDRESSES,
USDC_ADDRESSES,
USDC_NAMES
} from "@fretchen/chain-utils";
-// Re-export für Abwärtskompatibilität
export { getViemChain, fromCAIP2 };
-// Lokale Wrapper (falls nötig)
export function getGenImgContractConfig(network) {
- const address = GENAI_NFT_ADDRESSES[network];
+ const addresses = { ...MAINNET_GENAI_NFT_ADDRESSES, ...TESTNET_GENAI_NFT_ADDRESSES };
+ const address = addresses[network];
if (!address) throw new Error(`GenImg not deployed on ${network}`);
return { address };
}
@@ -558,113 +190,118 @@ export function getUSDCConfig(network) {
}
```
-**Gelöschter Code:** ~80 Zeilen (switch statements für Adressen)
-
---
-## Migration: x402_facilitator
+## Phase 1c: x402_facilitator Migration
-**Betroffene Dateien:**
-- `x402_facilitator/chain_utils.js` - Hauptdatei mit Chain-Logik
+**Datei:** `x402_facilitator/chain_utils.js`
-**Aktuell dupliziert:**
```javascript
-// x402_facilitator/chain_utils.js - VORHER
-export function getChainConfig(network) {
- if (network === "eip155:10") {
- return {
- chain: optimism,
- GENIMG_V4_ADDRESS: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", // Dupliziert!
- USDC_ADDRESS: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Dupliziert!
- ...
- };
- }
-}
-
-export const TOKEN_INFO = {
- "eip155:10": {
- "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85": { ... } // Dupliziert!
- },
-};
-```
-
-**Nach Migration:**
-```javascript
-// x402_facilitator/chain_utils.js - NACHHER
+// NACHHER
import {
getViemChain,
- GENAI_NFT_ADDRESSES,
- LLMV1_ADDRESSES,
+ fromCAIP2,
+ MAINNET_GENAI_NFT_ADDRESSES,
USDC_ADDRESSES,
USDC_NAMES
} from "@fretchen/chain-utils";
+export { getViemChain, fromCAIP2 };
+
export function getChainConfig(network) {
return {
- chain: getViemChain(network), // ← aus shared
- rpcUrl: getRpcUrl(network), // ← bleibt lokal (env vars)
- GENIMG_V4_ADDRESS: GENAI_NFT_ADDRESSES[network] || null, // ← aus shared
- LLMV1_ADDRESS: LLMV1_ADDRESSES[network] || null, // ← aus shared
- USDC_ADDRESS: USDC_ADDRESSES[network], // ← aus shared
- USDC_NAME: USDC_NAMES[network], // ← aus shared
+ chain: getViemChain(network),
+ rpcUrl: getRpcUrl(network), // bleibt lokal
+ GENIMG_V4_ADDRESS: MAINNET_GENAI_NFT_ADDRESSES[network] || null,
+ USDC_ADDRESS: USDC_ADDRESSES[network],
+ USDC_NAME: USDC_NAMES[network],
};
}
+```
+
+---
-// TOKEN_INFO kann dynamisch generiert werden
-export function getTokenInfo(network, tokenAddress) {
- if (tokenAddress === USDC_ADDRESSES[network]) {
- return {
- address: tokenAddress,
- symbol: "USDC",
- name: USDC_NAMES[network],
- decimals: 6,
- version: "2",
- };
+## Phase 2: GenImNFT-Komponenten migrieren
+
+**Betroffene Dateien:**
+- `MyNFTList.tsx` (4 Stellen)
+- `NFTCard.tsx` (8 Stellen)
+- `NFTList.tsx` (1 Stelle)
+- `EntryNftImage.tsx` (2 Stellen)
+- `PublicNFTList.tsx` (1 Stelle)
+- `nftLoader.ts` (2 Stellen)
+
+**Pattern-Änderung:**
+```tsx
+// VORHER
+import { getChain, genAiNFTContractConfig } from "../utils/getChain";
+const chain = getChain();
+const { data } = useReadContract({
+ ...genAiNFTContractConfig,
+ chainId: chain.id,
+});
+
+// NACHHER
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTConfig, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+
+const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+const config = getGenAiNFTConfig(network);
+const { data } = useReadContract({ ...config });
+```
+
+**useAutoNetwork Hook (website/hooks/):**
+```typescript
+export function useAutoNetwork(supportedNetworks: string[]): string {
+ const { chain } = useAccount();
+ if (chain) {
+ const userNetwork = toCAIP2(chain.id);
+ if (supportedNetworks.includes(userNetwork)) return userNetwork;
}
- throw new Error(`Unsupported token: ${tokenAddress}`);
+ return supportedNetworks[0]; // Fallback: Optimism
}
```
-**Gelöschter Code:** ~60 Zeilen (hardcoded Adressen, TOKEN_INFO Objekt)
-
---
-## Aktualisierter Implementierungsplan
+## Phase 3: CollectorNFT-Komponenten migrieren
-| Phase | Was | Projekte | Status |
-|-------|-----|----------|--------|
-| **1a** | `@fretchen/chain-utils` erstellen | shared/ | 🔜 Next |
-| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ⬜ Geplant |
-| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ Geplant |
-| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ Geplant |
-| **3** | CollectorNFT-Komponenten migrieren | website/ | ⬜ Geplant |
-| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später |
-| **5** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Später |
+**Betroffene Dateien:**
+- `SimpleCollectButton.tsx` (2 Stellen)
+
+Gleiches Pattern wie Phase 2.
+
+---
-**Neue Reihenfolge-Begründung:**
-- 1a → Shared package muss zuerst existieren
-- 1b/1c → Backend-Projekte migrieren (einfacher, weniger UI-Abhängigkeiten)
-- 2/3 → Frontend migrieren (komplexer, Tests nötig)
-- 4/5 → Neue Deployments (profitieren von shared package)
+## Phase 4: GenImNFTv4 auf Base deployen
+
+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
+5. `npm install` in allen Projekten
---
-## Betroffene Dateien
+## Phase 5: CollectorNFTv1 auf Base deployen
-**Phase 1 (getChain.ts):**
-- [utils/getChain.ts](utils/getChain.ts)
-- [utils/nodeChainUtils.ts](utils/nodeChainUtils.ts)
+Voraussetzung: GenImNFT muss auf Base existieren.
-**Phase 2-3 (Komponenten):**
-- [components/NFTCard.tsx](components/NFTCard.tsx)
-- [components/MyNFTList.tsx](components/MyNFTList.tsx)
-- [components/SimpleCollectButton.tsx](components/SimpleCollectButton.tsx)
-- [utils/nftLoader.ts](utils/nftLoader.ts)
+1. Deploy mit Base GenImNFT Adresse
+2. Verify
+3. Adresse in chain-utils hinzufügen
---
-## Nächste Schritte
+## Zusammenfassung
+
+**Prinzipien:**
+- CAIP-2 `"eip155:10"` ist überall der primäre Key
+- Getrennte Maps für Mainnet/Testnet
+- Kein Chain-Selector - automatische Wahl basierend auf User-Wallet
+- Eine Quelle für Adressen: `@fretchen/chain-utils`
-1. ✅ Review Proposal
-2. **Phase 1 starten**: getChain.ts auf CAIP-2 Pattern umstellen
-3. GenImNFT-Komponenten migrieren
+**Vorteile:**
+- Neue Chains: Adresse nur an einer Stelle hinzufügen
+- Konsistenz: Gleiches Pattern in website, scw_js, x402_facilitator
+- Weniger Code: ~140 Zeilen duplizierter Switch-Statements entfernt
From 115548256db2c9e522b83d5af1a5b05b0f857739 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 18:57:48 +0100
Subject: [PATCH 04/46] Created a share-utils
---
shared/chain-utils/package-lock.json | 250 +++++++++++++++++++++++++++
shared/chain-utils/package.json | 33 ++++
shared/chain-utils/src/addresses.ts | 171 ++++++++++++++++++
shared/chain-utils/src/index.ts | 96 ++++++++++
shared/chain-utils/tsconfig.json | 17 ++
5 files changed, 567 insertions(+)
create mode 100644 shared/chain-utils/package-lock.json
create mode 100644 shared/chain-utils/package.json
create mode 100644 shared/chain-utils/src/addresses.ts
create mode 100644 shared/chain-utils/src/index.ts
create mode 100644 shared/chain-utils/tsconfig.json
diff --git a/shared/chain-utils/package-lock.json b/shared/chain-utils/package-lock.json
new file mode 100644
index 000000000..2551fcb01
--- /dev/null
+++ b/shared/chain-utils/package-lock.json
@@ -0,0 +1,250 @@
+{
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "devDependencies": {
+ "typescript": "^5.0.0",
+ "viem": "^2.0.0"
+ },
+ "peerDependencies": {
+ "viem": "^2.0.0"
+ }
+ },
+ "node_modules/@adraffy/ens-normalize": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@noble/ciphers": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/curves": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
+ "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.8.0"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/base": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+ "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+ "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "~1.9.0",
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip39": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+ "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/abitype": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
+ "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/wevm"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4",
+ "zod": "^3.22.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isows": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+ "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
+ "node_modules/ox": {
+ "version": "0.11.3",
+ "resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz",
+ "integrity": "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@adraffy/ens-normalize": "^1.11.0",
+ "@noble/ciphers": "^1.3.0",
+ "@noble/curves": "1.9.1",
+ "@noble/hashes": "^1.8.0",
+ "@scure/bip32": "^1.7.0",
+ "@scure/bip39": "^1.6.0",
+ "abitype": "^1.2.3",
+ "eventemitter3": "5.0.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/viem": {
+ "version": "2.45.0",
+ "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.0.tgz",
+ "integrity": "sha512-iVA9qrAgRdtpWa80lCZ6Jri6XzmLOwwA1wagX2HnKejKeliFLpON0KOdyfqvcy+gUpBVP59LBxP2aKiL3aj8fg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/wevm"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "1.9.1",
+ "@noble/hashes": "1.8.0",
+ "@scure/bip32": "1.7.0",
+ "@scure/bip39": "1.6.0",
+ "abitype": "1.2.3",
+ "isows": "1.0.7",
+ "ox": "0.11.3",
+ "ws": "8.18.3"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/shared/chain-utils/package.json b/shared/chain-utils/package.json
new file mode 100644
index 000000000..83cd595f2
--- /dev/null
+++ b/shared/chain-utils/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "description": "Shared chain utilities with CAIP-2 pattern for fretchen.github.io projects",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./addresses": {
+ "types": "./dist/addresses.d.ts",
+ "default": "./dist/addresses.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "prepare": "npm run build",
+ "clean": "rm -rf dist"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0",
+ "viem": "^2.0.0"
+ },
+ "peerDependencies": {
+ "viem": "^2.0.0"
+ }
+}
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
new file mode 100644
index 000000000..8c7866f2e
--- /dev/null
+++ b/shared/chain-utils/src/addresses.ts
@@ -0,0 +1,171 @@
+/**
+ * Contract Address Maps
+ *
+ * Organized by contract, with separate maps for mainnet and testnet.
+ * All keys are CAIP-2 format: "eip155:"
+ */
+
+// ═══════════════════════════════════════════════════════════════
+// GenImNFT (GenAI NFT)
+// ═══════════════════════════════════════════════════════════════
+
+export const MAINNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", // Optimism
+ // "eip155:8453": "0x...", // Base - nach Deployment hinzufügen
+};
+
+export const TESTNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85", // Optimism Sepolia
+};
+
+/** All networks where GenImNFT is deployed */
+export const GENAI_NFT_NETWORKS = [
+ ...Object.keys(MAINNET_GENAI_NFT_ADDRESSES),
+ ...Object.keys(TESTNET_GENAI_NFT_ADDRESSES),
+] as const;
+
+// ═══════════════════════════════════════════════════════════════
+// CollectorNFT
+// ═══════════════════════════════════════════════════════════════
+
+export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea", // Optimism
+};
+
+export const TESTNET_COLLECTOR_NFT_ADDRESSES: Record = {
+ // Keine Testnet-Deployments aktuell
+};
+
+/** All networks where CollectorNFT is deployed */
+export const COLLECTOR_NFT_NETWORKS = [
+ ...Object.keys(MAINNET_COLLECTOR_NFT_ADDRESSES),
+ ...Object.keys(TESTNET_COLLECTOR_NFT_ADDRESSES),
+] as const;
+
+// ═══════════════════════════════════════════════════════════════
+// SupportV2
+// ═══════════════════════════════════════════════════════════════
+
+export const MAINNET_SUPPORT_V2_ADDRESSES: Record = {
+ "eip155:10": "0x4ca63f8A4Cd56287E854f53E18ca482D74391316", // Optimism
+ "eip155:8453": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694", // Base
+};
+
+export const TESTNET_SUPPORT_V2_ADDRESSES: Record = {
+ "eip155:11155420": "0x9859431b682e861b19e87Db14a04944BC747AB6d", // Optimism Sepolia
+ "eip155:84532": "0xaB44BE78499721b593a0f4BE2099b246e9C53B57", // Base Sepolia
+};
+
+/** All networks where SupportV2 is deployed */
+export const SUPPORT_V2_NETWORKS = [
+ ...Object.keys(MAINNET_SUPPORT_V2_ADDRESSES),
+ ...Object.keys(TESTNET_SUPPORT_V2_ADDRESSES),
+] as const;
+
+// ═══════════════════════════════════════════════════════════════
+// LLMv1 (out of scope for migration, but included for completeness)
+// ═══════════════════════════════════════════════════════════════
+
+export const MAINNET_LLM_V1_ADDRESSES: Record = {
+ "eip155:10": "0x7E8b7091a229B1004c4FBa25bB70d04595d3e848", // Optimism
+};
+
+export const TESTNET_LLM_V1_ADDRESSES: Record = {
+ "eip155:11155420": "0xA5b7f0A3f4104c97b46eafF2b0b4A457C5a73Bf4", // Optimism Sepolia
+};
+
+// ═══════════════════════════════════════════════════════════════
+// USDC (available on all chains)
+// ═══════════════════════════════════════════════════════════════
+
+export const USDC_ADDRESSES: Record = {
+ // Mainnets
+ "eip155:10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism
+ "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base
+ // Testnets
+ "eip155:11155420": "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", // Optimism Sepolia
+ "eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia
+};
+
+export const USDC_NAMES: Record = {
+ "eip155:10": "USD Coin",
+ "eip155:11155420": "USDC",
+ "eip155:8453": "USD Coin",
+ "eip155:84532": "USDC",
+};
+
+// ═══════════════════════════════════════════════════════════════
+// EIP3009 Splitter
+// ═══════════════════════════════════════════════════════════════
+
+export const MAINNET_EIP3009_SPLITTER_ADDRESSES: Record = {
+ "eip155:10": "0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf", // Optimism
+};
+
+export const TESTNET_EIP3009_SPLITTER_ADDRESSES: Record = {
+ "eip155:11155420": "0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a", // Optimism Sepolia
+};
+
+// ═══════════════════════════════════════════════════════════════
+// Helper Functions
+// ═══════════════════════════════════════════════════════════════
+
+/**
+ * Get GenAI NFT address for a network (mainnet or testnet).
+ * @throws Error if not deployed on that network
+ */
+export function getGenAiNFTAddress(network: string): `0x${string}` {
+ const address =
+ MAINNET_GENAI_NFT_ADDRESSES[network] || TESTNET_GENAI_NFT_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`GenAI NFT not deployed on ${network}`);
+ }
+ return address;
+}
+
+/**
+ * Get CollectorNFT address for a network (mainnet or testnet).
+ * @throws Error if not deployed on that network
+ */
+export function getCollectorNFTAddress(network: string): `0x${string}` {
+ const address =
+ MAINNET_COLLECTOR_NFT_ADDRESSES[network] ||
+ TESTNET_COLLECTOR_NFT_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`CollectorNFT not deployed on ${network}`);
+ }
+ return address;
+}
+
+/**
+ * Get SupportV2 address for a network (mainnet or testnet).
+ * @throws Error if not deployed on that network
+ */
+export function getSupportV2Address(network: string): `0x${string}` {
+ const address =
+ MAINNET_SUPPORT_V2_ADDRESSES[network] ||
+ TESTNET_SUPPORT_V2_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`SupportV2 not deployed on ${network}`);
+ }
+ return address;
+}
+
+/**
+ * Get USDC address for a network.
+ * @throws Error if USDC not available on that network
+ */
+export function getUSDCAddress(network: string): `0x${string}` {
+ const address = USDC_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`USDC not available on ${network}`);
+ }
+ return address;
+}
+
+/**
+ * Get USDC name for a network.
+ */
+export function getUSDCName(network: string): string {
+ return USDC_NAMES[network] || "USDC";
+}
diff --git a/shared/chain-utils/src/index.ts b/shared/chain-utils/src/index.ts
new file mode 100644
index 000000000..40ac6748f
--- /dev/null
+++ b/shared/chain-utils/src/index.ts
@@ -0,0 +1,96 @@
+/**
+ * @fretchen/chain-utils
+ *
+ * Shared chain utilities with CAIP-2 pattern.
+ * CAIP-2 format: "eip155:" (e.g., "eip155:10" for Optimism)
+ */
+
+import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
+import type { Chain } from "viem";
+
+// ═══════════════════════════════════════════════════════════════
+// CAIP-2 Conversion Utilities
+// ═══════════════════════════════════════════════════════════════
+
+/**
+ * Convert a numeric chain ID to CAIP-2 format.
+ * @example toCAIP2(10) → "eip155:10"
+ */
+export const toCAIP2 = (chainId: number): string => `eip155:${chainId}`;
+
+/**
+ * Parse a CAIP-2 string to get the numeric chain ID.
+ * @throws Error if the format is invalid
+ * @example fromCAIP2("eip155:10") → 10
+ */
+export const fromCAIP2 = (network: string): number => {
+ const match = network.match(/^eip155:(\d+)$/);
+ if (!match) {
+ throw new Error(`Invalid CAIP-2 format: ${network}`);
+ }
+ return parseInt(match[1], 10);
+};
+
+// ═══════════════════════════════════════════════════════════════
+// Supported Networks
+// ═══════════════════════════════════════════════════════════════
+
+/** All supported mainnet CAIP-2 identifiers */
+export const MAINNET_NETWORKS = ["eip155:10", "eip155:8453"] as const;
+
+/** All supported testnet CAIP-2 identifiers */
+export const TESTNET_NETWORKS = ["eip155:11155420", "eip155:84532"] as const;
+
+/** All supported CAIP-2 identifiers */
+export const ALL_NETWORKS = [...MAINNET_NETWORKS, ...TESTNET_NETWORKS] as const;
+
+export type MainnetNetwork = (typeof MAINNET_NETWORKS)[number];
+export type TestnetNetwork = (typeof TESTNET_NETWORKS)[number];
+export type Network = (typeof ALL_NETWORKS)[number];
+
+// ═══════════════════════════════════════════════════════════════
+// Viem Chain Mapping
+// ═══════════════════════════════════════════════════════════════
+
+const CHAIN_MAP: Record = {
+ "eip155:10": optimism,
+ "eip155:11155420": optimismSepolia,
+ "eip155:8453": base,
+ "eip155:84532": baseSepolia,
+};
+
+/**
+ * Get the Viem Chain object for a CAIP-2 network identifier.
+ * @throws Error if the network is not supported
+ */
+export function getViemChain(network: string): Chain {
+ const chain = CHAIN_MAP[network];
+ if (!chain) {
+ throw new Error(`Unsupported network: ${network}. Supported: ${Object.keys(CHAIN_MAP).join(", ")}`);
+ }
+ return chain;
+}
+
+/**
+ * Check if a CAIP-2 network identifier is supported.
+ */
+export function isNetworkSupported(network: string): network is Network {
+ return network in CHAIN_MAP;
+}
+
+/**
+ * Check if a CAIP-2 network identifier is a mainnet.
+ */
+export function isMainnet(network: string): network is MainnetNetwork {
+ return (MAINNET_NETWORKS as readonly string[]).includes(network);
+}
+
+/**
+ * Check if a CAIP-2 network identifier is a testnet.
+ */
+export function isTestnet(network: string): network is TestnetNetwork {
+ return (TESTNET_NETWORKS as readonly string[]).includes(network);
+}
+
+// Re-export addresses
+export * from "./addresses";
diff --git a/shared/chain-utils/tsconfig.json b/shared/chain-utils/tsconfig.json
new file mode 100644
index 000000000..5fd1f40ce
--- /dev/null
+++ b/shared/chain-utils/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
From 88034d307d0a1b7ad37e1d457038117a52427ce5 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 19:27:29 +0100
Subject: [PATCH 05/46] Start cleaning out scw_js
---
scw_js/genimg_x402_token.js | 11 +-
scw_js/getChain.js | 94 +-
scw_js/package-lock.json | 16 +
scw_js/package.json | 1 +
scw_js/test/getChain.test.js | 9 +-
shared/chain-utils/package-lock.json | 1441 ++++++++++++++++++++++++-
shared/chain-utils/package.json | 8 +-
shared/chain-utils/test/index.test.ts | 240 ++++
shared/chain-utils/vitest.config.ts | 8 +
9 files changed, 1721 insertions(+), 107 deletions(-)
create mode 100644 shared/chain-utils/test/index.test.ts
create mode 100644 shared/chain-utils/vitest.config.ts
diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js
index 5d91f0bea..b4522df10 100644
--- a/scw_js/genimg_x402_token.js
+++ b/scw_js/genimg_x402_token.js
@@ -12,13 +12,8 @@ import {
extractPaymentPayload,
createSettlementHeaders,
} from "./x402_server.js";
-import {
- getGenImgContractConfig,
- getViemChain,
- getUSDCConfig,
- validatePaymentNetwork,
- getChainNameFromEIP155,
-} from "./getChain.js";
+import { getViemChain, getGenAiNFTAddress } from "@fretchen/chain-utils";
+import { getUSDCConfig, validatePaymentNetwork, getChainNameFromEIP155 } from "./getChain.js";
// Re-export x402 functions for backward compatibility with tests
export { handle, create402Response };
@@ -475,7 +470,7 @@ async function handle(event, context, cb) {
// Get configurations for validated network
const usdcConfig = getUSDCConfig(clientNetwork);
- const contractAddress = getGenImgContractConfig(clientNetwork).address;
+ const contractAddress = getGenAiNFTAddress(clientNetwork);
console.log(`📍 Client selected network: ${usdcConfig.name} (${clientNetwork})`);
// Build payment requirements for the selected network
diff --git a/scw_js/getChain.js b/scw_js/getChain.js
index 45a3c9dd7..2a9ce83af 100644
--- a/scw_js/getChain.js
+++ b/scw_js/getChain.js
@@ -1,6 +1,8 @@
// @ts-check
-import { sepolia, optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
+import { sepolia, optimism, optimismSepolia } from "viem/chains";
import { LLMv1ABI } from "./llmv1_abi.js";
+import { getViemChain, fromCAIP2, getUSDCAddress, getUSDCName } from "@fretchen/chain-utils";
+
/**
* Get environment variable in both Node.js and Vite contexts
* @param {string} key
@@ -30,7 +32,13 @@ function getEnvironmentVariable(key, defaultValue) {
}
/**
- * Gibt das entsprechende Chain-Objekt basierend auf der CHAIN-Umgebungsvariable zurück
+ * Gibt das entsprechende Chain-Objekt basierend auf der CHAIN-Umgebungsvariable zurück.
+ *
+ * @deprecated TODO: Migrate to CAIP-2 pattern when LLMv1 gets multi-chain support.
+ * Currently used by:
+ * - llm_service.js: checkWalletBalance() (line ~454)
+ * - llm_service.js: processMerkleTree() (line ~572)
+ *
* @returns {import("viem/chains").Chain}
*/
export function getChain() {
@@ -62,40 +70,6 @@ export function getLLMv1ContractConfig() {
}
}
-/** returns the config of the GenImgV4 contract
- * @param {string} network - CAIP-2 network ID (e.g., "eip155:10", "eip155:11155420")
- * @returns {{ address: `0x${string}` }}
- */
-export function getGenImgContractConfig(network) {
- switch (network) {
- case "eip155:11155420": // Optimism Sepolia
- return { address: "0x10827cC42a09D0BAD2d43134C69F0e776D853D85" };
- case "eip155:10": // Optimism Mainnet
- return { address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb" };
- default:
- throw new Error(`GenImg contract not deployed on network: ${network}`);
- }
-}
-
-/** returns the viem chain object for a CAIP-2 network ID
- * @param {string} network - CAIP-2 network ID (e.g., "eip155:10", "eip155:11155420")
- * @returns {import("viem/chains").Chain}
- */
-export function getViemChain(network) {
- switch (network) {
- case "eip155:10": // Optimism Mainnet
- return optimism;
- case "eip155:11155420": // Optimism Sepolia
- return optimismSepolia;
- case "eip155:8453": // Base Mainnet
- return base;
- case "eip155:84532": // Base Sepolia
- return baseSepolia;
- default:
- throw new Error(`Unsupported network: ${network}`);
- }
-}
-
/**
* Get human-readable chain name from CAIP-2 network ID
* Uses viem chain names for consistency across the codebase
@@ -126,46 +100,14 @@ export function getChainNameFromEIP155(network) {
* @returns {USDCConfig}
*/
export function getUSDCConfig(network) {
- switch (network) {
- case "eip155:10": // Optimism Mainnet
- return {
- name: getChainNameFromEIP155(network),
- chainId: 10,
- address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
- decimals: 6,
- usdcName: "USD Coin", // Mainnet uses "USD Coin" for EIP-712 domain
- usdcVersion: "2",
- };
- case "eip155:11155420": // Optimism Sepolia
- return {
- name: getChainNameFromEIP155(network),
- chainId: 11155420,
- address: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
- decimals: 6,
- usdcName: "USDC", // Sepolia uses "USDC" for EIP-712 domain
- usdcVersion: "2",
- };
- case "eip155:8453": // Base Mainnet
- return {
- name: getChainNameFromEIP155(network),
- chainId: 8453,
- address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
- decimals: 6,
- usdcName: "USD Coin", // Mainnet uses "USD Coin" for EIP-712 domain
- usdcVersion: "2",
- };
- case "eip155:84532": // Base Sepolia
- return {
- name: getChainNameFromEIP155(network),
- chainId: 84532,
- address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
- decimals: 6,
- usdcName: "USDC",
- usdcVersion: "2",
- };
- default:
- throw new Error(`USDC not configured for network: ${network}`);
- }
+ return {
+ name: getChainNameFromEIP155(network),
+ chainId: fromCAIP2(network),
+ address: getUSDCAddress(network),
+ decimals: 6,
+ usdcName: getUSDCName(network),
+ usdcVersion: "2",
+ };
}
/**
diff --git a/scw_js/package-lock.json b/scw_js/package-lock.json
index 09a99a895..7a93ab06c 100644
--- a/scw_js/package-lock.json
+++ b/scw_js/package-lock.json
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "^3.914.0",
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@openzeppelin/merkle-tree": "^1.0.8",
"@x402/core": "^2.0.0",
"@x402/evm": "^2.0.0",
@@ -35,6 +36,17 @@
"vitest": "^4.0.1"
}
},
+ "../shared/chain-utils": {
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "devDependencies": {
+ "typescript": "^5.0.0",
+ "viem": "^2.0.0"
+ },
+ "peerDependencies": {
+ "viem": "^2.0.0"
+ }
+ },
"node_modules/@adraffy/ens-normalize": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
@@ -1891,6 +1903,10 @@
"fastify-plugin": "^5.0.0"
}
},
+ "node_modules/@fretchen/chain-utils": {
+ "resolved": "../shared/chain-utils",
+ "link": true
+ },
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
diff --git a/scw_js/package.json b/scw_js/package.json
index 42b247485..b03848ae2 100644
--- a/scw_js/package.json
+++ b/scw_js/package.json
@@ -37,6 +37,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.914.0",
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@openzeppelin/merkle-tree": "^1.0.8",
"@x402/core": "^2.0.0",
"@x402/evm": "^2.0.0",
diff --git a/scw_js/test/getChain.test.js b/scw_js/test/getChain.test.js
index 8584e4b6b..82b77934a 100644
--- a/scw_js/test/getChain.test.js
+++ b/scw_js/test/getChain.test.js
@@ -20,14 +20,15 @@ import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
// Import functions under test
import {
- getViemChain,
getChainNameFromEIP155,
getUSDCConfig,
- getGenImgContractConfig,
getExpectedNetwork,
validatePaymentNetwork,
} from "../getChain.js";
+// Import from chain-utils for consistency checks
+import { getViemChain } from "@fretchen/chain-utils";
+
// USDC contracts expose name() and version() functions for EIP-712 domain
// Note: They do NOT implement EIP-5267 eip712Domain(), so we read individual functions
const USDC_ABI = [
@@ -135,7 +136,7 @@ describe("getChain.js - Chain Configuration Tests", () => {
});
test("should throw for unconfigured network", () => {
- expect(() => getUSDCConfig("eip155:1")).toThrow("USDC not configured for network: eip155:1");
+ expect(() => getUSDCConfig("eip155:1")).toThrow("USDC not available on eip155:1");
});
test("should have consistent chainId between viem chain and USDC config", () => {
@@ -163,7 +164,7 @@ describe("getChain.js - Chain Configuration Tests", () => {
test("should throw for network without GenImg deployment", () => {
expect(() => getGenImgContractConfig("eip155:8453")).toThrow(
- "GenImg contract not deployed on network: eip155:8453",
+ "GenAI NFT not deployed on eip155:8453",
);
});
});
diff --git a/shared/chain-utils/package-lock.json b/shared/chain-utils/package-lock.json
index 2551fcb01..1bfd9c318 100644
--- a/shared/chain-utils/package-lock.json
+++ b/shared/chain-utils/package-lock.json
@@ -9,7 +9,8 @@
"version": "1.0.0",
"devDependencies": {
"typescript": "^5.0.0",
- "viem": "^2.0.0"
+ "viem": "^2.0.0",
+ "vitest": "^2.0.0"
},
"peerDependencies": {
"viem": "^2.0.0"
@@ -22,6 +23,404 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
@@ -64,6 +463,356 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
+ "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz",
+ "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz",
+ "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz",
+ "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz",
+ "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz",
+ "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz",
+ "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz",
+ "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz",
+ "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz",
+ "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz",
+ "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz",
+ "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz",
+ "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz",
+ "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz",
+ "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz",
+ "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz",
+ "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz",
+ "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz",
+ "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz",
+ "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz",
+ "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz",
+ "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz",
+ "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz",
+ "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz",
+ "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
@@ -71,36 +820,156 @@
"dev": true,
"license": "MIT",
"funding": {
- "url": "https://paulmillr.com/funding/"
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip32": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+ "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/curves": "~1.9.0",
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@scure/bip39": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+ "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "~1.8.0",
+ "@scure/base": "~1.2.5"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@scure/bip32": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
- "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@noble/curves": "~1.9.0",
- "@noble/hashes": "~1.8.0",
- "@scure/base": "~1.2.5"
+ "tinyspy": "^3.0.2"
},
"funding": {
- "url": "https://paulmillr.com/funding/"
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@scure/bip39": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
- "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@noble/hashes": "~1.8.0",
- "@scure/base": "~1.2.5"
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
},
"funding": {
- "url": "https://paulmillr.com/funding/"
+ "url": "https://opencollective.com/vitest"
}
},
"node_modules/abitype": {
@@ -125,6 +994,137 @@
}
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -132,6 +1132,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
@@ -148,6 +1173,49 @@
"ws": "*"
}
},
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/ox": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz",
@@ -179,6 +1247,179 @@
}
}
},
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.56.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
+ "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.56.0",
+ "@rollup/rollup-android-arm64": "4.56.0",
+ "@rollup/rollup-darwin-arm64": "4.56.0",
+ "@rollup/rollup-darwin-x64": "4.56.0",
+ "@rollup/rollup-freebsd-arm64": "4.56.0",
+ "@rollup/rollup-freebsd-x64": "4.56.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.56.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.56.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.56.0",
+ "@rollup/rollup-linux-arm64-musl": "4.56.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.56.0",
+ "@rollup/rollup-linux-loong64-musl": "4.56.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.56.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.56.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.56.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.56.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.56.0",
+ "@rollup/rollup-linux-x64-gnu": "4.56.0",
+ "@rollup/rollup-linux-x64-musl": "4.56.0",
+ "@rollup/rollup-openbsd-x64": "4.56.0",
+ "@rollup/rollup-openharmony-arm64": "4.56.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.56.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.56.0",
+ "@rollup/rollup-win32-x64-gnu": "4.56.0",
+ "@rollup/rollup-win32-x64-msvc": "4.56.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -224,6 +1465,172 @@
}
}
},
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
diff --git a/shared/chain-utils/package.json b/shared/chain-utils/package.json
index 83cd595f2..439495cad 100644
--- a/shared/chain-utils/package.json
+++ b/shared/chain-utils/package.json
@@ -21,11 +21,15 @@
"scripts": {
"build": "tsc",
"prepare": "npm run build",
- "clean": "rm -rf dist"
+ "clean": "rm -rf dist",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage"
},
"devDependencies": {
"typescript": "^5.0.0",
- "viem": "^2.0.0"
+ "viem": "^2.0.0",
+ "vitest": "^2.0.0"
},
"peerDependencies": {
"viem": "^2.0.0"
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
new file mode 100644
index 000000000..728469373
--- /dev/null
+++ b/shared/chain-utils/test/index.test.ts
@@ -0,0 +1,240 @@
+/**
+ * Tests for @fretchen/chain-utils
+ *
+ * Tests CAIP-2 utilities, chain configuration, and contract addresses.
+ */
+
+import { describe, test, expect } from "vitest";
+import {
+ toCAIP2,
+ fromCAIP2,
+ getViemChain,
+ isNetworkSupported,
+ isMainnet,
+ isTestnet,
+ MAINNET_NETWORKS,
+ TESTNET_NETWORKS,
+} from "../src/index";
+import {
+ getGenAiNFTAddress,
+ getCollectorNFTAddress,
+ getSupportV2Address,
+ getUSDCAddress,
+ getUSDCName,
+ MAINNET_GENAI_NFT_ADDRESSES,
+ TESTNET_GENAI_NFT_ADDRESSES,
+ MAINNET_SUPPORT_V2_ADDRESSES,
+ TESTNET_SUPPORT_V2_ADDRESSES,
+ USDC_ADDRESSES,
+} from "../src/addresses";
+
+describe("@fretchen/chain-utils", () => {
+ describe("CAIP-2 Conversion", () => {
+ describe("toCAIP2()", () => {
+ test("should convert chainId to CAIP-2 format", () => {
+ expect(toCAIP2(10)).toBe("eip155:10");
+ expect(toCAIP2(11155420)).toBe("eip155:11155420");
+ expect(toCAIP2(8453)).toBe("eip155:8453");
+ expect(toCAIP2(84532)).toBe("eip155:84532");
+ });
+ });
+
+ describe("fromCAIP2()", () => {
+ test("should parse CAIP-2 to numeric chainId", () => {
+ expect(fromCAIP2("eip155:10")).toBe(10);
+ expect(fromCAIP2("eip155:11155420")).toBe(11155420);
+ expect(fromCAIP2("eip155:8453")).toBe(8453);
+ expect(fromCAIP2("eip155:84532")).toBe(84532);
+ });
+
+ test("should throw for invalid CAIP-2 format", () => {
+ expect(() => fromCAIP2("invalid")).toThrow("Invalid CAIP-2 format");
+ expect(() => fromCAIP2("eip155:")).toThrow("Invalid CAIP-2 format");
+ expect(() => fromCAIP2("10")).toThrow("Invalid CAIP-2 format");
+ });
+ });
+
+ test("roundtrip: toCAIP2 -> fromCAIP2", () => {
+ const chainIds = [10, 11155420, 8453, 84532];
+ for (const chainId of chainIds) {
+ expect(fromCAIP2(toCAIP2(chainId))).toBe(chainId);
+ }
+ });
+ });
+
+ describe("getViemChain()", () => {
+ test("should return Optimism Mainnet for eip155:10", () => {
+ const chain = getViemChain("eip155:10");
+ expect(chain.id).toBe(10);
+ expect(chain.name).toBe("OP Mainnet");
+ });
+
+ test("should return Optimism Sepolia for eip155:11155420", () => {
+ const chain = getViemChain("eip155:11155420");
+ expect(chain.id).toBe(11155420);
+ expect(chain.name).toBe("OP Sepolia");
+ });
+
+ test("should return Base Mainnet for eip155:8453", () => {
+ const chain = getViemChain("eip155:8453");
+ expect(chain.id).toBe(8453);
+ expect(chain.name).toBe("Base");
+ });
+
+ test("should return Base Sepolia for eip155:84532", () => {
+ const chain = getViemChain("eip155:84532");
+ expect(chain.id).toBe(84532);
+ expect(chain.name).toBe("Base Sepolia");
+ });
+
+ test("should throw for unsupported network", () => {
+ expect(() => getViemChain("eip155:1")).toThrow("Unsupported network: eip155:1");
+ });
+ });
+
+ describe("Network Type Checks", () => {
+ test("isNetworkSupported()", () => {
+ expect(isNetworkSupported("eip155:10")).toBe(true);
+ expect(isNetworkSupported("eip155:8453")).toBe(true);
+ expect(isNetworkSupported("eip155:11155420")).toBe(true);
+ expect(isNetworkSupported("eip155:1")).toBe(false);
+ expect(isNetworkSupported("invalid")).toBe(false);
+ });
+
+ test("isMainnet()", () => {
+ expect(isMainnet("eip155:10")).toBe(true);
+ expect(isMainnet("eip155:8453")).toBe(true);
+ expect(isMainnet("eip155:11155420")).toBe(false);
+ expect(isMainnet("eip155:84532")).toBe(false);
+ });
+
+ test("isTestnet()", () => {
+ expect(isTestnet("eip155:11155420")).toBe(true);
+ expect(isTestnet("eip155:84532")).toBe(true);
+ expect(isTestnet("eip155:10")).toBe(false);
+ expect(isTestnet("eip155:8453")).toBe(false);
+ });
+
+ test("MAINNET_NETWORKS and TESTNET_NETWORKS are disjoint", () => {
+ for (const mainnet of MAINNET_NETWORKS) {
+ expect(TESTNET_NETWORKS).not.toContain(mainnet);
+ }
+ });
+ });
+
+ describe("Contract Addresses", () => {
+ describe("getGenAiNFTAddress()", () => {
+ test("should return Mainnet contract address", () => {
+ expect(getGenAiNFTAddress("eip155:10")).toBe(
+ "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"
+ );
+ });
+
+ test("should return Sepolia contract address", () => {
+ expect(getGenAiNFTAddress("eip155:11155420")).toBe(
+ "0x10827cC42a09D0BAD2d43134C69F0e776D853D85"
+ );
+ });
+
+ test("should throw for network without GenAI NFT deployment", () => {
+ expect(() => getGenAiNFTAddress("eip155:8453")).toThrow(
+ "GenAI NFT not deployed on eip155:8453"
+ );
+ });
+ });
+
+ describe("getCollectorNFTAddress()", () => {
+ test("should return Optimism Mainnet address", () => {
+ expect(getCollectorNFTAddress("eip155:10")).toBe(
+ "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea"
+ );
+ });
+
+ test("should throw for network without CollectorNFT", () => {
+ expect(() => getCollectorNFTAddress("eip155:8453")).toThrow(
+ "CollectorNFT not deployed on eip155:8453"
+ );
+ });
+ });
+
+ describe("getSupportV2Address()", () => {
+ test("should return addresses for all deployed networks", () => {
+ // Mainnets
+ expect(getSupportV2Address("eip155:10")).toBe(
+ "0x4ca63f8A4Cd56287E854f53E18ca482D74391316"
+ );
+ expect(getSupportV2Address("eip155:8453")).toBe(
+ "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694"
+ );
+ // Testnets
+ expect(getSupportV2Address("eip155:11155420")).toBe(
+ "0x9859431b682e861b19e87Db14a04944BC747AB6d"
+ );
+ expect(getSupportV2Address("eip155:84532")).toBe(
+ "0xaB44BE78499721b593a0f4BE2099b246e9C53B57"
+ );
+ });
+ });
+
+ describe("USDC Addresses", () => {
+ test("getUSDCAddress() returns valid addresses for all networks", () => {
+ expect(getUSDCAddress("eip155:10")).toBe(
+ "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
+ );
+ expect(getUSDCAddress("eip155:11155420")).toBe(
+ "0x5fd84259d66Cd46123540766Be93DFE6D43130D7"
+ );
+ expect(getUSDCAddress("eip155:8453")).toBe(
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
+ );
+ expect(getUSDCAddress("eip155:84532")).toBe(
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
+ );
+ });
+
+ test("getUSDCAddress() throws for unsupported network", () => {
+ expect(() => getUSDCAddress("eip155:1")).toThrow(
+ "USDC not available on eip155:1"
+ );
+ });
+
+ test("getUSDCName() returns correct names for EIP-712 domain", () => {
+ // Mainnets use "USD Coin"
+ expect(getUSDCName("eip155:10")).toBe("USD Coin");
+ expect(getUSDCName("eip155:8453")).toBe("USD Coin");
+ // Testnets use "USDC"
+ expect(getUSDCName("eip155:11155420")).toBe("USDC");
+ expect(getUSDCName("eip155:84532")).toBe("USDC");
+ });
+ });
+ });
+
+ describe("Address Map Consistency", () => {
+ test("all address maps have valid checksummed addresses", () => {
+ const allMaps = [
+ MAINNET_GENAI_NFT_ADDRESSES,
+ TESTNET_GENAI_NFT_ADDRESSES,
+ MAINNET_SUPPORT_V2_ADDRESSES,
+ TESTNET_SUPPORT_V2_ADDRESSES,
+ USDC_ADDRESSES,
+ ];
+
+ for (const map of allMaps) {
+ for (const [network, address] of Object.entries(map)) {
+ expect(network).toMatch(/^eip155:\d+$/);
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/);
+ }
+ }
+ });
+
+ test("chainId from viem matches fromCAIP2 for all networks", () => {
+ const networks = ["eip155:10", "eip155:11155420", "eip155:8453", "eip155:84532"];
+
+ for (const network of networks) {
+ const viemChain = getViemChain(network);
+ const parsedChainId = fromCAIP2(network);
+ expect(viemChain.id).toBe(parsedChainId);
+ }
+ });
+ });
+});
diff --git a/shared/chain-utils/vitest.config.ts b/shared/chain-utils/vitest.config.ts
new file mode 100644
index 000000000..3f824fb95
--- /dev/null
+++ b/shared/chain-utils/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ },
+});
From 0d5407fc04ea8f9e20fe94dde746088683624c72 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 20:07:39 +0100
Subject: [PATCH 06/46] Further cleaning in scw_js
---
scw_js/genimg_x402_token.js | 7 ++-
scw_js/getChain.js | 16 +------
scw_js/test/getChain.test.js | 83 +++++-------------------------------
3 files changed, 15 insertions(+), 91 deletions(-)
diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js
index b4522df10..257524158 100644
--- a/scw_js/genimg_x402_token.js
+++ b/scw_js/genimg_x402_token.js
@@ -13,7 +13,7 @@ import {
createSettlementHeaders,
} from "./x402_server.js";
import { getViemChain, getGenAiNFTAddress } from "@fretchen/chain-utils";
-import { getUSDCConfig, validatePaymentNetwork, getChainNameFromEIP155 } from "./getChain.js";
+import { getUSDCConfig, validatePaymentNetwork } from "./getChain.js";
// Re-export x402 functions for backward compatibility with tests
export { handle, create402Response };
@@ -531,8 +531,7 @@ async function handle(event, context, cb) {
try {
// Get chain from payment network (buyer determines the chain!)
const viemChain = getViemChain(clientNetwork);
- const chainName = getChainNameFromEIP155(clientNetwork);
- console.log(`🔗 Using chain: ${chainName} (${clientNetwork})`);
+ console.log(`🔗 Using chain: ${viemChain.name} (${clientNetwork})`);
const publicClient = createPublicClient({
chain: viemChain,
@@ -567,7 +566,7 @@ async function handle(event, context, cb) {
publicClient,
account.address,
mintPrice,
- chainName,
+ viemChain.name,
);
if (!preFlightResult.success) {
diff --git a/scw_js/getChain.js b/scw_js/getChain.js
index 2a9ce83af..72b872687 100644
--- a/scw_js/getChain.js
+++ b/scw_js/getChain.js
@@ -70,20 +70,6 @@ export function getLLMv1ContractConfig() {
}
}
-/**
- * Get human-readable chain name from CAIP-2 network ID
- * Uses viem chain names for consistency across the codebase
- * @param {string} network - CAIP-2 network ID (e.g., "eip155:10")
- * @returns {string} Human-readable chain name
- */
-export function getChainNameFromEIP155(network) {
- try {
- return getViemChain(network).name;
- } catch {
- return `Unknown (${network})`;
- }
-}
-
/**
* USDC configuration for supported networks
* @typedef {Object} USDCConfig
@@ -101,7 +87,7 @@ export function getChainNameFromEIP155(network) {
*/
export function getUSDCConfig(network) {
return {
- name: getChainNameFromEIP155(network),
+ name: getViemChain(network).name,
chainId: fromCAIP2(network),
address: getUSDCAddress(network),
decimals: 6,
diff --git a/scw_js/test/getChain.test.js b/scw_js/test/getChain.test.js
index 82b77934a..3c6d3d2e7 100644
--- a/scw_js/test/getChain.test.js
+++ b/scw_js/test/getChain.test.js
@@ -2,11 +2,11 @@
* Tests for getChain.js - Chain Configuration and USDC Domain Validation
*
* This test file validates:
- * 1. Chain configuration functions (getViemChain, getChainNameFromEIP155)
- * 2. USDC configuration for all supported networks
- * 3. EIP-712 domain names match on-chain USDC contracts (CRITICAL)
- * 4. GenImg contract configuration
- * 5. Network validation functions
+ * 1. USDC configuration for all supported networks
+ * 2. EIP-712 domain names match on-chain USDC contracts (CRITICAL)
+ * 3. Network validation functions
+ *
+ * Note: Chain utilities (getViemChain, CAIP-2 conversion) are tested in @fretchen/chain-utils
*
* Background: CVE-2025-12-26 (USDC Domain Name Mismatch)
* - USDC contracts use different EIP-712 domain names on different networks
@@ -19,12 +19,7 @@ import { createPublicClient, http } from "viem";
import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
// Import functions under test
-import {
- getChainNameFromEIP155,
- getUSDCConfig,
- getExpectedNetwork,
- validatePaymentNetwork,
-} from "../getChain.js";
+import { getUSDCConfig, getExpectedNetwork, validatePaymentNetwork } from "../getChain.js";
// Import from chain-utils for consistency checks
import { getViemChain } from "@fretchen/chain-utils";
@@ -49,49 +44,8 @@ const USDC_ABI = [
];
describe("getChain.js - Chain Configuration Tests", () => {
- describe("getViemChain()", () => {
- test("should return Optimism Mainnet for eip155:10", () => {
- const chain = getViemChain("eip155:10");
- expect(chain.id).toBe(10);
- expect(chain.name).toBe("OP Mainnet");
- });
-
- test("should return Optimism Sepolia for eip155:11155420", () => {
- const chain = getViemChain("eip155:11155420");
- expect(chain.id).toBe(11155420);
- expect(chain.name).toBe("OP Sepolia");
- });
-
- test("should return Base Mainnet for eip155:8453", () => {
- const chain = getViemChain("eip155:8453");
- expect(chain.id).toBe(8453);
- expect(chain.name).toBe("Base");
- });
-
- test("should return Base Sepolia for eip155:84532", () => {
- const chain = getViemChain("eip155:84532");
- expect(chain.id).toBe(84532);
- expect(chain.name).toBe("Base Sepolia");
- });
-
- test("should throw for unsupported network", () => {
- expect(() => getViemChain("eip155:1")).toThrow("Unsupported network: eip155:1");
- });
- });
-
- describe("getChainNameFromEIP155()", () => {
- test("should return human-readable names for supported networks", () => {
- expect(getChainNameFromEIP155("eip155:10")).toBe("OP Mainnet");
- expect(getChainNameFromEIP155("eip155:11155420")).toBe("OP Sepolia");
- expect(getChainNameFromEIP155("eip155:8453")).toBe("Base");
- expect(getChainNameFromEIP155("eip155:84532")).toBe("Base Sepolia");
- });
-
- test("should return fallback for unsupported networks", () => {
- expect(getChainNameFromEIP155("eip155:1")).toBe("Unknown (eip155:1)");
- expect(getChainNameFromEIP155("invalid")).toBe("Unknown (invalid)");
- });
- });
+ // Note: getViemChain() tests moved to @fretchen/chain-utils
+ // Note: getChainNameFromEIP155() removed - use getViemChain(network).name directly
describe("getUSDCConfig()", () => {
test("should return valid config for Optimism Mainnet", () => {
@@ -136,7 +90,7 @@ describe("getChain.js - Chain Configuration Tests", () => {
});
test("should throw for unconfigured network", () => {
- expect(() => getUSDCConfig("eip155:1")).toThrow("USDC not available on eip155:1");
+ expect(() => getUSDCConfig("eip155:1")).toThrow("Unsupported network: eip155:1");
});
test("should have consistent chainId between viem chain and USDC config", () => {
@@ -151,23 +105,8 @@ describe("getChain.js - Chain Configuration Tests", () => {
});
});
- describe("getGenImgContractConfig()", () => {
- test("should return Mainnet contract address", () => {
- const config = getGenImgContractConfig("eip155:10");
- expect(config.address).toBe("0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb");
- });
-
- test("should return Sepolia contract address", () => {
- const config = getGenImgContractConfig("eip155:11155420");
- expect(config.address).toBe("0x10827cC42a09D0BAD2d43134C69F0e776D853D85");
- });
-
- test("should throw for network without GenImg deployment", () => {
- expect(() => getGenImgContractConfig("eip155:8453")).toThrow(
- "GenAI NFT not deployed on eip155:8453",
- );
- });
- });
+ // 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", () => {
From 81270c1e0320089bb50b1c2a1e6fe382a7577167 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 20:29:47 +0100
Subject: [PATCH 07/46] More cleaning
---
scw_js/genimg_bfl.js | 2 +-
scw_js/genimg_x402_token.js | 6 +-
scw_js/getChain.js | 29 +-
scw_js/llmv1_abi.js | 586 -----------
scw_js/nft_abi.js | 871 ----------------
scw_js/nft_abi_v2.js | 943 ------------------
scw_js/readhandler_v2.js | 2 +-
scw_js/test/genimg_x402_token.test.js | 6 +-
scw_js/test/getChain.test.js | 6 +-
scw_js/test/nft_abi.test.js | 81 --
scw_js/x402_implementation_plan.md | 740 --------------
scw_js/x402_server.js | 2 +-
.../chain-utils/src/abi/EIP3009SplitterV1.ts | 82 ++
shared/chain-utils/src/abi/GenImNFTv4.ts | 111 +++
shared/chain-utils/src/abi/LLMv1.ts | 42 +
shared/chain-utils/src/abi/index.ts | 10 +
shared/chain-utils/src/addresses.ts | 23 +
shared/chain-utils/src/index.ts | 35 +
shared/chain-utils/test/abi.test.ts | 127 +++
shared/chain-utils/test/index.test.ts | 47 +
20 files changed, 490 insertions(+), 3261 deletions(-)
delete mode 100644 scw_js/llmv1_abi.js
delete mode 100644 scw_js/nft_abi.js
delete mode 100644 scw_js/nft_abi_v2.js
delete mode 100644 scw_js/test/nft_abi.test.js
delete mode 100644 scw_js/x402_implementation_plan.md
create mode 100644 shared/chain-utils/src/abi/EIP3009SplitterV1.ts
create mode 100644 shared/chain-utils/src/abi/GenImNFTv4.ts
create mode 100644 shared/chain-utils/src/abi/LLMv1.ts
create mode 100644 shared/chain-utils/src/abi/index.ts
create mode 100644 shared/chain-utils/test/abi.test.ts
diff --git a/scw_js/genimg_bfl.js b/scw_js/genimg_bfl.js
index 8ea5e8671..82e01f88e 100644
--- a/scw_js/genimg_bfl.js
+++ b/scw_js/genimg_bfl.js
@@ -1,4 +1,4 @@
-import { nftAbi } from "./nft_abi.js";
+import { GenImNFTv4ABI as nftAbi } from "@fretchen/chain-utils";
import { getContract, createWalletClient, createPublicClient, http } from "viem";
import { optimism } from "viem/chains";
import { generateAndUploadImage, JSON_BASE_PATH } from "./image_service.js";
diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js
index 257524158..0173cb7d4 100644
--- a/scw_js/genimg_x402_token.js
+++ b/scw_js/genimg_x402_token.js
@@ -1,7 +1,7 @@
// x402 v2 Token Payment Implementation for GenImg
// Uses official @x402/core and @x402/evm packages for payment handling
-import { nftAbi } from "./nft_abi.js";
+import { GenImNFTv4ABI as nftAbi } from "@fretchen/chain-utils";
import { getContract, createWalletClient, createPublicClient, http, parseEther } from "viem";
import { generateAndUploadImage, JSON_BASE_PATH } from "./image_service.js";
import { privateKeyToAccount } from "viem/accounts";
@@ -12,8 +12,8 @@ import {
extractPaymentPayload,
createSettlementHeaders,
} from "./x402_server.js";
-import { getViemChain, getGenAiNFTAddress } from "@fretchen/chain-utils";
-import { getUSDCConfig, validatePaymentNetwork } from "./getChain.js";
+import { getViemChain, getGenAiNFTAddress, getUSDCConfig } from "@fretchen/chain-utils";
+import { validatePaymentNetwork } from "./getChain.js";
// Re-export x402 functions for backward compatibility with tests
export { handle, create402Response };
diff --git a/scw_js/getChain.js b/scw_js/getChain.js
index 72b872687..043221958 100644
--- a/scw_js/getChain.js
+++ b/scw_js/getChain.js
@@ -1,7 +1,6 @@
// @ts-check
import { sepolia, optimism, optimismSepolia } from "viem/chains";
-import { LLMv1ABI } from "./llmv1_abi.js";
-import { getViemChain, fromCAIP2, getUSDCAddress, getUSDCName } from "@fretchen/chain-utils";
+import { LLMv1ABI } from "@fretchen/chain-utils";
/**
* Get environment variable in both Node.js and Vite contexts
@@ -70,32 +69,6 @@ export function getLLMv1ContractConfig() {
}
}
-/**
- * USDC configuration for supported networks
- * @typedef {Object} USDCConfig
- * @property {string} name - Human-readable network name
- * @property {number} chainId - EVM chain ID
- * @property {`0x${string}`} address - USDC contract address
- * @property {number} decimals - USDC decimals (always 6)
- * @property {string} usdcName - USDC permit name (for EIP-2612)
- * @property {string} usdcVersion - USDC permit version
- */
-
-/** returns the USDC configuration for a CAIP-2 network ID
- * @param {string} network - CAIP-2 network ID (e.g., "eip155:10", "eip155:11155420")
- * @returns {USDCConfig}
- */
-export function getUSDCConfig(network) {
- return {
- name: getViemChain(network).name,
- chainId: fromCAIP2(network),
- address: getUSDCAddress(network),
- decimals: 6,
- usdcName: getUSDCName(network),
- usdcVersion: "2",
- };
-}
-
/**
* Get the expected network for a given mode
* @param {boolean} sepoliaTest - Whether test mode is enabled
diff --git a/scw_js/llmv1_abi.js b/scw_js/llmv1_abi.js
deleted file mode 100644
index 7e8cb74a5..000000000
--- a/scw_js/llmv1_abi.js
+++ /dev/null
@@ -1,586 +0,0 @@
-// Auto-generated ABI for LLMv1
-// LLM Version 1 to interact with LLMs
-// Generated on: 2025-08-05T16:01:22.279Z
-
-export const LLMv1ABI = [
- {
- inputs: [],
- stateMutability: "nonpayable",
- type: "constructor",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "target",
- type: "address",
- },
- ],
- name: "AddressEmptyCode",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "ERC1967InvalidImplementation",
- type: "error",
- },
- {
- inputs: [],
- name: "ERC1967NonPayable",
- type: "error",
- },
- {
- inputs: [],
- name: "FailedCall",
- type: "error",
- },
- {
- inputs: [],
- name: "InvalidInitialization",
- type: "error",
- },
- {
- inputs: [],
- name: "NotInitializing",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "OwnableInvalidOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "account",
- type: "address",
- },
- ],
- name: "OwnableUnauthorizedAccount",
- type: "error",
- },
- {
- inputs: [],
- name: "UUPSUnauthorizedCallContext",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "bytes32",
- name: "slot",
- type: "bytes32",
- },
- ],
- name: "UUPSUnsupportedProxiableUUID",
- type: "error",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "bytes32",
- name: "root",
- type: "bytes32",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "totalCost",
- type: "uint256",
- },
- ],
- name: "BatchProcessed",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint256",
- name: "index",
- type: "uint256",
- },
- {
- indexed: false,
- internalType: "bytes32",
- name: "hash",
- type: "bytes32",
- },
- ],
- name: "DebugLeafHash",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint64",
- name: "version",
- type: "uint64",
- },
- ],
- name: "Initialized",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "user",
- type: "address",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "amount",
- type: "uint256",
- },
- ],
- name: "LLMDeposit",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "user",
- type: "address",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "amount",
- type: "uint256",
- },
- ],
- name: "LLMWithdraw",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "previousOwner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "OwnershipTransferred",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "provider",
- type: "address",
- },
- ],
- name: "ServiceProviderAdded",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "newProvider",
- type: "address",
- },
- ],
- name: "ServiceProviderChanged",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "provider",
- type: "address",
- },
- ],
- name: "ServiceProviderRemoved",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "Upgraded",
- type: "event",
- },
- {
- inputs: [],
- name: "UPGRADE_INTERFACE_VERSION",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "provider",
- type: "address",
- },
- ],
- name: "addServiceProvider",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- name: "authorizedProviders",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "user",
- type: "address",
- },
- ],
- name: "checkBalance",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "defaultServiceProvider",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "depositForLLM",
- outputs: [],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [],
- name: "initialize",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "provider",
- type: "address",
- },
- ],
- name: "isAuthorizedProvider",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- name: "llmBalance",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "owner",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "bytes32",
- name: "merkleRoot",
- type: "bytes32",
- },
- {
- components: [
- {
- internalType: "int256",
- name: "id",
- type: "int256",
- },
- {
- internalType: "address",
- name: "user",
- type: "address",
- },
- {
- internalType: "address",
- name: "serviceProvider",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenCount",
- type: "uint256",
- },
- {
- internalType: "uint256",
- name: "cost",
- type: "uint256",
- },
- {
- internalType: "string",
- name: "timestamp",
- type: "string",
- },
- ],
- internalType: "struct LLMv1.LLMLeaf[]",
- name: "leaves",
- type: "tuple[]",
- },
- {
- internalType: "bytes32[][]",
- name: "proofs",
- type: "bytes32[][]",
- },
- ],
- name: "processBatch",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "bytes32",
- name: "",
- type: "bytes32",
- },
- ],
- name: "processedBatches",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "proxiableUUID",
- outputs: [
- {
- internalType: "bytes32",
- name: "",
- type: "bytes32",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "provider",
- type: "address",
- },
- ],
- name: "removeServiceProvider",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [],
- name: "renounceOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newProvider",
- type: "address",
- },
- ],
- name: "setDefaultServiceProvider",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "transferOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newImplementation",
- type: "address",
- },
- {
- internalType: "bytes",
- name: "data",
- type: "bytes",
- },
- ],
- name: "upgradeToAndCall",
- outputs: [],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "bytes32[]",
- name: "proof",
- type: "bytes32[]",
- },
- {
- internalType: "bytes32",
- name: "root",
- type: "bytes32",
- },
- {
- internalType: "bytes32",
- name: "leaf",
- type: "bytes32",
- },
- ],
- name: "verifyMerkleProof",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "pure",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "amount",
- type: "uint256",
- },
- ],
- name: "withdrawBalance",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
-];
diff --git a/scw_js/nft_abi.js b/scw_js/nft_abi.js
deleted file mode 100644
index 27510362a..000000000
--- a/scw_js/nft_abi.js
+++ /dev/null
@@ -1,871 +0,0 @@
-export const nftAbi = [
- {
- inputs: [
- {
- internalType: "address",
- name: "target",
- type: "address",
- },
- ],
- name: "AddressEmptyCode",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "ERC1967InvalidImplementation",
- type: "error",
- },
- {
- inputs: [],
- name: "ERC1967NonPayable",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "sender",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "ERC721IncorrectOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ERC721InsufficientApproval",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "approver",
- type: "address",
- },
- ],
- name: "ERC721InvalidApprover",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- ],
- name: "ERC721InvalidOperator",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "ERC721InvalidOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "receiver",
- type: "address",
- },
- ],
- name: "ERC721InvalidReceiver",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "sender",
- type: "address",
- },
- ],
- name: "ERC721InvalidSender",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ERC721NonexistentToken",
- type: "error",
- },
- {
- inputs: [],
- name: "FailedCall",
- type: "error",
- },
- {
- inputs: [],
- name: "InvalidInitialization",
- type: "error",
- },
- {
- inputs: [],
- name: "NotInitializing",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "OwnableInvalidOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "account",
- type: "address",
- },
- ],
- name: "OwnableUnauthorizedAccount",
- type: "error",
- },
- {
- inputs: [],
- name: "UUPSUnauthorizedCallContext",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "bytes32",
- name: "slot",
- type: "bytes32",
- },
- ],
- name: "UUPSUnsupportedProxiableUUID",
- type: "error",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "approved",
- type: "address",
- },
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "Approval",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- indexed: false,
- internalType: "bool",
- name: "approved",
- type: "bool",
- },
- ],
- name: "ApprovalForAll",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint256",
- name: "_fromTokenId",
- type: "uint256",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "_toTokenId",
- type: "uint256",
- },
- ],
- name: "BatchMetadataUpdate",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- {
- indexed: false,
- internalType: "string",
- name: "imageUrl",
- type: "string",
- },
- ],
- name: "ImageUpdateRequested",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- ],
- name: "ImageUpdaterAuthorized",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint64",
- name: "version",
- type: "uint64",
- },
- ],
- name: "Initialized",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint256",
- name: "_tokenId",
- type: "uint256",
- },
- ],
- name: "MetadataUpdate",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "previousOwner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "OwnershipTransferred",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "Transfer",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "amount",
- type: "uint256",
- },
- ],
- name: "UpdaterPaid",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "Upgraded",
- type: "event",
- },
- {
- inputs: [],
- name: "UPGRADE_INTERFACE_VERSION",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "approve",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "balanceOf",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "burn",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "getApproved",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "initialize",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- ],
- name: "isApprovedForAll",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "isImageUpdated",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "mintPrice",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "name",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "owner",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ownerOf",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "proxiableUUID",
- outputs: [
- {
- internalType: "bytes32",
- name: "",
- type: "bytes32",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "renounceOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "string",
- name: "imageUrl",
- type: "string",
- },
- ],
- name: "requestImageUpdate",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "string",
- name: "uri",
- type: "string",
- },
- ],
- name: "safeMint",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "string",
- name: "uri",
- type: "string",
- },
- {
- internalType: "bool",
- name: "isListed",
- type: "bool",
- },
- ],
- name: "safeMint",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "safeTransferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "bytes",
- name: "data",
- type: "bytes",
- },
- ],
- name: "safeTransferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- internalType: "bool",
- name: "approved",
- type: "bool",
- },
- ],
- name: "setApprovalForAll",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "newPrice",
- type: "uint256",
- },
- ],
- name: "setMintPrice",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "bytes4",
- name: "interfaceId",
- type: "bytes4",
- },
- ],
- name: "supportsInterface",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "symbol",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "tokenURI",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "transferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "transferOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newImplementation",
- type: "address",
- },
- {
- internalType: "bytes",
- name: "data",
- type: "bytes",
- },
- ],
- name: "upgradeToAndCall",
- outputs: [],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [],
- name: "withdraw",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
-];
diff --git a/scw_js/nft_abi_v2.js b/scw_js/nft_abi_v2.js
deleted file mode 100644
index 3f7d02431..000000000
--- a/scw_js/nft_abi_v2.js
+++ /dev/null
@@ -1,943 +0,0 @@
-export const nftAbi = [
- {
- inputs: [
- {
- internalType: "address",
- name: "target",
- type: "address",
- },
- ],
- name: "AddressEmptyCode",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "ERC1967InvalidImplementation",
- type: "error",
- },
- {
- inputs: [],
- name: "ERC1967NonPayable",
- type: "error",
- },
- {
- inputs: [],
- name: "ERC721EnumerableForbiddenBatchMint",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "sender",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "ERC721IncorrectOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ERC721InsufficientApproval",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "approver",
- type: "address",
- },
- ],
- name: "ERC721InvalidApprover",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- ],
- name: "ERC721InvalidOperator",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "ERC721InvalidOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "receiver",
- type: "address",
- },
- ],
- name: "ERC721InvalidReceiver",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "sender",
- type: "address",
- },
- ],
- name: "ERC721InvalidSender",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ERC721NonexistentToken",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "index",
- type: "uint256",
- },
- ],
- name: "ERC721OutOfBoundsIndex",
- type: "error",
- },
- {
- inputs: [],
- name: "FailedCall",
- type: "error",
- },
- {
- inputs: [],
- name: "InvalidInitialization",
- type: "error",
- },
- {
- inputs: [],
- name: "NotInitializing",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "OwnableInvalidOwner",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "account",
- type: "address",
- },
- ],
- name: "OwnableUnauthorizedAccount",
- type: "error",
- },
- {
- inputs: [],
- name: "UUPSUnauthorizedCallContext",
- type: "error",
- },
- {
- inputs: [
- {
- internalType: "bytes32",
- name: "slot",
- type: "bytes32",
- },
- ],
- name: "UUPSUnsupportedProxiableUUID",
- type: "error",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "approved",
- type: "address",
- },
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "Approval",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- indexed: false,
- internalType: "bool",
- name: "approved",
- type: "bool",
- },
- ],
- name: "ApprovalForAll",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint256",
- name: "_fromTokenId",
- type: "uint256",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "_toTokenId",
- type: "uint256",
- },
- ],
- name: "BatchMetadataUpdate",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- {
- indexed: false,
- internalType: "string",
- name: "imageUrl",
- type: "string",
- },
- ],
- name: "ImageUpdateRequested",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- ],
- name: "ImageUpdaterAuthorized",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint64",
- name: "version",
- type: "uint64",
- },
- ],
- name: "Initialized",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: false,
- internalType: "uint256",
- name: "_tokenId",
- type: "uint256",
- },
- ],
- name: "MetadataUpdate",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "previousOwner",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "OwnershipTransferred",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- indexed: true,
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "Transfer",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- indexed: true,
- internalType: "address",
- name: "updater",
- type: "address",
- },
- {
- indexed: false,
- internalType: "uint256",
- name: "amount",
- type: "uint256",
- },
- ],
- name: "UpdaterPaid",
- type: "event",
- },
- {
- anonymous: false,
- inputs: [
- {
- indexed: true,
- internalType: "address",
- name: "implementation",
- type: "address",
- },
- ],
- name: "Upgraded",
- type: "event",
- },
- {
- inputs: [],
- name: "UPGRADE_INTERFACE_VERSION",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "approve",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- ],
- name: "balanceOf",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "burn",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "getApproved",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "getAuthorizedImageUpdater",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "initialize",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- ],
- name: "isApprovedForAll",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "isImageUpdated",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "mintPrice",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "name",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "owner",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "ownerOf",
- outputs: [
- {
- internalType: "address",
- name: "",
- type: "address",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "proxiableUUID",
- outputs: [
- {
- internalType: "bytes32",
- name: "",
- type: "bytes32",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "renounceOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "string",
- name: "imageUrl",
- type: "string",
- },
- ],
- name: "requestImageUpdate",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "string",
- name: "uri",
- type: "string",
- },
- ],
- name: "safeMint",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "safeTransferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- {
- internalType: "bytes",
- name: "data",
- type: "bytes",
- },
- ],
- name: "safeTransferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "operator",
- type: "address",
- },
- {
- internalType: "bool",
- name: "approved",
- type: "bool",
- },
- ],
- name: "setApprovalForAll",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "newPrice",
- type: "uint256",
- },
- ],
- name: "setMintPrice",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "bytes4",
- name: "interfaceId",
- type: "bytes4",
- },
- ],
- name: "supportsInterface",
- outputs: [
- {
- internalType: "bool",
- name: "",
- type: "bool",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "symbol",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "index",
- type: "uint256",
- },
- ],
- name: "tokenByIndex",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "owner",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "index",
- type: "uint256",
- },
- ],
- name: "tokenOfOwnerByIndex",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "tokenURI",
- outputs: [
- {
- internalType: "string",
- name: "",
- type: "string",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [],
- name: "totalSupply",
- outputs: [
- {
- internalType: "uint256",
- name: "",
- type: "uint256",
- },
- ],
- stateMutability: "view",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "from",
- type: "address",
- },
- {
- internalType: "address",
- name: "to",
- type: "address",
- },
- {
- internalType: "uint256",
- name: "tokenId",
- type: "uint256",
- },
- ],
- name: "transferFrom",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newOwner",
- type: "address",
- },
- ],
- name: "transferOwnership",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
- {
- inputs: [
- {
- internalType: "address",
- name: "newImplementation",
- type: "address",
- },
- {
- internalType: "bytes",
- name: "data",
- type: "bytes",
- },
- ],
- name: "upgradeToAndCall",
- outputs: [],
- stateMutability: "payable",
- type: "function",
- },
- {
- inputs: [],
- name: "withdraw",
- outputs: [],
- stateMutability: "nonpayable",
- type: "function",
- },
-];
diff --git a/scw_js/readhandler_v2.js b/scw_js/readhandler_v2.js
index 8ce788944..5c50b3739 100644
--- a/scw_js/readhandler_v2.js
+++ b/scw_js/readhandler_v2.js
@@ -1,4 +1,4 @@
-import { nftAbi } from "./nft_abi.js";
+import { GenImNFTv4ABI as nftAbi } from "@fretchen/chain-utils";
import { getContract, createWalletClient, createPublicClient, http } from "viem";
import { optimism } from "viem/chains";
import { generateAndUploadImage, JSON_BASE_PATH } from "./image_service.js";
diff --git a/scw_js/test/genimg_x402_token.test.js b/scw_js/test/genimg_x402_token.test.js
index 73b7b858b..0324b0bfa 100644
--- a/scw_js/test/genimg_x402_token.test.js
+++ b/scw_js/test/genimg_x402_token.test.js
@@ -1193,7 +1193,7 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => {
describe("Security: Payment Requirements", () => {
test("should create payment requirements with all supported networks", async () => {
const { createPaymentRequirements } = await import("../x402_server.js");
- const { getUSDCConfig } = await import("../getChain.js");
+ const { getUSDCConfig } = await import("@fretchen/chain-utils");
const requirements = createPaymentRequirements({
resourceUrl: "/test",
@@ -1218,7 +1218,7 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => {
test("should validate all supported networks are configured", async () => {
const { getSupportedNetworks } = await import("../x402_server.js");
- const { getUSDCConfig } = await import("../getChain.js");
+ const { getUSDCConfig } = await import("@fretchen/chain-utils");
const supportedNetworks = getSupportedNetworks();
@@ -1252,7 +1252,7 @@ describe("genimg_x402_token.js - x402 v2 Token Payment Tests", () => {
* - User loses money without receiving service!
*/
test("should use correct EIP-712 domain names for USDC contracts", async () => {
- const { getUSDCConfig } = await import("../getChain.js");
+ const { getUSDCConfig } = await import("@fretchen/chain-utils");
// Optimism Mainnet uses "USD Coin" (official Circle USDC deployment)
const mainnetConfig = getUSDCConfig("eip155:10");
diff --git a/scw_js/test/getChain.test.js b/scw_js/test/getChain.test.js
index 3c6d3d2e7..a6cd064f1 100644
--- a/scw_js/test/getChain.test.js
+++ b/scw_js/test/getChain.test.js
@@ -19,10 +19,10 @@ import { createPublicClient, http } from "viem";
import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
// Import functions under test
-import { getUSDCConfig, getExpectedNetwork, validatePaymentNetwork } from "../getChain.js";
+import { getExpectedNetwork, validatePaymentNetwork } from "../getChain.js";
-// Import from chain-utils for consistency checks
-import { getViemChain } from "@fretchen/chain-utils";
+// Import from chain-utils
+import { getViemChain, getUSDCConfig } from "@fretchen/chain-utils";
// USDC contracts expose name() and version() functions for EIP-712 domain
// Note: They do NOT implement EIP-5267 eip712Domain(), so we read individual functions
diff --git a/scw_js/test/nft_abi.test.js b/scw_js/test/nft_abi.test.js
deleted file mode 100644
index 5074320e3..000000000
--- a/scw_js/test/nft_abi.test.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Tests für NFT ABI und Contract-Interaktionen
- */
-
-import { describe, test, expect, beforeAll } from "vitest";
-
-describe("NFT ABI Tests", () => {
- let nftAbi;
-
- beforeAll(async () => {
- const module = await import("../nft_abi_v2.js");
- nftAbi = module.nftAbi;
- });
-
- describe("ABI Structure Tests", () => {
- test("sollte gültige ABI-Struktur haben", () => {
- expect(Array.isArray(nftAbi)).toBe(true);
- expect(nftAbi.length).toBeGreaterThan(0);
- });
-
- test("sollte erforderliche Funktionen enthalten", () => {
- const functionNames = nftAbi
- .filter((item) => item.type === "function")
- .map((item) => item.name);
-
- const requiredFunctions = ["ownerOf", "mintPrice", "isImageUpdated", "requestImageUpdate"];
-
- requiredFunctions.forEach((funcName) => {
- expect(functionNames).toContain(funcName);
- });
- });
-
- test("sollte korrekte Funktions-Signaturen haben", () => {
- const ownerOfFunction = nftAbi.find(
- (item) => item.type === "function" && item.name === "ownerOf",
- );
-
- expect(ownerOfFunction).toBeDefined();
- expect(ownerOfFunction.inputs).toHaveLength(1);
- expect(ownerOfFunction.inputs[0].type).toBe("uint256");
- expect(ownerOfFunction.outputs).toHaveLength(1);
- expect(ownerOfFunction.outputs[0].type).toBe("address");
-
- const requestImageUpdateFunction = nftAbi.find(
- (item) => item.type === "function" && item.name === "requestImageUpdate",
- );
-
- expect(requestImageUpdateFunction).toBeDefined();
- expect(requestImageUpdateFunction.inputs).toHaveLength(2);
- expect(requestImageUpdateFunction.inputs[0].type).toBe("uint256");
- expect(requestImageUpdateFunction.inputs[1].type).toBe("string");
- });
-
- test("sollte Events definiert haben", () => {
- const events = nftAbi.filter((item) => item.type === "event");
- expect(events.length).toBeGreaterThan(0);
- });
- });
-});
-
-describe("Contract Utility Tests", () => {
- test("sollte Contract-Adresse validieren", () => {
- const contractAddress = "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb";
-
- // Ethereum-Adresse Format prüfen
- expect(contractAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
- expect(contractAddress.length).toBe(42);
- });
-
- test("sollte BigInt-Konvertierungen korrekt handhaben", () => {
- const testValues = [
- { input: "1", expected: BigInt("1") },
- { input: "999999999999999999", expected: BigInt("999999999999999999") },
- { input: "0", expected: BigInt("0") },
- ];
-
- testValues.forEach(({ input, expected }) => {
- expect(BigInt(input)).toBe(expected);
- });
- });
-});
diff --git a/scw_js/x402_implementation_plan.md b/scw_js/x402_implementation_plan.md
deleted file mode 100644
index b5c2f65df..000000000
--- a/scw_js/x402_implementation_plan.md
+++ /dev/null
@@ -1,740 +0,0 @@
----
-publishing_date: 2025-12-21
-title: x402 Implementation Plan für ImageGen
-category: "blockchain"
-secondaryCategory: "webdev"
-description: "Detaillierter Implementierungsplan zur Integration des x402 Payment-Protokolls in den AI Image Generation Service mit Beibehaltung der NFT-Funktionalität. Der NFT-Mint dient als Payment-Proof."
----
-
-## Übersicht
-
-Integration des x402 Payment-Protokolls in den ImageGen-Service **mit Beibehaltung der NFT-Funktionalität**. Der NFT-Mint wird als Payment-Proof akzeptiert.
-
-Das x402-Protokoll nutzt den HTTP-Statuscode `402 Payment Required` für automatische Mikrozahlungen direkt im HTTP-Request/Response-Zyklus.
-
-### Technologie-Stack
-
-- **x402 Protocol Standard** - HTTP 402 Payment Required Protokoll (Manual Implementation)
-- **Viem** - Blockchain-Interaktion und Event-Parsing
-- **Custom Implementation** - Direkte On-Chain Verification ohne Facilitator
-
-**Aufteilung:**
-
-- ✅ x402-Style 402 Response: Manual (klar und direkt)
-- ✅ Transaction Verification: Viem (status, recipient, amount)
-- ✅ Mint-Event Parsing: Custom (NFT-spezifisch, TokenId Extraktion)
-
-**Warum KEIN [x402 npm Package](https://www.npmjs.com/package/x402)?**
-
-1. **Facilitator-Dependency**: x402 Package ist designed für Facilitator-basierte Verification (zentralisierter Service)
-
- ```javascript
- // x402 Package Design:
- import { verify } from "x402/verify";
- const result = await verify(payload, requirements); // ➜ Ruft Facilitator auf!
- ```
-
-2. **Self-Sovereign Approach**: Wir wollen direkt On-Chain verifizieren ohne externe Dependencies
-
- ```javascript
- // Unser Ansatz:
- const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
- const mintLog = receipt.logs.find(/* Transfer from 0x0 */);
- // ➜ Direkt auf Optimism, kein Middleman
- ```
-
-3. **NFT-Mint-Spezifisch**: x402 ist optimiert für Standard USDC-Transfers, nicht für NFT-Mint-Verification mit TokenId-Extraktion
-
-4. **Subpath Exports**: x402 Package nutzt Subpath Exports (`x402/client`, `x402/verify`), nicht Root-Import
-
-**Entscheidung**: Manual x402-konforme Implementation für maximale Kontrolle und Zero-Dependency-Approach
-
----
-
-## x402 Package vs. Manual Implementation
-
-### x402 Package Analyse
-
-Das [x402 npm Package](https://www.npmjs.com/package/x402) (70k+ Downloads) ist ein **production-ready** Package von Coinbase für das x402 Payment Protocol. Es bietet:
-
-**Verfügbare Module:**
-
-```javascript
-import { verify, settle } from "x402/verify"; // Facilitator-basierte Verification
-import { preparePaymentHeader } from "x402/client"; // Client-side Payment Header
-import { exact } from "x402/schemes"; // Payment Schemes
-```
-
-**Package Design:**
-
-- ✅ Middleware für Express/Hono/Next.js
-- ✅ Standardisierte 402 Response Formate
-- ✅ Facilitator-Integration für Payment Verification
-- ✅ Lifecycle Hooks (onBeforeVerify, onAfterSettle, etc.)
-- ✅ Multi-Network Support (EVM, Solana)
-
-**Warum nicht verwendet:**
-
-| Aspekt | x402 Package | Unsere Requirements |
-| ---------------- | ------------------------ | --------------------- |
-| **Verification** | Facilitator-Service | Direkt On-Chain |
-| **Architecture** | Middleware-basiert | Serverless Function |
-| **Payment Type** | Standard USDC Transfer | NFT Mint Transaction |
-| **Dependencies** | Facilitator erforderlich | Self-Sovereign |
-| **Use Case** | Generische API Payments | NFT-spezifischer Flow |
-
-**Beispiel: x402 Package mit Facilitator**
-
-```javascript
-import { useFacilitator } from "x402/verify";
-
-const { verify } = useFacilitator({
- url: "https://x402.org/facilitator", // ❌ Externe Dependency!
-});
-
-const result = await verify(paymentPayload, paymentRequirements);
-// ➜ Ruft Facilitator auf, nicht direkt Blockchain
-```
-
-**Unsere Manual Implementation**
-
-```javascript
-// Direkte On-Chain Verification ohne Facilitator
-const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
-
-// Basic Checks
-if (receipt.status !== "success") return { valid: false };
-if (tx.to !== CONTRACT_ADDRESS) return { valid: false };
-if (tx.value < MINT_PRICE) return { valid: false };
-
-// NFT-spezifisch: TokenId aus Mint-Event extrahieren
-const mintLog = receipt.logs.find(log => /* Transfer from 0x0 */);
-const tokenId = parseInt(mintLog.topics[3], 16);
-
-return { valid: true, tokenId, payer };
-```
-
-**Vorteile unserer Manual Implementation:**
-
-- ✅ **Self-Sovereign**: Keine Abhängigkeit von Facilitator-Services
-- ✅ **NFT-Optimiert**: TokenId-Extraktion aus Mint-Event
-- ✅ **Serverless-Ready**: Passt perfekt zu Scaleway Functions
-- ✅ **Transparent**: Klarer, direkter Code ohne Abstraction-Layer
-- ✅ **Zero External Dependencies**: Nur Viem für Blockchain-Zugriff
-
-**Wann x402 Package verwenden?**
-
-- Express/Hono/Next.js Middleware-Integration
-- Standard USDC Payment Flows
-- Facilitator-basierte Verification gewünscht
-- Multi-Network Support benötigt
-
----
-
-## Aktueller Workflow vs. x402 Workflow
-
-### Aktueller Workflow (Mint-then-Generate)
-
-```
-┌─────────────┐ ┌──────────────────┐
-│ Browser │ │ GenImNFTv4 │
-│ (User) │ │ Smart Contract │
-└──────┬──────┘ └────────┬─────────┘
- │ │
- │ 1. User gibt Prompt ein │
- │ │
- │ 2. mint() mit ETH ────────────────────────▶│
- │ │
- │ 3. TokenId zurück ◀─────────────────────────│
- │ │
- │ ┌───────────────┴───────────────┐
- │ │ genimg_bfl.js │
- │ │ (Serverless) │
- │ └───────────────┬───────────────┘
- │ │
- │ 4. POST /genimg { tokenId, prompt } ───────▶│
- │ │
- │ (Server prüft: Token existiert? │
- │ Hat Token schon Bild?) │
- │ │
- │ 5. Bild generieren (BFL API) │
- │ │
- │ 6. TokenURI on-chain updaten │
- │ │
- │ 7. Response { image_url } ◀─────────────────│
- ▼ ▼
-```
-
-**Probleme:**
-
-- Zwei separate User-Interaktionen (Mint + Generate)
-- Server muss Token-Status on-chain prüfen
-- Kein standardisiertes Payment-Protokoll
-
-### Neuer Workflow (x402 mit NFT-Mint als Payment)
-
-```
-┌─────────────┐ ┌──────────────────┐
-│ Browser │ │ genimg_bfl.js │
-│ (User) │ │ (Serverless) │
-└──────┬──────┘ └────────┬─────────┘
- │ │
- │ 1. POST /genimg { prompt } ────────────────▶│
- │ │
- │ 2. 402 Payment Required ◀───────────────────│
- │ X-Payment: { │
- │ "scheme": "exact", │
- │ "network": "optimism", │
- │ "maxAmountRequired": "500000...", │
- │ "contractAddress": "0x80f95d...", │
- │ "contractMethod": "mint()" │
- │ } │
- │ │
- │ ┌───────────────┴───────────────┐
- │ │ GenImNFTv4 │
- │ │ Smart Contract │
- │ └───────────────┬───────────────┘
- │ │
- │ 3. mint() mit ETH ─────────────────────────▶│
- │ │
- │ 4. txHash + tokenId ◀───────────────────────│
- │ │
- │ ┌───────────────┴───────────────┐
- │ │ genimg_bfl.js │
- │ └───────────────┬───────────────┘
- │ │
- │ 5. POST /genimg { prompt } │
- │ X-Payment: { txHash, tokenId } ─────────▶│
- │ │
- │ (Server verifiziert Mint-Event) │
- │ │
- │ 6. Bild generieren + TokenURI updaten │
- │ │
- │ 7. Response { image_url, tokenId } ◀────────│
- ▼ ▼
-```
-
-**Vorteile:**
-
-- ✅ NFT-Funktionalität bleibt erhalten
-- ✅ x402-konformes Payment-Protokoll
-- ✅ Ein zusammenhängender User-Flow
-- ✅ Standardisierte Payment-Discovery
-- ✅ Mint-Transaktion IST der Payment-Proof
-
----
-
-## Phase 1: Server-Side (genimg_bfl.js)
-
-**Ziel:** 402-Response bei fehlendem Payment, Mint-Verification bei vorhandenem Payment
-
-### 1.0 Dependencies
-
-```bash
-cd scw_js
-# Viem bereits installiert (v2.38.3)
-# Keine zusätzlichen Dependencies nötig
-```
-
-**Note**: `viem` bereits vorhanden für Blockchain-Interaktion. Keine x402 Package Dependency.
-
-### 1.1 Request-Handler erweitern
-
-- Prüfe `X-Payment` Header auf eingehende Requests
-- Ohne Payment → Return `402` via x402 Package
-- Mit Payment → Verifiziere Mint-Event und generiere Bild
-
-### 1.2 402 Response (x402-konform, Manual)
-
-```javascript
-// Config
-const MINT_PRICE = "500000000000000"; // 0.0005 ETH
-const GENIMG_CONTRACT_ADDRESS = "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb";
-
-// 402-Response erstellen (x402-Protocol-konform)
-function create402Response() {
- const paymentInfo = {
- scheme: "exact",
- network: "optimism",
- maxAmountRequired: MINT_PRICE,
- recipient: GENIMG_CONTRACT_ADDRESS,
- metadata: {
- resource: "genimg",
- description: "Mint an NFT to generate your AI image",
- paymentType: "contract-call",
- contractAddress: GENIMG_CONTRACT_ADDRESS,
- contractMethod: "mint()",
- },
- };
-
- return {
- statusCode: 402,
- headers: {
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Headers": "*",
- "Access-Control-Allow-Methods": "*",
- "Content-Type": "application/json",
- "X-Payment": JSON.stringify(paymentInfo),
- },
- body: JSON.stringify({
- error: "Payment required",
- message: "Please mint an NFT to generate your image",
- payment: paymentInfo,
- }),
- };
-}
-```
-
-**Generiertes Format:**
-
-```http
-HTTP/1.1 402 Payment Required
-X-Payment: {
- "scheme": "exact",
- "network": "optimism",
- "maxAmountRequired": "500000000000000",
- "recipient": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- "metadata": {
- "resource": "genimg",
- "description": "Mint an NFT to generate your AI image",
- "paymentType": "contract-call",
- "contractAddress": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- "contractMethod": "mint()"
- }
-}
-```
-
-### 1.3 Mint-Verification (Direkte On-Chain Verification)
-
-Direkte On-Chain Verification ohne Facilitator:
-
-```javascript
-import { createPublicClient, http, parseAbiItem } from 'viem';
-import { optimism } from 'viem/chains';
-
-const publicClient = createPublicClient({
- chain: optimism,
- transport: http()
-});
-
-// Transfer-Event für Mint-Detection
-const TRANSFER_EVENT = parseAbiItem(
- 'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'
-);
-
-async function verifyMintPayment(publicClient, txHash) {
- try {
- // 1. Get Transaction Receipt
- const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
-
- if (!receipt || receipt.status !== "success") {
- return { valid: false, error: "Transaction failed or not found" };
- }
-
- // 2. Get Transaction Details
- const tx = await publicClient.getTransaction({ hash: txHash });
-
- // 3. Verify Recipient (Contract Address)
- if (tx.to?.toLowerCase() !== GENIMG_CONTRACT_ADDRESS.toLowerCase()) {
- return { valid: false, error: "Transaction not sent to correct contract" };
- }
-
- // 4. Verify Transaction Value >= MINT_PRICE
- if (BigInt(tx.value) < BigInt(MINT_PRICE)) {
- return {
- valid: false,
- error: `Insufficient payment. Expected at least ${MINT_PRICE}, got ${tx.value}`
- };
- }
-
- // 2. Custom: Mint-Event aus Receipt extrahieren
- const receipt = await publicClient.getTransactionReceipt({ hash: txHash });
-
- // Finde Transfer-Event mit from=0x0 (Mint)
- const mintLog = receipt.logs.find(log => {
- if (log.address.toLowerCase() !== GENIMG_CONTRACT_ADDRESS.toLowerCase()) {
- return false;
- }
- // Transfer von 0x0 = Mint
- return log.topics[0] === TRANSFER_EVENT.signature &&
- log.topics[1] === '0x0000000000000000000000000000000000000000000000000000000000000000';
- });
-
- if (!mintLog) {
- return { valid: false, error: 'No mint event found in transaction' };
- }
-
- // 3. TokenId aus Event extrahieren
- const mintedTokenId = parseInt(mintLog.topics[3], 16);
- const minterAddress = '0x' + mintLog.topics[2].slice(26);
-
- return {
- valid: true,
- tokenId: mintedTokenId,
- payer: minterAddress,
- txHash
- };
-}
-```
-
-**Was wird geprüft:**
-
-- ✅ Transaction Status (success/reverted)
-- ✅ Recipient (Contract Address match)
-- ✅ Amount (≥ MINT_PRICE)
-- ✅ Mint-Event Detection (Transfer from 0x0)
-- ✅ TokenId Extraktion aus Event
-- ✅ Minter-Adresse extrahieren
-
-**Vorteile gegenüber Facilitator:**
-
-- ✅ Self-Sovereign (keine externe Dependency)
-- ✅ Transparent (direkter Blockchain-Zugriff)
-- ✅ NFT-optimiert (TokenId-Extraktion)
-
-### 1.4 Vollständiger Handler (x402-konform)
-
-```javascript
-import { createPublicClient, http, parseAbiItem } from "viem";
-import { optimism } from "viem/chains";
-
-// Config
-const MINT_PRICE = "500000000000000"; // 0.0005 ETH
-const GENIMG_CONTRACT_ADDRESS = "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb";
-
-const publicClient = createPublicClient({
- chain: optimism,
- transport: http(process.env.OPTIMISM_RPC_URL),
-});
-
-export async function handler(event) {
- const paymentProof = event.headers["x-payment"];
- const { prompt } = JSON.parse(event.body);
-
- // Kein Payment → 402 via x402 Package
- if (!paymentProof) {
- const payment = createPaymentRequired({
- amount: MINT_PRICE,
- recipient: GENIMG_CONTRACT_ADDRESS,
- network: "optimism",
- metadata: {
- resource: "genimg",
- description: "Mint an NFT to generate your AI image",
- paymentType: "contract-call",
- contractAddress: GENIMG_CONTRACT_ADDRESS,
- contractMethod: "mint()",
- },
- });
-
- return {
- statusCode: 402,
- headers: {
- "X-Payment": JSON.stringify(payment),
- },
- body: JSON.stringify({
- error: "Payment required",
- message: "Please mint an NFT to generate your image",
- }),
- };
- }
-
- // Payment verifizieren (x402 + Custom Mint-Check)
- const verification = await verifyMintPayment(paymentProof);
- if (!verification.valid) {
- return {
- statusCode: 402,
- body: JSON.stringify({ error: verification.error }),
- };
- }
-
- // Bild generieren mit verifiziertem tokenId
- const result = await generateImageAndUpdateNFT(prompt, verification.tokenId, verification.payer);
-
- return {
- statusCode: 200,
- body: JSON.stringify(result),
- };
-}
-```
-
----
-
-## Phase 2: Client-Side (ImageGenerator.tsx)
-
-**Ziel:** Automatisches 402-Handling mit NFT-Mint als Payment
-
-### 2.1 Fetch-Wrapper mit 402-Handling
-
-- Erster Request ohne Payment
-- Bei 402 → Parse Payment-Details, führe `mint()` aus
-- Retry mit txHash + tokenId als Payment-Proof
-
-### 2.2 UX-Flow (vereinfacht)
-
-```
-┌────────────────────────────────────────────────────────────┐
-│ ImageGenerator UI │
-├────────────────────────────────────────────────────────────┤
-│ │
-│ Prompt: [A beautiful sunset over mountains ] │
-│ │
-│ ┌──────────────────────────────────────────────────────┐ │
-│ │ 💰 Price: 0.0005 ETH (~$1.20) │ │
-│ │ 🎨 You'll receive an NFT with your generated image │ │
-│ └──────────────────────────────────────────────────────┘ │
-│ │
-│ [ 🖼️ Generate Image (Mint NFT) ] │
-│ │
-└────────────────────────────────────────────────────────────┘
-
- ↓ Click
-
-┌────────────────────────────────────────────────────────────┐
-│ Wallet Popup: Confirm mint() transaction │
-│ Amount: 0.0005 ETH │
-│ Contract: GenImNFTv4 │
-└────────────────────────────────────────────────────────────┘
-
- ↓ Confirm
-
-┌────────────────────────────────────────────────────────────┐
-│ ⏳ Generating your image... │
-│ (Automatic retry with payment proof) │
-└────────────────────────────────────────────────────────────┘
-
- ↓ Complete
-
-┌────────────────────────────────────────────────────────────┐
-│ ✅ Your NFT #42 has been created! │
-│ [Generated Image Preview] │
-│ View on Etherscan | View NFT │
-└────────────────────────────────────────────────────────────┘
-```
-
-### 2.3 Änderungen am aktuellen Flow
-
-| Aktuell | Neu (x402) |
-| ----------------------------------- | --------------------------------- |
-| Separater Mint-Button | Ein "Generate"-Button |
-| User wartet auf Mint, dann Generate | Ein durchgehender Flow |
-| TokenId manuell übergeben | TokenId aus Mint-Event extrahiert |
-| Server prüft Token on-chain | Server verifiziert Payment-Proof |
-
-### 2.4 Pseudocode
-
-```typescript
-async function generateImageWithPayment(prompt: string) {
- // 1. Erster Request - löst 402 aus
- let response = await fetch(GENIMG_ENDPOINT, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ prompt }),
- });
-
- if (response.status === 402) {
- // 2. Parse Payment-Details
- const paymentDetails = JSON.parse(response.headers.get("X-Payment") || "{}");
-
- // 3. NFT Minting durchführen
- const { hash: txHash } = await writeContract({
- address: paymentDetails.contractAddress,
- abi: GenImNFTv4ABI,
- functionName: "mint",
- value: BigInt(paymentDetails.maxAmountRequired),
- });
-
- // 4. Auf Confirmation warten
- const receipt = await waitForTransactionReceipt({ hash: txHash });
-
- // 5. TokenId aus Mint-Event extrahieren
- const mintEvent = receipt.logs.find((log) => log.topics[0] === TRANSFER_EVENT_SIGNATURE);
- const tokenId = parseInt(mintEvent.topics[3], 16);
-
- // 6. Retry mit Payment-Proof
- response = await fetch(GENIMG_ENDPOINT, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Payment": JSON.stringify({ txHash, tokenId }),
- },
- body: JSON.stringify({ prompt }),
- });
- }
-
- if (!response.ok) {
- throw new Error("Image generation failed");
- }
-
- return response.json();
-}
-```
-
----
-
-## Phase 3: Smart Contract (GenImNFTv4)
-
-**Entscheidung:** Der bestehende GenImNFTv4 Contract bleibt unverändert!
-
-### Warum keine Contract-Änderungen nötig sind
-
-Der aktuelle Contract unterstützt bereits alles, was für x402 benötigt wird:
-
-| Funktion | Bereits vorhanden | Für x402 genutzt |
-| ---------------------- | ----------------- | ---------------------------- |
-| `mint()` | ✅ | Payment-Transaktion |
-| `Transfer` Event | ✅ | TokenId + Payer extrahieren |
-| `requestImageUpdate()` | ✅ | Bild on-chain speichern |
-| Whitelisted Provider | ✅ | Server kann TokenURI updaten |
-
-### Mint-Event als Payment-Proof
-
-Das `Transfer`-Event beim Minting enthält alle nötigen Informationen:
-
-```solidity
-event Transfer(
- address indexed from, // 0x0 bei Mint
- address indexed to, // Payer/Owner
- uint256 indexed tokenId // Neuer Token
-);
-```
-
-Der Server kann aus dem txHash:
-
-1. Prüfen ob Transaktion erfolgreich war
-2. TokenId extrahieren
-3. Payer-Adresse verifizieren
-4. Gezahlten Betrag prüfen
-
----
-
-## Phase 4: Integration & Testing
-
-### 4.1 Lokales Testing
-
-- Mock-402-Responses für UI-Entwicklung
-- Testnet-Transaktionen (Optimism Sepolia)
-- End-to-End Flow mit echtem Mint
-
-### 4.2 Edge Cases
-
-| Edge Case | Handling |
-| ---------------------------------- | ------------------------------------------ |
-| Payment Success, Generation Failed | Bild später generieren (TokenId existiert) |
-| Doppelte Payments verhindern | Server prüft ob TokenId schon Bild hat |
-| Timeout bei Mint | Client zeigt Retry-Option |
-| Invalid txHash | 402 mit Fehlermeldung |
-| Falscher Contract | 402 - nur GenImNFTv4 akzeptiert |
-
-### 4.3 Monitoring
-
-- Payment-Success-Rate tracken
-- Generation-Success-Rate nach Payment
-- Durchschnittliche Zeit: Request → Bild
-
----
-
-## Zusammenfassung der Änderungen
-
-### Server (genimg_bfl.js)
-
-```diff
-+ // Neuer 402-Handler am Anfang
-+ if (!event.headers['x-payment']) {
-+ return { statusCode: 402, headers: { 'X-Payment': ... } };
-+ }
-+
-+ // Mint-Verification statt Token-Check
-- const tokenExists = await contract.ownerOf(tokenId);
-+ const { valid, tokenId } = await verifyMintPayment(paymentProof);
-
- // Rest bleibt gleich
- const image = await generateWithBFL(prompt);
- await updateTokenURI(tokenId, image);
-```
-
-### Client (ImageGenerator.tsx)
-
-```diff
-- // Separater Mint-Schritt
-- const { mint } = useWriteContract();
-- await mint();
-- // ... warten ...
-- await generateImage(tokenId);
-
-+ // Ein zusammenhängender Flow
-+ const response = await fetch('/genimg', { body: { prompt } });
-+ if (response.status === 402) {
-+ const tx = await mint(); // Automatisch getriggert
-+ await fetch('/genimg', {
-+ headers: { 'X-Payment': { txHash: tx.hash } }
-+ });
-+ }
-```
-
----
-
-## Dateien zu ändern
-
-| Datei | Änderungen |
-| --------------------------------------- | ------------------------------------------------------------------- |
-| `scw_js/package.json` | Dependencies: `viem` (bereits vorhanden) |
-| `scw_js/genimg_x402.js` | **NEU**: 402-Response (manual), Mint-Verification (direct on-chain) |
-| `website/components/ImageGenerator.tsx` | 402-Handling, vereinfachter Flow |
-| `website/hooks/useImageGeneration.ts` | (neu) Fetch + 402 + Mint + Retry |
-| `website/public/openapi.json` | 402-Response dokumentieren |
-
----
-
-## Zeitschätzung
-
-| Phase | Aufwand | Status |
-| ----------------------------------- | ------- | -------------------------------- |
-| Phase 1: Server (402 + Mint-Verify) | 3-4h | ✅ **Fertig** (`genimg_x402.js`) |
-| Phase 2: Client (402-Handling) | 3-4h | ⏳ Todo |
-| Phase 3: Contract | 0h | ✅ Keine Änderungen |
-| Phase 4: Testing | 2-3h | ⏳ Unit Tests vorhanden |
-| **Total** | 8-11h | **Phase 1 abgeschlossen** |
-
-**Manual Implementation:** Klarer, direkter Code ohne Facilitator-Overhead
-
----
-
-## Integration mit EIP-8004
-
-x402 und EIP-8004 ergänzen sich gut:
-
-- **EIP-8004**: Agent-Discovery & Trust (wer ist der Agent?)
-- **x402**: Payment-Protokoll (wie zahle ich?)
-
-Die `agent-registration.json` könnte ein x402-Payment-Schema referenzieren:
-
-```json
-{
- "endpoints": [
- {
- "name": "genimg",
- "endpoint": "https://...",
- "paymentProtocol": "x402"
- }
- ]
-}
-```
-
----
-
-## Referenzen
-
-- [x402 npm Package](https://www.npmjs.com/package/x402) - Offizielles x402 Core Package (für Facilitator-basierte Flows)
-- [x402 Protocol Specification](https://github.com/coinbase/x402) - Standard für Payment Required (Coinbase)
-- [x402 Express Examples](https://github.com/coinbase/x402/tree/main/examples/typescript/servers/advanced) - Advanced Server Patterns
-- [HTTP 402 Payment Required](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402)
-- [EIP-8004 Trustless Agents](https://eips.ethereum.org/EIPS/eip-8004)
-- [Viem Documentation](https://viem.sh/) - TypeScript Interface für Ethereum
-- [Optimism Documentation](https://docs.optimism.io/)
-
-## Implementation Status
-
-- ✅ **genimg_x402.js**: Vollständige Server-Implementation mit x402-konformer 402 Response und direkter On-Chain Verification
-- ✅ **Unit Tests**: Comprehensive Test-Suite mit 22 Tests
-- ✅ **Demo Notebook**: `notebooks/genimg_x402_demo.ipynb` für lokales Testing
-- ⏳ **Client Integration**: Website-Integration folgt in Phase 2
diff --git a/scw_js/x402_server.js b/scw_js/x402_server.js
index 52b844bcd..d0764feef 100644
--- a/scw_js/x402_server.js
+++ b/scw_js/x402_server.js
@@ -7,7 +7,7 @@
import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
-import { getUSDCConfig } from "./getChain.js";
+import { getUSDCConfig } from "@fretchen/chain-utils";
// Facilitator configuration
const FACILITATOR_URL = process.env.FACILITATOR_URL || "https://facilitator.fretchen.eu";
diff --git a/shared/chain-utils/src/abi/EIP3009SplitterV1.ts b/shared/chain-utils/src/abi/EIP3009SplitterV1.ts
new file mode 100644
index 000000000..ea19a4bb9
--- /dev/null
+++ b/shared/chain-utils/src/abi/EIP3009SplitterV1.ts
@@ -0,0 +1,82 @@
+/**
+ * EIP3009SplitterV1 Minimal ABI
+ *
+ * Contains only the functions used by x402_facilitator.
+ * Full ABI available in: eth/abi/contracts/EIP3009SplitterV1.ts
+ *
+ * Used by:
+ * - x402_facilitator/x402_splitter_settle.js: executeSplit
+ * - x402_facilitator/x402_splitter_verify.js: isAuthorizationUsed, facilitatorWallet, fixedFee
+ */
+
+export const EIP3009SplitterV1ABI = [
+ // ═══════════════════════════════════════════════════════════════
+ // Read Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "facilitatorWallet",
+ type: "function",
+ stateMutability: "view",
+ inputs: [],
+ outputs: [{ name: "", type: "address" }],
+ },
+ {
+ name: "fixedFee",
+ type: "function",
+ stateMutability: "view",
+ inputs: [],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ name: "isAuthorizationUsed",
+ type: "function",
+ stateMutability: "view",
+ inputs: [
+ { name: "token", type: "address" },
+ { name: "authorizer", type: "address" },
+ { name: "nonce", type: "bytes32" },
+ ],
+ outputs: [{ name: "", type: "bool" }],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Write Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "executeSplit",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "token", type: "address" },
+ { name: "buyer", type: "address" },
+ { name: "seller", type: "address" },
+ { name: "salt", type: "bytes32" },
+ { name: "totalAmount", type: "uint256" },
+ { name: "validAfter", type: "uint256" },
+ { name: "validBefore", type: "uint256" },
+ { name: "nonce", type: "bytes32" },
+ { name: "v", type: "uint8" },
+ { name: "r", type: "bytes32" },
+ { name: "s", type: "bytes32" },
+ ],
+ outputs: [],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Events
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "SplitExecuted",
+ type: "event",
+ inputs: [
+ { name: "buyer", type: "address", indexed: true },
+ { name: "seller", type: "address", indexed: true },
+ { name: "totalAmount", type: "uint256", indexed: false },
+ { name: "sellerAmount", type: "uint256", indexed: false },
+ { name: "facilitatorFee", type: "uint256", indexed: false },
+ ],
+ },
+] as const;
diff --git a/shared/chain-utils/src/abi/GenImNFTv4.ts b/shared/chain-utils/src/abi/GenImNFTv4.ts
new file mode 100644
index 000000000..d4a17f256
--- /dev/null
+++ b/shared/chain-utils/src/abi/GenImNFTv4.ts
@@ -0,0 +1,111 @@
+/**
+ * GenImNFTv4 Minimal ABI
+ *
+ * Contains only the functions used by scw_js and x402_facilitator.
+ * Full ABI available in: eth/abi/contracts/GenImNFTv4.ts (after export)
+ *
+ * Used by:
+ * - scw_js/genimg_bfl.js: ownerOf, requestImageUpdate, mintPrice, isImageUpdated
+ * - scw_js/genimg_x402_token.js: safeMint, safeTransferFrom, mintPrice
+ * - scw_js/readhandler_v2.js: ownerOf, requestImageUpdate, mintPrice, isImageUpdated
+ * - x402_facilitator/x402_whitelist.js: isAuthorizedAgent
+ */
+
+export const GenImNFTv4ABI = [
+ // ═══════════════════════════════════════════════════════════════
+ // Read Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "ownerOf",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "tokenId", type: "uint256" }],
+ outputs: [{ name: "", type: "address" }],
+ },
+ {
+ name: "mintPrice",
+ type: "function",
+ stateMutability: "view",
+ inputs: [],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ name: "isImageUpdated",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "tokenId", type: "uint256" }],
+ outputs: [{ name: "", type: "bool" }],
+ },
+ {
+ name: "isAuthorizedAgent",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "agent", type: "address" }],
+ outputs: [{ name: "", type: "bool" }],
+ },
+ {
+ name: "tokenURI",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "tokenId", type: "uint256" }],
+ outputs: [{ name: "", type: "string" }],
+ },
+ {
+ name: "isListed",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "tokenId", type: "uint256" }],
+ outputs: [{ name: "", type: "bool" }],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Write Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "safeMint",
+ type: "function",
+ stateMutability: "payable",
+ inputs: [
+ { name: "metadataUrl", type: "string" },
+ { name: "_isListed", type: "bool" },
+ ],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ name: "requestImageUpdate",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "tokenId", type: "uint256" },
+ { name: "metadataUrl", type: "string" },
+ ],
+ outputs: [],
+ },
+ {
+ name: "safeTransferFrom",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "from", type: "address" },
+ { name: "to", type: "address" },
+ { name: "tokenId", type: "uint256" },
+ ],
+ outputs: [],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Events (for parsing transaction receipts)
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "Transfer",
+ type: "event",
+ inputs: [
+ { name: "from", type: "address", indexed: true },
+ { name: "to", type: "address", indexed: true },
+ { name: "tokenId", type: "uint256", indexed: true },
+ ],
+ },
+] as const;
diff --git a/shared/chain-utils/src/abi/LLMv1.ts b/shared/chain-utils/src/abi/LLMv1.ts
new file mode 100644
index 000000000..1cd4816ee
--- /dev/null
+++ b/shared/chain-utils/src/abi/LLMv1.ts
@@ -0,0 +1,42 @@
+/**
+ * LLMv1 Minimal ABI
+ *
+ * Contains only the functions used by scw_js for LLM service.
+ * Note: LLMv1 is legacy and out of scope for multi-chain migration.
+ *
+ * Used by:
+ * - scw_js/llm_service.js: checkBalance, processBatch
+ */
+
+export const LLMv1ABI = [
+ // ═══════════════════════════════════════════════════════════════
+ // Read Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "checkBalance",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "user", type: "address" }],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // Write Functions
+ // ═══════════════════════════════════════════════════════════════
+
+ {
+ name: "processBatch",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "root", type: "bytes32" },
+ { name: "leaves", type: "tuple[]", components: [
+ { name: "user", type: "address" },
+ { name: "amount", type: "uint256" },
+ ]},
+ { name: "proofs", type: "bytes32[][]" },
+ ],
+ outputs: [],
+ },
+] as const;
diff --git a/shared/chain-utils/src/abi/index.ts b/shared/chain-utils/src/abi/index.ts
new file mode 100644
index 000000000..072d62967
--- /dev/null
+++ b/shared/chain-utils/src/abi/index.ts
@@ -0,0 +1,10 @@
+/**
+ * ABI Exports
+ *
+ * Minimal ABIs for contracts used across the monorepo.
+ * Only includes functions that are actually called.
+ */
+
+export { GenImNFTv4ABI } from "./GenImNFTv4";
+export { EIP3009SplitterV1ABI } from "./EIP3009SplitterV1";
+export { LLMv1ABI } from "./LLMv1";
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index 8c7866f2e..3f0a77a2f 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -169,3 +169,26 @@ export function getUSDCAddress(network: string): `0x${string}` {
export function getUSDCName(network: string): string {
return USDC_NAMES[network] || "USDC";
}
+
+// ═══════════════════════════════════════════════════════════════
+// USDC Configuration
+// ═══════════════════════════════════════════════════════════════
+
+/**
+ * Complete USDC configuration for EIP-712 / EIP-3009 payments.
+ * Critical for x402 payment verification and settlement.
+ */
+export interface USDCConfig {
+ /** Human-readable network name (e.g., "OP Mainnet") */
+ name: string;
+ /** EVM chain ID (e.g., 10 for Optimism) */
+ chainId: number;
+ /** USDC contract address */
+ address: `0x${string}`;
+ /** USDC decimals (always 6) */
+ decimals: 6;
+ /** USDC EIP-712 domain name - CRITICAL for signature verification */
+ usdcName: string;
+ /** USDC EIP-712 domain version */
+ usdcVersion: string;
+}
diff --git a/shared/chain-utils/src/index.ts b/shared/chain-utils/src/index.ts
index 40ac6748f..d4b225326 100644
--- a/shared/chain-utils/src/index.ts
+++ b/shared/chain-utils/src/index.ts
@@ -94,3 +94,38 @@ export function isTestnet(network: string): network is TestnetNetwork {
// Re-export addresses
export * from "./addresses";
+
+// ═══════════════════════════════════════════════════════════════
+// USDC Configuration
+// ═══════════════════════════════════════════════════════════════
+
+import { getUSDCAddress, getUSDCName, type USDCConfig } from "./addresses";
+
+/**
+ * Get complete USDC configuration for a CAIP-2 network ID.
+ * Used for EIP-712 / EIP-3009 payment verification and settlement.
+ *
+ * @throws Error if the network is not supported
+ * @example
+ * const config = getUSDCConfig("eip155:10");
+ * // { name: "OP Mainnet", chainId: 10, address: "0x...", decimals: 6, usdcName: "USD Coin", usdcVersion: "2" }
+ */
+export function getUSDCConfig(network: string): USDCConfig {
+ return {
+ name: getViemChain(network).name,
+ chainId: fromCAIP2(network),
+ address: getUSDCAddress(network),
+ decimals: 6,
+ usdcName: getUSDCName(network),
+ usdcVersion: "2",
+ };
+}
+
+// Re-export USDCConfig type
+export type { USDCConfig };
+
+// ═══════════════════════════════════════════════════════════════
+// Contract ABIs
+// ═══════════════════════════════════════════════════════════════
+
+export * from "./abi";
diff --git a/shared/chain-utils/test/abi.test.ts b/shared/chain-utils/test/abi.test.ts
new file mode 100644
index 000000000..bc9034fca
--- /dev/null
+++ b/shared/chain-utils/test/abi.test.ts
@@ -0,0 +1,127 @@
+/**
+ * Tests for Contract ABIs exported by @fretchen/chain-utils
+ */
+
+import { describe, test, expect } from "vitest";
+import { GenImNFTv4ABI, EIP3009SplitterV1ABI, LLMv1ABI } from "../src/index";
+
+describe("GenImNFTv4ABI", () => {
+ test("should be a valid ABI array", () => {
+ expect(Array.isArray(GenImNFTv4ABI)).toBe(true);
+ expect(GenImNFTv4ABI.length).toBeGreaterThan(0);
+ });
+
+ test("should contain required read functions", () => {
+ const functionNames = GenImNFTv4ABI
+ .filter((item) => item.type === "function")
+ .map((item) => item.name);
+
+ const requiredFunctions = [
+ "ownerOf",
+ "mintPrice",
+ "isImageUpdated",
+ "isAuthorizedAgent",
+ ];
+
+ requiredFunctions.forEach((funcName) => {
+ expect(functionNames).toContain(funcName);
+ });
+ });
+
+ test("should contain required write functions", () => {
+ const functionNames = GenImNFTv4ABI
+ .filter((item) => item.type === "function")
+ .map((item) => item.name);
+
+ const requiredFunctions = [
+ "safeMint",
+ "requestImageUpdate",
+ "safeTransferFrom",
+ ];
+
+ requiredFunctions.forEach((funcName) => {
+ expect(functionNames).toContain(funcName);
+ });
+ });
+
+ test("should have correct ownerOf signature", () => {
+ const ownerOfFunction = GenImNFTv4ABI.find(
+ (item) => item.type === "function" && item.name === "ownerOf"
+ );
+
+ expect(ownerOfFunction).toBeDefined();
+ expect(ownerOfFunction!.inputs).toHaveLength(1);
+ expect(ownerOfFunction!.inputs![0].type).toBe("uint256");
+ expect(ownerOfFunction!.outputs).toHaveLength(1);
+ expect(ownerOfFunction!.outputs![0].type).toBe("address");
+ });
+
+ test("should have correct requestImageUpdate signature", () => {
+ const func = GenImNFTv4ABI.find(
+ (item) => item.type === "function" && item.name === "requestImageUpdate"
+ );
+
+ expect(func).toBeDefined();
+ expect(func!.inputs).toHaveLength(2);
+ expect(func!.inputs![0].type).toBe("uint256");
+ expect(func!.inputs![1].type).toBe("string");
+ });
+
+ test("should contain Transfer event", () => {
+ const events = GenImNFTv4ABI.filter((item) => item.type === "event");
+ const transferEvent = events.find((e) => e.name === "Transfer");
+
+ expect(transferEvent).toBeDefined();
+ expect(transferEvent!.inputs).toHaveLength(3);
+ });
+});
+
+describe("EIP3009SplitterV1ABI", () => {
+ test("should be a valid ABI array", () => {
+ expect(Array.isArray(EIP3009SplitterV1ABI)).toBe(true);
+ expect(EIP3009SplitterV1ABI.length).toBeGreaterThan(0);
+ });
+
+ test("should contain required functions", () => {
+ const functionNames = EIP3009SplitterV1ABI
+ .filter((item) => item.type === "function")
+ .map((item) => item.name);
+
+ const requiredFunctions = [
+ "facilitatorWallet",
+ "fixedFee",
+ "isAuthorizationUsed",
+ "executeSplit",
+ ];
+
+ requiredFunctions.forEach((funcName) => {
+ expect(functionNames).toContain(funcName);
+ });
+ });
+
+ test("should contain SplitExecuted event", () => {
+ const events = EIP3009SplitterV1ABI.filter((item) => item.type === "event");
+ const splitEvent = events.find((e) => e.name === "SplitExecuted");
+
+ expect(splitEvent).toBeDefined();
+ });
+});
+
+describe("LLMv1ABI", () => {
+ test("should be a valid ABI array", () => {
+ expect(Array.isArray(LLMv1ABI)).toBe(true);
+ expect(LLMv1ABI.length).toBeGreaterThan(0);
+ });
+
+ test("should contain required functions", () => {
+ const functionNames = LLMv1ABI
+ .filter((item) => item.type === "function")
+ .map((item) => item.name);
+
+ const requiredFunctions = ["checkBalance", "processBatch"];
+
+ requiredFunctions.forEach((funcName) => {
+ expect(functionNames).toContain(funcName);
+ });
+ });
+});
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
index 728469373..2526a88e2 100644
--- a/shared/chain-utils/test/index.test.ts
+++ b/shared/chain-utils/test/index.test.ts
@@ -12,6 +12,7 @@ import {
isNetworkSupported,
isMainnet,
isTestnet,
+ getUSDCConfig,
MAINNET_NETWORKS,
TESTNET_NETWORKS,
} from "../src/index";
@@ -207,6 +208,52 @@ describe("@fretchen/chain-utils", () => {
expect(getUSDCName("eip155:84532")).toBe("USDC");
});
});
+
+ describe("getUSDCConfig()", () => {
+ test("should return complete config for Optimism Mainnet", () => {
+ const config = getUSDCConfig("eip155:10");
+ expect(config.name).toBe("OP Mainnet");
+ expect(config.chainId).toBe(10);
+ expect(config.address).toBe("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85");
+ expect(config.decimals).toBe(6);
+ expect(config.usdcName).toBe("USD Coin");
+ expect(config.usdcVersion).toBe("2");
+ });
+
+ test("should return complete config for Optimism Sepolia", () => {
+ const config = getUSDCConfig("eip155:11155420");
+ expect(config.name).toBe("OP Sepolia");
+ expect(config.chainId).toBe(11155420);
+ expect(config.address).toBe("0x5fd84259d66Cd46123540766Be93DFE6D43130D7");
+ expect(config.decimals).toBe(6);
+ expect(config.usdcName).toBe("USDC");
+ expect(config.usdcVersion).toBe("2");
+ });
+
+ test("should return complete config for Base Mainnet", () => {
+ const config = getUSDCConfig("eip155:8453");
+ expect(config.name).toBe("Base");
+ expect(config.chainId).toBe(8453);
+ expect(config.address).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
+ expect(config.decimals).toBe(6);
+ expect(config.usdcName).toBe("USD Coin");
+ expect(config.usdcVersion).toBe("2");
+ });
+
+ test("should return complete config for Base Sepolia", () => {
+ const config = getUSDCConfig("eip155:84532");
+ expect(config.name).toBe("Base Sepolia");
+ expect(config.chainId).toBe(84532);
+ expect(config.address).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e");
+ expect(config.decimals).toBe(6);
+ expect(config.usdcName).toBe("USDC");
+ expect(config.usdcVersion).toBe("2");
+ });
+
+ test("should throw for unsupported network", () => {
+ expect(() => getUSDCConfig("eip155:1")).toThrow("Unsupported network: eip155:1");
+ });
+ });
});
describe("Address Map Consistency", () => {
From a2307b9f44344e4b149ccd48c6d4037b689345b1 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 20:38:27 +0100
Subject: [PATCH 08/46] Further cleaning with shared...
---
.github/workflows/test-chain-utils.yml | 56 +
scw_js/test/nft_abi.test.js | 81 ++
shared/chain-utils/.prettierignore | 4 +
shared/chain-utils/.prettierrc | 7 +
shared/chain-utils/eslint.config.js | 19 +
shared/chain-utils/package-lock.json | 1569 +++++++++++++++++++++++-
shared/chain-utils/package.json | 10 +
shared/chain-utils/src/abi/LLMv1.ts | 12 +-
shared/chain-utils/src/addresses.ts | 10 +-
shared/chain-utils/src/index.ts | 4 +-
shared/chain-utils/test/abi.test.ts | 37 +-
shared/chain-utils/test/index.test.ts | 26 +-
12 files changed, 1740 insertions(+), 95 deletions(-)
create mode 100644 .github/workflows/test-chain-utils.yml
create mode 100644 scw_js/test/nft_abi.test.js
create mode 100644 shared/chain-utils/.prettierignore
create mode 100644 shared/chain-utils/.prettierrc
create mode 100644 shared/chain-utils/eslint.config.js
diff --git a/.github/workflows/test-chain-utils.yml b/.github/workflows/test-chain-utils.yml
new file mode 100644
index 000000000..e5549815b
--- /dev/null
+++ b/.github/workflows/test-chain-utils.yml
@@ -0,0 +1,56 @@
+name: Test Chain Utils
+
+permissions:
+ contents: read
+
+on:
+ pull_request:
+ paths:
+ - "shared/chain-utils/**"
+ - ".github/workflows/test-chain-utils.yml"
+ push:
+ branches:
+ - main
+ paths:
+ - "shared/chain-utils/**"
+
+jobs:
+ test-chain-utils:
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: ./shared/chain-utils
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ cache: "npm"
+ cache-dependency-path: shared/chain-utils/package-lock.json
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run quality checks (lint + format + test)
+ run: npm run check
+ env:
+ CI: true
+
+ - name: Run tests with coverage
+ run: npm run test:coverage
+ env:
+ CI: true
+
+ - name: Upload coverage reports to Codecov (optional)
+ uses: codecov/codecov-action@v3
+ with:
+ directory: ./shared/chain-utils/coverage
+ flags: chain-utils
+ name: chain-utils-coverage
+ fail_ci_if_error: false
+ continue-on-error: true
diff --git a/scw_js/test/nft_abi.test.js b/scw_js/test/nft_abi.test.js
new file mode 100644
index 000000000..4bfff56fc
--- /dev/null
+++ b/scw_js/test/nft_abi.test.js
@@ -0,0 +1,81 @@
+/**
+ * Tests für NFT ABI und Contract-Interaktionen
+ */
+
+import { describe, test, expect, beforeAll } from "vitest";
+import { GenImNFTv4ABI } from "@fretchen/chain-utils";
+
+describe("NFT ABI Tests", () => {
+ let nftAbi;
+
+ beforeAll(async () => {
+ nftAbi = GenImNFTv4ABI;
+ });
+
+ describe("ABI Structure Tests", () => {
+ test("sollte gültige ABI-Struktur haben", () => {
+ expect(Array.isArray(nftAbi)).toBe(true);
+ expect(nftAbi.length).toBeGreaterThan(0);
+ });
+
+ test("sollte erforderliche Funktionen enthalten", () => {
+ const functionNames = nftAbi
+ .filter((item) => item.type === "function")
+ .map((item) => item.name);
+
+ const requiredFunctions = ["ownerOf", "mintPrice", "isImageUpdated", "requestImageUpdate"];
+
+ requiredFunctions.forEach((funcName) => {
+ expect(functionNames).toContain(funcName);
+ });
+ });
+
+ test("sollte korrekte Funktions-Signaturen haben", () => {
+ const ownerOfFunction = nftAbi.find(
+ (item) => item.type === "function" && item.name === "ownerOf",
+ );
+
+ expect(ownerOfFunction).toBeDefined();
+ expect(ownerOfFunction.inputs).toHaveLength(1);
+ expect(ownerOfFunction.inputs[0].type).toBe("uint256");
+ expect(ownerOfFunction.outputs).toHaveLength(1);
+ expect(ownerOfFunction.outputs[0].type).toBe("address");
+
+ const requestImageUpdateFunction = nftAbi.find(
+ (item) => item.type === "function" && item.name === "requestImageUpdate",
+ );
+
+ expect(requestImageUpdateFunction).toBeDefined();
+ expect(requestImageUpdateFunction.inputs).toHaveLength(2);
+ expect(requestImageUpdateFunction.inputs[0].type).toBe("uint256");
+ expect(requestImageUpdateFunction.inputs[1].type).toBe("string");
+ });
+
+ test("sollte Events definiert haben", () => {
+ const events = nftAbi.filter((item) => item.type === "event");
+ expect(events.length).toBeGreaterThan(0);
+ });
+ });
+});
+
+describe("Contract Utility Tests", () => {
+ test("sollte Contract-Adresse validieren", () => {
+ const contractAddress = "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb";
+
+ // Ethereum-Adresse Format prüfen
+ expect(contractAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
+ expect(contractAddress.length).toBe(42);
+ });
+
+ test("sollte BigInt-Konvertierungen korrekt handhaben", () => {
+ const testValues = [
+ { input: "1", expected: BigInt("1") },
+ { input: "999999999999999999", expected: BigInt("999999999999999999") },
+ { input: "0", expected: BigInt("0") },
+ ];
+
+ testValues.forEach(({ input, expected }) => {
+ expect(BigInt(input)).toBe(expected);
+ });
+ });
+});
diff --git a/shared/chain-utils/.prettierignore b/shared/chain-utils/.prettierignore
new file mode 100644
index 000000000..1aa395915
--- /dev/null
+++ b/shared/chain-utils/.prettierignore
@@ -0,0 +1,4 @@
+dist
+node_modules
+coverage
+*.json
diff --git a/shared/chain-utils/.prettierrc b/shared/chain-utils/.prettierrc
new file mode 100644
index 000000000..1a88ab198
--- /dev/null
+++ b/shared/chain-utils/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "printWidth": 100
+}
diff --git a/shared/chain-utils/eslint.config.js b/shared/chain-utils/eslint.config.js
new file mode 100644
index 000000000..46673be57
--- /dev/null
+++ b/shared/chain-utils/eslint.config.js
@@ -0,0 +1,19 @@
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ ignores: ["dist/**", "node_modules/**", "coverage/**"],
+ },
+ {
+ files: ["src/**/*.ts", "test/**/*.ts", "*.ts"],
+ rules: {
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/no-explicit-any": "warn",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ },
+ }
+);
diff --git a/shared/chain-utils/package-lock.json b/shared/chain-utils/package-lock.json
index 1bfd9c318..569851479 100644
--- a/shared/chain-utils/package-lock.json
+++ b/shared/chain-utils/package-lock.json
@@ -8,7 +8,13 @@
"name": "@fretchen/chain-utils",
"version": "1.0.0",
"devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "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"
},
@@ -414,6 +420,247 @@
"node": ">=12"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -859,6 +1106,246 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
+ "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.53.1",
+ "@typescript-eslint/type-utils": "8.53.1",
+ "@typescript-eslint/utils": "8.53.1",
+ "@typescript-eslint/visitor-keys": "8.53.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.53.1",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz",
+ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.53.1",
+ "@typescript-eslint/types": "8.53.1",
+ "@typescript-eslint/typescript-estree": "8.53.1",
+ "@typescript-eslint/visitor-keys": "8.53.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz",
+ "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.53.1",
+ "@typescript-eslint/types": "^8.53.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz",
+ "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.1",
+ "@typescript-eslint/visitor-keys": "8.53.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz",
+ "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz",
+ "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.1",
+ "@typescript-eslint/typescript-estree": "8.53.1",
+ "@typescript-eslint/utils": "8.53.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz",
+ "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz",
+ "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.53.1",
+ "@typescript-eslint/tsconfig-utils": "8.53.1",
+ "@typescript-eslint/types": "8.53.1",
+ "@typescript-eslint/visitor-keys": "8.53.1",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz",
+ "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.53.1",
+ "@typescript-eslint/types": "8.53.1",
+ "@typescript-eslint/typescript-estree": "8.53.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz",
+ "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.1",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
@@ -994,45 +1481,152 @@
}
}
},
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=0.4.0"
}
},
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=8"
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
},
- "engines": {
- "node": ">=18"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/check-error": {
- "version": "2.1.3",
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
"dev": true,
@@ -1041,6 +1635,48 @@
"node": ">= 16"
}
},
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1069,6 +1705,13 @@
"node": ">=6"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -1115,6 +1758,223 @@
"@esbuild/win32-x64": "0.21.5"
}
},
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -1125,38 +1985,241 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
},
- "node_modules/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
"engines": {
- "node": ">=12.0.0"
+ "node": ">=0.10.0"
}
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
- "hasInstallScript": true,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
"engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ "node": ">=0.10.0"
}
},
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
@@ -1173,6 +2236,87 @@
"ws": "*"
}
},
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@@ -1190,6 +2334,22 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1216,6 +2376,31 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/ox": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.11.3.tgz",
@@ -1247,6 +2432,71 @@
}
}
},
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -1271,6 +2521,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -1300,6 +2563,52 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/rollup": {
"version": "4.56.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
@@ -1345,6 +2654,42 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1376,6 +2721,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -1390,6 +2761,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
@@ -1420,6 +2808,32 @@
"node": ">=14.0.0"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1434,6 +2848,40 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.53.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz",
+ "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.53.1",
+ "@typescript-eslint/parser": "8.53.1",
+ "@typescript-eslint/typescript-estree": "8.53.1",
+ "@typescript-eslint/utils": "8.53.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/viem": {
"version": "2.45.0",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.45.0.tgz",
@@ -1614,6 +3062,22 @@
}
}
},
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -1631,6 +3095,16 @@
"node": ">=8"
}
},
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@@ -1652,6 +3126,19 @@
"optional": true
}
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/shared/chain-utils/package.json b/shared/chain-utils/package.json
index 439495cad..71ccf1e91 100644
--- a/shared/chain-utils/package.json
+++ b/shared/chain-utils/package.json
@@ -22,12 +22,22 @@
"build": "tsc",
"prepare": "npm run build",
"clean": "rm -rf dist",
+ "lint": "eslint .",
+ "format": "prettier --check .",
+ "format:fix": "prettier --write .",
+ "check": "npm run lint && npm run format && npm run test",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "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"
},
diff --git a/shared/chain-utils/src/abi/LLMv1.ts b/shared/chain-utils/src/abi/LLMv1.ts
index 1cd4816ee..5abb8bb0a 100644
--- a/shared/chain-utils/src/abi/LLMv1.ts
+++ b/shared/chain-utils/src/abi/LLMv1.ts
@@ -31,10 +31,14 @@ export const LLMv1ABI = [
stateMutability: "nonpayable",
inputs: [
{ name: "root", type: "bytes32" },
- { name: "leaves", type: "tuple[]", components: [
- { name: "user", type: "address" },
- { name: "amount", type: "uint256" },
- ]},
+ {
+ name: "leaves",
+ type: "tuple[]",
+ components: [
+ { name: "user", type: "address" },
+ { name: "amount", type: "uint256" },
+ ],
+ },
{ name: "proofs", type: "bytes32[][]" },
],
outputs: [],
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index 3f0a77a2f..947e5b8ef 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -115,8 +115,7 @@ export const TESTNET_EIP3009_SPLITTER_ADDRESSES: Record =
* @throws Error if not deployed on that network
*/
export function getGenAiNFTAddress(network: string): `0x${string}` {
- const address =
- MAINNET_GENAI_NFT_ADDRESSES[network] || TESTNET_GENAI_NFT_ADDRESSES[network];
+ const address = MAINNET_GENAI_NFT_ADDRESSES[network] || TESTNET_GENAI_NFT_ADDRESSES[network];
if (!address) {
throw new Error(`GenAI NFT not deployed on ${network}`);
}
@@ -129,8 +128,7 @@ export function getGenAiNFTAddress(network: string): `0x${string}` {
*/
export function getCollectorNFTAddress(network: string): `0x${string}` {
const address =
- MAINNET_COLLECTOR_NFT_ADDRESSES[network] ||
- TESTNET_COLLECTOR_NFT_ADDRESSES[network];
+ MAINNET_COLLECTOR_NFT_ADDRESSES[network] || TESTNET_COLLECTOR_NFT_ADDRESSES[network];
if (!address) {
throw new Error(`CollectorNFT not deployed on ${network}`);
}
@@ -142,9 +140,7 @@ export function getCollectorNFTAddress(network: string): `0x${string}` {
* @throws Error if not deployed on that network
*/
export function getSupportV2Address(network: string): `0x${string}` {
- const address =
- MAINNET_SUPPORT_V2_ADDRESSES[network] ||
- TESTNET_SUPPORT_V2_ADDRESSES[network];
+ const address = MAINNET_SUPPORT_V2_ADDRESSES[network] || TESTNET_SUPPORT_V2_ADDRESSES[network];
if (!address) {
throw new Error(`SupportV2 not deployed on ${network}`);
}
diff --git a/shared/chain-utils/src/index.ts b/shared/chain-utils/src/index.ts
index d4b225326..84c82c122 100644
--- a/shared/chain-utils/src/index.ts
+++ b/shared/chain-utils/src/index.ts
@@ -66,7 +66,9 @@ const CHAIN_MAP: Record = {
export function getViemChain(network: string): Chain {
const chain = CHAIN_MAP[network];
if (!chain) {
- throw new Error(`Unsupported network: ${network}. Supported: ${Object.keys(CHAIN_MAP).join(", ")}`);
+ throw new Error(
+ `Unsupported network: ${network}. Supported: ${Object.keys(CHAIN_MAP).join(", ")}`
+ );
}
return chain;
}
diff --git a/shared/chain-utils/test/abi.test.ts b/shared/chain-utils/test/abi.test.ts
index bc9034fca..fbd9b1a21 100644
--- a/shared/chain-utils/test/abi.test.ts
+++ b/shared/chain-utils/test/abi.test.ts
@@ -12,16 +12,11 @@ describe("GenImNFTv4ABI", () => {
});
test("should contain required read functions", () => {
- const functionNames = GenImNFTv4ABI
- .filter((item) => item.type === "function")
- .map((item) => item.name);
+ const functionNames = GenImNFTv4ABI.filter((item) => item.type === "function").map(
+ (item) => item.name
+ );
- const requiredFunctions = [
- "ownerOf",
- "mintPrice",
- "isImageUpdated",
- "isAuthorizedAgent",
- ];
+ const requiredFunctions = ["ownerOf", "mintPrice", "isImageUpdated", "isAuthorizedAgent"];
requiredFunctions.forEach((funcName) => {
expect(functionNames).toContain(funcName);
@@ -29,15 +24,11 @@ describe("GenImNFTv4ABI", () => {
});
test("should contain required write functions", () => {
- const functionNames = GenImNFTv4ABI
- .filter((item) => item.type === "function")
- .map((item) => item.name);
+ const functionNames = GenImNFTv4ABI.filter((item) => item.type === "function").map(
+ (item) => item.name
+ );
- const requiredFunctions = [
- "safeMint",
- "requestImageUpdate",
- "safeTransferFrom",
- ];
+ const requiredFunctions = ["safeMint", "requestImageUpdate", "safeTransferFrom"];
requiredFunctions.forEach((funcName) => {
expect(functionNames).toContain(funcName);
@@ -83,9 +74,9 @@ describe("EIP3009SplitterV1ABI", () => {
});
test("should contain required functions", () => {
- const functionNames = EIP3009SplitterV1ABI
- .filter((item) => item.type === "function")
- .map((item) => item.name);
+ const functionNames = EIP3009SplitterV1ABI.filter((item) => item.type === "function").map(
+ (item) => item.name
+ );
const requiredFunctions = [
"facilitatorWallet",
@@ -114,9 +105,9 @@ describe("LLMv1ABI", () => {
});
test("should contain required functions", () => {
- const functionNames = LLMv1ABI
- .filter((item) => item.type === "function")
- .map((item) => item.name);
+ const functionNames = LLMv1ABI.filter((item) => item.type === "function").map(
+ (item) => item.name
+ );
const requiredFunctions = ["checkBalance", "processBatch"];
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
index 2526a88e2..55bd7fe49 100644
--- a/shared/chain-utils/test/index.test.ts
+++ b/shared/chain-utils/test/index.test.ts
@@ -126,9 +126,7 @@ describe("@fretchen/chain-utils", () => {
describe("Contract Addresses", () => {
describe("getGenAiNFTAddress()", () => {
test("should return Mainnet contract address", () => {
- expect(getGenAiNFTAddress("eip155:10")).toBe(
- "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"
- );
+ expect(getGenAiNFTAddress("eip155:10")).toBe("0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb");
});
test("should return Sepolia contract address", () => {
@@ -161,9 +159,7 @@ describe("@fretchen/chain-utils", () => {
describe("getSupportV2Address()", () => {
test("should return addresses for all deployed networks", () => {
// Mainnets
- expect(getSupportV2Address("eip155:10")).toBe(
- "0x4ca63f8A4Cd56287E854f53E18ca482D74391316"
- );
+ expect(getSupportV2Address("eip155:10")).toBe("0x4ca63f8A4Cd56287E854f53E18ca482D74391316");
expect(getSupportV2Address("eip155:8453")).toBe(
"0xB70EA4d714Fed01ce20E93F9033008BadA1c8694"
);
@@ -179,24 +175,16 @@ describe("@fretchen/chain-utils", () => {
describe("USDC Addresses", () => {
test("getUSDCAddress() returns valid addresses for all networks", () => {
- expect(getUSDCAddress("eip155:10")).toBe(
- "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
- );
+ expect(getUSDCAddress("eip155:10")).toBe("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85");
expect(getUSDCAddress("eip155:11155420")).toBe(
"0x5fd84259d66Cd46123540766Be93DFE6D43130D7"
);
- expect(getUSDCAddress("eip155:8453")).toBe(
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
- );
- expect(getUSDCAddress("eip155:84532")).toBe(
- "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
- );
+ expect(getUSDCAddress("eip155:8453")).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
+ expect(getUSDCAddress("eip155:84532")).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e");
});
test("getUSDCAddress() throws for unsupported network", () => {
- expect(() => getUSDCAddress("eip155:1")).toThrow(
- "USDC not available on eip155:1"
- );
+ expect(() => getUSDCAddress("eip155:1")).toThrow("USDC not available on eip155:1");
});
test("getUSDCName() returns correct names for EIP-712 domain", () => {
@@ -276,7 +264,7 @@ describe("@fretchen/chain-utils", () => {
test("chainId from viem matches fromCAIP2 for all networks", () => {
const networks = ["eip155:10", "eip155:11155420", "eip155:8453", "eip155:84532"];
-
+
for (const network of networks) {
const viemChain = getViemChain(network);
const parsedChainId = fromCAIP2(network);
From 32c424ca2c0b890a6331e7d3689a6ff67e429bd5 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:34:46 +0100
Subject: [PATCH 09/46] Update package-lock.json
---
scw_js/package-lock.json | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/scw_js/package-lock.json b/scw_js/package-lock.json
index 9ee513192..edec45936 100644
--- a/scw_js/package-lock.json
+++ b/scw_js/package-lock.json
@@ -41,8 +41,15 @@
"name": "@fretchen/chain-utils",
"version": "1.0.0",
"devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "eslint": "^9.39.2",
+ "prettier": "^3.8.1",
"typescript": "^5.0.0",
- "viem": "^2.0.0"
+ "typescript-eslint": "^8.53.1",
+ "viem": "^2.0.0",
+ "vitest": "^2.0.0"
},
"peerDependencies": {
"viem": "^2.0.0"
From 1dabd8c04d39345a13ddbb487a6e78eedfabafa4 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:39:55 +0100
Subject: [PATCH 10/46] Update test-scw-js.yml
---
.github/workflows/test-scw-js.yml | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/test-scw-js.yml b/.github/workflows/test-scw-js.yml
index 5156a68a5..ad0b3aff1 100644
--- a/.github/workflows/test-scw-js.yml
+++ b/.github/workflows/test-scw-js.yml
@@ -7,12 +7,14 @@ on:
pull_request:
paths:
- "scw_js/**"
+ - "shared/chain-utils/**"
- ".github/workflows/test-scw-js.yml"
push:
branches:
- main
paths:
- "scw_js/**"
+ - "shared/chain-utils/**"
jobs:
test-scw-js:
@@ -31,7 +33,13 @@ jobs:
with:
node-version: "22"
cache: "npm"
- cache-dependency-path: scw_js/package.json
+ cache-dependency-path: |
+ scw_js/package-lock.json
+ shared/chain-utils/package-lock.json
+
+ - name: Install and build chain-utils
+ working-directory: ./shared/chain-utils
+ run: npm install
- name: Install dependencies
run: npm install
From f92832497dc63d4b264cd0c47cad3ff9a4b8465c Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:42:50 +0100
Subject: [PATCH 11/46] Update genimg_x402_token.js
---
scw_js/genimg_x402_token.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/scw_js/genimg_x402_token.js b/scw_js/genimg_x402_token.js
index 0173cb7d4..7bfde51bc 100644
--- a/scw_js/genimg_x402_token.js
+++ b/scw_js/genimg_x402_token.js
@@ -1,7 +1,12 @@
// x402 v2 Token Payment Implementation for GenImg
// Uses official @x402/core and @x402/evm packages for payment handling
-import { GenImNFTv4ABI as nftAbi } from "@fretchen/chain-utils";
+import {
+ GenImNFTv4ABI as nftAbi,
+ getViemChain,
+ getGenAiNFTAddress,
+ getUSDCConfig,
+} from "@fretchen/chain-utils";
import { getContract, createWalletClient, createPublicClient, http, parseEther } from "viem";
import { generateAndUploadImage, JSON_BASE_PATH } from "./image_service.js";
import { privateKeyToAccount } from "viem/accounts";
@@ -12,7 +17,6 @@ import {
extractPaymentPayload,
createSettlementHeaders,
} from "./x402_server.js";
-import { getViemChain, getGenAiNFTAddress, getUSDCConfig } from "@fretchen/chain-utils";
import { validatePaymentNetwork } from "./getChain.js";
// Re-export x402 functions for backward compatibility with tests
From 5910e8eb574d7a1b475c22f2c8dd68927f80e7af Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:45:21 +0100
Subject: [PATCH 12/46] Clean the problems
---
shared/chain-utils/package-lock.json | 624 +++++++++++++++++++++++++++
shared/chain-utils/package.json | 1 +
2 files changed, 625 insertions(+)
diff --git a/shared/chain-utils/package-lock.json b/shared/chain-utils/package-lock.json
index 569851479..147ea1dab 100644
--- a/shared/chain-utils/package-lock.json
+++ b/shared/chain-utils/package-lock.json
@@ -11,6 +11,7 @@
"@eslint/js": "^9.39.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
+ "@vitest/coverage-v8": "^2.0.0",
"eslint": "^9.39.2",
"prettier": "^3.8.1",
"typescript": "^5.0.0",
@@ -29,6 +30,77 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -661,6 +733,55 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -668,6 +789,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
@@ -710,6 +842,17 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.56.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
@@ -1346,6 +1489,39 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz",
+ "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^0.2.3",
+ "debug": "^4.3.7",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.12",
+ "magicast": "^0.3.5",
+ "std-env": "^3.8.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "2.1.9",
+ "vitest": "2.1.9"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
@@ -1521,6 +1697,19 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -1712,6 +1901,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -2102,6 +2305,23 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2117,6 +2337,27 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2153,6 +2394,13 @@
"node": ">=8"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -2200,6 +2448,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -2236,6 +2494,76 @@
"ws": "*"
}
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -2324,6 +2652,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2334,6 +2669,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -2350,6 +2713,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2464,6 +2837,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2497,6 +2877,23 @@
"node": ">=8"
}
},
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -2697,6 +3094,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2721,6 +3131,110 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -2747,6 +3261,21 @@
"node": ">=8"
}
},
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -3105,6 +3634,101 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
diff --git a/shared/chain-utils/package.json b/shared/chain-utils/package.json
index 71ccf1e91..3668d53b0 100644
--- a/shared/chain-utils/package.json
+++ b/shared/chain-utils/package.json
@@ -34,6 +34,7 @@
"@eslint/js": "^9.39.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
+ "@vitest/coverage-v8": "^2.0.0",
"eslint": "^9.39.2",
"prettier": "^3.8.1",
"typescript": "^5.0.0",
From a07937e89e2b55abff4ed3bf6d70bd15b5701dc7 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:48:13 +0100
Subject: [PATCH 13/46] Create README.md
---
shared/chain-utils/README.md | 167 +++++++++++++++++++++++++++++++++++
1 file changed, 167 insertions(+)
create mode 100644 shared/chain-utils/README.md
diff --git a/shared/chain-utils/README.md b/shared/chain-utils/README.md
new file mode 100644
index 000000000..426b13820
--- /dev/null
+++ b/shared/chain-utils/README.md
@@ -0,0 +1,167 @@
+# @fretchen/chain-utils
+
+Shared chain utilities for the fretchen.github.io monorepo. Provides contract addresses, ABIs, and CAIP-2 utilities for multi-chain deployments.
+
+## Installation
+
+```bash
+npm install @fretchen/chain-utils
+```
+
+Or as a local dependency in the monorepo:
+
+```json
+{
+ "dependencies": {
+ "@fretchen/chain-utils": "file:../shared/chain-utils"
+ }
+}
+```
+
+## Usage
+
+```typescript
+import {
+ getViemChain,
+ getGenAiNFTAddress,
+ getUSDCConfig,
+ GenImNFTv4ABI,
+ toCAIP2,
+ fromCAIP2,
+} from "@fretchen/chain-utils";
+
+// Get viem chain from CAIP-2 identifier
+const chain = getViemChain("eip155:10"); // Optimism
+
+// Get contract address
+const nftAddress = getGenAiNFTAddress("eip155:10");
+
+// CAIP-2 conversion
+const caip2 = toCAIP2(10); // "eip155:10"
+const chainId = fromCAIP2("eip155:10"); // 10
+```
+
+## Supported Networks
+
+| Network | CAIP-2 ID | Type |
+| ---------------- | ----------------- | ------- |
+| Optimism | `eip155:10` | Mainnet |
+| Base | `eip155:8453` | Mainnet |
+| Optimism Sepolia | `eip155:11155420` | Testnet |
+| Base Sepolia | `eip155:84532` | Testnet |
+
+## Contract Overview
+
+### GenImNFTv4 (AI Image NFT)
+
+Upgradeable ERC-721 for AI-generated images. Uses UUPS proxy pattern.
+
+| Network | Address |
+| ---------------- | -------------------------------------------- |
+| Optimism | `0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb` |
+| Optimism Sepolia | `0x10827cC42a09D0BAD2d43134C69F0e776D853D85` |
+
+```typescript
+import { GenImNFTv4ABI, getGenAiNFTAddress } from "@fretchen/chain-utils";
+```
+
+### SupportV2 (Tipping Contract)
+
+Accepts ETH tips with optional messages.
+
+| Network | Address |
+| ---------------- | -------------------------------------------- |
+| Optimism | `0x4ca63f8A4Cd56287E854f53E18ca482D74391316` |
+| Base | `0xB70EA4d714Fed01ce20E93F9033008BadA1c8694` |
+| Optimism Sepolia | `0x9859431b682e861b19e87Db14a04944BC747AB6d` |
+| Base Sepolia | `0xaB44BE78499721b593a0f4BE2099b246e9C53B57` |
+
+```typescript
+import { getSupportV2Address } from "@fretchen/chain-utils";
+```
+
+### LLMv1 (AI Chat Contract)
+
+On-chain AI chat with per-token billing.
+
+| Network | Address |
+| ---------------- | -------------------------------------------- |
+| Optimism | `0x7E8b7091a229B1004c4FBa25bB70d04595d3e848` |
+| Optimism Sepolia | `0xA5b7f0A3f4104c97b46eafF2b0b4A457C5a73Bf4` |
+
+```typescript
+import { LLMv1ABI } from "@fretchen/chain-utils";
+```
+
+### EIP3009SplitterV1 (Payment Splitter)
+
+Splits EIP-3009 USDC payments between recipient and platform.
+
+| Network | Address |
+| ---------------- | -------------------------------------------- |
+| Optimism | `0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf` |
+| Optimism Sepolia | `0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a` |
+
+```typescript
+import { EIP3009SplitterV1ABI } from "@fretchen/chain-utils";
+```
+
+### USDC (Stablecoin)
+
+Circle's USD Coin with EIP-3009 support for gasless transfers.
+
+| Network | Address |
+| ---------------- | -------------------------------------------- |
+| Optimism | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
+| Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
+| Optimism Sepolia | `0x5fd84259d66Cd46123540766Be93DFE6D43130D7` |
+| Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
+
+```typescript
+import { getUSDCConfig, getUSDCAddress } from "@fretchen/chain-utils";
+
+// Full config for EIP-712 signatures
+const config = getUSDCConfig("eip155:10");
+// { name: "OP Mainnet", chainId: 10, address: "0x...", usdcName: "USD Coin", ... }
+```
+
+## API Reference
+
+### CAIP-2 Utilities
+
+- `toCAIP2(chainId: number): string` - Convert chain ID to CAIP-2
+- `fromCAIP2(network: string): number` - Parse CAIP-2 to chain ID
+- `isMainnet(network: string): boolean` - Check if network is mainnet
+- `isTestnet(network: string): boolean` - Check if network is testnet
+
+### Chain Utilities
+
+- `getViemChain(network: string): Chain` - Get viem Chain object
+
+### Address Getters
+
+- `getGenAiNFTAddress(network: string): Address`
+- `getCollectorNFTAddress(network: string): Address`
+- `getSupportV2Address(network: string): Address`
+- `getUSDCAddress(network: string): Address`
+- `getUSDCConfig(network: string): USDCConfig`
+
+### ABIs
+
+- `GenImNFTv4ABI` - AI Image NFT contract
+- `LLMv1ABI` - AI Chat contract
+- `EIP3009SplitterV1ABI` - Payment splitter
+
+## Development
+
+```bash
+npm install
+npm run build # Compile TypeScript
+npm test # Run tests
+npm run lint # ESLint check
+npm run format # Prettier check
+```
+
+## License
+
+ISC
From d1c992a4b2f1d687e08942279e00bf94caf8e365 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 21:59:50 +0100
Subject: [PATCH 14/46] Further cleaning
---
.github/workflows/ci.yml | 7 +-----
.github/workflows/test-chain-utils.yml | 7 +-----
.github/workflows/test-facilitator.yml | 8 +-----
.github/workflows/test-scw-js.yml | 8 +-----
scw_js/package.json | 2 +-
shared/chain-utils/README.md | 2 ++
shared/chain-utils/package.json | 7 +-----
shared/chain-utils/src/addresses.ts | 22 +++++++++++++++--
shared/chain-utils/test/index.test.ts | 34 ++++++++++++++++++++++++++
x402_facilitator/package.json | 2 +-
10 files changed, 63 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index da7f87c8d..c16738dbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,12 +47,7 @@ jobs:
npm ci
npm run build
- - name: Run tests
- run: |
- cd website
- npm test
-
- - name: Test coverage
+ - name: Run tests with coverage
run: |
cd website
npm run test:coverage
diff --git a/.github/workflows/test-chain-utils.yml b/.github/workflows/test-chain-utils.yml
index e5549815b..aea651fc5 100644
--- a/.github/workflows/test-chain-utils.yml
+++ b/.github/workflows/test-chain-utils.yml
@@ -36,16 +36,11 @@ jobs:
- name: Install dependencies
run: npm install
- - name: Run quality checks (lint + format + test)
+ - name: Run quality checks (lint + format + test with coverage)
run: npm run check
env:
CI: true
- - name: Run tests with coverage
- run: npm run test:coverage
- env:
- CI: true
-
- name: Upload coverage reports to Codecov (optional)
uses: codecov/codecov-action@v3
with:
diff --git a/.github/workflows/test-facilitator.yml b/.github/workflows/test-facilitator.yml
index c97aebab0..bd56fc672 100644
--- a/.github/workflows/test-facilitator.yml
+++ b/.github/workflows/test-facilitator.yml
@@ -37,18 +37,12 @@ jobs:
- name: Install dependencies
run: npm install
- - name: Run quality checks (lint + format + test)
+ - name: Run quality checks (lint + format + test with coverage)
run: npm run check
env:
NODE_ENV: test
CI: true
- - name: Run tests with coverage
- run: npm run test:coverage
- env:
- NODE_ENV: test
- CI: true
-
- name: Upload coverage reports to Codecov (optional)
uses: codecov/codecov-action@v3
with:
diff --git a/.github/workflows/test-scw-js.yml b/.github/workflows/test-scw-js.yml
index ad0b3aff1..0bb61ce32 100644
--- a/.github/workflows/test-scw-js.yml
+++ b/.github/workflows/test-scw-js.yml
@@ -44,18 +44,12 @@ jobs:
- name: Install dependencies
run: npm install
- - name: Run quality checks (lint + format + test)
+ - name: Run quality checks (lint + format + test with coverage)
run: npm run check
env:
NODE_ENV: test
CI: true
- - name: Run tests with coverage
- run: npm run test:coverage
- env:
- NODE_ENV: test
- CI: true
-
- name: Upload coverage reports to Codecov (optional)
uses: codecov/codecov-action@v3
with:
diff --git a/scw_js/package.json b/scw_js/package.json
index 80933c10d..4bd1b93dd 100644
--- a/scw_js/package.json
+++ b/scw_js/package.json
@@ -13,7 +13,7 @@
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
- "check": "npm run lint && npm run format:check && npm run test",
+ "check": "npm run lint && npm run format:check && npm run test:coverage",
"dev:x402": "NODE_ENV=test node genimg_x402_token.js",
"dev:bfl": "NODE_ENV=test node genimg_bfl.js"
},
diff --git a/shared/chain-utils/README.md b/shared/chain-utils/README.md
index 426b13820..562a557ff 100644
--- a/shared/chain-utils/README.md
+++ b/shared/chain-utils/README.md
@@ -18,6 +18,8 @@ Or as a local dependency in the monorepo:
}
```
+> **Note:** This package must be built before use. Run `npm install && npm run build` in `shared/chain-utils/` before installing consuming packages. The CI workflow handles this automatically.
+
## Usage
```typescript
diff --git a/shared/chain-utils/package.json b/shared/chain-utils/package.json
index 3668d53b0..349041986 100644
--- a/shared/chain-utils/package.json
+++ b/shared/chain-utils/package.json
@@ -9,10 +9,6 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
- },
- "./addresses": {
- "types": "./dist/addresses.d.ts",
- "default": "./dist/addresses.js"
}
},
"files": [
@@ -20,12 +16,11 @@
],
"scripts": {
"build": "tsc",
- "prepare": "npm run build",
"clean": "rm -rf dist",
"lint": "eslint .",
"format": "prettier --check .",
"format:fix": "prettier --write .",
- "check": "npm run lint && npm run format && npm run test",
+ "check": "npm run lint && npm run format && npm run test:coverage",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index 947e5b8ef..1cfc905dd 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 - nach Deployment hinzufügen
+ // "eip155:8453": "0x...", // Base - add after Deployment
};
export const TESTNET_GENAI_NFT_ADDRESSES: Record = {
@@ -147,6 +147,19 @@ export function getSupportV2Address(network: string): `0x${string}` {
return address;
}
+/**
+ * Get EIP3009 Splitter address for a network (mainnet or testnet).
+ * @throws Error if not deployed on that network
+ */
+export function getEIP3009SplitterAddress(network: string): `0x${string}` {
+ const address =
+ MAINNET_EIP3009_SPLITTER_ADDRESSES[network] || TESTNET_EIP3009_SPLITTER_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`EIP3009 Splitter not deployed on ${network}`);
+ }
+ return address;
+}
+
/**
* Get USDC address for a network.
* @throws Error if USDC not available on that network
@@ -161,9 +174,14 @@ export function getUSDCAddress(network: string): `0x${string}` {
/**
* Get USDC name for a network.
+ * @throws Error if USDC not available on that network
*/
export function getUSDCName(network: string): string {
- return USDC_NAMES[network] || "USDC";
+ const name = USDC_NAMES[network];
+ if (!name) {
+ throw new Error(`USDC not available on ${network}`);
+ }
+ return name;
}
// ═══════════════════════════════════════════════════════════════
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
index 55bd7fe49..63a5ff114 100644
--- a/shared/chain-utils/test/index.test.ts
+++ b/shared/chain-utils/test/index.test.ts
@@ -20,12 +20,15 @@ import {
getGenAiNFTAddress,
getCollectorNFTAddress,
getSupportV2Address,
+ getEIP3009SplitterAddress,
getUSDCAddress,
getUSDCName,
MAINNET_GENAI_NFT_ADDRESSES,
TESTNET_GENAI_NFT_ADDRESSES,
MAINNET_SUPPORT_V2_ADDRESSES,
TESTNET_SUPPORT_V2_ADDRESSES,
+ MAINNET_EIP3009_SPLITTER_ADDRESSES,
+ TESTNET_EIP3009_SPLITTER_ADDRESSES,
USDC_ADDRESSES,
} from "../src/addresses";
@@ -173,6 +176,37 @@ describe("@fretchen/chain-utils", () => {
});
});
+ describe("getEIP3009SplitterAddress()", () => {
+ test("should return addresses for all deployed networks", () => {
+ // Mainnet
+ expect(getEIP3009SplitterAddress("eip155:10")).toBe(
+ "0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf"
+ );
+ // Testnet
+ expect(getEIP3009SplitterAddress("eip155:11155420")).toBe(
+ "0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a"
+ );
+ });
+
+ test("should throw for unsupported network", () => {
+ expect(() => getEIP3009SplitterAddress("eip155:1")).toThrow(
+ "EIP3009 Splitter not deployed on eip155:1"
+ );
+ expect(() => getEIP3009SplitterAddress("eip155:8453")).toThrow(
+ "EIP3009 Splitter not deployed on eip155:8453"
+ );
+ });
+
+ test("all addresses should be valid checksummed addresses", () => {
+ Object.entries(MAINNET_EIP3009_SPLITTER_ADDRESSES).forEach(([network, address]) => {
+ expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ });
+ Object.entries(TESTNET_EIP3009_SPLITTER_ADDRESSES).forEach(([network, address]) => {
+ expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ });
+ });
+ });
+
describe("USDC Addresses", () => {
test("getUSDCAddress() returns valid addresses for all networks", () => {
expect(getUSDCAddress("eip155:10")).toBe("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85");
diff --git a/x402_facilitator/package.json b/x402_facilitator/package.json
index 2623a5c08..555e59e1b 100644
--- a/x402_facilitator/package.json
+++ b/x402_facilitator/package.json
@@ -13,7 +13,7 @@
"lint:fix": "eslint . --fix",
"format": "prettier --write \"**/*.{js,json,md}\"",
"format:check": "prettier --check \"**/*.{js,json,md}\"",
- "check": "npm run lint && npm run format:check && npm run test",
+ "check": "npm run lint && npm run format:check && npm run test:coverage",
"deploy": "serverless deploy",
"deploy:prod": "serverless deploy --stage production",
"info": "serverless info",
From 441f42949d694bd42cdf192b70a868c0b69a048d Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 22:01:27 +0100
Subject: [PATCH 15/46] Cleaning the chain-utils a bit more
---
shared/chain-utils/src/addresses.ts | 12 ++++++++++
shared/chain-utils/test/index.test.ts | 32 +++++++++++++++++++++++++--
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index 1cfc905dd..a6a41a88c 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -135,6 +135,18 @@ export function getCollectorNFTAddress(network: string): `0x${string}` {
return address;
}
+/**
+ * Get LLMv1 address for a network (mainnet or testnet).
+ * @throws Error if not deployed on that network
+ */
+export function getLLMv1Address(network: string): `0x${string}` {
+ const address = MAINNET_LLM_V1_ADDRESSES[network] || TESTNET_LLM_V1_ADDRESSES[network];
+ if (!address) {
+ throw new Error(`LLMv1 not deployed on ${network}`);
+ }
+ return address;
+}
+
/**
* Get SupportV2 address for a network (mainnet or testnet).
* @throws Error if not deployed on that network
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
index 63a5ff114..a3eb8f51b 100644
--- a/shared/chain-utils/test/index.test.ts
+++ b/shared/chain-utils/test/index.test.ts
@@ -19,12 +19,15 @@ import {
import {
getGenAiNFTAddress,
getCollectorNFTAddress,
+ getLLMv1Address,
getSupportV2Address,
getEIP3009SplitterAddress,
getUSDCAddress,
getUSDCName,
MAINNET_GENAI_NFT_ADDRESSES,
TESTNET_GENAI_NFT_ADDRESSES,
+ MAINNET_LLM_V1_ADDRESSES,
+ TESTNET_LLM_V1_ADDRESSES,
MAINNET_SUPPORT_V2_ADDRESSES,
TESTNET_SUPPORT_V2_ADDRESSES,
MAINNET_EIP3009_SPLITTER_ADDRESSES,
@@ -159,6 +162,31 @@ describe("@fretchen/chain-utils", () => {
});
});
+ describe("getLLMv1Address()", () => {
+ test("should return addresses for all deployed networks", () => {
+ // Mainnet
+ expect(getLLMv1Address("eip155:10")).toBe("0x7E8b7091a229B1004c4FBa25bB70d04595d3e848");
+ // Testnet
+ expect(getLLMv1Address("eip155:11155420")).toBe(
+ "0xA5b7f0A3f4104c97b46eafF2b0b4A457C5a73Bf4"
+ );
+ });
+
+ test("should throw for unsupported network", () => {
+ expect(() => getLLMv1Address("eip155:1")).toThrow("LLMv1 not deployed on eip155:1");
+ expect(() => getLLMv1Address("eip155:8453")).toThrow("LLMv1 not deployed on eip155:8453");
+ });
+
+ test("all addresses should be valid checksummed addresses", () => {
+ Object.entries(MAINNET_LLM_V1_ADDRESSES).forEach(([, address]) => {
+ expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ });
+ Object.entries(TESTNET_LLM_V1_ADDRESSES).forEach(([, address]) => {
+ expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
+ });
+ });
+ });
+
describe("getSupportV2Address()", () => {
test("should return addresses for all deployed networks", () => {
// Mainnets
@@ -198,10 +226,10 @@ describe("@fretchen/chain-utils", () => {
});
test("all addresses should be valid checksummed addresses", () => {
- Object.entries(MAINNET_EIP3009_SPLITTER_ADDRESSES).forEach(([network, address]) => {
+ Object.entries(MAINNET_EIP3009_SPLITTER_ADDRESSES).forEach(([_network, address]) => {
expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
});
- Object.entries(TESTNET_EIP3009_SPLITTER_ADDRESSES).forEach(([network, address]) => {
+ Object.entries(TESTNET_EIP3009_SPLITTER_ADDRESSES).forEach(([_network, address]) => {
expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
});
});
From 1c4e0f5d4e748a1a11e10298d4d3308957d31a2c Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 22:05:28 +0100
Subject: [PATCH 16/46] Update test-scw-js.yml
---
.github/workflows/test-scw-js.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test-scw-js.yml b/.github/workflows/test-scw-js.yml
index 0bb61ce32..766060b65 100644
--- a/.github/workflows/test-scw-js.yml
+++ b/.github/workflows/test-scw-js.yml
@@ -39,7 +39,7 @@ jobs:
- name: Install and build chain-utils
working-directory: ./shared/chain-utils
- run: npm install
+ run: npm install && npm run build
- name: Install dependencies
run: npm install
From 40dcf5bc1a55b51a180a38f4b4265bbc17ee6ed5 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 22:15:51 +0100
Subject: [PATCH 17/46] Update MULTICHAIN_EXPANSION_PROPOSAL.md
---
website/MULTICHAIN_EXPANSION_PROPOSAL.md | 206 +++++++++--------------
1 file changed, 77 insertions(+), 129 deletions(-)
diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
index 398ffc4ae..4a3394516 100644
--- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md
+++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
@@ -7,9 +7,10 @@
| Contract | Optimism | Base | Multi-Chain Ready |
|----------|:--------:|:----:|:-----------------:|
| **SupportV2** | ✅ | ✅ | ✅ Ja |
-| **GenImNFTv4** | ✅ | ❌ | ❌ Nein |
+| **GenImNFTv4** | ✅ | ❌ | ✅ Ja (Backend ready) |
| **CollectorNFTv1** | ✅ | ❌ | ❌ Nein |
| **LLMv1** | ✅ | ❌ | ❌ (out of scope) |
+| **EIP3009SplitterV1** | ✅ | ❌ | ✅ Ja |
---
@@ -17,9 +18,9 @@
| Phase | Was | Projekte | Status |
|-------|-----|----------|--------|
-| **1a** | `@fretchen/chain-utils` erstellen | shared/ | 🔜 Next |
-| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ⬜ |
-| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ |
+| **1a** | `@fretchen/chain-utils` erstellen | shared/ | ✅ Fertig |
+| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ✅ Fertig |
+| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ Next |
| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ |
| **3** | CollectorNFT-Komponenten migrieren | website/ | ⬜ |
| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später |
@@ -27,7 +28,7 @@
---
-## Phase 1a: @fretchen/chain-utils erstellen
+## Phase 1a: @fretchen/chain-utils ✅ FERTIG
**Struktur:**
```
@@ -36,118 +37,61 @@ fretchen.github.io/
│ └── chain-utils/
│ ├── package.json
│ ├── tsconfig.json
-│ └── src/
-│ ├── index.ts # Core utilities
-│ └── addresses.ts # Contract address maps
+│ ├── vitest.config.ts
+│ ├── eslint.config.js
+│ ├── README.md
+│ ├── src/
+│ │ ├── index.ts # Core utilities + re-exports
+│ │ ├── addresses.ts # Contract address maps + getters
+│ │ └── abi/
+│ │ ├── index.ts
+│ │ ├── GenImNFTv4.ts
+│ │ ├── LLMv1.ts
+│ │ └── EIP3009SplitterV1.ts
+│ └── test/
+│ ├── index.test.ts
+│ └── abi.test.ts
```
-**package.json:**
-```json
-{
- "name": "@fretchen/chain-utils",
- "version": "1.0.0",
- "type": "module",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "exports": {
- ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
- "./addresses": { "types": "./dist/addresses.d.ts", "default": "./dist/addresses.js" }
- },
- "scripts": {
- "build": "tsc",
- "prepare": "npm run build"
- },
- "devDependencies": {
- "typescript": "^5.0.0",
- "viem": "^2.0.0"
- }
-}
-```
+**Implementiert:**
+- CAIP-2 Utilities: `toCAIP2()`, `fromCAIP2()`, `isMainnet()`, `isTestnet()`
+- Chain Mapping: `getViemChain()`
+- Contract Adressen: Separate Maps für Mainnet/Testnet
+- Getter Funktionen: `getGenAiNFTAddress()`, `getCollectorNFTAddress()`, `getLLMv1Address()`, `getSupportV2Address()`, `getEIP3009SplitterAddress()`, `getUSDCAddress()`, `getUSDCConfig()`
+- ABIs: `GenImNFTv4ABI`, `LLMv1ABI`, `EIP3009SplitterV1ABI`
+- 46 Tests mit 98.75% Coverage
+- CI/CD Pipeline: `.github/workflows/test-chain-utils.yml`
-**src/index.ts:**
-```typescript
-import { optimism, optimismSepolia, base, baseSepolia } from "viem/chains";
-import type { Chain } from "viem";
-
-// ═══════════════════════════════════════════════════════════════
-// CAIP-2 Utilities
-// ═══════════════════════════════════════════════════════════════
-
-export const toCAIP2 = (chainId: number): string => `eip155:${chainId}`;
-
-export const fromCAIP2 = (network: string): number => {
- const match = network.match(/^eip155:(\d+)$/);
- if (!match) throw new Error(`Invalid CAIP-2: ${network}`);
- return parseInt(match[1], 10);
-};
-
-export function getViemChain(network: string): Chain {
- switch (network) {
- case "eip155:10": return optimism;
- case "eip155:11155420": return optimismSepolia;
- case "eip155:8453": return base;
- case "eip155:84532": return baseSepolia;
- default: throw new Error(`Unsupported network: ${network}`);
- }
-}
+**Wichtig:** Kein `prepare` Script - muss manuell mit `npm run build` gebaut werden. CI Workflows bauen chain-utils vor Installation der Consumer.
-export * from "./addresses";
-```
+---
-**src/addresses.ts:**
-```typescript
-// ═══════════════════════════════════════════════════════════════
-// Contract Adressen (CAIP-2 als Key, getrennt nach Mainnet/Testnet)
-// ═══════════════════════════════════════════════════════════════
-
-// GenImNFT
-export const MAINNET_GENAI_NFT_ADDRESSES: Record = {
- "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- // "eip155:8453": "0x...", // Base - nach Deployment
-};
-
-export const TESTNET_GENAI_NFT_ADDRESSES: Record = {
- "eip155:11155420": "0x10827cC42a09D0BAD2d43134C69F0e776D853D85",
-};
-
-// CollectorNFT
-export const MAINNET_COLLECTOR_NFT_ADDRESSES: Record = {
- "eip155:10": "0x584c40d8a7cA164933b5F90a2dC11ddCB4a924ea",
-};
-
-export const TESTNET_COLLECTOR_NFT_ADDRESSES: Record = {};
-
-// SupportV2
-export const MAINNET_SUPPORT_V2_ADDRESSES: Record = {
- "eip155:10": "0x4ca63f8A4Cd56287E854f53E18ca482D74391316",
- "eip155:8453": "0xB70EA4d714Fed01ce20E93F9033008BadA1c8694",
-};
-
-export const TESTNET_SUPPORT_V2_ADDRESSES: Record = {
- "eip155:11155420": "0x9859431b682e861b19e87Db14a04944BC747AB6d",
- "eip155:84532": "0xaB44BE78499721b593a0f4BE2099b246e9C53B57",
-};
-
-// USDC
-export const USDC_ADDRESSES: Record = {
- "eip155:10": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
- "eip155:11155420": "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
- "eip155:8453": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
- "eip155:84532": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
-};
-
-export const USDC_NAMES: Record = {
- "eip155:10": "USD Coin",
- "eip155:11155420": "USDC",
- "eip155:8453": "USD Coin",
- "eip155:84532": "USDC",
-};
-```
+## Phase 1b: scw_js Migration ✅ FERTIG
+
+**Änderungen:**
+- `package.json`: Dependency `"@fretchen/chain-utils": "file:../shared/chain-utils"`
+- `tsup.config.js`: Bundling mit tsup für Scaleway Deployment
+- ABIs: Importiert aus `@fretchen/chain-utils` statt lokaler Kopien
+- `getChain.js`, `genimg_bfl.js`, `genimg_x402_token.js`, `x402_server.js`: Nutzen `getViemChain()`, `getGenAiNFTAddress()`, `getUSDCConfig()`
+
+**tsup Bundling:**
+- Löst das Symlink-Problem für Scaleway Deployment
+- `createRequire` Banner für pino ESM Kompatibilität
+- Node.js Builtins als external
+- 175 Tests bestanden
+
+**Entfernte Dateien:**
+- `nft_abi.js` → importiert aus chain-utils
+- `nft_abi.test.js` → verschoben nach chain-utils
-**Konsumenten installieren via file: Link:**
-```json
-{
- "dependencies": {
+---
+
+## Phase 1c: x402_facilitator Migration ⬜ NEXT
+
+**Geplant:**
+- `chain_utils.js` durch Imports aus `@fretchen/chain-utils` ersetzen
+- tsup Bundling hinzufügen (gleiches Pattern wie scw_js)
+- ABIs aus chain-utils importieren {
"@fretchen/chain-utils": "file:../shared/chain-utils"
}
}
@@ -221,7 +165,7 @@ export function getChainConfig(network) {
---
-## Phase 2: GenImNFT-Komponenten migrieren
+## Phase 2: GenImNFT-Komponenten migrieren (Website)
**Betroffene Dateien:**
- `MyNFTList.tsx` (4 Stellen)
@@ -243,23 +187,13 @@ const { data } = useReadContract({
// NACHHER
import { useAutoNetwork } from "../hooks/useAutoNetwork";
-import { getGenAiNFTConfig, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
const network = useAutoNetwork(GENAI_NFT_NETWORKS);
-const config = getGenAiNFTConfig(network);
-const { data } = useReadContract({ ...config });
-```
-
-**useAutoNetwork Hook (website/hooks/):**
-```typescript
-export function useAutoNetwork(supportedNetworks: string[]): string {
- const { chain } = useAccount();
- if (chain) {
- const userNetwork = toCAIP2(chain.id);
- if (supportedNetworks.includes(userNetwork)) return userNetwork;
- }
- return supportedNetworks[0]; // Fallback: Optimism
-}
+const { data } = useReadContract({
+ address: getGenAiNFTAddress(network),
+ abi: GenImNFTv4ABI,
+});
```
---
@@ -278,8 +212,15 @@ Gleiches Pattern wie Phase 2.
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
-5. `npm install` in allen Projekten
+4. Adresse in `@fretchen/chain-utils/src/addresses.ts` hinzufügen:
+ ```typescript
+ export const MAINNET_GENAI_NFT_ADDRESSES: Record = {
+ "eip155:10": "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
+ "eip155:8453": "0x...", // Base
+ };
+ ```
+5. `npm run build` in chain-utils
+6. `npm install` in allen Projekten
---
@@ -300,8 +241,15 @@ Voraussetzung: GenImNFT muss auf Base existieren.
- Getrennte Maps für Mainnet/Testnet
- Kein Chain-Selector - automatische Wahl basierend auf User-Wallet
- Eine Quelle für Adressen: `@fretchen/chain-utils`
+- tsup Bundling für Serverless Deployment
**Vorteile:**
- Neue Chains: Adresse nur an einer Stelle hinzufügen
- Konsistenz: Gleiches Pattern in website, scw_js, x402_facilitator
- Weniger Code: ~140 Zeilen duplizierter Switch-Statements entfernt
+- Zuverlässiges Deployment: Symlink-Problem durch Bundling gelöst
+
+**Tests:**
+- chain-utils: 46 Tests, 98.75% Coverage
+- scw_js: 175 Tests
+- CI Pipelines für alle Packages
From 10dcf0aef6e4e5c074bcb06bf7d06af8e14f8c52 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 22:26:57 +0100
Subject: [PATCH 18/46] Start the transition
---
shared/chain-utils/src/addresses.ts | 8 +-
x402_facilitator/chain_utils.js | 79 ++--
x402_facilitator/eip3009_splitter_abi.js | 83 +---
x402_facilitator/package-lock.json | 418 +++++++++++++++++++
x402_facilitator/package.json | 4 +
x402_facilitator/serverless.yml | 10 +-
x402_facilitator/test/x402_supported.test.js | 9 +-
x402_facilitator/tsup.config.js | 23 +
8 files changed, 509 insertions(+), 125 deletions(-)
create mode 100644 x402_facilitator/tsup.config.js
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index a6a41a88c..f2e9747b5 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -67,11 +67,11 @@ export const SUPPORT_V2_NETWORKS = [
// ═══════════════════════════════════════════════════════════════
export const MAINNET_LLM_V1_ADDRESSES: Record = {
- "eip155:10": "0x7E8b7091a229B1004c4FBa25bB70d04595d3e848", // Optimism
+ "eip155:10": "0x833F39D6e67390324796f861990ce9B7cf9F5dE1", // Optimism
};
export const TESTNET_LLM_V1_ADDRESSES: Record = {
- "eip155:11155420": "0xA5b7f0A3f4104c97b46eafF2b0b4A457C5a73Bf4", // Optimism Sepolia
+ "eip155:11155420": "0xB3dbD44477a7bcf253f2fA68eDb4be5aF2F2cA56", // Optimism Sepolia
};
// ═══════════════════════════════════════════════════════════════
@@ -99,11 +99,11 @@ export const USDC_NAMES: Record = {
// ═══════════════════════════════════════════════════════════════
export const MAINNET_EIP3009_SPLITTER_ADDRESSES: Record = {
- "eip155:10": "0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf", // Optimism
+ // "eip155:10": "0x...", // Optimism - not yet deployed
};
export const TESTNET_EIP3009_SPLITTER_ADDRESSES: Record = {
- "eip155:11155420": "0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a", // Optimism Sepolia
+ "eip155:11155420": "0x7e67bf96ADbf4a813DD7b0A3Ca3060a937018946", // Optimism Sepolia
};
// ═══════════════════════════════════════════════════════════════
diff --git a/x402_facilitator/chain_utils.js b/x402_facilitator/chain_utils.js
index be67bc7ca..bb9eca830 100644
--- a/x402_facilitator/chain_utils.js
+++ b/x402_facilitator/chain_utils.js
@@ -3,25 +3,23 @@
/**
* Chain Utilities for x402 Facilitator
* Shared functions for network/chain handling
+ *
+ * Uses @fretchen/chain-utils for chain/address data,
+ * adds local RPC URL handling and facilitator-specific config.
*/
-import { optimism, optimismSepolia } from "viem/chains";
+import {
+ getViemChain,
+ fromCAIP2,
+ getGenAiNFTAddress,
+ getLLMv1Address,
+ getEIP3009SplitterAddress,
+ getUSDCAddress,
+ getUSDCName,
+} from "@fretchen/chain-utils";
-/**
- * Convert x402 network identifier to viem chain object
- * @param {string} network - Network ID (eip155:10 or eip155:11155420)
- * @returns {import("viem/chains").Chain} Viem chain object
- * @throws {Error} If network is not supported
- */
-export function getChain(network) {
- if (network === "eip155:10") {
- return optimism;
- }
- if (network === "eip155:11155420") {
- return optimismSepolia;
- }
- throw new Error(`Unsupported network: ${network}`);
-}
+// Re-export getViemChain as getChain for backward compatibility
+export { getViemChain as getChain };
/**
* Get RPC URL for a network with fallback handling
@@ -53,6 +51,20 @@ export function getRpcUrl(network) {
throw new Error(`Unsupported network: ${network}`);
}
+/**
+ * Safely get an address, returning null if not deployed
+ * @param {function(string): string} getter - Address getter function
+ * @param {string} network - Network ID
+ * @returns {string|null} Address or null if not deployed
+ */
+function safeGetAddress(getter, network) {
+ try {
+ return getter(network);
+ } catch {
+ return null;
+ }
+}
+
/**
* Get chain configuration including RPC URL and contract addresses
* @param {string} network - Network ID (eip155:10 or eip155:11155420)
@@ -60,29 +72,15 @@ export function getRpcUrl(network) {
* @throws {Error} If network is not supported
*/
export function getChainConfig(network) {
- if (network === "eip155:10") {
- return {
- chain: optimism,
- rpcUrl: getRpcUrl(network),
- GENIMG_V4_ADDRESS: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- LLMV1_ADDRESS: "0x833F39D6e67390324796f861990ce9B7cf9F5dE1",
- SPLITTER_ADDRESS: null, // Not yet deployed on mainnet
- USDC_ADDRESS: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
- USDC_NAME: "USD Coin",
- };
- }
- if (network === "eip155:11155420") {
- return {
- chain: optimismSepolia,
- rpcUrl: getRpcUrl(network),
- GENIMG_V4_ADDRESS: null,
- LLMV1_ADDRESS: null,
- SPLITTER_ADDRESS: "0x7e67bf96ADbf4a813DD7b0A3Ca3060a937018946",
- USDC_ADDRESS: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
- USDC_NAME: "USDC",
- };
- }
- throw new Error(`Unsupported network: ${network}`);
+ return {
+ chain: getViemChain(network),
+ rpcUrl: getRpcUrl(network),
+ GENIMG_V4_ADDRESS: safeGetAddress(getGenAiNFTAddress, network),
+ LLMV1_ADDRESS: safeGetAddress(getLLMv1Address, network),
+ SPLITTER_ADDRESS: safeGetAddress(getEIP3009SplitterAddress, network),
+ USDC_ADDRESS: getUSDCAddress(network),
+ USDC_NAME: getUSDCName(network),
+ };
}
/**
@@ -139,8 +137,7 @@ export function getTokenInfo(network, tokenAddress) {
* @throws {Error} If network is not supported
*/
export function getChainId(network) {
- const chain = getChain(network);
- return chain.id;
+ return fromCAIP2(network);
}
/**
diff --git a/x402_facilitator/eip3009_splitter_abi.js b/x402_facilitator/eip3009_splitter_abi.js
index b72f1fea4..07a022d93 100644
--- a/x402_facilitator/eip3009_splitter_abi.js
+++ b/x402_facilitator/eip3009_splitter_abi.js
@@ -3,85 +3,18 @@
/**
* EIP3009SplitterV1 Contract ABI
*
- * Minimal ABI containing only the functions needed by the x402 splitter facilitator.
+ * Re-exports from @fretchen/chain-utils for backward compatibility.
* Full ABI available in: ../eth/abi/contracts/EIP3009SplitterV1.json
*
* Deployed Addresses:
- * - Optimism Sepolia: 0x7e67bf96ADbf4a813DD7b0A3Ca3060a937018946
- * - Optimism Mainnet: TBD
+ * - Optimism Sepolia: 0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a
+ * - Optimism Mainnet: 0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf
*/
-/**
- * Minimal ABI for x402 facilitator operations
- * Only includes: executeSplit, facilitatorWallet, fixedFee, isAuthorizationUsed
- */
-export const SPLITTER_ABI = [
- {
- name: "executeSplit",
- type: "function",
- stateMutability: "nonpayable",
- inputs: [
- { name: "token", type: "address" },
- { name: "buyer", type: "address" },
- { name: "seller", type: "address" },
- { name: "salt", type: "bytes32" },
- { name: "totalAmount", type: "uint256" },
- { name: "validAfter", type: "uint256" },
- { name: "validBefore", type: "uint256" },
- { name: "nonce", type: "bytes32" },
- { name: "v", type: "uint8" },
- { name: "r", type: "bytes32" },
- { name: "s", type: "bytes32" },
- ],
- outputs: [],
- },
- {
- name: "facilitatorWallet",
- type: "function",
- stateMutability: "view",
- inputs: [],
- outputs: [{ name: "", type: "address" }],
- },
- {
- name: "fixedFee",
- type: "function",
- stateMutability: "view",
- inputs: [],
- outputs: [{ name: "", type: "uint256" }],
- },
- {
- name: "isAuthorizationUsed",
- type: "function",
- stateMutability: "view",
- inputs: [
- { name: "token", type: "address" },
- { name: "authorizer", type: "address" },
- { name: "nonce", type: "bytes32" },
- ],
- outputs: [{ name: "", type: "bool" }],
- },
-];
+import { EIP3009SplitterV1ABI, getEIP3009SplitterAddress } from "@fretchen/chain-utils";
-/**
- * Deployed splitter contract addresses by network
- * Network format: CAIP-2 (eip155:chainId)
- */
-export const SPLITTER_ADDRESSES = {
- "eip155:10": process.env.SPLITTER_ADDRESS_MAINNET || "", // Optimism Mainnet (TBD)
- "eip155:11155420":
- process.env.SPLITTER_ADDRESS_SEPOLIA || "0x7e67bf96ADbf4a813DD7b0A3Ca3060a937018946", // Optimism Sepolia
-};
+// Re-export ABI with legacy name for backward compatibility
+export const SPLITTER_ABI = EIP3009SplitterV1ABI;
-/**
- * Get splitter address for a given network
- * @param {string} network - CAIP-2 network identifier (e.g., "eip155:10")
- * @returns {string} Splitter contract address
- * @throws {Error} If network is not supported
- */
-export function getSplitterAddress(network) {
- const address = SPLITTER_ADDRESSES[network];
- if (!address) {
- throw new Error(`Splitter not deployed on network ${network}`);
- }
- return address;
-}
+// Re-export getter function
+export { getEIP3009SplitterAddress as getSplitterAddress };
diff --git a/x402_facilitator/package-lock.json b/x402_facilitator/package-lock.json
index 157cfcbaf..a7fe83dfe 100644
--- a/x402_facilitator/package-lock.json
+++ b/x402_facilitator/package-lock.json
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@coinbase/x402": "^2.0.0",
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@x402/core": "^2.0.0",
"@x402/evm": "^2.0.0",
"dotenv": "^16.0.0",
@@ -23,12 +24,32 @@
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"serverless-scaleway-functions": ">=0.4.16",
+ "tsup": "^8.3.5",
"vitest": "^4.0.16"
},
"engines": {
"node": ">=18.0.0"
}
},
+ "../shared/chain-utils": {
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "@vitest/coverage-v8": "^2.0.0",
+ "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"
+ },
+ "peerDependencies": {
+ "viem": "^2.0.0"
+ }
+ },
"node_modules/@adraffy/ens-normalize": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
@@ -1565,6 +1586,10 @@
"fastify-plugin": "^4.0.0"
}
},
+ "node_modules/@fretchen/chain-utils": {
+ "resolved": "../shared/chain-utils",
+ "link": true
+ },
"node_modules/@grpc/grpc-js": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
@@ -1691,6 +1716,17 @@
}
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -3025,6 +3061,13 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/aproba": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
@@ -3389,6 +3432,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bundle-require": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz",
+ "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-tsconfig": "^0.2.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "esbuild": ">=0.18"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -3536,6 +3605,22 @@
"node": "*"
}
},
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -3784,6 +3869,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -5144,6 +5246,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/fix-dts-default-cjs-exports": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
+ "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.17",
+ "mlly": "^1.7.4",
+ "rollup": "^4.34.8"
+ }
+ },
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -6056,6 +6170,16 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/joycon": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
@@ -6163,6 +6287,36 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/load-tsconfig": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
+ "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6501,6 +6655,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mlly": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
+ "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.1"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6514,6 +6681,18 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
"node_modules/nan": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz",
@@ -7042,6 +7221,28 @@
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -7081,6 +7282,49 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7315,6 +7559,20 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@@ -7750,6 +8008,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -7938,6 +8206,39 @@
"url": "https://github.com/sponsors/Borewit"
}
},
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/superstruct": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
@@ -8090,6 +8391,29 @@
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
},
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
@@ -8213,6 +8537,16 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/trim-repeated": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
@@ -8236,12 +8570,89 @@
"node": ">=0.8.0"
}
},
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsup": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz",
+ "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-require": "^5.1.0",
+ "cac": "^6.7.14",
+ "chokidar": "^4.0.3",
+ "consola": "^3.4.0",
+ "debug": "^4.4.0",
+ "esbuild": "^0.27.0",
+ "fix-dts-default-cjs-exports": "^1.0.0",
+ "joycon": "^3.1.1",
+ "picocolors": "^1.1.1",
+ "postcss-load-config": "^6.0.1",
+ "resolve-from": "^5.0.0",
+ "rollup": "^4.34.8",
+ "source-map": "^0.7.6",
+ "sucrase": "^3.35.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.11",
+ "tree-kill": "^1.2.2"
+ },
+ "bin": {
+ "tsup": "dist/cli-default.js",
+ "tsup-node": "dist/cli-node.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@microsoft/api-extractor": "^7.36.0",
+ "@swc/core": "^1",
+ "postcss": "^8.4.12",
+ "typescript": ">=4.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@microsoft/api-extractor": {
+ "optional": true
+ },
+ "@swc/core": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsup/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tsup/node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -8311,6 +8722,13 @@
"node": ">=14.17"
}
},
+ "node_modules/ufo": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
diff --git a/x402_facilitator/package.json b/x402_facilitator/package.json
index 555e59e1b..146468dcd 100644
--- a/x402_facilitator/package.json
+++ b/x402_facilitator/package.json
@@ -5,6 +5,7 @@
"type": "module",
"main": "x402_facilitator.js",
"scripts": {
+ "build": "tsup",
"dev": "NODE_ENV=test node x402_facilitator.js",
"test": "vitest run",
"test:watch": "vitest",
@@ -14,6 +15,7 @@
"format": "prettier --write \"**/*.{js,json,md}\"",
"format:check": "prettier --check \"**/*.{js,json,md}\"",
"check": "npm run lint && npm run format:check && npm run test:coverage",
+ "predeploy": "npm run build",
"deploy": "serverless deploy",
"deploy:prod": "serverless deploy --stage production",
"info": "serverless info",
@@ -31,6 +33,7 @@
"license": "MIT",
"dependencies": {
"@coinbase/x402": "^2.0.0",
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@x402/core": "^2.0.0",
"@x402/evm": "^2.0.0",
"dotenv": "^16.0.0",
@@ -44,6 +47,7 @@
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"serverless-scaleway-functions": ">=0.4.16",
+ "tsup": "^8.3.5",
"vitest": "^4.0.16"
},
"engines": {
diff --git a/x402_facilitator/serverless.yml b/x402_facilitator/serverless.yml
index a0e358166..6252eabe1 100644
--- a/x402_facilitator/serverless.yml
+++ b/x402_facilitator/serverless.yml
@@ -34,6 +34,9 @@ plugins:
package:
patterns:
+ # Only include bundled output
+ - "dist/**"
+ # Exclude everything else
- "!.gitignore"
- "!.git/**"
- "!test/**"
@@ -41,14 +44,17 @@ package:
- "!.prettierrc"
- "!eslint.config.js"
- "!vitest.config.js"
+ - "!tsup.config.js"
- "!README.md"
- "!EIP712_VERIFICATION.md"
+ - "!*.js"
+ - "!node_modules/**"
functions:
# Single function with path-based routing
# Handles /verify, /settle, and /supported endpoints
facilitator:
- handler: x402_facilitator.handle
+ handler: dist/x402_facilitator.handle
description: x402 v2 payment facilitator with path-based routing
# Memory and timeout for the combined function
memoryLimit: 512
@@ -58,5 +64,5 @@ functions:
- facilitator.fretchen.eu
feefacilitator:
- handler: x402_splitter_facilitator.handle
+ handler: dist/x402_splitter_facilitator.handle
description: Testnet facilitator with fee for x402 v2 payments
diff --git a/x402_facilitator/test/x402_supported.test.js b/x402_facilitator/test/x402_supported.test.js
index 049efb678..4fd12a7ad 100644
--- a/x402_facilitator/test/x402_supported.test.js
+++ b/x402_facilitator/test/x402_supported.test.js
@@ -114,7 +114,7 @@ describe("x402 /supported endpoint", () => {
expect(whitelistExtension.contracts).toBeDefined();
});
- test("whitelist extension includes contracts for mainnet only", () => {
+ test("whitelist extension includes contracts for both mainnet and sepolia", () => {
const capabilities = getSupportedCapabilities();
const whitelistExtension = capabilities.extensions.find(
@@ -126,8 +126,10 @@ describe("x402 /supported endpoint", () => {
expect(Array.isArray(whitelistExtension.contracts["eip155:10"])).toBe(true);
expect(whitelistExtension.contracts["eip155:10"].length).toBe(2);
- // Sepolia has no contracts deployed
- expect(whitelistExtension.contracts["eip155:11155420"]).toBeUndefined();
+ // Sepolia also has contracts deployed
+ expect(whitelistExtension.contracts["eip155:11155420"]).toBeDefined();
+ expect(Array.isArray(whitelistExtension.contracts["eip155:11155420"])).toBe(true);
+ expect(whitelistExtension.contracts["eip155:11155420"].length).toBe(2);
});
test("whitelist extension includes GenImNFTv4 and LLMv1 contracts on mainnet", () => {
@@ -147,6 +149,7 @@ describe("x402 /supported endpoint", () => {
expect(genimgMainnet.method).toBe("isAuthorizedAgent(address)");
expect(llmMainnet).toBeDefined();
+ // LLMv1 on Optimism Mainnet
expect(llmMainnet.address).toBe("0x833F39D6e67390324796f861990ce9B7cf9F5dE1");
expect(llmMainnet.method).toBe("isAuthorizedAgent(address)");
});
diff --git a/x402_facilitator/tsup.config.js b/x402_facilitator/tsup.config.js
new file mode 100644
index 000000000..9f8ae98c0
--- /dev/null
+++ b/x402_facilitator/tsup.config.js
@@ -0,0 +1,23 @@
+import { defineConfig } from "tsup";
+import { builtinModules } from "module";
+
+export default defineConfig({
+ entry: ["x402_facilitator.js", "x402_splitter_facilitator.js"],
+ format: ["esm"],
+ platform: "node",
+ target: "node22",
+ outDir: "dist",
+ splitting: false,
+ clean: true,
+ // Keep readable for debugging
+ minify: false,
+ sourcemap: true,
+ // Node.js built-ins must be external (async_hooks, fs, path, etc.)
+ external: [...builtinModules, ...builtinModules.map((m) => `node:${m}`)],
+ // Bundle all npm dependencies
+ noExternal: [/.*/],
+ // Add createRequire banner for packages that use dynamic require() (like pino)
+ banner: {
+ js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
+ },
+});
From e49c4ec3162141882c1a9f2b46e4fd3dd22d573e Mon Sep 17 00:00:00 2001
From: fretchen
Date: Sun, 25 Jan 2026 22:28:55 +0100
Subject: [PATCH 19/46] Further simplification
---
.github/workflows/test-facilitator.yml | 12 ++++++++----
x402_facilitator/eip3009_splitter_abi.js | 20 --------------------
x402_facilitator/x402_splitter_settle.js | 6 +++++-
x402_facilitator/x402_splitter_verify.js | 4 ++--
4 files changed, 15 insertions(+), 27 deletions(-)
delete mode 100644 x402_facilitator/eip3009_splitter_abi.js
diff --git a/.github/workflows/test-facilitator.yml b/.github/workflows/test-facilitator.yml
index bd56fc672..9788be468 100644
--- a/.github/workflows/test-facilitator.yml
+++ b/.github/workflows/test-facilitator.yml
@@ -7,22 +7,20 @@ on:
pull_request:
paths:
- "x402_facilitator/**"
+ - "shared/chain-utils/**"
- ".github/workflows/test-facilitator.yml"
push:
branches:
- main
paths:
- "x402_facilitator/**"
+ - "shared/chain-utils/**"
- ".github/workflows/test-facilitator.yml"
jobs:
test-facilitator:
runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: ./x402_facilitator
-
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -34,10 +32,16 @@ jobs:
cache: "npm"
cache-dependency-path: x402_facilitator/package.json
+ - name: Build chain-utils dependency
+ working-directory: ./shared/chain-utils
+ run: npm install && npm run build
+
- name: Install dependencies
+ working-directory: ./x402_facilitator
run: npm install
- name: Run quality checks (lint + format + test with coverage)
+ working-directory: ./x402_facilitator
run: npm run check
env:
NODE_ENV: test
diff --git a/x402_facilitator/eip3009_splitter_abi.js b/x402_facilitator/eip3009_splitter_abi.js
deleted file mode 100644
index 07a022d93..000000000
--- a/x402_facilitator/eip3009_splitter_abi.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// @ts-check
-
-/**
- * EIP3009SplitterV1 Contract ABI
- *
- * Re-exports from @fretchen/chain-utils for backward compatibility.
- * Full ABI available in: ../eth/abi/contracts/EIP3009SplitterV1.json
- *
- * Deployed Addresses:
- * - Optimism Sepolia: 0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a
- * - Optimism Mainnet: 0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf
- */
-
-import { EIP3009SplitterV1ABI, getEIP3009SplitterAddress } from "@fretchen/chain-utils";
-
-// Re-export ABI with legacy name for backward compatibility
-export const SPLITTER_ABI = EIP3009SplitterV1ABI;
-
-// Re-export getter function
-export { getEIP3009SplitterAddress as getSplitterAddress };
diff --git a/x402_facilitator/x402_splitter_settle.js b/x402_facilitator/x402_splitter_settle.js
index 953995ce7..228884f5c 100644
--- a/x402_facilitator/x402_splitter_settle.js
+++ b/x402_facilitator/x402_splitter_settle.js
@@ -28,9 +28,13 @@ import {
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import pino from "pino";
+import { EIP3009SplitterV1ABI, getEIP3009SplitterAddress } from "@fretchen/chain-utils";
import { getChain, getTokenInfo } from "./chain_utils.js";
import { verifySplitterPayment } from "./x402_splitter_verify.js";
-import { SPLITTER_ABI, getSplitterAddress } from "./eip3009_splitter_abi.js";
+
+// Alias for backward compatibility
+const SPLITTER_ABI = EIP3009SplitterV1ABI;
+const getSplitterAddress = getEIP3009SplitterAddress;
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
diff --git a/x402_facilitator/x402_splitter_verify.js b/x402_facilitator/x402_splitter_verify.js
index 0cfddd2f2..25c39e5ca 100644
--- a/x402_facilitator/x402_splitter_verify.js
+++ b/x402_facilitator/x402_splitter_verify.js
@@ -131,11 +131,11 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
// Validate recipient address matches splitter contract
// For splitter facilitator: to = splitter address (not token address!)
// The splitter contract will receive the funds and split them
- const { getSplitterAddress } = await import("./eip3009_splitter_abi.js");
+ const { getEIP3009SplitterAddress } = await import("@fretchen/chain-utils");
let splitterAddress;
try {
- splitterAddress = getSplitterAddress(network);
+ splitterAddress = getEIP3009SplitterAddress(network);
} catch (_error) {
logger.warn({ network }, "Splitter not deployed on network");
return { isValid: false, invalidReason: "unsupported_network" };
From ee7b38890e707a610dccc0f047244bc16f9601a9 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Mon, 26 Jan 2026 06:23:02 +0100
Subject: [PATCH 20/46] More cleaning
---
shared/chain-utils/src/addresses.ts | 35 +++++++
x402_facilitator/chain_utils.js | 125 ++---------------------
x402_facilitator/facilitator_instance.js | 6 +-
x402_facilitator/serverless.yml | 11 --
x402_facilitator/x402_splitter_settle.js | 14 ++-
x402_facilitator/x402_splitter_verify.js | 27 +++--
x402_facilitator/x402_whitelist.js | 10 +-
7 files changed, 79 insertions(+), 149 deletions(-)
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index f2e9747b5..cbb8d281f 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -122,6 +122,13 @@ export function getGenAiNFTAddress(network: string): `0x${string}` {
return address;
}
+/**
+ * Try to get GenAI NFT address, returns null if not deployed.
+ */
+export function tryGetGenAiNFTAddress(network: string): `0x${string}` | null {
+ return MAINNET_GENAI_NFT_ADDRESSES[network] || TESTNET_GENAI_NFT_ADDRESSES[network] || null;
+}
+
/**
* Get CollectorNFT address for a network (mainnet or testnet).
* @throws Error if not deployed on that network
@@ -135,6 +142,13 @@ export function getCollectorNFTAddress(network: string): `0x${string}` {
return address;
}
+/**
+ * Try to get CollectorNFT address, returns null if not deployed.
+ */
+export function tryGetCollectorNFTAddress(network: string): `0x${string}` | null {
+ return MAINNET_COLLECTOR_NFT_ADDRESSES[network] || TESTNET_COLLECTOR_NFT_ADDRESSES[network] || null;
+}
+
/**
* Get LLMv1 address for a network (mainnet or testnet).
* @throws Error if not deployed on that network
@@ -147,6 +161,13 @@ export function getLLMv1Address(network: string): `0x${string}` {
return address;
}
+/**
+ * Try to get LLMv1 address, returns null if not deployed.
+ */
+export function tryGetLLMv1Address(network: string): `0x${string}` | null {
+ return MAINNET_LLM_V1_ADDRESSES[network] || TESTNET_LLM_V1_ADDRESSES[network] || null;
+}
+
/**
* Get SupportV2 address for a network (mainnet or testnet).
* @throws Error if not deployed on that network
@@ -159,6 +180,13 @@ export function getSupportV2Address(network: string): `0x${string}` {
return address;
}
+/**
+ * Try to get SupportV2 address, returns null if not deployed.
+ */
+export function tryGetSupportV2Address(network: string): `0x${string}` | null {
+ return MAINNET_SUPPORT_V2_ADDRESSES[network] || TESTNET_SUPPORT_V2_ADDRESSES[network] || null;
+}
+
/**
* Get EIP3009 Splitter address for a network (mainnet or testnet).
* @throws Error if not deployed on that network
@@ -172,6 +200,13 @@ export function getEIP3009SplitterAddress(network: string): `0x${string}` {
return address;
}
+/**
+ * Try to get EIP3009 Splitter address, returns null if not deployed.
+ */
+export function tryGetEIP3009SplitterAddress(network: string): `0x${string}` | null {
+ return MAINNET_EIP3009_SPLITTER_ADDRESSES[network] || TESTNET_EIP3009_SPLITTER_ADDRESSES[network] || null;
+}
+
/**
* Get USDC address for a network.
* @throws Error if USDC not available on that network
diff --git a/x402_facilitator/chain_utils.js b/x402_facilitator/chain_utils.js
index bb9eca830..d967dd945 100644
--- a/x402_facilitator/chain_utils.js
+++ b/x402_facilitator/chain_utils.js
@@ -4,142 +4,35 @@
* Chain Utilities for x402 Facilitator
* Shared functions for network/chain handling
*
- * Uses @fretchen/chain-utils for chain/address data,
- * adds local RPC URL handling and facilitator-specific config.
+ * Uses @fretchen/chain-utils for chain/address data.
*/
import {
getViemChain,
- fromCAIP2,
- getGenAiNFTAddress,
- getLLMv1Address,
- getEIP3009SplitterAddress,
+ tryGetGenAiNFTAddress,
+ tryGetLLMv1Address,
+ tryGetEIP3009SplitterAddress,
getUSDCAddress,
getUSDCName,
} from "@fretchen/chain-utils";
-// Re-export getViemChain as getChain for backward compatibility
-export { getViemChain as getChain };
-
/**
- * Get RPC URL for a network with fallback handling
- *
- * Environment Variable Behavior:
- * - undefined/not set → Uses default public endpoint
- * - empty string "" → Treated as undefined (via .trim())
- * - whitespace " " → Treated as undefined (via .trim())
- * - valid URL → Uses custom endpoint
- *
- * This centralizes RPC URL logic and ensures consistent fallback behavior
- * across all facilitator functions. For production deployments, set custom
- * RPC URLs via OPTIMISM_RPC_URL and OPTIMISM_SEPOLIA_RPC_URL environment
- * variables to avoid rate limits on public endpoints.
- *
+ * Get chain configuration including contract addresses
* @param {string} network - Network ID (eip155:10 or eip155:11155420)
- * @returns {string} RPC URL to use for this network
- * @throws {Error} If network is not supported
- */
-export function getRpcUrl(network) {
- if (network === "eip155:10") {
- const customRpcUrl = process.env.OPTIMISM_RPC_URL?.trim();
- return customRpcUrl || "https://mainnet.optimism.io";
- }
- if (network === "eip155:11155420") {
- const customRpcUrl = process.env.OPTIMISM_SEPOLIA_RPC_URL?.trim();
- return customRpcUrl || "https://sepolia.optimism.io";
- }
- throw new Error(`Unsupported network: ${network}`);
-}
-
-/**
- * Safely get an address, returning null if not deployed
- * @param {function(string): string} getter - Address getter function
- * @param {string} network - Network ID
- * @returns {string|null} Address or null if not deployed
- */
-function safeGetAddress(getter, network) {
- try {
- return getter(network);
- } catch {
- return null;
- }
-}
-
-/**
- * Get chain configuration including RPC URL and contract addresses
- * @param {string} network - Network ID (eip155:10 or eip155:11155420)
- * @returns {{chain: import("viem/chains").Chain, rpcUrl: string, GENIMG_V4_ADDRESS: string|null, LLMV1_ADDRESS: string|null, SPLITTER_ADDRESS: string|null, USDC_ADDRESS: string, USDC_NAME: string}} Chain config with RPC URL and contract addresses
+ * @returns {{chain: import("viem/chains").Chain, GENIMG_V4_ADDRESS: string|null, LLMV1_ADDRESS: string|null, SPLITTER_ADDRESS: string|null, USDC_ADDRESS: string, USDC_NAME: string}} Chain config with contract addresses
* @throws {Error} If network is not supported
*/
export function getChainConfig(network) {
return {
chain: getViemChain(network),
- rpcUrl: getRpcUrl(network),
- GENIMG_V4_ADDRESS: safeGetAddress(getGenAiNFTAddress, network),
- LLMV1_ADDRESS: safeGetAddress(getLLMv1Address, network),
- SPLITTER_ADDRESS: safeGetAddress(getEIP3009SplitterAddress, network),
+ GENIMG_V4_ADDRESS: tryGetGenAiNFTAddress(network),
+ LLMV1_ADDRESS: tryGetLLMv1Address(network),
+ SPLITTER_ADDRESS: tryGetEIP3009SplitterAddress(network),
USDC_ADDRESS: getUSDCAddress(network),
USDC_NAME: getUSDCName(network),
};
}
-/**
- * Token information for supported networks
- */
-export const TOKEN_INFO = {
- // Optimism Mainnet
- "eip155:10": {
- "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85": {
- address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
- symbol: "USDC",
- name: "USD Coin",
- decimals: 6,
- version: "2",
- },
- },
- // Optimism Sepolia
- "eip155:11155420": {
- "0x5fd84259d66Cd46123540766Be93DFE6D43130D7": {
- address: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
- symbol: "USDC",
- name: "USDC",
- decimals: 6,
- version: "2",
- },
- },
-};
-
-/**
- * Get token information for a specific token on a network
- * @param {string} network - Network ID
- * @param {string} tokenAddress - Token contract address
- * @returns {Object} Token information
- * @throws {Error} If token is not supported
- */
-export function getTokenInfo(network, tokenAddress) {
- const networkTokens = TOKEN_INFO[network];
- if (!networkTokens) {
- throw new Error(`Unsupported network: ${network}`);
- }
-
- const tokenInfo = networkTokens[tokenAddress];
- if (!tokenInfo) {
- throw new Error(`Unsupported token: ${tokenAddress} on network ${network}`);
- }
-
- return tokenInfo;
-}
-
-/**
- * Convert CAIP-2 network identifier to numeric chainId
- * @param {string} network - Network ID (eip155:10 or eip155:11155420)
- * @returns {number} Numeric chainId
- * @throws {Error} If network is not supported
- */
-export function getChainId(network) {
- return fromCAIP2(network);
-}
-
/**
* Get all supported networks
* @returns {string[]} Array of supported network identifiers
diff --git a/x402_facilitator/facilitator_instance.js b/x402_facilitator/facilitator_instance.js
index 512d5c545..8010ddb2e 100644
--- a/x402_facilitator/facilitator_instance.js
+++ b/x402_facilitator/facilitator_instance.js
@@ -31,13 +31,13 @@ function createSignerForNetwork(account, network) {
const publicClient = createPublicClient({
chain: config.chain,
- transport: http(config.rpcUrl),
+ transport: http(),
});
const walletClient = createWalletClient({
account,
chain: config.chain,
- transport: http(config.rpcUrl),
+ transport: http(),
});
// Create signer bound to this specific chain
@@ -74,7 +74,7 @@ export function createReadOnlyFacilitator() {
const publicClient = createPublicClient({
chain: config.chain,
- transport: http(config.rpcUrl),
+ transport: http(),
});
// Read-only signer (no wallet operations)
diff --git a/x402_facilitator/serverless.yml b/x402_facilitator/serverless.yml
index 6252eabe1..7a0964c97 100644
--- a/x402_facilitator/serverless.yml
+++ b/x402_facilitator/serverless.yml
@@ -17,17 +17,6 @@ provider:
# Secrets (set via Scaleway Console or CLI)
secret:
FACILITATOR_WALLET_PRIVATE_KEY: ${env:FACILITATOR_WALLET_PRIVATE_KEY}
- # Optional: Custom RPC endpoints
- # RPC URL fallback behavior (handled by getRpcUrl() in chain_utils.js):
- # - Not set/undefined → Uses default public endpoint
- # - Empty string "" → Treated as not set (via .trim())
- # - Whitespace " " → Treated as not set (via .trim())
- # - Valid URL → Uses custom endpoint
- #
- # For production: Set custom RPC URLs to avoid rate limits on public endpoints
- # Default fallbacks: https://mainnet.optimism.io and https://sepolia.optimism.io
- # OPTIMISM_RPC_URL: ${env:OPTIMISM_RPC_URL, ""}
- # OPTIMISM_SEPOLIA_RPC_URL: ${env:OPTIMISM_SEPOLIA_RPC_URL, ""}
plugins:
- serverless-scaleway-functions
diff --git a/x402_facilitator/x402_splitter_settle.js b/x402_facilitator/x402_splitter_settle.js
index 228884f5c..37c72a51f 100644
--- a/x402_facilitator/x402_splitter_settle.js
+++ b/x402_facilitator/x402_splitter_settle.js
@@ -28,8 +28,12 @@ import {
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import pino from "pino";
-import { EIP3009SplitterV1ABI, getEIP3009SplitterAddress } from "@fretchen/chain-utils";
-import { getChain, getTokenInfo } from "./chain_utils.js";
+import {
+ EIP3009SplitterV1ABI,
+ getEIP3009SplitterAddress,
+ getViemChain,
+ getUSDCAddress,
+} from "@fretchen/chain-utils";
import { verifySplitterPayment } from "./x402_splitter_verify.js";
// Alias for backward compatibility
@@ -63,7 +67,7 @@ export async function settleSplitterPayment(paymentPayload, paymentRequirements)
// Step 2: Extract data from payload
const network = paymentPayload.accepted?.network;
- const chain = getChain(network);
+ const chain = getViemChain(network);
const auth = paymentPayload.payload.authorization;
const { from: buyer, value: totalAmountStr, validAfter, validBefore } = auth;
@@ -120,12 +124,12 @@ export async function settleSplitterPayment(paymentPayload, paymentRequirements)
// Get contract addresses
const splitterAddress = getSplitterAddress(network);
- const tokenInfo = getTokenInfo(network, paymentPayload.accepted?.asset);
+ const usdcAddress = getUSDCAddress(network);
logger.info(
{
splitter: splitterAddress,
- token: tokenInfo.address,
+ token: usdcAddress,
network,
},
"Settlement contract addresses",
diff --git a/x402_facilitator/x402_splitter_verify.js b/x402_facilitator/x402_splitter_verify.js
index 25c39e5ca..e29de5e8b 100644
--- a/x402_facilitator/x402_splitter_verify.js
+++ b/x402_facilitator/x402_splitter_verify.js
@@ -14,7 +14,7 @@
import { verifyTypedData, createPublicClient, http } from "viem";
import pino from "pino";
-import { getChain, getChainConfig, getTokenInfo } from "./chain_utils.js";
+import { getViemChain, getUSDCName, getUSDCAddress } from "@fretchen/chain-utils";
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
@@ -111,7 +111,7 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
}
// Get chain config
- const chain = getChain(network);
+ const chain = getViemChain(network);
// Validate token is USDC
const tokenAddress = paymentPayload.accepted?.asset;
@@ -120,11 +120,20 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
return { isValid: false, invalidReason: "invalid_payload" };
}
- let tokenInfo;
+ // Validate token is USDC on this network
+ let usdcAddress;
try {
- tokenInfo = getTokenInfo(network, tokenAddress);
+ usdcAddress = getUSDCAddress(network);
} catch (_error) {
- logger.warn({ tokenAddress, network }, "Unsupported token");
+ logger.warn({ network }, "Unsupported network");
+ return { isValid: false, invalidReason: "unsupported_network" };
+ }
+
+ if (tokenAddress.toLowerCase() !== usdcAddress.toLowerCase()) {
+ logger.warn(
+ { tokenAddress, expected: usdcAddress, network },
+ "Unsupported token - only USDC accepted",
+ );
return { isValid: false, invalidReason: "invalid_token_address" };
}
@@ -157,9 +166,10 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
}
// Verify EIP-712 signature
+ // USDC version is always "2" across all supported chains
const domain = {
- name: tokenInfo.name,
- version: tokenInfo.version,
+ name: getUSDCName(network),
+ version: "2",
chainId: chain.id,
verifyingContract: tokenAddress,
};
@@ -240,10 +250,9 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
// Check if authorization already used (requires splitter contract call)
// For verify, we'll check this on-chain during settlement
// The contract will revert if nonce is reused
- const chainConfig = getChainConfig(network);
const publicClient = createPublicClient({
chain,
- transport: http(chainConfig.rpcUrl),
+ transport: http(),
});
// Check buyer has sufficient balance
diff --git a/x402_facilitator/x402_whitelist.js b/x402_facilitator/x402_whitelist.js
index fcd7604de..f87127e9a 100644
--- a/x402_facilitator/x402_whitelist.js
+++ b/x402_facilitator/x402_whitelist.js
@@ -7,7 +7,7 @@
import { createPublicClient, http, getContract } from "viem";
import pino from "pino";
-import { getChain, getChainConfig } from "./chain_utils.js";
+import { getChainConfig } from "./chain_utils.js";
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
@@ -107,9 +107,9 @@ async function checkGenImgV4(address, network) {
logger.warn({ network }, "GenImNFTv4 contract address not configured");
return false;
}
- const chain = getChain(network);
+
const publicClient = createPublicClient({
- chain,
+ chain: chainConfig.chain,
transport: http(),
});
@@ -161,9 +161,9 @@ async function checkLLMv1(address, network) {
logger.debug({ network }, "LLMv1 contract address not configured");
return false;
}
- const chain = getChain(network);
+
const publicClient = createPublicClient({
- chain,
+ chain: chainConfig.chain,
transport: http(),
});
From e14471b95c5c505823043f11bcf40745245dbd20 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Mon, 26 Jan 2026 22:50:04 +0100
Subject: [PATCH 21/46] Move the facilitator to typescript and enable base
---
notebooks/x402_facilitator_demo.ipynb | 82 ++--
x402_facilitator/chain_utils.js | 2 +-
x402_facilitator/package-lock.json | 9 +-
x402_facilitator/package.json | 6 +-
.../test/x402_facilitator.test.ts | 356 ++++++++++++++++++
x402_facilitator/test/x402_settle.test.js | 162 ++++++++
x402_facilitator/tsconfig.json | 20 +
x402_facilitator/tsup.config.js | 2 +-
...402_facilitator.js => x402_facilitator.ts} | 90 ++++-
9 files changed, 671 insertions(+), 58 deletions(-)
create mode 100644 x402_facilitator/test/x402_facilitator.test.ts
create mode 100644 x402_facilitator/tsconfig.json
rename x402_facilitator/{x402_facilitator.js => x402_facilitator.ts} (77%)
diff --git a/notebooks/x402_facilitator_demo.ipynb b/notebooks/x402_facilitator_demo.ipynb
index dcd875396..ec916a21e 100644
--- a/notebooks/x402_facilitator_demo.ipynb
+++ b/notebooks/x402_facilitator_demo.ipynb
@@ -12,7 +12,7 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": 2,
"id": "71447da2",
"metadata": {},
"outputs": [
@@ -73,7 +73,7 @@
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 3,
"id": "30236d53",
"metadata": {},
"outputs": [
@@ -111,6 +111,18 @@
" \"address\": \"0x833F39D6e67390324796f861990ce9B7cf9F5dE1\",\n",
" \"method\": \"isAuthorizedAgent(address)\"\n",
" }\n",
+ " ],\n",
+ " \"eip155:11155420\": [\n",
+ " {\n",
+ " \"name\": \"GenImNFTv4\",\n",
+ " \"address\": \"0x10827cC42a09D0BAD2d43134C69F0e776D853D85\",\n",
+ " \"method\": \"isAuthorizedAgent(address)\"\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"LLMv1\",\n",
+ " \"address\": \"0xB3dbD44477a7bcf253f2fA68eDb4be5aF2F2cA56\",\n",
+ " \"method\": \"isAuthorizedAgent(address)\"\n",
+ " }\n",
" ]\n",
" }\n",
" }\n",
@@ -176,7 +188,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"id": "56489ea0",
"metadata": {},
"outputs": [
@@ -184,8 +196,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Payer Address: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
- "Recipient Address: 0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20\n",
+ "Payer Address: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
+ "Recipient Address: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
"🧪 Using testnet: Optimism Sepolia (Testnet)\n",
"Chain ID: 11155420\n",
@@ -197,11 +209,11 @@
"\n",
"🔐 Signature Details:\n",
"Network: Optimism Sepolia (Testnet)\n",
- "EIP-712 Hash: e123134dfc61c252106b689e2ff4f40ca6bfbd09e7b3d3121bb0949e2adf1c99\n",
- "Signature: 0xbc8615f028a0acfb119da2db26afbccb200e366fe9fa6571adef65c85fe8737f35a39cedde8df3570c4d5b22b259a393bcb51493e8a56e84a0525a34e807cf651b\n",
+ "EIP-712 Hash: 747828ab29c5273e7d8c49f5ece00407a675198bd6d3e3f7d931f3bbd3cadf54\n",
+ "Signature: 0x9382b9ff607c207dc0297cc192260f16ae739a2aa3e0d844e51ffc8521b9f7d01a7b1e85e51cdfc159333b35710efe601fe33c7eb93d5df2ce4121091240bc5d1c\n",
"Signature length: 132 chars (should be 132 with 0x)\n",
- "v: 27, r: bc8615f028a0acfb..., s: 35a39cedde8df357...\n",
- "Nonce: 0x8a76e0d140e6c48b59269845f722782d504d4105b983ff67ad208757a48caf48\n",
+ "v: 28, r: 9382b9ff607c207d..., s: 1a7b1e85e51cdfc1...\n",
+ "Nonce: 0x47f06b85d2c061b48f1bef992c3b0753e08b582307611201b263edebc145f141\n",
"\n",
"✅ Authorized $0.02 USDC payment\n",
"\n",
@@ -381,7 +393,7 @@
},
{
"cell_type": "code",
- "execution_count": 26,
+ "execution_count": 5,
"id": "cdbfa701",
"metadata": {},
"outputs": [
@@ -545,7 +557,7 @@
},
{
"cell_type": "code",
- "execution_count": 27,
+ "execution_count": 6,
"id": "8ba26f28",
"metadata": {},
"outputs": [
@@ -553,8 +565,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Prüfe Nonce: 0x8a76e0d140e6c48b59269845f722782d504d4105b983ff67ad208757a48caf48\n",
- "Payer: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
+ "Prüfe Nonce: 0x47f06b85d2c061b48f1bef992c3b0753e08b582307611201b263edebc145f141\n",
+ "Payer: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
"Network: Optimism Sepolia (Testnet)\n",
"\n",
"✅ Diese Nonce ist noch verfügbar und kann verwendet werden.\n"
@@ -612,7 +624,7 @@
},
{
"cell_type": "code",
- "execution_count": 28,
+ "execution_count": 7,
"id": "b738e68e",
"metadata": {},
"outputs": [
@@ -624,12 +636,12 @@
"📤 Request Summary:\n",
" Network: Optimism Sepolia (Testnet)\n",
" Payment Amount: $0.02 USDC\n",
- " Recipient: 0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20\n",
+ " Recipient: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
"✅ Verify Response (Status 200):\n",
"{\n",
" \"isValid\": true,\n",
- " \"payer\": \"0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\"\n",
+ " \"payer\": \"0x553179556FC2A39e535D65b921e01fA995E79101\"\n",
"}\n",
"\n",
"🎉 Payment signature is valid and ready for settlement!\n",
@@ -797,7 +809,7 @@
},
{
"cell_type": "code",
- "execution_count": 29,
+ "execution_count": 8,
"id": "e44a3bf8",
"metadata": {},
"outputs": [
@@ -807,7 +819,7 @@
"text": [
"Facilitator Wallet: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
"Network: Optimism Sepolia (Testnet)\n",
- "ETH Balance: 0.04999826754546407 ETH\n",
+ "ETH Balance: 0.049997203839746895 ETH\n",
"Geschätzte Gas-Kosten: ~0.000000 ETH\n",
"\n",
"✅ Ausreichend ETH für Settlement vorhanden\n"
@@ -865,7 +877,7 @@
},
{
"cell_type": "code",
- "execution_count": 30,
+ "execution_count": 9,
"id": "964e5fde",
"metadata": {},
"outputs": [
@@ -876,20 +888,20 @@
"\n",
"💸 Attempting Settlement...\n",
" Network: Optimism Sepolia (Testnet)\n",
- " Payment: $0.02 USDC → 0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20\n",
+ " Payment: $0.02 USDC → 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
"📦 Settle Response (Status 200):\n",
"{\n",
" \"success\": true,\n",
- " \"payer\": \"0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\",\n",
- " \"transaction\": \"0xf4e03fe0a9033f211c57ee94363553f7ffe9e3442064e0ed361c0793f92a0c3f\",\n",
+ " \"payer\": \"0x553179556FC2A39e535D65b921e01fA995E79101\",\n",
+ " \"transaction\": \"0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\",\n",
" \"network\": \"eip155:11155420\"\n",
"}\n",
"\n",
"🎉 Settlement erfolgreich!\n",
"\n",
"📝 Transaction Details:\n",
- " Hash: 0xf4e03fe0a9033f211c57ee94363553f7ffe9e3442064e0ed361c0793f92a0c3f\n",
+ " Hash: 0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
" Recipient received: $0.02 USDC\n"
]
}
@@ -939,7 +951,7 @@
},
{
"cell_type": "code",
- "execution_count": 31,
+ "execution_count": 10,
"id": "b77cfca9",
"metadata": {},
"outputs": [
@@ -949,10 +961,10 @@
"text": [
"\n",
"🔍 Block Explorer:\n",
- " https://sepolia-optimism.etherscan.io/tx/0xf4e03fe0a9033f211c57ee94363553f7ffe9e3442064e0ed361c0793f92a0c3f\n",
+ " https://sepolia-optimism.etherscan.io/tx/0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
"\n",
"📊 Transaktionsdetails:\n",
- " • Transaction hash: 0xf4e03fe0a9033f211c57ee94363553f7ffe9e3442064e0ed361c0793f92a0c3f\n",
+ " • Transaction hash: 0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
" • Network (Response): eip155:11155420\n",
" • Network (Erwartet): eip155:11155420\n",
" • Token: USDC at 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n"
@@ -1017,7 +1029,7 @@
},
{
"cell_type": "code",
- "execution_count": 32,
+ "execution_count": 11,
"id": "3e84ad5b",
"metadata": {},
"outputs": [
@@ -1025,11 +1037,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "🔍 Cross-Chain Balance Check für Recipient: 0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20\n",
+ "🔍 Cross-Chain Balance Check für Recipient: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
" Erwartetes Settlement-Network: eip155:11155420\n",
"\n",
- " Optimism Mainnet (eip155:10): $10.72 USDC \n",
- " Optimism Sepolia (eip155:11155420): $0.95 USDC ← Erwartet\n",
+ " Optimism Mainnet (eip155:10): $1.63 USDC \n",
+ " Optimism Sepolia (eip155:11155420): $1.48 USDC ← Erwartet\n",
"\n",
"💡 Tipp: Vergleiche die Balances vor und nach dem Settlement,\n",
" um zu verifizieren, dass das Geld auf dem richtigen Network ankam.\n"
@@ -1093,7 +1105,7 @@
},
{
"cell_type": "code",
- "execution_count": 33,
+ "execution_count": 12,
"id": "81bfd50f",
"metadata": {},
"outputs": [
@@ -1104,9 +1116,9 @@
"💰 Checking USDC Balances on Optimism Sepolia (Testnet)...\n",
"\n",
"📊 USDC Balances:\n",
- " Payer (0xAAEBC144...): $1.251000 USDC\n",
- " Recipient (0x073f26F0...): $0.970000 USDC\n",
- " Facilitator (0x3F8d2Fb6...): $0.000000 USDC\n",
+ " Payer (0x55317955...): $0.519000 USDC\n",
+ " Recipient (0xAAEBC144...): $1.481000 USDC\n",
+ " Facilitator (0x3F8d2Fb6...): $0.030000 USDC\n",
"\n",
"💡 Warum erscheint die Transaktion nicht in MetaMask Activity?\n",
" • TransferWithAuthorization wird vom Facilitator ausgeführt\n",
@@ -1115,7 +1127,7 @@
" • Prüfe den Token-Balance in MetaMask (nicht die Activity)\n",
"\n",
"🔍 So siehst du den Transfer:\n",
- " 1. Öffne Etherscan: https://sepolia-optimism.etherscan.io/tx/0xf4e03fe0a9033f211c57ee94363553f7ffe9e3442064e0ed361c0793f92a0c3f\n",
+ " 1. Öffne Etherscan: https://sepolia-optimism.etherscan.io/tx/0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
" 2. Klicke auf 'Logs' Tab um die Transfer Events zu sehen\n",
" 3. Oder gehe zu Token Transfers auf der Payer/Recipient Adresse\n"
]
diff --git a/x402_facilitator/chain_utils.js b/x402_facilitator/chain_utils.js
index d967dd945..2a6d9816e 100644
--- a/x402_facilitator/chain_utils.js
+++ b/x402_facilitator/chain_utils.js
@@ -38,5 +38,5 @@ export function getChainConfig(network) {
* @returns {string[]} Array of supported network identifiers
*/
export function getSupportedNetworks() {
- return ["eip155:10", "eip155:11155420"];
+ return ["eip155:10", "eip155:11155420", "eip155:8453", "eip155:84532"];
}
diff --git a/x402_facilitator/package-lock.json b/x402_facilitator/package-lock.json
index a7fe83dfe..258e70952 100644
--- a/x402_facilitator/package-lock.json
+++ b/x402_facilitator/package-lock.json
@@ -20,11 +20,13 @@
"devDependencies": {
"@eslint/js": "^9.0.0",
"@scaleway/serverless-functions": "^1.1.2",
+ "@types/node": "^25.0.10",
"@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"serverless-scaleway-functions": ">=0.4.16",
"tsup": "^8.3.5",
+ "typescript": "^5.9.3",
"vitest": "^4.0.16"
},
"engines": {
@@ -2651,9 +2653,9 @@
}
},
"node_modules/@types/node": {
- "version": "25.0.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
- "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
+ "version": "25.0.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
+ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@@ -8713,7 +8715,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/x402_facilitator/package.json b/x402_facilitator/package.json
index 146468dcd..ce6ee96c2 100644
--- a/x402_facilitator/package.json
+++ b/x402_facilitator/package.json
@@ -3,10 +3,10 @@
"version": "1.0.0",
"description": "x402 v2 Payment Facilitator for Optimism",
"type": "module",
- "main": "x402_facilitator.js",
+ "main": "dist/x402_facilitator.js",
"scripts": {
"build": "tsup",
- "dev": "NODE_ENV=test node x402_facilitator.js",
+ "dev": "NODE_ENV=test node dist/x402_facilitator.js",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
@@ -43,11 +43,13 @@
"devDependencies": {
"@eslint/js": "^9.0.0",
"@scaleway/serverless-functions": "^1.1.2",
+ "@types/node": "^25.0.10",
"@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"serverless-scaleway-functions": ">=0.4.16",
"tsup": "^8.3.5",
+ "typescript": "^5.9.3",
"vitest": "^4.0.16"
},
"engines": {
diff --git a/x402_facilitator/test/x402_facilitator.test.ts b/x402_facilitator/test/x402_facilitator.test.ts
new file mode 100644
index 000000000..1774b8ad4
--- /dev/null
+++ b/x402_facilitator/test/x402_facilitator.test.ts
@@ -0,0 +1,356 @@
+// @ts-check
+
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import {
+ handleVerify,
+ handleSettle,
+ handleSupported,
+ handle,
+} from "../x402_facilitator.ts";
+
+// Mock the dependencies
+vi.mock("../x402_verify.js", () => ({
+ verifyPayment: vi.fn(),
+}));
+
+vi.mock("../x402_settle.js", () => ({
+ settlePayment: vi.fn(),
+}));
+
+vi.mock("../x402_supported.js", () => ({
+ getSupportedCapabilities: vi.fn(() => ({
+ x402Version: 2,
+ kinds: ["exact"],
+ networks: ["eip155:10", "eip155:11155420"],
+ assets: {
+ "eip155:10": ["USDC"],
+ "eip155:11155420": ["USDC"],
+ },
+ })),
+}));
+
+describe("x402_facilitator handlers", () => {
+ let verifyPayment;
+ let settlePayment;
+
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ const verifyModule = await import("../x402_verify.js");
+ const settleModule = await import("../x402_settle.js");
+ verifyPayment = verifyModule.verifyPayment;
+ settlePayment = settleModule.settlePayment;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("handleVerify", () => {
+ it("should handle CORS preflight OPTIONS request", async () => {
+ const event = { httpMethod: "OPTIONS" };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(result.headers["Access-Control-Allow-Origin"]).toBe("*");
+ expect(result.body).toBe("");
+ });
+
+ it("should reject non-POST requests", async () => {
+ const event = { httpMethod: "GET" };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(405);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Method not allowed. Use POST.",
+ });
+ });
+
+ it("should handle invalid JSON body", async () => {
+ const event = {
+ httpMethod: "POST",
+ body: "not-valid-json{",
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(400);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Invalid JSON in request body",
+ });
+ });
+
+ it("should reject missing paymentPayload", async () => {
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(400);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Request must include both paymentPayload and paymentRequirements",
+ });
+ });
+
+ it("should reject missing paymentRequirements", async () => {
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ }),
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(400);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Request must include both paymentPayload and paymentRequirements",
+ });
+ });
+
+ it("should return valid payment result", async () => {
+ verifyPayment.mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(200);
+ const body = JSON.parse(result.body);
+ expect(body.isValid).toBe(true);
+ expect(body.payer).toBe("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
+ });
+
+ it("should return invalid payment result with reason", async () => {
+ verifyPayment.mockResolvedValue({
+ isValid: false,
+ invalidReason: "insufficient_funds",
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(200);
+ const body = JSON.parse(result.body);
+ expect(body.isValid).toBe(false);
+ expect(body.invalidReason).toBe("insufficient_funds");
+ });
+
+ it("should handle unexpected verification error", async () => {
+ verifyPayment.mockRejectedValue(new Error("Unexpected error"));
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(500);
+ const body = JSON.parse(result.body);
+ expect(body.error).toBe("Internal server error");
+ expect(body.isValid).toBe(false);
+ expect(body.invalidReason).toBe("unexpected_verify_error");
+ });
+
+ it("should handle object body (pre-parsed)", async () => {
+ verifyPayment.mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ const event = {
+ httpMethod: "POST",
+ body: {
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ },
+ };
+ const result = await handleVerify(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(JSON.parse(result.body).isValid).toBe(true);
+ });
+ });
+
+ describe("handleSettle", () => {
+ it("should handle CORS preflight OPTIONS request", async () => {
+ const event = { httpMethod: "OPTIONS" };
+ const result = await handleSettle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(result.body).toBe("");
+ });
+
+ it("should reject non-POST requests", async () => {
+ const event = { httpMethod: "GET" };
+ const result = await handleSettle(event, {});
+
+ expect(result.statusCode).toBe(405);
+ });
+
+ it("should return successful settlement", async () => {
+ settlePayment.mockResolvedValue({
+ success: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ transaction: "0xabc123",
+ network: "eip155:10",
+ });
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleSettle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ const body = JSON.parse(result.body);
+ expect(body.success).toBe(true);
+ expect(body.payer).toBe("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
+ expect(body.transaction).toBe("0xabc123");
+ });
+
+ it("should return failed settlement with error reason", async () => {
+ settlePayment.mockResolvedValue({
+ success: false,
+ errorReason: "insufficient_funds",
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ network: "eip155:10",
+ });
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleSettle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ const body = JSON.parse(result.body);
+ expect(body.success).toBe(false);
+ expect(body.errorReason).toBe("insufficient_funds");
+ expect(body.transaction).toBe("");
+ });
+
+ it("should handle unexpected settlement error", async () => {
+ settlePayment.mockRejectedValue(new Error("Unexpected error"));
+
+ const event = {
+ httpMethod: "POST",
+ body: JSON.stringify({
+ paymentPayload: { accepted: { network: "eip155:10" } },
+ paymentRequirements: { amount: "1000000" },
+ }),
+ };
+ const result = await handleSettle(event, {});
+
+ expect(result.statusCode).toBe(500);
+ const body = JSON.parse(result.body);
+ expect(body.error).toBe("Internal server error");
+ expect(body.success).toBe(false);
+ expect(body.errorReason).toBe("unexpected_settlement_error");
+ });
+ });
+
+ describe("handleSupported", () => {
+ it("should handle CORS preflight OPTIONS request", async () => {
+ const event = { httpMethod: "OPTIONS" };
+ const result = await handleSupported(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(result.body).toBe("");
+ });
+
+ it("should reject non-GET requests", async () => {
+ const event = { httpMethod: "POST" };
+ const result = await handleSupported(event, {});
+
+ expect(result.statusCode).toBe(405);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Method not allowed. Use GET.",
+ });
+ });
+
+ it("should return supported capabilities", async () => {
+ const event = { httpMethod: "GET" };
+ const result = await handleSupported(event, {});
+
+ expect(result.statusCode).toBe(200);
+ const body = JSON.parse(result.body);
+ expect(body.x402Version).toBe(2);
+ expect(body.networks).toContain("eip155:10");
+ });
+ });
+
+ describe("handle (router)", () => {
+ it("should route /supported to handleSupported", async () => {
+ const event = { httpMethod: "GET", path: "/supported" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(JSON.parse(result.body).x402Version).toBe(2);
+ });
+
+ it("should route /verify to handleVerify", async () => {
+ const event = { httpMethod: "OPTIONS", path: "/verify" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(result.body).toBe("");
+ });
+
+ it("should route /settle to handleSettle", async () => {
+ const event = { httpMethod: "OPTIONS", path: "/settle" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ expect(result.body).toBe("");
+ });
+
+ it("should use rawUrl for routing", async () => {
+ const event = { httpMethod: "GET", rawUrl: "https://api.example.com/supported" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(200);
+ });
+
+ it("should return 404 for unknown endpoints", async () => {
+ const event = { httpMethod: "GET", path: "/unknown" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(404);
+ expect(JSON.parse(result.body)).toEqual({
+ error: "Endpoint not found. Use /verify, /settle, or /supported",
+ });
+ });
+
+ it("should return 404 for empty path", async () => {
+ const event = { httpMethod: "GET" };
+ const result = await handle(event, {});
+
+ expect(result.statusCode).toBe(404);
+ });
+ });
+});
diff --git a/x402_facilitator/test/x402_settle.test.js b/x402_facilitator/test/x402_settle.test.js
index cdf96a6d2..adcd383ac 100644
--- a/x402_facilitator/test/x402_settle.test.js
+++ b/x402_facilitator/test/x402_settle.test.js
@@ -2,6 +2,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { settlePayment } from "../x402_settle.js";
+import * as facilitatorInstance from "../facilitator_instance.js";
+import * as verifyModule from "../x402_verify.js";
// Mock viem
vi.mock("viem", async () => {
@@ -345,3 +347,163 @@ describe("x402_settle", () => {
// 2. The facilitator uses separate ExactEvmScheme per network
// Since settlePayment() calls verifyPayment() internally, the same security applies.
});
+
+describe("x402_settle with mocked facilitator", () => {
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ process.env.FACILITATOR_WALLET_PRIVATE_KEY =
+ "0x1234567890123456789012345678901234567890123456789012345678901234";
+ process.env.TEST_WALLETS =
+ "0x209693Bc6afc0C5328bA36FaF03C514EF312287C,0xDifferentAddress000000000000000000000000";
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ process.env = { ...originalEnv };
+ vi.restoreAllMocks();
+ });
+
+ const paymentAmount = "100000";
+ const tokenAddress = "0x5fd84259d66Cd46123540766Be93DFE6D43130D7";
+
+ const validPaymentPayload = {
+ x402Version: 2,
+ accepted: {
+ scheme: "exact",
+ network: "eip155:11155420",
+ amount: paymentAmount,
+ asset: tokenAddress,
+ payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
+ },
+ payload: {
+ signature: "0x" + "ab".repeat(65),
+ authorization: {
+ from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ to: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
+ value: paymentAmount,
+ validAfter: "0",
+ validBefore: "9999999999",
+ nonce: "0xf374661300000000000000000000000000000000000000000000000000000000",
+ },
+ },
+ };
+
+ const validPaymentRequirements = {
+ scheme: "exact",
+ network: "eip155:11155420",
+ amount: paymentAmount,
+ asset: tokenAddress,
+ payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
+ };
+
+ it("returns success when verification and settlement succeed", async () => {
+ const mockFacilitator = {
+ settle: vi.fn().mockResolvedValue({
+ success: true,
+ transaction: "0xabc123def456",
+ }),
+ };
+
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockReturnValue(mockFacilitator);
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(true);
+ expect(result.payer).toBe("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
+ expect(result.transaction).toBe("0xabc123def456");
+ expect(result.network).toBe("eip155:11155420");
+ });
+
+ it("returns failure when facilitator settle returns failure", async () => {
+ const mockFacilitator = {
+ settle: vi.fn().mockResolvedValue({
+ success: false,
+ errorReason: "insufficient_allowance",
+ }),
+ };
+
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockReturnValue(mockFacilitator);
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(false);
+ expect(result.errorReason).toBe("insufficient_allowance");
+ expect(result.payer).toBe("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
+ expect(result.transaction).toBe("");
+ });
+
+ it("extracts insufficient_funds error reason from exception", async () => {
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
+ throw new Error("Transaction failed: insufficient funds for gas");
+ });
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(false);
+ expect(result.errorReason).toBe("insufficient_funds");
+ });
+
+ it("extracts authorization_already_used error reason from nonce error", async () => {
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
+ throw new Error("nonce already used");
+ });
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(false);
+ expect(result.errorReason).toBe("authorization_already_used");
+ });
+
+ it("extracts authorization_expired error reason from expired error", async () => {
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
+ throw new Error("authorization expired");
+ });
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(false);
+ expect(result.errorReason).toBe("authorization_expired");
+ });
+
+ it("returns generic settlement_failed for unknown errors", async () => {
+ vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
+ isValid: true,
+ payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ });
+
+ vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
+ throw new Error("Unknown blockchain error");
+ });
+
+ const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
+
+ expect(result.success).toBe(false);
+ expect(result.errorReason).toBe("settlement_failed");
+ });
+});
diff --git a/x402_facilitator/tsconfig.json b/x402_facilitator/tsconfig.json
new file mode 100644
index 000000000..28221d721
--- /dev/null
+++ b/x402_facilitator/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "./dist",
+ "rootDir": "./",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true
+ },
+ "include": ["*.ts"],
+ "exclude": ["node_modules", "dist", "test", "coverage"]
+}
diff --git a/x402_facilitator/tsup.config.js b/x402_facilitator/tsup.config.js
index 9f8ae98c0..f277dcef5 100644
--- a/x402_facilitator/tsup.config.js
+++ b/x402_facilitator/tsup.config.js
@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
import { builtinModules } from "module";
export default defineConfig({
- entry: ["x402_facilitator.js", "x402_splitter_facilitator.js"],
+ entry: ["x402_facilitator.ts", "x402_splitter_facilitator.js"],
format: ["esm"],
platform: "node",
target: "node22",
diff --git a/x402_facilitator/x402_facilitator.js b/x402_facilitator/x402_facilitator.ts
similarity index 77%
rename from x402_facilitator/x402_facilitator.js
rename to x402_facilitator/x402_facilitator.ts
index 0b4990572..ccbf7bf5f 100644
--- a/x402_facilitator/x402_facilitator.js
+++ b/x402_facilitator/x402_facilitator.ts
@@ -1,5 +1,3 @@
-// @ts-check
-
/**
* x402 v2 Facilitator - Main Handler
* Handles POST /verify and POST /settle endpoints
@@ -12,10 +10,57 @@ import pino from "pino";
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
+/**
+ * Scaleway Functions event object
+ */
+export interface ScalewayEvent {
+ httpMethod: string;
+ path?: string;
+ rawUrl?: string;
+ body?: string | Record;
+ headers?: Record;
+ queryStringParameters?: Record;
+}
+
+/**
+ * Scaleway Functions context object
+ */
+export interface ScalewayContext {
+ memoryLimitInMb?: number;
+ functionName?: string;
+ functionVersion?: string;
+}
+
+/**
+ * Scaleway Functions response object
+ */
+export interface ScalewayResponse {
+ statusCode: number;
+ headers: Record;
+ body: string;
+}
+
+/**
+ * Payment request body structure
+ */
+interface PaymentRequestBody {
+ paymentPayload?: {
+ accepted?: {
+ network?: string;
+ scheme?: string;
+ };
+ [key: string]: unknown;
+ };
+ paymentRequirements?: {
+ amount?: string;
+ [key: string]: unknown;
+ };
+}
+
/**
* Common headers for all responses
*/
-const CORS_HEADERS = {
+const CORS_HEADERS: Record = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
@@ -25,21 +70,30 @@ const CORS_HEADERS = {
/**
* Handle /verify endpoint - off-chain verification
*/
-export async function handleVerify(event, _context) {
+export async function handleVerify(
+ event: ScalewayEvent,
+ _context: ScalewayContext,
+): Promise {
return handlePaymentRequest(event, _context, false);
}
/**
* Handle /settle endpoint - on-chain execution
*/
-export async function handleSettle(event, _context) {
+export async function handleSettle(
+ event: ScalewayEvent,
+ _context: ScalewayContext,
+): Promise {
return handlePaymentRequest(event, _context, true);
}
/**
* Handle /supported endpoint - capability discovery
*/
-export async function handleSupported(event, _context) {
+export async function handleSupported(
+ event: ScalewayEvent,
+ _context: ScalewayContext,
+): Promise {
// Handle CORS preflight
if (event.httpMethod === "OPTIONS") {
return {
@@ -67,12 +121,12 @@ export async function handleSupported(event, _context) {
/**
* Unified handler for verify and settle endpoints
- * @param {Object} event - The event object
- * @param {Object} _context - The invocation context
- * @param {boolean} isSettle - Whether this is a settle request
- * @returns {Promise<{body: string, statusCode: number, headers: Record}>}
*/
-async function handlePaymentRequest(event, _context, isSettle) {
+async function handlePaymentRequest(
+ event: ScalewayEvent,
+ _context: ScalewayContext,
+ isSettle: boolean,
+): Promise {
// Handle CORS preflight requests
if (event.httpMethod === "OPTIONS") {
return {
@@ -91,9 +145,12 @@ async function handlePaymentRequest(event, _context, isSettle) {
};
}
- let body;
+ let body: PaymentRequestBody;
try {
- body = typeof event.body === "string" ? JSON.parse(event.body) : event.body;
+ body =
+ typeof event.body === "string"
+ ? (JSON.parse(event.body) as PaymentRequestBody)
+ : (event.body as PaymentRequestBody);
} catch (error) {
logger.error({ err: error }, "Failed to parse request body");
return {
@@ -221,7 +278,10 @@ async function handlePaymentRequest(event, _context, isSettle) {
* Local development server with routing
* This simulates the separate Scaleway Functions deployment locally
*/
-export async function handle(event, context) {
+export async function handle(
+ event: ScalewayEvent,
+ context: ScalewayContext,
+): Promise {
const path = event.path || event.rawUrl || "";
if (path.includes("/supported")) {
@@ -257,5 +317,5 @@ if (process.env.NODE_ENV === "test") {
logger.info(" POST http://localhost:8080/verify");
logger.info(" POST http://localhost:8080/settle");
logger.info(" GET http://localhost:8080/supported");
- })().catch((err) => logger.error({ err }, "Error starting local server"));
+ })().catch((err: Error) => logger.error({ err }, "Error starting local server"));
}
From ac0b9afd9c7aef248a0dfe16088906b0e8042684 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Tue, 27 Jan 2026 07:26:47 +0100
Subject: [PATCH 22/46] More work with two networks
---
notebooks/x402_facilitator_demo.ipynb | 543 ++++++++++++-------
x402_facilitator/test/x402_settle.test.js | 16 +-
x402_facilitator/test/x402_supported.test.js | 20 +
3 files changed, 361 insertions(+), 218 deletions(-)
diff --git a/notebooks/x402_facilitator_demo.ipynb b/notebooks/x402_facilitator_demo.ipynb
index ec916a21e..da4ebaf62 100644
--- a/notebooks/x402_facilitator_demo.ipynb
+++ b/notebooks/x402_facilitator_demo.ipynb
@@ -5,14 +5,14 @@
"id": "69a76c73",
"metadata": {},
"source": [
- "# x402 Facilitator for Optimism \n",
+ "# x402 Facilitator Demo\n",
"\n",
- "In this notebook, we test the x402 facilitator for Optimism network."
+ "Test the x402 facilitator for Optimism and Base networks (mainnet & testnet)."
]
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 13,
"id": "71447da2",
"metadata": {},
"outputs": [
@@ -73,7 +73,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 14,
"id": "30236d53",
"metadata": {},
"outputs": [
@@ -93,6 +93,16 @@
" \"x402Version\": 2,\n",
" \"scheme\": \"exact\",\n",
" \"network\": \"eip155:11155420\"\n",
+ " },\n",
+ " {\n",
+ " \"x402Version\": 2,\n",
+ " \"scheme\": \"exact\",\n",
+ " \"network\": \"eip155:8453\"\n",
+ " },\n",
+ " {\n",
+ " \"x402Version\": 2,\n",
+ " \"scheme\": \"exact\",\n",
+ " \"network\": \"eip155:84532\"\n",
" }\n",
" ],\n",
" \"extensions\": [\n",
@@ -137,6 +147,8 @@
"✅ Supported Networks:\n",
" - eip155:10 (exact scheme, x402 v2)\n",
" - eip155:11155420 (exact scheme, x402 v2)\n",
+ " - eip155:8453 (exact scheme, x402 v2)\n",
+ " - eip155:84532 (exact scheme, x402 v2)\n",
"\n",
"🔧 Custom Extensions:\n",
" - recipient_whitelist: Payment recipients must be authorized through smart contract whitelist. Clients can verify authorization by calling isAuthorizedAgent(address) on the contracts below.\n"
@@ -169,26 +181,25 @@
"id": "03e16a04",
"metadata": {},
"source": [
- "## Create EIP-712 Signature\n",
- "\n",
- "In dieser Zelle kannst du zwischen **Testnet** (Optimism Sepolia) und **Mainnet** (Optimism mit echtem Geld) wechseln.\n",
+ "## Create an EIP-712 Signed Authorization\n",
"\n",
- "**🧪 Testnet (Standard):**\n",
- "- Optimism Sepolia (Chain ID: 11155420)\n",
- "- USDC: `0x5fd84259d66Cd46123540766Be93DFE6D43130D7`\n",
- "- Kein echtes Geld - zum Testen gedacht\n",
+ "Now we'll create a signed EIP-712 message that authorizes the transfer.\n",
"\n",
- "**💰 Mainnet (Echtes Geld):**\n",
- "- Optimism Mainnet (Chain ID: 10)\n",
- "- USDC: `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85`\n",
- "- ⚠️ **VORSICHT: Verwendet echtes USDC!**\n",
+ "**Supported Networks:**\n",
+ "| Chain | Testnet | Mainnet |\n",
+ "|-------|---------|---------|\n",
+ "| Optimism | `eip155:11155420` | `eip155:10` |\n",
+ "| Base | `eip155:84532` | `eip155:8453` |\n",
"\n",
- "Um zu Mainnet zu wechseln, setze `USE_MAINNET = True` in der nächsten Zelle.\n"
+ "**To switch networks:**\n",
+ "1. Set `USE_BASE = True` for Base, `False` for Optimism\n",
+ "2. Set `USE_MAINNET = True` for mainnet (⚠️ REAL MONEY!)\n",
+ "\n"
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 15,
"id": "56489ea0",
"metadata": {},
"outputs": [
@@ -199,25 +210,11 @@
"Payer Address: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
"Recipient Address: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
- "🧪 Using testnet: Optimism Sepolia (Testnet)\n",
- "Chain ID: 11155420\n",
- "USDC Address: 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n",
+ "🧪 Using testnet: Base Sepolia (Testnet)\n",
+ "Chain ID: 84532\n",
+ "USDC Address: 0x036CbD53842c5426634e7929541eC2318f3dCF7e\n",
"USDC Name: USDC\n",
- "\n",
- "💰 Payment Details:\n",
- " Payment amount: 20000 (0.02 USDC)\n",
- "\n",
- "🔐 Signature Details:\n",
- "Network: Optimism Sepolia (Testnet)\n",
- "EIP-712 Hash: 747828ab29c5273e7d8c49f5ece00407a675198bd6d3e3f7d931f3bbd3cadf54\n",
- "Signature: 0x9382b9ff607c207dc0297cc192260f16ae739a2aa3e0d844e51ffc8521b9f7d01a7b1e85e51cdfc159333b35710efe601fe33c7eb93d5df2ce4121091240bc5d1c\n",
- "Signature length: 132 chars (should be 132 with 0x)\n",
- "v: 28, r: 9382b9ff607c207d..., s: 1a7b1e85e51cdfc1...\n",
- "Nonce: 0x47f06b85d2c061b48f1bef992c3b0753e08b582307611201b263edebc145f141\n",
- "\n",
- "✅ Authorized $0.02 USDC payment\n",
- "\n",
- "💡 Using correct domain name: 'USDC' for Optimism Sepolia (Testnet)\n"
+ "CAIP-2 Network: eip155:84532\n"
]
}
],
@@ -247,88 +244,171 @@
"print(f\"Payer Address: {from_address}\")\n",
"print(f\"Recipient Address: {pay_to_address}\")\n",
"\n",
- "# ⚠️ NETWORK SELECTION - Change this to switch between testnet and mainnet\n",
- "USE_MAINNET = False # Set to True for Optimism Mainnet with REAL MONEY\n",
+ "# ═══════════════════════════════════════════════════════════════\n",
+ "# ⚠️ NETWORK SELECTION - Choose your network here\n",
+ "# ═══════════════════════════════════════════════════════════════\n",
+ "\n",
+ "# Step 1: Choose the chain (Optimism or Base)\n",
+ "USE_BASE = True # Set to True for Base, False for Optimism\n",
+ "\n",
+ "# Step 2: Choose testnet or mainnet\n",
+ "USE_MAINNET = False # Set to True for mainnet with REAL MONEY\n",
+ "\n",
+ "# Network configuration\n",
+ "NETWORK_CONFIG = {\n",
+ " # Optimism networks\n",
+ " (\"optimism\", False): {\n",
+ " \"chain_id\": 11155420,\n",
+ " \"usdc_address\": \"0x5fd84259d66Cd46123540766Be93DFE6D43130D7\",\n",
+ " \"usdc_name\": \"USDC\",\n",
+ " \"network_name\": \"Optimism Sepolia (Testnet)\",\n",
+ " \"caip2\": \"eip155:11155420\",\n",
+ " },\n",
+ " (\"optimism\", True): {\n",
+ " \"chain_id\": 10,\n",
+ " \"usdc_address\": \"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85\",\n",
+ " \"usdc_name\": \"USD Coin\",\n",
+ " \"network_name\": \"Optimism Mainnet\",\n",
+ " \"caip2\": \"eip155:10\",\n",
+ " },\n",
+ " # Base networks\n",
+ " (\"base\", False): {\n",
+ " \"chain_id\": 84532,\n",
+ " \"usdc_address\": \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\",\n",
+ " \"usdc_name\": \"USDC\",\n",
+ " \"network_name\": \"Base Sepolia (Testnet)\",\n",
+ " \"caip2\": \"eip155:84532\",\n",
+ " },\n",
+ " (\"base\", True): {\n",
+ " \"chain_id\": 8453,\n",
+ " \"usdc_address\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\n",
+ " \"usdc_name\": \"USD Coin\",\n",
+ " \"network_name\": \"Base Mainnet\",\n",
+ " \"caip2\": \"eip155:8453\",\n",
+ " },\n",
+ "}\n",
+ "\n",
+ "# Get the selected network configuration\n",
+ "chain_key = (\"base\" if USE_BASE else \"optimism\", USE_MAINNET)\n",
+ "config = NETWORK_CONFIG[chain_key]\n",
+ "\n",
+ "CHAIN_ID = config[\"chain_id\"]\n",
+ "USDC_ADDRESS = config[\"usdc_address\"]\n",
+ "USDC_NAME = config[\"usdc_name\"]\n",
+ "NETWORK_NAME = config[\"network_name\"]\n",
+ "CAIP2_NETWORK = config[\"caip2\"]\n",
"\n",
"if USE_MAINNET:\n",
- " CHAIN_ID = 10 # Optimism Mainnet\n",
- " USDC_ADDRESS = \"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85\" # USDC on Optimism Mainnet\n",
- " USDC_NAME = \"USD Coin\" # Mainnet uses \"USD Coin\"\n",
- " NETWORK_NAME = \"Optimism Mainnet\"\n",
- " CAIP2_NETWORK = \"eip155:10\"\n",
" print(f\"\\n🚨 WARNING: Using REAL MONEY on {NETWORK_NAME}!\")\n",
"else:\n",
- " CHAIN_ID = 11155420 # Optimism Sepolia\n",
- " USDC_ADDRESS = \"0x5fd84259d66Cd46123540766Be93DFE6D43130D7\" # USDC on Optimism Sepolia\n",
- " USDC_NAME = \"USDC\" # Testnet uses \"USDC\"\n",
- " NETWORK_NAME = \"Optimism Sepolia (Testnet)\"\n",
- " CAIP2_NETWORK = \"eip155:11155420\"\n",
" print(f\"\\n🧪 Using testnet: {NETWORK_NAME}\")\n",
"\n",
"print(f\"Chain ID: {CHAIN_ID}\")\n",
"print(f\"USDC Address: {USDC_ADDRESS}\")\n",
"print(f\"USDC Name: {USDC_NAME}\")\n",
+ "print(f\"CAIP-2 Network: {CAIP2_NETWORK}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ef3dc975",
+ "metadata": {},
+ "source": [
+ "### Create EIP-712 Signature\n",
"\n",
- "# Payment configuration\n",
- "payment_amount = \"20000\" # $0.02 USDC (6 decimals) - amount for recipient\n",
+ "Now we create an EIP-712 signed authorization for `transferWithAuthorization` (EIP-3009):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "ddcb1c8c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "📝 Creating EIP-712 Authorization:\n",
+ " Network: Base Sepolia (Testnet)\n",
+ " Amount: $0.01 USDC\n",
+ " From: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
+ " To: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
+ " Nonce: 0xe4414fe1c539926a43...\n",
+ " Valid until: 2026-01-27 08:22:42\n",
+ "\n",
+ "🔐 Signature Details:\n",
+ "EIP-712 Hash: 2e383fb651869a7d336c4935928dd94dd879ef13538ae239b299213e0e1ee395\n",
+ "Signature: 0xd70c382380fa68dc7408b032a1ef42509d18e7ea225e1a129003c51fa2d949526b430bce4d47100ccc98cc6587c16034913306fa526f6c9426115137e7e02c621b\n",
+ "Signature length: 132 chars (should be 132 with 0x)\n",
+ "v: 27, r: d70c382380fa68dc..., s: 6b430bce4d47100c...\n",
+ "Nonce: 0xe4414fe1c539926a43a826cbcaa61beb1ab6432a896c0e4d2b22e5184aa684e1\n",
+ "\n",
+ "✅ Authorized $0.01 USDC payment\n",
+ "\n",
+ "💡 Using correct domain name: 'USDC' for Base Sepolia (Testnet)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Create EIP-712 Signature for TransferWithAuthorization (EIP-3009)\n",
+ "# ✅ Use eth_keys for USDC-compatible signatures (manual EIP-712 hash)\n",
+ "import time\n",
+ "from eth_hash.auto import keccak\n",
"\n",
- "print(f\"\\n💰 Payment Details:\")\n",
- "print(f\" Payment amount: {payment_amount} ({int(payment_amount)/1e6:.2f} USDC)\")\n",
+ "# Payment amount: $0.01 USDC (6 decimals)\n",
+ "payment_amount = \"10000\" # 0.01 USDC = 10000 (6 decimals)\n",
"\n",
- "# Create authorization data\n",
- "valid_after = int(datetime.now().timestamp()) - 60\n",
- "valid_before = int(datetime.now().timestamp()) + 3600\n",
+ "# Generate a random nonce (32 bytes)\n",
"nonce = \"0x\" + secrets.token_hex(32)\n",
"\n",
- "# EIP-712 domain for USDC on Optimism\n",
- "# CRITICAL: Mainnet uses \"USD Coin\", Testnet uses \"USDC\"!\n",
- "domain_data = {\n",
- " \"name\": USDC_NAME, # Network-specific name\n",
- " \"version\": \"2\",\n",
- " \"chainId\": CHAIN_ID,\n",
- " \"verifyingContract\": USDC_ADDRESS\n",
- "}\n",
- "\n",
- "# EIP-712 types for TransferWithAuthorization\n",
- "types = {\n",
- " \"EIP712Domain\": [\n",
- " {\"name\": \"name\", \"type\": \"string\"},\n",
- " {\"name\": \"version\", \"type\": \"string\"},\n",
- " {\"name\": \"chainId\", \"type\": \"uint256\"},\n",
- " {\"name\": \"verifyingContract\", \"type\": \"address\"}\n",
- " ],\n",
- " \"TransferWithAuthorization\": [\n",
- " {\"name\": \"from\", \"type\": \"address\"},\n",
- " {\"name\": \"to\", \"type\": \"address\"},\n",
- " {\"name\": \"value\", \"type\": \"uint256\"},\n",
- " {\"name\": \"validAfter\", \"type\": \"uint256\"},\n",
- " {\"name\": \"validBefore\", \"type\": \"uint256\"},\n",
- " {\"name\": \"nonce\", \"type\": \"bytes32\"}\n",
- " ]\n",
- "}\n",
+ "# Validity window\n",
+ "valid_after = 0 # Valid immediately\n",
+ "valid_before = int(time.time()) + 3600 # Valid for 1 hour\n",
"\n",
- "# Message data - sign the payment amount\n",
- "message_data = {\n",
- " \"from\": from_address,\n",
- " \"to\": pay_to_address,\n",
- " \"value\": int(payment_amount),\n",
- " \"validAfter\": int(valid_after),\n",
- " \"validBefore\": int(valid_before),\n",
- " \"nonce\": nonce\n",
- "}\n",
+ "print(f\"📝 Creating EIP-712 Authorization:\")\n",
+ "print(f\" Network: {NETWORK_NAME}\")\n",
+ "print(f\" Amount: ${int(payment_amount)/1e6:.2f} USDC\")\n",
+ "print(f\" From: {from_address}\")\n",
+ "print(f\" To: {pay_to_address}\")\n",
+ "print(f\" Nonce: {nonce[:20]}...\")\n",
+ "print(f\" Valid until: {datetime.fromtimestamp(valid_before)}\")\n",
"\n",
- "# Create the full EIP-712 message\n",
+ "# EIP-712 typed data structure for TransferWithAuthorization\n",
"full_message = {\n",
- " \"types\": types,\n",
+ " \"types\": {\n",
+ " \"EIP712Domain\": [\n",
+ " {\"name\": \"name\", \"type\": \"string\"},\n",
+ " {\"name\": \"version\", \"type\": \"string\"},\n",
+ " {\"name\": \"chainId\", \"type\": \"uint256\"},\n",
+ " {\"name\": \"verifyingContract\", \"type\": \"address\"}\n",
+ " ],\n",
+ " \"TransferWithAuthorization\": [\n",
+ " {\"name\": \"from\", \"type\": \"address\"},\n",
+ " {\"name\": \"to\", \"type\": \"address\"},\n",
+ " {\"name\": \"value\", \"type\": \"uint256\"},\n",
+ " {\"name\": \"validAfter\", \"type\": \"uint256\"},\n",
+ " {\"name\": \"validBefore\", \"type\": \"uint256\"},\n",
+ " {\"name\": \"nonce\", \"type\": \"bytes32\"}\n",
+ " ]\n",
+ " },\n",
" \"primaryType\": \"TransferWithAuthorization\",\n",
- " \"domain\": domain_data,\n",
- " \"message\": message_data\n",
+ " \"domain\": {\n",
+ " \"name\": USDC_NAME,\n",
+ " \"version\": \"2\",\n",
+ " \"chainId\": CHAIN_ID,\n",
+ " \"verifyingContract\": USDC_ADDRESS\n",
+ " },\n",
+ " \"message\": {\n",
+ " \"from\": from_address,\n",
+ " \"to\": pay_to_address,\n",
+ " \"value\": int(payment_amount),\n",
+ " \"validAfter\": valid_after,\n",
+ " \"validBefore\": valid_before,\n",
+ " \"nonce\": nonce\n",
+ " }\n",
"}\n",
"\n",
- "# ✅ Use eth_keys for USDC-compatible signatures (manual EIP-712 hash)\n",
- "# This is what USDC contract expects - raw signature over the EIP-712 hash\n",
- "from eth_hash.auto import keccak\n",
- "\n",
"# Encode to get the EIP-712 hash components\n",
"signable_message = encode_typed_data(full_message=full_message)\n",
"\n",
@@ -361,24 +441,13 @@
"signature_hex = '0x' + r_bytes.hex() + s_bytes.hex() + format(v, '02x')\n",
"\n",
"print(f\"\\n🔐 Signature Details:\")\n",
- "print(f\"Network: {NETWORK_NAME}\")\n",
"print(f\"EIP-712 Hash: {full_eip712_hash.hex()}\")\n",
"print(f\"Signature: {signature_hex}\")\n",
"print(f\"Signature length: {len(signature_hex)} chars (should be 132 with 0x)\")\n",
"print(f\"v: {v}, r: {r_bytes.hex()[:16]}..., s: {s_bytes.hex()[:16]}...\")\n",
"print(f\"Nonce: {nonce}\")\n",
"print(f\"\\n✅ Authorized ${int(payment_amount)/1e6:.2f} USDC payment\")\n",
- "print(f\"\\n💡 Using correct domain name: '{USDC_NAME}' for {NETWORK_NAME}\")\n"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "ef3dc975",
- "metadata": {},
- "source": [
- "### Debug: Prüfe ob Nonce bereits verwendet wurde\n",
- "\n",
- "Manchmal wurde eine Nonce bereits in einem früheren Test verwendet. Lass uns prüfen:"
+ "print(f\"\\n💡 Using correct domain name: '{USDC_NAME}' for {NETWORK_NAME}\")"
]
},
{
@@ -393,7 +462,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 17,
"id": "cdbfa701",
"metadata": {},
"outputs": [
@@ -401,31 +470,26 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "🔍 Debugging EIP-712 Domain Parameters for Optimism Sepolia (Testnet)\n",
- "Contract: 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n",
+ "🔍 Debugging EIP-712 Domain Parameters for Base Sepolia (Testnet)\n",
+ "Contract: 0x036CbD53842c5426634e7929541eC2318f3dCF7e\n",
"\n",
- "❌ eip712Domain(): ('execution reverted', 'no data')...\n",
+ "❌ eip712Domain(): ('execution reverted', '0x')...\n",
"✅ name(): USDC\n",
"✅ version(): 2\n",
- "✅ DOMAIN_SEPARATOR(): b'\\t\\xd08\\xa3\\xe4`@\\xfc7\\xeb\\x01\\x17M\\xbb\\xcd\\xb7\\x98\\x1f\\xbd\\x8e\\xaf\\xd9\\xe1\\xa8W\\xb1\\xc6x\\x05\\xdf\\xb2\\x9e'\n",
+ "✅ DOMAIN_SEPARATOR(): b'q\\xf1z;/\\xf3s\\xb8\\x03\\xd7\\nZ\\x07\\xc0F\\xc1\\xa2\\xbc\\x8e\\x89\\xc0\\x9e\\xf7\"\\xfc\\xb0G\\xab\\xe9L\\x98\\x18'\n",
"\n",
"📊 Comparison:\n",
"Our assumptions:\n",
" name: 'USDC'\n",
" version: '2'\n",
- " chainId: 11155420\n",
- " verifyingContract: 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n",
+ " chainId: 84532\n",
+ " verifyingContract: 0x036CbD53842c5426634e7929541eC2318f3dCF7e\n",
"\n",
"Contract's name: 'USDC'\n",
"Contract's version: '2'\n",
"\n",
"🔐 Contract's DOMAIN_SEPARATOR:\n",
- " 09d038a3e46040fc37eb01174dbbcdb7981fbd8eafd9e1a857b1c67805dfb29e\n",
- "\n",
- "Our computed domain separator:\n",
- " 09d038a3e46040fc37eb01174dbbcdb7981fbd8eafd9e1a857b1c67805dfb29e\n",
- "\n",
- "✅ Domain separators match!\n"
+ " 71f17a3b2ff373b803d70a5a07c046c1a2bc8e89c09ef722fcb047abe94c9818\n"
]
}
],
@@ -434,6 +498,14 @@
"print(f\"🔍 Debugging EIP-712 Domain Parameters for {NETWORK_NAME}\")\n",
"print(f\"Contract: {USDC_ADDRESS}\\n\")\n",
"\n",
+ "# Define domain_data based on selected network configuration\n",
+ "domain_data = {\n",
+ " \"name\": USDC_NAME,\n",
+ " \"version\": \"2\",\n",
+ " \"chainId\": CHAIN_ID,\n",
+ " \"verifyingContract\": USDC_ADDRESS\n",
+ "}\n",
+ "\n",
"# Try different methods to get domain info\n",
"methods_to_try = [\n",
" # Method 1: ERC-5267 standard (eip712Domain)\n",
@@ -489,12 +561,15 @@
" }\n",
"]\n",
"\n",
- "# Connect to network\n",
- "if USE_MAINNET:\n",
- " rpc_url = 'https://mainnet.optimism.io'\n",
- "else:\n",
- " rpc_url = 'https://sepolia.optimism.io'\n",
+ "# Connect to network based on selection\n",
+ "network_rpc = {\n",
+ " \"eip155:10\": \"https://mainnet.optimism.io\",\n",
+ " \"eip155:11155420\": \"https://sepolia.optimism.io\",\n",
+ " \"eip155:8453\": \"https://mainnet.base.org\",\n",
+ " \"eip155:84532\": \"https://sepolia.base.org\",\n",
+ "}\n",
"\n",
+ "rpc_url = network_rpc.get(CAIP2_NETWORK, \"https://sepolia.optimism.io\")\n",
"w3 = Web3(Web3.HTTPProvider(rpc_url))\n",
"\n",
"contract_info = {}\n",
@@ -532,6 +607,8 @@
" print(f\"⚠️ MISMATCH: version is '{version}' not '{domain_data['version']}'!\")\n",
" if chainId != domain_data['chainId']:\n",
" print(f\"⚠️ MISMATCH: chainId is {chainId} not {domain_data['chainId']}!\")\n",
+ " else:\n",
+ " print(f\"\\n✅ All domain parameters match!\")\n",
"else:\n",
" if \"name\" in contract_info:\n",
" print(f\"\\nContract's name: '{contract_info['name']}'\")\n",
@@ -545,19 +622,12 @@
"\n",
"if \"DOMAIN_SEPARATOR\" in contract_info:\n",
" print(f\"\\n🔐 Contract's DOMAIN_SEPARATOR:\")\n",
- " print(f\" {contract_info['DOMAIN_SEPARATOR'].hex()}\")\n",
- " print(f\"\\nOur computed domain separator:\")\n",
- " print(f\" {signable_message.header.hex()}\")\n",
- " \n",
- " if contract_info['DOMAIN_SEPARATOR'] != signable_message.header:\n",
- " print(f\"\\n❌ DOMAIN_SEPARATOR MISMATCH! This explains the signature failure!\")\n",
- " else:\n",
- " print(f\"\\n✅ Domain separators match!\")"
+ " print(f\" {contract_info['DOMAIN_SEPARATOR'].hex()}\")"
]
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 18,
"id": "8ba26f28",
"metadata": {},
"outputs": [
@@ -565,9 +635,9 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Prüfe Nonce: 0x47f06b85d2c061b48f1bef992c3b0753e08b582307611201b263edebc145f141\n",
+ "Prüfe Nonce: 0xe4414fe1c539926a43a826cbcaa61beb1ab6432a896c0e4d2b22e5184aa684e1\n",
"Payer: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
- "Network: Optimism Sepolia (Testnet)\n",
+ "Network: Base Sepolia (Testnet)\n",
"\n",
"✅ Diese Nonce ist noch verfügbar und kann verwendet werden.\n"
]
@@ -624,7 +694,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 19,
"id": "b738e68e",
"metadata": {},
"outputs": [
@@ -634,8 +704,8 @@
"text": [
"\n",
"📤 Request Summary:\n",
- " Network: Optimism Sepolia (Testnet)\n",
- " Payment Amount: $0.02 USDC\n",
+ " Network: Base Sepolia (Testnet)\n",
+ " Payment Amount: $0.01 USDC\n",
" Recipient: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
"✅ Verify Response (Status 200):\n",
@@ -645,7 +715,7 @@
"}\n",
"\n",
"🎉 Payment signature is valid and ready for settlement!\n",
- " Facilitator will transfer $0.02 to recipient\n"
+ " Facilitator will transfer $0.01 to recipient\n"
]
}
],
@@ -809,7 +879,7 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 25,
"id": "e44a3bf8",
"metadata": {},
"outputs": [
@@ -817,37 +887,72 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Facilitator Wallet: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
- "Network: Optimism Sepolia (Testnet)\n",
- "ETH Balance: 0.049997203839746895 ETH\n",
- "Geschätzte Gas-Kosten: ~0.000000 ETH\n",
+ "🔍 Pre-Settlement Check for Base Sepolia (Testnet)\n",
+ " Network ID: eip155:84532\n",
+ "============================================================\n",
+ "\n",
+ "💰 Facilitator Wallet (pays gas):\n",
+ " Address: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
+ " ETH Balance: 0 ETH\n",
+ " Estimated Gas: ~0.000000 ETH\n",
"\n",
- "✅ Ausreichend ETH für Settlement vorhanden\n"
+ " ❌ ERROR: Facilitator has NO ETH for gas!\n",
+ " Send ETH to: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
+ " Faucet: https://www.alchemy.com/faucets/base-sepolia\n",
+ "\n",
+ "💵 Payer Wallet (sends USDC):\n",
+ " Address: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
+ " USDC Balance: $19.990000 USDC\n",
+ " Required: $0.010000 USDC\n",
+ " ✅ Sufficient USDC for payment\n",
+ "\n",
+ "============================================================\n",
+ "❌ Cannot settle - fix the issues above:\n",
+ " • Facilitator needs ETH on Base Sepolia (Testnet)\n"
]
}
],
"source": [
- "# Check facilitator wallet balance using public key from .env\n",
+ "# Check facilitator wallet balance AND payer USDC balance\n",
+ "# Now supports all 4 networks (Optimism + Base, Mainnet + Testnet)\n",
+ "\n",
"facilitator_address = os.getenv('FACILITATOR_WALLET_PUBLIC_KEY')\n",
"\n",
+ "# RPC endpoints for all networks\n",
+ "network_rpc = {\n",
+ " \"eip155:10\": \"https://mainnet.optimism.io\",\n",
+ " \"eip155:11155420\": \"https://sepolia.optimism.io\",\n",
+ " \"eip155:8453\": \"https://mainnet.base.org\",\n",
+ " \"eip155:84532\": \"https://sepolia.base.org\",\n",
+ "}\n",
+ "\n",
+ "# Faucet/bridge URLs\n",
+ "network_faucets = {\n",
+ " \"eip155:10\": \"Bridge: https://app.optimism.io/bridge\",\n",
+ " \"eip155:11155420\": \"Faucet: https://app.optimism.io/faucet\",\n",
+ " \"eip155:8453\": \"Bridge: https://bridge.base.org\",\n",
+ " \"eip155:84532\": \"Faucet: https://www.alchemy.com/faucets/base-sepolia\",\n",
+ "}\n",
+ "\n",
"if facilitator_address:\n",
" facilitator_address = Web3.to_checksum_address(facilitator_address)\n",
" \n",
- " # Connect to appropriate network\n",
- " if USE_MAINNET:\n",
- " rpc_url = 'https://mainnet.optimism.io'\n",
- " else:\n",
- " rpc_url = 'https://sepolia.optimism.io'\n",
- " \n",
+ " # Connect to the SELECTED network (based on USE_BASE and USE_MAINNET)\n",
+ " rpc_url = network_rpc.get(CAIP2_NETWORK, \"https://sepolia.optimism.io\")\n",
" w3 = Web3(Web3.HTTPProvider(rpc_url))\n",
" \n",
" if w3.is_connected():\n",
+ " print(f\"🔍 Pre-Settlement Check for {NETWORK_NAME}\")\n",
+ " print(f\" Network ID: {CAIP2_NETWORK}\")\n",
+ " print(\"=\" * 60)\n",
+ " \n",
+ " # 1. Check Facilitator ETH Balance (for gas)\n",
" balance_wei = w3.eth.get_balance(facilitator_address)\n",
" balance_eth = w3.from_wei(balance_wei, 'ether')\n",
" \n",
- " print(f\"Facilitator Wallet: {facilitator_address}\")\n",
- " print(f\"Network: {NETWORK_NAME}\")\n",
- " print(f\"ETH Balance: {balance_eth} ETH\")\n",
+ " print(f\"\\n💰 Facilitator Wallet (pays gas):\")\n",
+ " print(f\" Address: {facilitator_address}\")\n",
+ " print(f\" ETH Balance: {balance_eth} ETH\")\n",
" \n",
" # Estimate gas cost (rough estimate: ~100k gas)\n",
" estimated_gas = 100000\n",
@@ -855,29 +960,71 @@
" estimated_cost_wei = estimated_gas * gas_price\n",
" estimated_cost_eth = w3.from_wei(estimated_cost_wei, 'ether')\n",
" \n",
- " print(f\"Geschätzte Gas-Kosten: ~{estimated_cost_eth:.6f} ETH\")\n",
+ " print(f\" Estimated Gas: ~{estimated_cost_eth:.6f} ETH\")\n",
" \n",
" if balance_wei == 0:\n",
- " print(f\"\\n❌ FEHLER: Facilitator Wallet hat kein ETH!\")\n",
- " print(f\"Sende ETH an: {facilitator_address}\")\n",
- " if USE_MAINNET:\n",
- " print(f\"Bridge: https://app.optimism.io/bridge\")\n",
- " else:\n",
- " print(f\"Faucet: https://app.optimism.io/faucet\")\n",
+ " print(f\"\\n ❌ ERROR: Facilitator has NO ETH for gas!\")\n",
+ " print(f\" Send ETH to: {facilitator_address}\")\n",
+ " print(f\" {network_faucets.get(CAIP2_NETWORK, 'Get ETH from faucet/bridge')}\")\n",
" elif balance_wei < estimated_cost_wei:\n",
- " print(f\"\\n⚠️ WARNUNG: Balance könnte zu niedrig für Gas sein\")\n",
- " print(f\"Empfohlen: Mindestens {estimated_cost_eth:.6f} ETH\")\n",
+ " print(f\"\\n ⚠️ WARNING: ETH balance might be too low for gas\")\n",
+ " print(f\" Recommended: At least {estimated_cost_eth:.6f} ETH\")\n",
+ " else:\n",
+ " print(f\" ✅ Sufficient ETH for gas\")\n",
+ " \n",
+ " # 2. Check Payer USDC Balance (for payment)\n",
+ " erc20_abi = [\n",
+ " {\n",
+ " \"inputs\": [{\"name\": \"account\", \"type\": \"address\"}],\n",
+ " \"name\": \"balanceOf\",\n",
+ " \"outputs\": [{\"name\": \"\", \"type\": \"uint256\"}],\n",
+ " \"stateMutability\": \"view\",\n",
+ " \"type\": \"function\"\n",
+ " }\n",
+ " ]\n",
+ " \n",
+ " usdc_contract = w3.eth.contract(address=USDC_ADDRESS, abi=erc20_abi)\n",
+ " payer_usdc_balance = usdc_contract.functions.balanceOf(from_address).call()\n",
+ " required_amount = int(payment_amount)\n",
+ " \n",
+ " print(f\"\\n💵 Payer Wallet (sends USDC):\")\n",
+ " print(f\" Address: {from_address}\")\n",
+ " print(f\" USDC Balance: ${payer_usdc_balance/1e6:.6f} USDC\")\n",
+ " print(f\" Required: ${required_amount/1e6:.6f} USDC\")\n",
+ " \n",
+ " if payer_usdc_balance == 0:\n",
+ " print(f\"\\n ❌ ERROR: Payer has NO USDC!\")\n",
+ " print(f\" Get USDC from: https://faucet.circle.com/ (select {NETWORK_NAME})\")\n",
+ " elif payer_usdc_balance < required_amount:\n",
+ " shortfall = required_amount - payer_usdc_balance\n",
+ " print(f\"\\n ❌ ERROR: Insufficient USDC!\")\n",
+ " print(f\" Shortfall: ${shortfall/1e6:.6f} USDC\")\n",
+ " print(f\" Get USDC from: https://faucet.circle.com/\")\n",
+ " else:\n",
+ " print(f\" ✅ Sufficient USDC for payment\")\n",
+ " \n",
+ " # 3. Summary\n",
+ " print(f\"\\n\" + \"=\" * 60)\n",
+ " facilitator_ok = balance_wei >= estimated_cost_wei\n",
+ " payer_ok = payer_usdc_balance >= required_amount\n",
+ " \n",
+ " if facilitator_ok and payer_ok:\n",
+ " print(f\"✅ Ready for settlement on {NETWORK_NAME}\")\n",
" else:\n",
- " print(f\"\\n✅ Ausreichend ETH für Settlement vorhanden\")\n",
+ " print(f\"❌ Cannot settle - fix the issues above:\")\n",
+ " if not facilitator_ok:\n",
+ " print(f\" • Facilitator needs ETH on {NETWORK_NAME}\")\n",
+ " if not payer_ok:\n",
+ " print(f\" • Payer needs USDC on {NETWORK_NAME}\")\n",
" else:\n",
- " print(f\"❌ Konnte nicht mit {NETWORK_NAME} verbinden\")\n",
+ " print(f\"❌ Could not connect to {NETWORK_NAME}\")\n",
"else:\n",
- " print(\"❌ FACILITATOR_WALLET_PUBLIC_KEY nicht in .env konfiguriert\")\n"
+ " print(\"❌ FACILITATOR_WALLET_PUBLIC_KEY not configured in .env\")"
]
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 21,
"id": "964e5fde",
"metadata": {},
"outputs": [
@@ -887,22 +1034,20 @@
"text": [
"\n",
"💸 Attempting Settlement...\n",
- " Network: Optimism Sepolia (Testnet)\n",
- " Payment: $0.02 USDC → 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
+ " Network: Base Sepolia (Testnet)\n",
+ " Payment: $0.01 USDC → 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
"\n",
"📦 Settle Response (Status 200):\n",
"{\n",
- " \"success\": true,\n",
+ " \"success\": false,\n",
+ " \"errorReason\": \"transaction_failed\",\n",
" \"payer\": \"0x553179556FC2A39e535D65b921e01fA995E79101\",\n",
- " \"transaction\": \"0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\",\n",
- " \"network\": \"eip155:11155420\"\n",
+ " \"transaction\": \"\",\n",
+ " \"network\": \"eip155:84532\"\n",
"}\n",
"\n",
- "🎉 Settlement erfolgreich!\n",
- "\n",
- "📝 Transaction Details:\n",
- " Hash: 0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
- " Recipient received: $0.02 USDC\n"
+ "❌ Settlement fehlgeschlagen\n",
+ " Grund: transaction_failed\n"
]
}
],
@@ -951,7 +1096,7 @@
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 22,
"id": "b77cfca9",
"metadata": {},
"outputs": [
@@ -960,14 +1105,7 @@
"output_type": "stream",
"text": [
"\n",
- "🔍 Block Explorer:\n",
- " https://sepolia-optimism.etherscan.io/tx/0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
- "\n",
- "📊 Transaktionsdetails:\n",
- " • Transaction hash: 0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
- " • Network (Response): eip155:11155420\n",
- " • Network (Erwartet): eip155:11155420\n",
- " • Token: USDC at 0x5fd84259d66Cd46123540766Be93DFE6D43130D7\n"
+ "⚠️ No transaction hash available\n"
]
}
],
@@ -1029,7 +1167,7 @@
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 23,
"id": "3e84ad5b",
"metadata": {},
"outputs": [
@@ -1038,10 +1176,10 @@
"output_type": "stream",
"text": [
"🔍 Cross-Chain Balance Check für Recipient: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
- " Erwartetes Settlement-Network: eip155:11155420\n",
+ " Erwartetes Settlement-Network: eip155:84532\n",
"\n",
" Optimism Mainnet (eip155:10): $1.63 USDC \n",
- " Optimism Sepolia (eip155:11155420): $1.48 USDC ← Erwartet\n",
+ " Optimism Sepolia (eip155:11155420): $1.48 USDC \n",
"\n",
"💡 Tipp: Vergleiche die Balances vor und nach dem Settlement,\n",
" um zu verifizieren, dass das Geld auf dem richtigen Network ankam.\n"
@@ -1105,7 +1243,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 24,
"id": "81bfd50f",
"metadata": {},
"outputs": [
@@ -1113,23 +1251,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "💰 Checking USDC Balances on Optimism Sepolia (Testnet)...\n",
- "\n",
- "📊 USDC Balances:\n",
- " Payer (0x55317955...): $0.519000 USDC\n",
- " Recipient (0xAAEBC144...): $1.481000 USDC\n",
- " Facilitator (0x3F8d2Fb6...): $0.030000 USDC\n",
- "\n",
- "💡 Warum erscheint die Transaktion nicht in MetaMask Activity?\n",
- " • TransferWithAuthorization wird vom Facilitator ausgeführt\n",
- " • MetaMask zeigt nur selbst gesendete Transaktionen\n",
- " • Die USDC wurden trotzdem korrekt transferiert!\n",
- " • Prüfe den Token-Balance in MetaMask (nicht die Activity)\n",
"\n",
- "🔍 So siehst du den Transfer:\n",
- " 1. Öffne Etherscan: https://sepolia-optimism.etherscan.io/tx/0x3266d9617df12d65f5969474ab2f262efd728a86fc9a3bbd22e91f0c62cf0152\n",
- " 2. Klicke auf 'Logs' Tab um die Transfer Events zu sehen\n",
- " 3. Oder gehe zu Token Transfers auf der Payer/Recipient Adresse\n"
+ "⚠️ No transaction hash available\n"
]
}
],
diff --git a/x402_facilitator/test/x402_settle.test.js b/x402_facilitator/test/x402_settle.test.js
index adcd383ac..a4a4e45f3 100644
--- a/x402_facilitator/test/x402_settle.test.js
+++ b/x402_facilitator/test/x402_settle.test.js
@@ -404,12 +404,12 @@ describe("x402_settle with mocked facilitator", () => {
transaction: "0xabc123def456",
}),
};
-
+
vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockReturnValue(mockFacilitator);
const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
@@ -427,12 +427,12 @@ describe("x402_settle with mocked facilitator", () => {
errorReason: "insufficient_allowance",
}),
};
-
+
vi.spyOn(verifyModule, "verifyPayment").mockResolvedValue({
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockReturnValue(mockFacilitator);
const result = await settlePayment(validPaymentPayload, validPaymentRequirements);
@@ -448,7 +448,7 @@ describe("x402_settle with mocked facilitator", () => {
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
throw new Error("Transaction failed: insufficient funds for gas");
});
@@ -464,7 +464,7 @@ describe("x402_settle with mocked facilitator", () => {
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
throw new Error("nonce already used");
});
@@ -480,7 +480,7 @@ describe("x402_settle with mocked facilitator", () => {
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
throw new Error("authorization expired");
});
@@ -496,7 +496,7 @@ describe("x402_settle with mocked facilitator", () => {
isValid: true,
payer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
});
-
+
vi.spyOn(facilitatorInstance, "getFacilitator").mockImplementation(() => {
throw new Error("Unknown blockchain error");
});
diff --git a/x402_facilitator/test/x402_supported.test.js b/x402_facilitator/test/x402_supported.test.js
index 4fd12a7ad..a0fb748be 100644
--- a/x402_facilitator/test/x402_supported.test.js
+++ b/x402_facilitator/test/x402_supported.test.js
@@ -54,6 +54,26 @@ describe("x402 /supported endpoint", () => {
// x402 v2 getSupported() does NOT include assets in kinds
});
+ test("includes Base Mainnet support", () => {
+ const capabilities = getSupportedCapabilities();
+
+ const baseSupport = capabilities.kinds.find((k) => k.network === "eip155:8453");
+
+ expect(baseSupport).toBeDefined();
+ expect(baseSupport.x402Version).toBe(2);
+ expect(baseSupport.scheme).toBe("exact");
+ });
+
+ test("includes Base Sepolia support", () => {
+ const capabilities = getSupportedCapabilities();
+
+ const baseSepoliaSupport = capabilities.kinds.find((k) => k.network === "eip155:84532");
+
+ expect(baseSepoliaSupport).toBeDefined();
+ expect(baseSepoliaSupport.x402Version).toBe(2);
+ expect(baseSepoliaSupport.scheme).toBe("exact");
+ });
+
// Note: x402 v2 getSupported() does not include asset details (USDC, USDT, etc.)
// Asset information is not provided in the kinds array by the facilitator
// If needed, asset details would need to be added separately in the response
From b6ce3f5578161f15169d8517af35b3d54ed96af7 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 07:46:17 +0100
Subject: [PATCH 23/46] Update x402_facilitator_demo.ipynb
---
notebooks/x402_facilitator_demo.ipynb | 124 ++++++++++++++++++--------
1 file changed, 88 insertions(+), 36 deletions(-)
diff --git a/notebooks/x402_facilitator_demo.ipynb b/notebooks/x402_facilitator_demo.ipynb
index da4ebaf62..f0f76cffe 100644
--- a/notebooks/x402_facilitator_demo.ipynb
+++ b/notebooks/x402_facilitator_demo.ipynb
@@ -12,7 +12,7 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 27,
"id": "71447da2",
"metadata": {},
"outputs": [
@@ -73,7 +73,7 @@
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 28,
"id": "30236d53",
"metadata": {},
"outputs": [
@@ -199,7 +199,7 @@
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": 29,
"id": "56489ea0",
"metadata": {},
"outputs": [
@@ -321,7 +321,7 @@
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": 30,
"id": "ddcb1c8c",
"metadata": {},
"outputs": [
@@ -334,15 +334,15 @@
" Amount: $0.01 USDC\n",
" From: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
" To: 0xAAEBC1441323B8ad6Bdf6793A8428166b510239C\n",
- " Nonce: 0xe4414fe1c539926a43...\n",
- " Valid until: 2026-01-27 08:22:42\n",
+ " Nonce: 0x784b0e4eebe37f29c9...\n",
+ " Valid until: 2026-01-27 18:31:24\n",
"\n",
"🔐 Signature Details:\n",
- "EIP-712 Hash: 2e383fb651869a7d336c4935928dd94dd879ef13538ae239b299213e0e1ee395\n",
- "Signature: 0xd70c382380fa68dc7408b032a1ef42509d18e7ea225e1a129003c51fa2d949526b430bce4d47100ccc98cc6587c16034913306fa526f6c9426115137e7e02c621b\n",
+ "EIP-712 Hash: 1b9550f0ec9fe168bfe3941ca9a9bb55be6610759654ea051d5fb30cbc263777\n",
+ "Signature: 0x8fc915a3a31342cd8712bd23f3f597dc511b6ce43ec7cf1ca5dfaea3fad5935e7b9b20426dc7764e516cf61c66fe8e79741ae9c1bef9f5ed1af61d2cd1399a3a1c\n",
"Signature length: 132 chars (should be 132 with 0x)\n",
- "v: 27, r: d70c382380fa68dc..., s: 6b430bce4d47100c...\n",
- "Nonce: 0xe4414fe1c539926a43a826cbcaa61beb1ab6432a896c0e4d2b22e5184aa684e1\n",
+ "v: 28, r: 8fc915a3a31342cd..., s: 7b9b20426dc7764e...\n",
+ "Nonce: 0x784b0e4eebe37f29c9bfb4db913498a1eb5d1832b5bc0a6d9eb3882ef4555c16\n",
"\n",
"✅ Authorized $0.01 USDC payment\n",
"\n",
@@ -462,7 +462,7 @@
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": 31,
"id": "cdbfa701",
"metadata": {},
"outputs": [
@@ -627,7 +627,7 @@
},
{
"cell_type": "code",
- "execution_count": 18,
+ "execution_count": 32,
"id": "8ba26f28",
"metadata": {},
"outputs": [
@@ -635,7 +635,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Prüfe Nonce: 0xe4414fe1c539926a43a826cbcaa61beb1ab6432a896c0e4d2b22e5184aa684e1\n",
+ "Prüfe Nonce: 0x784b0e4eebe37f29c9bfb4db913498a1eb5d1832b5bc0a6d9eb3882ef4555c16\n",
"Payer: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
"Network: Base Sepolia (Testnet)\n",
"\n",
@@ -694,7 +694,7 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": 33,
"id": "b738e68e",
"metadata": {},
"outputs": [
@@ -879,7 +879,7 @@
},
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 34,
"id": "e44a3bf8",
"metadata": {},
"outputs": [
@@ -893,12 +893,9 @@
"\n",
"💰 Facilitator Wallet (pays gas):\n",
" Address: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
- " ETH Balance: 0 ETH\n",
+ " ETH Balance: 0.01 ETH\n",
" Estimated Gas: ~0.000000 ETH\n",
- "\n",
- " ❌ ERROR: Facilitator has NO ETH for gas!\n",
- " Send ETH to: 0x3F8d2Fb6fEA24E70155bC61471936F3c9C30c206\n",
- " Faucet: https://www.alchemy.com/faucets/base-sepolia\n",
+ " ✅ Sufficient ETH for gas\n",
"\n",
"💵 Payer Wallet (sends USDC):\n",
" Address: 0x553179556FC2A39e535D65b921e01fA995E79101\n",
@@ -907,8 +904,7 @@
" ✅ Sufficient USDC for payment\n",
"\n",
"============================================================\n",
- "❌ Cannot settle - fix the issues above:\n",
- " • Facilitator needs ETH on Base Sepolia (Testnet)\n"
+ "✅ Ready for settlement on Base Sepolia (Testnet)\n"
]
}
],
@@ -1024,7 +1020,7 @@
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": 35,
"id": "964e5fde",
"metadata": {},
"outputs": [
@@ -1039,15 +1035,17 @@
"\n",
"📦 Settle Response (Status 200):\n",
"{\n",
- " \"success\": false,\n",
- " \"errorReason\": \"transaction_failed\",\n",
+ " \"success\": true,\n",
" \"payer\": \"0x553179556FC2A39e535D65b921e01fA995E79101\",\n",
- " \"transaction\": \"\",\n",
+ " \"transaction\": \"0x6379e34692892294bc8edc93159200b9232be913c2ab93fcd902e06fe3d25781\",\n",
" \"network\": \"eip155:84532\"\n",
"}\n",
"\n",
- "❌ Settlement fehlgeschlagen\n",
- " Grund: transaction_failed\n"
+ "🎉 Settlement erfolgreich!\n",
+ "\n",
+ "📝 Transaction Details:\n",
+ " Hash: 0x6379e34692892294bc8edc93159200b9232be913c2ab93fcd902e06fe3d25781\n",
+ " Recipient received: $0.01 USDC\n"
]
}
],
@@ -1096,7 +1094,7 @@
},
{
"cell_type": "code",
- "execution_count": 22,
+ "execution_count": 36,
"id": "b77cfca9",
"metadata": {},
"outputs": [
@@ -1105,7 +1103,14 @@
"output_type": "stream",
"text": [
"\n",
- "⚠️ No transaction hash available\n"
+ "🔍 Block Explorer:\n",
+ " https://sepolia.basescan.org/tx/0x6379e34692892294bc8edc93159200b9232be913c2ab93fcd902e06fe3d25781\n",
+ "\n",
+ "📊 Transaktionsdetails:\n",
+ " • Transaction hash: 0x6379e34692892294bc8edc93159200b9232be913c2ab93fcd902e06fe3d25781\n",
+ " • Network (Response): eip155:84532\n",
+ " • Network (Erwartet): eip155:84532\n",
+ " • Token: USDC at 0x036CbD53842c5426634e7929541eC2318f3dCF7e\n"
]
}
],
@@ -1167,7 +1172,7 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": null,
"id": "3e84ad5b",
"metadata": {},
"outputs": [
@@ -1188,7 +1193,7 @@
],
"source": [
"# Cross-Chain Balance Verification\n",
- "# Prüfe USDC Balances auf BEIDEN Networks um sicherzustellen, dass Settlement korrekt war\n",
+ "# Prüfe USDC Balances auf ALLEN Networks um sicherzustellen, dass Settlement korrekt war\n",
"\n",
"from web3 import Web3\n",
"\n",
@@ -1203,7 +1208,7 @@
" }\n",
"]\n",
"\n",
- "# Network configurations\n",
+ "# Network configurations - all 4 supported networks\n",
"networks = {\n",
" \"eip155:10\": {\n",
" \"name\": \"Optimism Mainnet\",\n",
@@ -1214,13 +1219,45 @@
" \"name\": \"Optimism Sepolia\",\n",
" \"rpc\": \"https://sepolia.optimism.io\", \n",
" \"usdc\": \"0x5fd84259d66Cd46123540766Be93DFE6D43130D7\"\n",
+ " },\n",
+ " \"eip155:8453\": {\n",
+ " \"name\": \"Base Mainnet\",\n",
+ " \"rpc\": \"https://mainnet.base.org\",\n",
+ " \"usdc\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"\n",
+ " },\n",
+ " \"eip155:84532\": {\n",
+ " \"name\": \"Base Sepolia\",\n",
+ " \"rpc\": \"https://sepolia.base.org\",\n",
+ " \"usdc\": \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\"\n",
" }\n",
"}\n",
"\n",
"print(f\"🔍 Cross-Chain Balance Check für Recipient: {pay_to_address}\")\n",
"print(f\" Erwartetes Settlement-Network: {CAIP2_NETWORK}\\n\")\n",
"\n",
- "for network_id, config in networks.items():\n",
+ "# Group by chain type for better readability\n",
+ "print(\"📊 Optimism Networks:\")\n",
+ "for network_id in [\"eip155:10\", \"eip155:11155420\"]:\n",
+ " config = networks[network_id]\n",
+ " try:\n",
+ " w3 = Web3(Web3.HTTPProvider(config['rpc']))\n",
+ " usdc = w3.eth.contract(address=config['usdc'], abi=balance_abi)\n",
+ " balance = usdc.functions.balanceOf(pay_to_address).call()\n",
+ " balance_usdc = balance / 1e6\n",
+ " \n",
+ " # Markiere das erwartete Network\n",
+ " marker = \"← Erwartet\" if network_id == CAIP2_NETWORK else \"\"\n",
+ " # Markiere wenn Response ein anderes Network anzeigte\n",
+ " if 'response_network' in dir() and response_network == network_id and network_id != CAIP2_NETWORK:\n",
+ " marker = \"← ⚠️ SETTLEMENT HIER!\"\n",
+ " \n",
+ " print(f\" {config['name']} ({network_id}): ${balance_usdc:.2f} USDC {marker}\")\n",
+ " except Exception as e:\n",
+ " print(f\" {config['name']}: Fehler - {e}\")\n",
+ "\n",
+ "print(\"\\n📊 Base Networks:\")\n",
+ "for network_id in [\"eip155:8453\", \"eip155:84532\"]:\n",
+ " config = networks[network_id]\n",
" try:\n",
" w3 = Web3(Web3.HTTPProvider(config['rpc']))\n",
" usdc = w3.eth.contract(address=config['usdc'], abi=balance_abi)\n",
@@ -1243,7 +1280,7 @@
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 38,
"id": "81bfd50f",
"metadata": {},
"outputs": [
@@ -1251,8 +1288,23 @@
"name": "stdout",
"output_type": "stream",
"text": [
+ "💰 Checking USDC Balances on Base Sepolia (Testnet)...\n",
+ "\n",
+ "📊 USDC Balances:\n",
+ " Payer (0x55317955...): $0.000000 USDC\n",
+ " Recipient (0xAAEBC144...): $0.000000 USDC\n",
+ " Facilitator (0x3F8d2Fb6...): $0.000000 USDC\n",
+ "\n",
+ "💡 Warum erscheint die Transaktion nicht in MetaMask Activity?\n",
+ " • TransferWithAuthorization wird vom Facilitator ausgeführt\n",
+ " • MetaMask zeigt nur selbst gesendete Transaktionen\n",
+ " • Die USDC wurden trotzdem korrekt transferiert!\n",
+ " • Prüfe den Token-Balance in MetaMask (nicht die Activity)\n",
"\n",
- "⚠️ No transaction hash available\n"
+ "🔍 So siehst du den Transfer:\n",
+ " 1. Öffne Etherscan: https://sepolia.basescan.org/tx/0x6379e34692892294bc8edc93159200b9232be913c2ab93fcd902e06fe3d25781\n",
+ " 2. Klicke auf 'Logs' Tab um die Transfer Events zu sehen\n",
+ " 3. Oder gehe zu Token Transfers auf der Payer/Recipient Adresse\n"
]
}
],
From 95280ebfea914e13739c13813615cb17b90174ea Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 08:06:01 +0100
Subject: [PATCH 24/46] Fix the tests and npm run check
---
shared/chain-utils/src/addresses.ts | 10 ++++++++--
shared/chain-utils/test/index.test.ts | 15 +++++++--------
2 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index cbb8d281f..d406e7d80 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -146,7 +146,9 @@ export function getCollectorNFTAddress(network: string): `0x${string}` {
* Try to get CollectorNFT address, returns null if not deployed.
*/
export function tryGetCollectorNFTAddress(network: string): `0x${string}` | null {
- return MAINNET_COLLECTOR_NFT_ADDRESSES[network] || TESTNET_COLLECTOR_NFT_ADDRESSES[network] || null;
+ return (
+ MAINNET_COLLECTOR_NFT_ADDRESSES[network] || TESTNET_COLLECTOR_NFT_ADDRESSES[network] || null
+ );
}
/**
@@ -204,7 +206,11 @@ export function getEIP3009SplitterAddress(network: string): `0x${string}` {
* Try to get EIP3009 Splitter address, returns null if not deployed.
*/
export function tryGetEIP3009SplitterAddress(network: string): `0x${string}` | null {
- return MAINNET_EIP3009_SPLITTER_ADDRESSES[network] || TESTNET_EIP3009_SPLITTER_ADDRESSES[network] || null;
+ return (
+ MAINNET_EIP3009_SPLITTER_ADDRESSES[network] ||
+ TESTNET_EIP3009_SPLITTER_ADDRESSES[network] ||
+ null
+ );
}
/**
diff --git a/shared/chain-utils/test/index.test.ts b/shared/chain-utils/test/index.test.ts
index a3eb8f51b..f4cdd2446 100644
--- a/shared/chain-utils/test/index.test.ts
+++ b/shared/chain-utils/test/index.test.ts
@@ -165,10 +165,10 @@ describe("@fretchen/chain-utils", () => {
describe("getLLMv1Address()", () => {
test("should return addresses for all deployed networks", () => {
// Mainnet
- expect(getLLMv1Address("eip155:10")).toBe("0x7E8b7091a229B1004c4FBa25bB70d04595d3e848");
+ expect(getLLMv1Address("eip155:10")).toBe("0x833F39D6e67390324796f861990ce9B7cf9F5dE1");
// Testnet
expect(getLLMv1Address("eip155:11155420")).toBe(
- "0xA5b7f0A3f4104c97b46eafF2b0b4A457C5a73Bf4"
+ "0xB3dbD44477a7bcf253f2fA68eDb4be5aF2F2cA56"
);
});
@@ -206,13 +206,9 @@ describe("@fretchen/chain-utils", () => {
describe("getEIP3009SplitterAddress()", () => {
test("should return addresses for all deployed networks", () => {
- // Mainnet
- expect(getEIP3009SplitterAddress("eip155:10")).toBe(
- "0x4a0EA6E7A8B23C95Da07d59a8e36E9c5C5f6c5Bf"
- );
- // Testnet
+ // Testnet only (Mainnet not yet deployed)
expect(getEIP3009SplitterAddress("eip155:11155420")).toBe(
- "0x7F2b5E60e26B31E32c40F48e0e7D1CA5E62C5b7a"
+ "0x7e67bf96ADbf4a813DD7b0A3Ca3060a937018946"
);
});
@@ -220,6 +216,9 @@ describe("@fretchen/chain-utils", () => {
expect(() => getEIP3009SplitterAddress("eip155:1")).toThrow(
"EIP3009 Splitter not deployed on eip155:1"
);
+ expect(() => getEIP3009SplitterAddress("eip155:10")).toThrow(
+ "EIP3009 Splitter not deployed on eip155:10"
+ );
expect(() => getEIP3009SplitterAddress("eip155:8453")).toThrow(
"EIP3009 Splitter not deployed on eip155:8453"
);
From 6339d15bd87e83dc5157dbfe0e9bafa5749dd98c Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 08:10:20 +0100
Subject: [PATCH 25/46] Clean up mode
---
x402_facilitator/package.json | 2 +-
x402_facilitator/test/x402_facilitator.test.ts | 2 --
x402_facilitator/x402_splitter_verify.js | 9 ++++++---
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/x402_facilitator/package.json b/x402_facilitator/package.json
index ce6ee96c2..42ff36e50 100644
--- a/x402_facilitator/package.json
+++ b/x402_facilitator/package.json
@@ -1,7 +1,7 @@
{
"name": "x402-facilitator",
"version": "1.0.0",
- "description": "x402 v2 Payment Facilitator for Optimism",
+ "description": "x402 v2 Payment Facilitator for Optimism and Base",
"type": "module",
"main": "dist/x402_facilitator.js",
"scripts": {
diff --git a/x402_facilitator/test/x402_facilitator.test.ts b/x402_facilitator/test/x402_facilitator.test.ts
index 1774b8ad4..6f5cafa20 100644
--- a/x402_facilitator/test/x402_facilitator.test.ts
+++ b/x402_facilitator/test/x402_facilitator.test.ts
@@ -1,5 +1,3 @@
-// @ts-check
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
handleVerify,
diff --git a/x402_facilitator/x402_splitter_verify.js b/x402_facilitator/x402_splitter_verify.js
index e29de5e8b..dcd314d38 100644
--- a/x402_facilitator/x402_splitter_verify.js
+++ b/x402_facilitator/x402_splitter_verify.js
@@ -14,7 +14,12 @@
import { verifyTypedData, createPublicClient, http } from "viem";
import pino from "pino";
-import { getViemChain, getUSDCName, getUSDCAddress } from "@fretchen/chain-utils";
+import {
+ getViemChain,
+ getUSDCName,
+ getUSDCAddress,
+ getEIP3009SplitterAddress,
+} from "@fretchen/chain-utils";
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
@@ -140,8 +145,6 @@ export async function verifySplitterPayment(paymentPayload, paymentRequirements)
// Validate recipient address matches splitter contract
// For splitter facilitator: to = splitter address (not token address!)
// The splitter contract will receive the funds and split them
- const { getEIP3009SplitterAddress } = await import("@fretchen/chain-utils");
-
let splitterAddress;
try {
splitterAddress = getEIP3009SplitterAddress(network);
From 25ceaecd655eeb0083c8ce54c0ea175949931e35 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 17:29:49 +0100
Subject: [PATCH 26/46] Update MULTICHAIN_EXPANSION_PROPOSAL.md
---
website/MULTICHAIN_EXPANSION_PROPOSAL.md | 333 ++++++++++++++++++++---
1 file changed, 299 insertions(+), 34 deletions(-)
diff --git a/website/MULTICHAIN_EXPANSION_PROPOSAL.md b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
index 4a3394516..b134158f8 100644
--- a/website/MULTICHAIN_EXPANSION_PROPOSAL.md
+++ b/website/MULTICHAIN_EXPANSION_PROPOSAL.md
@@ -20,8 +20,8 @@
|-------|-----|----------|--------|
| **1a** | `@fretchen/chain-utils` erstellen | shared/ | ✅ Fertig |
| **1b** | scw_js auf chain-utils migrieren | scw_js/ | ✅ Fertig |
-| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ⬜ Next |
-| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ |
+| **1c** | x402_facilitator auf chain-utils migrieren | x402_facilitator/ | ✅ Fertig |
+| **2** | GenImNFT-Komponenten migrieren | website/ | ⬜ Next |
| **3** | CollectorNFT-Komponenten migrieren | website/ | ⬜ |
| **4** | GenImNFTv4 auf Base deployen | eth/, shared/ | ⬜ Später |
| **5** | CollectorNFTv1 auf Base deployen | eth/, shared/ | ⬜ Später |
@@ -86,16 +86,24 @@ fretchen.github.io/
---
-## Phase 1c: x402_facilitator Migration ⬜ NEXT
+## Phase 1c: x402_facilitator Migration ✅ FERTIG
-**Geplant:**
-- `chain_utils.js` durch Imports aus `@fretchen/chain-utils` ersetzen
-- tsup Bundling hinzufügen (gleiches Pattern wie scw_js)
-- ABIs aus chain-utils importieren {
- "@fretchen/chain-utils": "file:../shared/chain-utils"
- }
-}
-```
+**Änderungen:**
+- `package.json`: Dependency `"@fretchen/chain-utils": "file:../shared/chain-utils"`
+- `tsup.config.js`: Bundling mit tsup für Scaleway Deployment
+- `chain_utils.js`: Nutzt `getViemChain()`, `tryGetGenAiNFTAddress()`, `tryGetLLMv1Address()`, `getUSDCAddress()`, `getUSDCName()` aus chain-utils
+- `x402_splitter_verify.js`: Importiert `getViemChain`, `getUSDCName`, `getUSDCAddress`, `getEIP3009SplitterAddress` aus chain-utils
+- `x402_splitter_settle.js`: Importiert `EIP3009SplitterV1ABI`, `getEIP3009SplitterAddress`, `getViemChain`, `getUSDCAddress` aus chain-utils
+- `x402_whitelist.js`: Nutzt `getChainConfig()` für Contract-Adressen
+- `facilitator_instance.js`: Nutzt `getSupportedNetworks()` aus chain_utils.js
+
+**Unterstützte Netzwerke:**
+- Optimism Mainnet (`eip155:10`)
+- Optimism Sepolia (`eip155:11155420`)
+- Base Mainnet (`eip155:8453`)
+- Base Sepolia (`eip155:84532`)
+
+**Tests:** 153 Tests bestanden, 73.47% Coverage
---
@@ -165,37 +173,294 @@ export function getChainConfig(network) {
---
-## Phase 2: GenImNFT-Komponenten migrieren (Website)
+## Phase 2: GenImNFT Website Components Migration ⬜ NEXT
+
+This phase performs a **clean break** from the existing `utils/getChain.ts` to consistently use `@fretchen/chain-utils`. The goal is simplicity over backward compatibility.
+
+### Implementation Strategy: Two PRs
+
+Phase 2 is split into two PRs to minimize risk and allow staged deployment:
+
+| PR | Name | Content | Breaking? | Deployable? |
+|----|------|---------|-----------|-------------|
+| **2a** | Add chain-utils infrastructure | Dependency + `useAutoNetwork` hook + re-exports (keep old) | ❌ No | ✅ Yes |
+| **2b** | Migrate GenImNFT components | All component migrations + remove old exports | ⚠️ Yes | ✅ Yes |
+
+**Why two PRs:**
+1. **PR 2a is low-risk:** Adds foundation without changing behavior. Verifiable via build + tests.
+2. **PR 2b is atomic:** Components are tightly coupled — can't have half on old API, half on new.
+3. **Clear rollback point:** If PR 2b causes issues, revert to PR 2a state.
+
+---
+
+### PR 2a: Add chain-utils Infrastructure
+
+**Files to create/modify:**
+- `package.json` — Add @fretchen/chain-utils dependency
+- `hooks/useAutoNetwork.ts` — **CREATE NEW**
+- `utils/getChain.ts` — Add re-exports (keep old exports)
+- `utils/nodeChainUtils.ts` — Add `getDefaultNetwork()`
+
+**Acceptance criteria:**
+- [ ] `npm run build` passes
+- [ ] `npm test` passes
+- [ ] No behavior change in production
+
+---
+
+### PR 2b: Migrate GenImNFT Components
+
+**Files to modify:**
+- `utils/getChain.ts` — Remove old exports
+- `utils/nftLoader.ts` — Use chain-utils
+- `utils/nodeNftLoader.ts` — Use chain-utils
+- `components/MyNFTList.tsx` — Use `useAutoNetwork()`
+- `components/NFTCard.tsx` — Use `useAutoNetwork()` + `getGenAiNFTAddress()`
+- `components/NFTList.tsx` — Add network prop
+- `components/PublicNFTList.tsx` — Add network prop
+- `components/EntryNftImage.tsx` — Use `getDefaultNetwork()` for SSR
+- `components/NFTFloatImage.tsx` — Update to use network
+- `components/ImageGenerator.tsx` — Remove hardcoded chain ID
+- `components/AgentInfoPanel.tsx` — Use `useAutoNetwork()`
+- `test/*.test.tsx` — Update mocks
+
+**Acceptance criteria:**
+- [ ] `npm run build` passes
+- [ ] `npm test` passes
+- [ ] Manual test: Connect wallet on Optimism → NFTs load
+- [ ] Manual test: Connect wallet on unsupported chain → Auto-switches to Optimism
+
+---
+
+### Step 0: Add chain-utils Dependency
+
+**File:** `website/package.json`
+
+```bash
+npm install @fretchen/chain-utils@file:../shared/chain-utils
+```
+
+### Step 1: Create `useAutoNetwork` Hook (NEW)
+
+**File:** `website/hooks/useAutoNetwork.ts`
+
+This hook replaces the scattered chain detection logic with a centralized, reusable pattern.
+
+**Behavior:**
+- Detects user's connected wallet chain
+- If chain is in `supportedNetworks` → return CAIP-2 network string
+- If chain is NOT supported → automatically switch to default chain (first in list)
+- If no wallet connected → return default network
+
+```typescript
+import { useAccount, useChainId, useSwitchChain } from "wagmi";
+import { useEffect, useMemo } from "react";
+import { toCAIP2, fromCAIP2 } from "@fretchen/chain-utils";
+
+/**
+ * Returns the current CAIP-2 network if supported, otherwise switches to default.
+ * @param supportedNetworks - Array of CAIP-2 network strings (e.g., ["eip155:10", "eip155:11155420"])
+ * @returns Current CAIP-2 network string
+ */
+export function useAutoNetwork(supportedNetworks: string[]): string {
+ const chainId = useChainId();
+ const { isConnected } = useAccount();
+ const { switchChain } = useSwitchChain();
+
+ const defaultNetwork = supportedNetworks[0];
+ const currentNetwork = toCAIP2(chainId);
+ const isSupported = supportedNetworks.includes(currentNetwork);
+
+ useEffect(() => {
+ if (isConnected && !isSupported && switchChain) {
+ switchChain({ chainId: fromCAIP2(defaultNetwork) });
+ }
+ }, [isConnected, isSupported, switchChain, defaultNetwork]);
+
+ return isSupported ? currentNetwork : defaultNetwork;
+}
+```
+
+### Step 2: Replace `utils/getChain.ts` (BREAKING CHANGE)
+
+**File:** `website/utils/getChain.ts`
+
+Delete all legacy code and replace with thin re-exports from chain-utils. This is a **clean break** - components must be updated to use the new pattern.
+
+```typescript
+// Re-export everything from chain-utils for convenience
+export {
+ toCAIP2,
+ fromCAIP2,
+ getViemChain,
+ isMainnet,
+ isTestnet,
+ getGenAiNFTAddress,
+ getCollectorNFTAddress,
+ GenImNFTv4ABI,
+ GENAI_NFT_NETWORKS,
+ COLLECTOR_NFT_NETWORKS,
+} from "@fretchen/chain-utils";
+
+// REMOVED: getChain(), genAiNFTContractConfig, collectorNFTContractConfig
+// Use useAutoNetwork() + getGenAiNFTAddress(network) instead
+```
+
+### Step 3: Update `utils/nodeChainUtils.ts` (Server-Side)
+
+**File:** `website/utils/nodeChainUtils.ts`
+
+For SSR/server-side rendering, we need a non-hook version:
+
+```typescript
+import {
+ getViemChain,
+ getGenAiNFTAddress,
+ isMainnet
+} from "@fretchen/chain-utils";
+
+/**
+ * Get default network for server-side rendering.
+ * Uses mainnet for production, testnet for development.
+ */
+export function getDefaultNetwork(): string {
+ const isProd = process.env.NODE_ENV === "production";
+ return isProd ? "eip155:10" : "eip155:11155420";
+}
+
+// Re-export chain-utils functions for server use
+export { getViemChain, getGenAiNFTAddress, isMainnet };
+```
+
+### Step 4: Update `utils/nftLoader.ts` (Viem-only)
+
+**File:** `website/utils/nftLoader.ts`
+
+```typescript
+// BEFORE
+import { getChain, genAiNFTContractConfig } from "./getChain";
+const chain = getChain();
+
+// AFTER
+import { getViemChain, getGenAiNFTAddress, GenImNFTv4ABI } from "@fretchen/chain-utils";
+
+export async function loadNFT(network: string, tokenId: bigint) {
+ const chain = getViemChain(network);
+ const client = createPublicClient({ chain, transport: http() });
+
+ return client.readContract({
+ address: getGenAiNFTAddress(network),
+ abi: GenImNFTv4ABI,
+ functionName: "tokenURI",
+ args: [tokenId],
+ });
+}
+```
+
+### Step 5: Update `utils/nodeNftLoader.ts` (SSR)
+
+**File:** `website/utils/nodeNftLoader.ts`
+
+Same pattern as nftLoader.ts but with explicit network parameter for SSR context.
+
+### Step 6: Migrate Components (8 files)
+
+Each component follows the same pattern:
+
+| File | Changes |
+|------|---------|
+| `components/MyNFTList.tsx` | Replace `getChain()` with `useAutoNetwork(GENAI_NFT_NETWORKS)` |
+| `components/NFTCard.tsx` | Replace `genAiNFTContractConfig` with `getGenAiNFTAddress(network)` + ABI |
+| `components/NFTList.tsx` | Add network parameter, use chain-utils |
+| `components/PublicNFTList.tsx` | Add network parameter |
+| `components/EntryNftImage.tsx` | Use `getDefaultNetwork()` for SSR |
+| `components/NFTFloatImage.tsx` | Update to use network prop |
+| `components/ImageGenerator.tsx` | **Remove hardcoded `11155420` check**, use `isTestnet(network)` |
+| `components/AgentInfoPanel.tsx` | Use `useAutoNetwork()` |
+
+**Example Migration (NFTCard.tsx):**
-**Betroffene Dateien:**
-- `MyNFTList.tsx` (4 Stellen)
-- `NFTCard.tsx` (8 Stellen)
-- `NFTList.tsx` (1 Stelle)
-- `EntryNftImage.tsx` (2 Stellen)
-- `PublicNFTList.tsx` (1 Stelle)
-- `nftLoader.ts` (2 Stellen)
-
-**Pattern-Änderung:**
```tsx
-// VORHER
+// BEFORE
import { getChain, genAiNFTContractConfig } from "../utils/getChain";
-const chain = getChain();
-const { data } = useReadContract({
- ...genAiNFTContractConfig,
- chainId: chain.id,
-});
-// NACHHER
+function NFTCard({ tokenId }) {
+ const chain = getChain();
+ const { data } = useReadContract({
+ ...genAiNFTContractConfig,
+ chainId: chain.id,
+ functionName: "tokenURI",
+ args: [tokenId],
+ });
+}
+
+// AFTER
import { useAutoNetwork } from "../hooks/useAutoNetwork";
-import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
-const network = useAutoNetwork(GENAI_NFT_NETWORKS);
-const { data } = useReadContract({
- address: getGenAiNFTAddress(network),
- abi: GenImNFTv4ABI,
-});
+function NFTCard({ tokenId }) {
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { data } = useReadContract({
+ address: getGenAiNFTAddress(network),
+ abi: GenImNFTv4ABI,
+ chainId: fromCAIP2(network),
+ functionName: "tokenURI",
+ args: [tokenId],
+ });
+}
```
+### Step 7: Update Tests
+
+**Files:**
+- `test/ContractChainSelection.test.ts`
+- `test/nftLoader.test.ts` (if exists)
+
+Update tests to use CAIP-2 networks and mock `useAutoNetwork`:
+
+```typescript
+import { vi } from "vitest";
+
+vi.mock("../hooks/useAutoNetwork", () => ({
+ useAutoNetwork: () => "eip155:11155420", // Mock testnet
+}));
+```
+
+---
+
+### Risk Assessment
+
+| Risk | Severity | Mitigation |
+|------|----------|------------|
+| **Breaking Change** | 🟡 Medium | All getChain() usages updated in PR 2b (atomic) |
+| **SSR Hydration Mismatch** | 🟡 Medium | Use `getDefaultNetwork()` for server, `useAutoNetwork()` for client |
+| **Hardcoded Chain IDs** | 🟢 Low | Grep for `11155420`, `10`, `8453`, `84532` and replace |
+| **Wagmi Hook Context** | 🟢 Low | `useAutoNetwork` only used in components with WagmiProvider |
+
+### Checklists by PR
+
+**PR 2a: Infrastructure (non-breaking)**
+- [ ] `package.json` - Add @fretchen/chain-utils dependency
+- [ ] `hooks/useAutoNetwork.ts` - **CREATE NEW**
+- [ ] `utils/getChain.ts` - Add re-exports (keep old exports)
+- [ ] `utils/nodeChainUtils.ts` - Add `getDefaultNetwork()`
+
+**PR 2b: Component Migration (breaking)**
+- [ ] `utils/getChain.ts` - Remove old exports (`getChain`, `genAiNFTContractConfig`)
+- [ ] `utils/nftLoader.ts` - Use chain-utils
+- [ ] `utils/nodeNftLoader.ts` - Use chain-utils
+- [ ] `components/MyNFTList.tsx` - Use `useAutoNetwork()`
+- [ ] `components/NFTCard.tsx` - Use `useAutoNetwork()` + `getGenAiNFTAddress()`
+- [ ] `components/NFTList.tsx` - Add network prop
+- [ ] `components/PublicNFTList.tsx` - Add network prop
+- [ ] `components/EntryNftImage.tsx` - Use `getDefaultNetwork()` for SSR
+- [ ] `components/NFTFloatImage.tsx` - Update to use network
+- [ ] `components/ImageGenerator.tsx` - Remove hardcoded chain ID
+- [ ] `components/AgentInfoPanel.tsx` - Use `useAutoNetwork()`
+- [ ] `test/ContractChainSelection.test.ts` - Update mocks
+- [ ] `test/MyNFTList.test.tsx` - Update mocks
+- [ ] `test/ImageGenerator.test.tsx` - Update if needed
+
---
## Phase 3: CollectorNFT-Komponenten migrieren
From ba520f72cbc9259edbc220ba240c4d7a2ca2bc57 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 17:37:58 +0100
Subject: [PATCH 27/46] Set up the useAutoNetwork hook
---
website/hooks/useAutoNetwork.ts | 44 ++++++++
website/package-lock.json | 24 +++++
website/package.json | 1 +
website/test/useAutoNetwork.test.ts | 161 ++++++++++++++++++++++++++++
website/utils/getChain.ts | 41 +++++++
website/utils/nodeChainUtils.ts | 32 ++++++
6 files changed, 303 insertions(+)
create mode 100644 website/hooks/useAutoNetwork.ts
create mode 100644 website/test/useAutoNetwork.test.ts
diff --git a/website/hooks/useAutoNetwork.ts b/website/hooks/useAutoNetwork.ts
new file mode 100644
index 000000000..1689b8c9b
--- /dev/null
+++ b/website/hooks/useAutoNetwork.ts
@@ -0,0 +1,44 @@
+/**
+ * Hook for automatic network detection and switching.
+ *
+ * Detects the user's connected wallet chain and returns the CAIP-2 network string.
+ * If the chain is not in the supported networks, automatically switches to the default.
+ */
+
+import { useAccount, useChainId, useSwitchChain } from "wagmi";
+import { useEffect } from "react";
+import { toCAIP2, fromCAIP2 } from "@fretchen/chain-utils";
+
+/**
+ * Returns the current CAIP-2 network if supported, otherwise switches to default.
+ *
+ * @param supportedNetworks - Array of CAIP-2 network strings (e.g., ["eip155:10", "eip155:11155420"])
+ * @returns Current CAIP-2 network string
+ *
+ * @example
+ * ```tsx
+ * import { GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+ *
+ * function MyComponent() {
+ * const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ * // network is "eip155:10" or "eip155:11155420"
+ * }
+ * ```
+ */
+export function useAutoNetwork(supportedNetworks: readonly string[]): string {
+ const chainId = useChainId();
+ const { isConnected } = useAccount();
+ const { switchChain } = useSwitchChain();
+
+ const defaultNetwork = supportedNetworks[0];
+ const currentNetwork = toCAIP2(chainId);
+ const isSupported = supportedNetworks.includes(currentNetwork);
+
+ useEffect(() => {
+ if (isConnected && !isSupported && switchChain) {
+ switchChain({ chainId: fromCAIP2(defaultNetwork) });
+ }
+ }, [isConnected, isSupported, switchChain, defaultNetwork]);
+
+ return isSupported ? currentNetwork : defaultNetwork;
+}
diff --git a/website/package-lock.json b/website/package-lock.json
index 470d2c834..82b7f4a5c 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mdx-js/rollup": "^3.1.1",
@@ -60,6 +61,25 @@
"vitest": "^4.0.1"
}
},
+ "../shared/chain-utils": {
+ "name": "@fretchen/chain-utils",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.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": "^4.0.18"
+ },
+ "peerDependencies": {
+ "viem": "^2.0.0"
+ }
+ },
"node_modules/@adobe/css-tools": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz",
@@ -1698,6 +1718,10 @@
"@scure/bip39": "1.3.0"
}
},
+ "node_modules/@fretchen/chain-utils": {
+ "resolved": "../shared/chain-utils",
+ "link": true
+ },
"node_modules/@gemini-wallet/core": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@gemini-wallet/core/-/core-0.2.0.tgz",
diff --git a/website/package.json b/website/package.json
index c8c6a29f9..bc3fb16ae 100644
--- a/website/package.json
+++ b/website/package.json
@@ -15,6 +15,7 @@
"send-webmentions": "tsx ./utils/sendWebmentions.ts"
},
"dependencies": {
+ "@fretchen/chain-utils": "file:../shared/chain-utils",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@mdx-js/rollup": "^3.1.1",
diff --git a/website/test/useAutoNetwork.test.ts b/website/test/useAutoNetwork.test.ts
new file mode 100644
index 000000000..50218092f
--- /dev/null
+++ b/website/test/useAutoNetwork.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderHook } from "@testing-library/react";
+
+// Mock wagmi hooks
+const mockSwitchChain = vi.fn();
+vi.mock("wagmi", () => ({
+ useChainId: vi.fn(() => 10), // Default: Optimism mainnet
+ useAccount: vi.fn(() => ({ isConnected: true })),
+ useSwitchChain: vi.fn(() => ({ switchChain: mockSwitchChain })),
+}));
+
+// Mock chain-utils
+vi.mock("@fretchen/chain-utils", () => ({
+ toCAIP2: (chainId: number) => `eip155:${chainId}`,
+ fromCAIP2: (network: string) => parseInt(network.split(":")[1]),
+}));
+
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { useChainId, useAccount } from "wagmi";
+
+describe("useAutoNetwork Hook", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("when on a supported network", () => {
+ it("should return the current network as CAIP-2", () => {
+ vi.mocked(useChainId).mockReturnValue(10); // Optimism
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toBe("eip155:10");
+ expect(mockSwitchChain).not.toHaveBeenCalled();
+ });
+
+ it("should return testnet network when on testnet", () => {
+ vi.mocked(useChainId).mockReturnValue(11155420); // Optimism Sepolia
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toBe("eip155:11155420");
+ expect(mockSwitchChain).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("when on an unsupported network", () => {
+ it("should switch to default network when connected", () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Ethereum mainnet (unsupported)
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 10 });
+ });
+
+ it("should return default network while switching", () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Ethereum mainnet (unsupported)
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ // Returns default even though switch is pending
+ expect(result.current).toBe("eip155:10");
+ });
+
+ it("should switch to first network in list as default", () => {
+ vi.mocked(useChainId).mockReturnValue(137); // Polygon (unsupported)
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ // First network is testnet
+ const supportedNetworks = ["eip155:11155420", "eip155:10"];
+ renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 11155420 });
+ });
+ });
+
+ describe("when wallet is not connected", () => {
+ it("should not attempt to switch chain", () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Unsupported chain
+ vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(mockSwitchChain).not.toHaveBeenCalled();
+ });
+
+ it("should return default network", () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Unsupported chain
+ vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toBe("eip155:10");
+ });
+ });
+
+ describe("chain switching behavior", () => {
+ it("should only switch once per render cycle", () => {
+ vi.mocked(useChainId).mockReturnValue(1);
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10"];
+ const { rerender } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ // Initial switch
+ expect(mockSwitchChain).toHaveBeenCalledTimes(1);
+
+ // Re-render with same props should not trigger another switch
+ rerender();
+ expect(mockSwitchChain).toHaveBeenCalledTimes(1);
+ });
+
+ it("should switch when user connects wallet on unsupported chain", () => {
+ vi.mocked(useChainId).mockReturnValue(1);
+ vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
+
+ const supportedNetworks = ["eip155:10"];
+ const { rerender } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(mockSwitchChain).not.toHaveBeenCalled();
+
+ // User connects wallet
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+ rerender();
+
+ expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 10 });
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should handle single network in list", () => {
+ vi.mocked(useChainId).mockReturnValue(10);
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toBe("eip155:10");
+ });
+
+ it("should handle Base network", () => {
+ vi.mocked(useChainId).mockReturnValue(8453); // Base mainnet
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:10", "eip155:8453"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toBe("eip155:8453");
+ expect(mockSwitchChain).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/website/utils/getChain.ts b/website/utils/getChain.ts
index 3e152c5d1..8f45e8cd8 100644
--- a/website/utils/getChain.ts
+++ b/website/utils/getChain.ts
@@ -5,6 +5,47 @@ import GenImNFTv3ABI from "../../eth/abi/contracts/GenImNFTv3.json";
import SupportV2ABI from "../../eth/abi/contracts/SupportV2.json";
import LLMv1ABI from "../../eth/abi/contracts/LLMv1.json";
+// ═══════════════════════════════════════════════════════════════
+// Re-exports from @fretchen/chain-utils (PR 2a: Infrastructure)
+// These are the new patterns - use these in new code
+// ═══════════════════════════════════════════════════════════════
+export {
+ // CAIP-2 utilities
+ toCAIP2,
+ fromCAIP2,
+ isMainnet,
+ isTestnet,
+ isNetworkSupported,
+ // Chain utilities
+ getViemChain,
+ // Contract addresses (GenAI NFT)
+ getGenAiNFTAddress,
+ tryGetGenAiNFTAddress,
+ GENAI_NFT_NETWORKS,
+ MAINNET_GENAI_NFT_ADDRESSES,
+ TESTNET_GENAI_NFT_ADDRESSES,
+ // Contract addresses (Collector NFT)
+ getCollectorNFTAddress,
+ tryGetCollectorNFTAddress,
+ COLLECTOR_NFT_NETWORKS,
+ MAINNET_COLLECTOR_NFT_ADDRESSES,
+ // Contract addresses (LLMv1)
+ getLLMv1Address,
+ tryGetLLMv1Address,
+ LLM_V1_NETWORKS,
+ // ABIs
+ GenImNFTv4ABI,
+ LLMv1ABI as ChainUtilsLLMv1ABI,
+ // Network lists
+ ALL_NETWORKS,
+ MAINNET_NETWORKS,
+ TESTNET_NETWORKS,
+} from "@fretchen/chain-utils";
+
+// ═══════════════════════════════════════════════════════════════
+// Legacy exports (PR 2b will remove these)
+// ═══════════════════════════════════════════════════════════════
+
/**
* Get PUBLIC_ENV__CHAIN_NAME in Vite context (Browser)
* Direct inline implementation for simplicity
diff --git a/website/utils/nodeChainUtils.ts b/website/utils/nodeChainUtils.ts
index c9784cb34..1b277999c 100644
--- a/website/utils/nodeChainUtils.ts
+++ b/website/utils/nodeChainUtils.ts
@@ -10,6 +10,38 @@ import CollectorNFTv1ABI from "../../eth/abi/contracts/CollectorNFTv1.json";
import GenImNFTv3ABI from "../../eth/abi/contracts/GenImNFTv3.json";
import LLMv1ABI from "../../eth/abi/contracts/LLMv1.json";
+// ═══════════════════════════════════════════════════════════════
+// Re-exports from @fretchen/chain-utils (PR 2a: Infrastructure)
+// ═══════════════════════════════════════════════════════════════
+export {
+ getViemChain,
+ getGenAiNFTAddress,
+ tryGetGenAiNFTAddress,
+ isMainnet,
+ isTestnet,
+ GenImNFTv4ABI,
+ GENAI_NFT_NETWORKS,
+} from "@fretchen/chain-utils";
+
+// ═══════════════════════════════════════════════════════════════
+// SSR/Node.js specific utilities
+// ═══════════════════════════════════════════════════════════════
+
+/**
+ * Get default CAIP-2 network for server-side rendering.
+ * Uses mainnet for production, testnet for development.
+ *
+ * @returns CAIP-2 network string (e.g., "eip155:10")
+ */
+export function getDefaultNetwork(): string {
+ const isProd = process.env.NODE_ENV === "production";
+ return isProd ? "eip155:10" : "eip155:11155420";
+}
+
+// ═══════════════════════════════════════════════════════════════
+// Legacy exports (PR 2b may refactor these)
+// ═══════════════════════════════════════════════════════════════
+
/**
* Get environment variable in Node.js context
*/
From c587fbd0919ef325bfd78b72b474023934c5d98e Mon Sep 17 00:00:00 2001
From: fretchen
Date: Wed, 28 Jan 2026 17:52:59 +0100
Subject: [PATCH 28/46] Did majore updates to the GenImg
---
shared/chain-utils/src/addresses.ts | 6 ++
website/components/AgentInfoPanel.tsx | 9 ++-
website/components/EntryNftImage.tsx | 11 +--
website/components/ImageGenerator.tsx | 23 ++++---
website/components/MyNFTList.tsx | 22 +++---
website/components/NFTCard.tsx | 35 ++++++----
website/components/NFTFloatImage.tsx | 14 ++--
website/components/NFTList.tsx | 13 +++-
website/components/PublicNFTList.tsx | 13 ++--
website/hooks/useConfiguredPublicClient.ts | 11 +--
website/test/ContractChainSelection.test.ts | 22 ++++--
website/test/MyNFTList.test.tsx | 67 +++++++++----------
.../test/useConfiguredPublicClient.test.ts | 18 +++--
website/utils/getChain.ts | 16 ++++-
website/utils/nftLoader.ts | 21 ++++--
website/utils/nodeNftLoader.ts | 25 +++++--
16 files changed, 209 insertions(+), 117 deletions(-)
diff --git a/shared/chain-utils/src/addresses.ts b/shared/chain-utils/src/addresses.ts
index d406e7d80..fee1dace5 100644
--- a/shared/chain-utils/src/addresses.ts
+++ b/shared/chain-utils/src/addresses.ts
@@ -74,6 +74,12 @@ export const TESTNET_LLM_V1_ADDRESSES: Record = {
"eip155:11155420": "0xB3dbD44477a7bcf253f2fA68eDb4be5aF2F2cA56", // Optimism Sepolia
};
+/** All networks where LLMv1 is deployed */
+export const LLM_V1_NETWORKS = [
+ ...Object.keys(MAINNET_LLM_V1_ADDRESSES),
+ ...Object.keys(TESTNET_LLM_V1_ADDRESSES),
+] as const;
+
// ═══════════════════════════════════════════════════════════════
// USDC (available on all chains)
// ═══════════════════════════════════════════════════════════════
diff --git a/website/components/AgentInfoPanel.tsx b/website/components/AgentInfoPanel.tsx
index d44b7cc2d..95d92af94 100644
--- a/website/components/AgentInfoPanel.tsx
+++ b/website/components/AgentInfoPanel.tsx
@@ -18,7 +18,8 @@ import React, { useState } from "react";
import { css } from "../styled-system/css";
import { useAgentInfo } from "../hooks/useAgentInfo";
import { useLocale } from "../hooks/useLocale";
-import { genAiNFTContractConfig, llmV1ContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, getLLMv1Address, GENAI_NFT_NETWORKS, LLM_V1_NETWORKS } from "@fretchen/chain-utils";
interface AgentInfoPanelProps {
// Service context (for display purposes)
@@ -68,7 +69,11 @@ export function AgentInfoPanel({ service = "genimg", variant = "footer" }: Agent
const serviceEndpoint = service === "genimg" ? agent.genimgEndpoint : agent.llmEndpoint;
const serviceHostname = serviceEndpoint ? new URL(serviceEndpoint).hostname : null;
- const contractAddress = service === "genimg" ? genAiNFTContractConfig.address : llmV1ContractConfig.address;
+
+ // Get contract address based on service - use appropriate network list
+ const genimgNetwork = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const llmNetwork = useAutoNetwork(LLM_V1_NETWORKS);
+ const contractAddress = service === "genimg" ? getGenAiNFTAddress(genimgNetwork) : getLLMv1Address(llmNetwork);
// Sidebar variant - vertical layout
if (isSidebar) {
diff --git a/website/components/EntryNftImage.tsx b/website/components/EntryNftImage.tsx
index cccbc4bb5..77ec8598d 100644
--- a/website/components/EntryNftImage.tsx
+++ b/website/components/EntryNftImage.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
import { entryList } from "../layouts/styles";
-import { genAiNFTContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { NFTMetadata } from "../types/components";
@@ -16,7 +17,9 @@ interface EntryNftImageProps {
export const EntryNftImage: React.FC = ({ tokenId, fallbackImageUrl, nftName }) => {
const [imageUrl, setImageUrl] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
- const publicClient = useConfiguredPublicClient();
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const contractAddress = getGenAiNFTAddress(network);
+ const publicClient = useConfiguredPublicClient(network);
// Fetch metadata from tokenURI
const fetchNFTMetadata = async (tokenURI: string): Promise => {
@@ -53,8 +56,8 @@ export const EntryNftImage: React.FC = ({ tokenId, fallbackI
if (tokenId && publicClient) {
// Get token URI using public client (same as NFTFloatImage)
const tokenURIResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "tokenURI",
args: [BigInt(tokenId)],
});
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index 0fd40438c..fd175d073 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { useAccount, useConnect, useSwitchChain, useChainId } from "wagmi";
import { css } from "../styled-system/css";
-import { getChain } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { GENAI_NFT_NETWORKS, fromCAIP2, isTestnet, getViemChain } from "@fretchen/chain-utils";
import { ImageGeneratorProps } from "../types/components";
import * as styles from "../layouts/styles";
import InfoIcon from "./InfoIcon";
@@ -113,9 +114,11 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const { switchChainAsync, isPending: isSwitchingChain } = useSwitchChain();
const currentChainId = useChainId();
- // Determine target chain from centralized config (PUBLIC_ENV__CHAIN_NAME)
- const targetChain = getChain();
- const useTestnet = targetChain.id === 11155420; // optimismSepolia.id
+ // Determine target chain from useAutoNetwork
+ const network = 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";
@@ -273,10 +276,10 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
// === Automatic Chain Switch ===
// Ensure user is on the correct chain before making payment
- if (currentChainId !== targetChain.id) {
- console.log(`[x402] Chain mismatch: current=${currentChainId}, target=${targetChain.id} (${targetChain.name})`);
+ if (currentChainId !== targetChainId) {
+ console.log(`[x402] Chain mismatch: current=${currentChainId}, target=${targetChainId} (${targetChain.name})`);
try {
- await switchChainAsync({ chainId: targetChain.id });
+ await switchChainAsync({ chainId: targetChainId });
console.log(`[x402] Successfully switched to ${targetChain.name}`);
} catch (switchError) {
console.error("[x402] Chain switch failed:", switchError);
@@ -302,10 +305,10 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
size,
mode,
referenceImage: isEditMode ? referenceImageBase64 : undefined,
- // Use testnet: derived from PUBLIC_ENV__CHAIN_NAME
- sepoliaTest: useTestnet,
+ // Use testnet: derived from useAutoNetwork
+ sepoliaTest: useTestnetFlag,
// Pass expected chain ID for validation in hook
- expectedChainId: targetChain.id,
+ expectedChainId: targetChainId,
// Whether to list in public gallery
isListed,
});
diff --git a/website/components/MyNFTList.tsx b/website/components/MyNFTList.tsx
index ad86b8457..c6e766ec3 100644
--- a/website/components/MyNFTList.tsx
+++ b/website/components/MyNFTList.tsx
@@ -2,7 +2,8 @@ import React, { useState, useEffect, useCallback } from "react";
import { useAccount, useReadContract } from "wagmi";
import { readContract } from "wagmi/actions";
import { config } from "../wagmi.config";
-import { getChain, genAiNFTContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
import { NFTMetadata, ModalImageData } from "../types/components";
import * as styles from "../layouts/styles";
import { NFTCard } from "./NFTCard";
@@ -19,7 +20,9 @@ interface MyNFTListProps {
export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps) {
const { address, isConnected } = useAccount();
- const chain = getChain();
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const chainId = fromCAIP2(network);
+ const contractAddress = getGenAiNFTAddress(network);
// My NFTs state - now just store token IDs
const [tokenIds, setTokenIds] = useState([]);
@@ -29,10 +32,11 @@ export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps
// Get user's NFT balance
const { data: userBalance, isLoading: isLoadingBalance } = useReadContract({
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
- chainId: chain.id,
+ chainId,
query: {
enabled: !!address && isConnected,
},
@@ -55,10 +59,11 @@ export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps
try {
// Get fresh balance directly from contract to avoid stale state
const freshBalance = await readContract(config, {
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "balanceOf",
args: [address],
- chainId: chain.id,
+ chainId,
});
if (!freshBalance || freshBalance === 0n) {
@@ -74,10 +79,11 @@ export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps
try {
// Get token ID at index using wagmi's readContract
const tokenIdResult = await readContract(config, {
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "tokenOfOwnerByIndex",
args: [address, BigInt(i)],
- chainId: chain.id,
+ chainId,
});
return tokenIdResult as bigint;
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 6af7628a6..8d9f9fa64 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from "react";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { readContract } from "wagmi/actions";
import { config } from "../wagmi.config";
-import { genAiNFTContractConfig, getChain } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2, isMainnet } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { NFTCardProps, NFT, NFTMetadata } from "../types/components";
import { useToast } from "./Toast";
@@ -43,8 +44,13 @@ export function NFTCard({
const deleteLabel = useLocale({ label: "imagegen.delete" });
+ // Get network and contract address from chain-utils
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const chainId = fromCAIP2(network);
+ const contractAddress = getGenAiNFTAddress(network);
+
// Use the custom hook for a stable public client reference
- const publicClient = useConfiguredPublicClient();
+ const publicClient = useConfiguredPublicClient(network);
// Fetch metadata from tokenURI
const fetchNFTMetadata = async (tokenURI: string): Promise => {
@@ -76,8 +82,8 @@ export function NFTCard({
// Get token URI using public client
const tokenURIResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "tokenURI",
args: [tokenId],
});
@@ -98,8 +104,8 @@ export function NFTCard({
let nftOwner = "";
if (isPublicView) {
const ownerResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "ownerOf",
args: [tokenId],
});
@@ -111,12 +117,12 @@ export function NFTCard({
let isListed: boolean | undefined;
if (!isPublicView) {
try {
- const chain = getChain();
const isListedResult = await readContract(config, {
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "isTokenListed",
args: [tokenId],
- chainId: chain.id,
+ chainId,
});
isListed = isListedResult as boolean;
} catch (error) {
@@ -224,8 +230,9 @@ export function NFTCard({
* Uses the Optimism network OpenSea URL format
*/
const handleShare = async () => {
- const contractAddress = genAiNFTContractConfig.address;
- const openSeaUrl = `https://opensea.io/item/optimism/${contractAddress}/${nft.tokenId}`;
+ // Determine OpenSea network based on mainnet/testnet
+ const openSeaNetwork = isMainnet(network) ? "optimism" : "optimism-sepolia";
+ const openSeaUrl = `https://opensea.io/item/${openSeaNetwork}/${contractAddress}/${nft.tokenId}`;
try {
await navigator.clipboard.writeText(openSeaUrl);
@@ -260,7 +267,8 @@ export function NFTCard({
try {
await writeContract({
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "burn",
args: [nft.tokenId],
});
@@ -282,7 +290,8 @@ export function NFTCard({
// Call contract
await writeListingContract({
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "setTokenListed",
args: [nft.tokenId, newListedStatus],
});
diff --git a/website/components/NFTFloatImage.tsx b/website/components/NFTFloatImage.tsx
index 58c1da13c..1aee3bf4b 100644
--- a/website/components/NFTFloatImage.tsx
+++ b/website/components/NFTFloatImage.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
-import { genAiNFTContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { extractPromptFromDescription } from "../utils/nftMetadataUtils";
import * as styles from "../layouts/styles";
@@ -17,8 +18,9 @@ export function NFTFloatImage({ tokenId }: NFTFloatImageProps) {
const [nftDescription, setNftDescription] = useState(null);
const [error, setError] = useState(null);
- // Memoize the contract config to prevent infinite re-renders
- // The stable genAiNFTContractConfig constant is used directly
+ // Get network and contract address from chain-utils
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const contractAddress = getGenAiNFTAddress(network);
// Extract prompt from description for display (reusing utility function)
const getPromptPreview = (description: string | null): string => {
@@ -36,7 +38,7 @@ export function NFTFloatImage({ tokenId }: NFTFloatImageProps) {
};
// Use the custom hook for a stable public client reference
- const publicClient = useConfiguredPublicClient();
+ const publicClient = useConfiguredPublicClient(network);
// Fetch metadata from tokenURI
const fetchNFTMetadata = async (tokenURI: string): Promise => {
@@ -67,8 +69,8 @@ export function NFTFloatImage({ tokenId }: NFTFloatImageProps) {
// Get token URI using public client
const tokenURIResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "tokenURI",
args: [BigInt(tokenId)],
});
diff --git a/website/components/NFTList.tsx b/website/components/NFTList.tsx
index 3802a0287..82c87fe47 100644
--- a/website/components/NFTList.tsx
+++ b/website/components/NFTList.tsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useAccount, useReadContract } from "wagmi";
-import { getChain, genAiNFTContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
import { NFTListProps } from "../types/components";
import * as styles from "../layouts/styles";
import { Tab } from "./Tab";
@@ -23,12 +24,18 @@ export function NFTList({
const activeTab = controlledActiveTab ?? localActiveTab;
const setActiveTab = onTabChange ?? setLocalActiveTab;
+ // Get network from useAutoNetwork
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const chainId = fromCAIP2(network);
+ const contractAddress = getGenAiNFTAddress(network);
+
// Get user's NFT balance for display in tab
const userBalanceQuery = useReadContract({
- ...genAiNFTContractConfig,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
- chainId: getChain().id,
+ chainId,
query: {
enabled: !!address && isConnected,
},
diff --git a/website/components/PublicNFTList.tsx b/website/components/PublicNFTList.tsx
index 0317940d8..f7b6c5491 100644
--- a/website/components/PublicNFTList.tsx
+++ b/website/components/PublicNFTList.tsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
-import { genAiNFTContractConfig } from "../utils/getChain";
+import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { ModalImageData } from "../types/components";
import * as styles from "../layouts/styles";
@@ -7,7 +8,9 @@ import { NFTCard } from "./NFTCard";
import { ImageModal } from "./ImageModal";
export function PublicNFTList() {
- // Use the stable genAiNFTContractConfig constant
+ // Get network and contract address from chain-utils
+ const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const contractAddress = getGenAiNFTAddress(network);
// Public NFTs state - now just store token IDs
const [publicTokenIds, setPublicTokenIds] = useState([]);
@@ -15,7 +18,7 @@ export function PublicNFTList() {
const [selectedImage, setSelectedImage] = useState(null);
// Use the custom hook for a stable public client reference
- const publicClient = useConfiguredPublicClient();
+ const publicClient = useConfiguredPublicClient(network);
// Load all public token IDs using getAllPublicTokens
const loadPublicTokenIds = useCallback(async () => {
@@ -24,8 +27,8 @@ export function PublicNFTList() {
try {
// Get all public token IDs using the public client
const tokenIds = (await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "getAllPublicTokens",
})) as bigint[];
diff --git a/website/hooks/useConfiguredPublicClient.ts b/website/hooks/useConfiguredPublicClient.ts
index e2b802b56..6f343bb44 100644
--- a/website/hooks/useConfiguredPublicClient.ts
+++ b/website/hooks/useConfiguredPublicClient.ts
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { getPublicClient } from "@wagmi/core";
import { config } from "../wagmi.config";
-import { getChain } from "../utils/getChain";
+import { fromCAIP2 } from "@fretchen/chain-utils";
/**
* Custom hook that provides a stable reference to the configured public client.
@@ -10,12 +10,13 @@ import { getChain } from "../utils/getChain";
* The client is created once per component mount and remains stable throughout
* the component's lifecycle.
*
+ * @param network - CAIP-2 network string (e.g., "eip155:10"). Required.
* @returns A stable public client instance configured with the correct chain
*/
-export function useConfiguredPublicClient() {
+export function useConfiguredPublicClient(network: string) {
return useMemo(() => {
- const chain = getChain();
+ const chainId = fromCAIP2(network);
// Must pass chainId explicitly to get the correct chain's public client
- return getPublicClient(config, { chainId: chain.id });
- }, []);
+ return getPublicClient(config, { chainId });
+ }, [network]);
}
diff --git a/website/test/ContractChainSelection.test.ts b/website/test/ContractChainSelection.test.ts
index b8b5b4766..b3dff32f2 100644
--- a/website/test/ContractChainSelection.test.ts
+++ b/website/test/ContractChainSelection.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
-import { genAiNFTContractConfig, getChain } from "../utils/getChain";
+import { getGenAiNFTAddress, GenImNFTv4ABI, getViemChain } from "@fretchen/chain-utils";
import { getPublicClient } from "@wagmi/core";
import { config } from "../wagmi.config";
@@ -17,21 +17,31 @@ vi.mock("@wagmi/core", () => ({
}),
}));
+vi.mock("@fretchen/chain-utils", () => ({
+ getGenAiNFTAddress: vi.fn(() => "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"),
+ GenImNFTv4ABI: [],
+ fromCAIP2: vi.fn((network: string) => parseInt(network.split(":")[1])),
+ getViemChain: vi.fn(() => ({ id: 10, name: "Optimism" })),
+}));
+
describe("Contract Chain Selection", () => {
it("should use configured chain even when wallet is on different chain", async () => {
- // Create client directly like the hook does now
- const chain = getChain();
+ // Use chain-utils pattern
+ const network = "eip155:10"; // Optimism mainnet
+ const chain = getViemChain(network);
const client = getPublicClient({ ...config, chains: [chain] });
expect(client).toBeDefined();
- // Try to read from contract
+ // Try to read from contract using chain-utils
+ const contractAddress = getGenAiNFTAddress(network);
const result = await client.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "getAllPublicTokens",
});
// We mainly care that the call succeeded - if it did, we used the right chain
expect(result).toBeDefined();
+ expect(contractAddress).toBe("0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb");
});
});
diff --git a/website/test/MyNFTList.test.tsx b/website/test/MyNFTList.test.tsx
index b95b8e18b..9392c5875 100644
--- a/website/test/MyNFTList.test.tsx
+++ b/website/test/MyNFTList.test.tsx
@@ -25,14 +25,17 @@ vi.mock("wagmi/actions", () => ({
readContract: (...args: unknown[]) => mockReadContract(...args),
}));
-// Mock getChain to provide the stable constant
-vi.mock("../utils/getChain", () => ({
- getChain: vi.fn(() => ({ id: 10 })),
- // Stable constant that our components now use
- genAiNFTContractConfig: {
- address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb",
- abi: [], // Simplified for test
- },
+// Mock useAutoNetwork hook
+vi.mock("../hooks/useAutoNetwork", () => ({
+ useAutoNetwork: vi.fn(() => "eip155:10"), // Default to Optimism mainnet
+}));
+
+// Mock chain-utils
+vi.mock("@fretchen/chain-utils", () => ({
+ getGenAiNFTAddress: vi.fn(() => "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"),
+ GenImNFTv4ABI: [],
+ GENAI_NFT_NETWORKS: ["eip155:10", "eip155:11155420"],
+ fromCAIP2: vi.fn((network: string) => parseInt(network.split(":")[1])),
}));
// Mock config
@@ -124,55 +127,47 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
console.log(`Contract config calls: ${contractConfigCallCount}`);
});
- it("should show that stable constant has consistent reference", async () => {
- // Import the new stable constant
- const { genAiNFTContractConfig } = await import("../utils/getChain");
-
- // Multiple imports should return the same reference
- const config1 = genAiNFTContractConfig;
- const config2 = genAiNFTContractConfig;
+ it("should show that chain-utils provides consistent addresses", async () => {
+ // Import from chain-utils
+ const { getGenAiNFTAddress } = await import("@fretchen/chain-utils");
- // They should be exactly the same object (same content AND reference)
- expect(config1).toEqual(config2); // Content is the same
- expect(config1).toBe(config2); // And references are the same - THIS IS THE FIX!
+ // Multiple calls with same network should return the same address
+ const address1 = getGenAiNFTAddress("eip155:10");
+ const address2 = getGenAiNFTAddress("eip155:10");
- // This proves why the useEffect with contract config in dependencies
- // causes infinite re-renders
+ // They should be exactly the same
+ expect(address1).toEqual(address2);
+ expect(address1).toBe(address2);
});
- it("should demonstrate stable dependency with fixed implementation", async () => {
+ it("should demonstrate stable useAutoNetwork hook", async () => {
let effectRunCount = 0;
- // Create a component that uses the stable constant
- function FixedComponent() {
- const [contractConfig, setContractConfig] = React.useState | null>(null);
+ // Import useAutoNetwork at the top of the test
+ const { useAutoNetwork } = await import("../hooks/useAutoNetwork");
- React.useEffect(() => {
- import("../utils/getChain").then(({ genAiNFTContractConfig }) => {
- setContractConfig(genAiNFTContractConfig);
- });
- }, []);
+ // Create a component that uses the useAutoNetwork hook
+ function FixedComponent() {
+ const network = useAutoNetwork(["eip155:10", "eip155:11155420"]);
React.useEffect(() => {
effectRunCount++;
- console.log(`Effect run #${effectRunCount}`);
- // Simulate loading token IDs
- }, [contractConfig]); // This dependency causes the loop!
+ console.log(`Effect run #${effectRunCount}, network: ${network}`);
+ }, [network]);
- return Component
;
+ return Component on {network}
;
}
render();
await waitFor(
() => {
- // With stable constants, the effect only runs 2 times
- // (once initial, once after the first useEffect update)
+ // With useAutoNetwork, the effect only runs once
expect(effectRunCount).toBeLessThanOrEqual(2);
},
{ timeout: 1000 },
);
- console.log(`Effect ran ${effectRunCount} times with stable dependency`);
+ console.log(`Effect ran ${effectRunCount} times with useAutoNetwork`);
});
});
diff --git a/website/test/useConfiguredPublicClient.test.ts b/website/test/useConfiguredPublicClient.test.ts
index 93d54b15f..1883f4177 100644
--- a/website/test/useConfiguredPublicClient.test.ts
+++ b/website/test/useConfiguredPublicClient.test.ts
@@ -2,11 +2,12 @@ import { describe, it, expect, vi } from "vitest";
import { renderHook } from "@testing-library/react";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
-// Mock the dependencies
-vi.mock("../utils/getChain", () => ({
- getChain: vi.fn(() => ({
+// Mock the dependencies - no longer need getChain
+vi.mock("@fretchen/chain-utils", () => ({
+ fromCAIP2: vi.fn((network: string) => parseInt(network.split(":")[1])),
+ getViemChain: vi.fn(() => ({
id: 10,
- name: "optimism",
+ name: "OP Mainnet",
})),
}));
@@ -27,7 +28,8 @@ vi.mock("@wagmi/core", () => ({
describe("useConfiguredPublicClient Hook", () => {
it("should return a stable client reference", () => {
- const { result, rerender } = renderHook(() => useConfiguredPublicClient());
+ // Now requires network parameter (CAIP-2 format)
+ const { result, rerender } = renderHook(() => useConfiguredPublicClient("eip155:10"));
const firstClient = result.current;
@@ -46,7 +48,8 @@ describe("useConfiguredPublicClient Hook", () => {
// Reset all mocks
vi.clearAllMocks();
- const { result, rerender } = renderHook(() => useConfiguredPublicClient());
+ // Now requires network parameter (CAIP-2 format)
+ const { result, rerender } = renderHook(() => useConfiguredPublicClient("eip155:10"));
const initialClient = result.current;
@@ -61,7 +64,8 @@ describe("useConfiguredPublicClient Hook", () => {
});
it("should prevent infinite re-render loops in useEffect dependencies", () => {
- const { result } = renderHook(() => useConfiguredPublicClient());
+ // Now requires network parameter (CAIP-2 format)
+ const { result } = renderHook(() => useConfiguredPublicClient("eip155:10"));
const client1 = result.current;
const client2 = result.current;
diff --git a/website/utils/getChain.ts b/website/utils/getChain.ts
index 8f45e8cd8..0471acab0 100644
--- a/website/utils/getChain.ts
+++ b/website/utils/getChain.ts
@@ -110,10 +110,18 @@ export function isSupportV2Chain(chainId: number): boolean {
}
// ═══════════════════════════════════════════════════════════════
-// Other Contract Configurations
+// Legacy Contract Configurations
+//
+// NOTE: genAiNFTContractConfig is DEPRECATED - use chain-utils instead:
+// import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+// const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+// const address = getGenAiNFTAddress(network);
+//
+// collectorNFTContractConfig and llmV1ContractConfig will be migrated in Phase 3
// ═══════════════════════════════════════════════════════════════
// Create stable contract config references at module level - computed once when module loads
+/** @deprecated Use chain-utils getGenAiNFTAddress() instead */
const STABLE_GENAI_NFT_CONTRACT_CONFIG = (() => {
switch (CHAIN_NAME) {
case "sepolia":
@@ -162,11 +170,17 @@ const STABLE_LLM_V1_CONTRACT_CONFIG = (() => {
})();
// Export stable references directly - these objects never change reference
+/** @deprecated Use chain-utils getGenAiNFTAddress() + GenImNFTv4ABI instead */
export const genAiNFTContractConfig = STABLE_GENAI_NFT_CONTRACT_CONFIG;
+/** Phase 3: Will be migrated to chain-utils */
export const collectorNFTContractConfig = STABLE_COLLECTOR_NFT_CONTRACT_CONFIG;
+/** Out of scope: LLMv1 stays in legacy config */
export const llmV1ContractConfig = STABLE_LLM_V1_CONTRACT_CONFIG;
/**
+ * @deprecated Use useAutoNetwork() + fromCAIP2() instead for GenImNFT components.
+ * This function is still used by CollectorNFT and LLMv1 components.
+ *
* Gibt das entsprechende Chain-Objekt basierend auf der CHAIN-Umgebungsvariable zurück
* @returns Das Chain-Objekt aus wagmi/chains
*/
diff --git a/website/utils/nftLoader.ts b/website/utils/nftLoader.ts
index e5a331b1b..6f029d3dd 100644
--- a/website/utils/nftLoader.ts
+++ b/website/utils/nftLoader.ts
@@ -1,4 +1,4 @@
-import { genAiNFTContractConfig } from "./getChain";
+import { getGenAiNFTAddress, GenImNFTv4ABI } from "@fretchen/chain-utils";
import { NFTMetadata } from "../types/BlogPost";
import type { PublicClient } from "viem";
@@ -10,15 +10,22 @@ interface NFTMetadataJSON {
/**
* Simple NFT loader that fetches metadata for a single token
+ * @param tokenID The token ID to load
+ * @param publicClient The public client to use for contract calls
+ * @param network CAIP-2 network string (e.g., "eip155:10")
*/
-export async function loadNFTMetadata(tokenID: number, publicClient: PublicClient): Promise {
+export async function loadNFTMetadata(
+ tokenID: number,
+ publicClient: PublicClient,
+ network: string,
+): Promise {
try {
- // Verwende die stabile genAiNFTContractConfig Konstante
+ const contractAddress = getGenAiNFTAddress(network);
// Get token URI from contract
const tokenURIResult = await publicClient.readContract({
- address: genAiNFTContractConfig.address,
- abi: genAiNFTContractConfig.abi,
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
functionName: "tokenURI",
args: [BigInt(tokenID)],
});
@@ -58,11 +65,13 @@ export async function loadNFTMetadata(tokenID: number, publicClient: PublicClien
* Load multiple NFT metadata entries with controlled concurrency
* @param tokenIDs Array of token IDs to load
* @param publicClient The public client to use for contract calls
+ * @param network CAIP-2 network string (e.g., "eip155:10")
* @param concurrency Maximum number of concurrent requests (default: 3)
*/
export async function loadMultipleNFTMetadata(
tokenIDs: number[],
publicClient: PublicClient,
+ network: string,
concurrency = 3,
): Promise> {
const results: Record = {};
@@ -71,7 +80,7 @@ export async function loadMultipleNFTMetadata(
const processBatch = async (batch: number[]): Promise => {
const promises = batch.map(async (tokenID) => {
console.log(`Loading NFT metadata for token ${tokenID}...`);
- const metadata = await loadNFTMetadata(tokenID, publicClient);
+ const metadata = await loadNFTMetadata(tokenID, publicClient, network);
if (metadata) {
results[tokenID] = metadata;
}
diff --git a/website/utils/nodeNftLoader.ts b/website/utils/nodeNftLoader.ts
index 8efd93762..c1a5fae82 100644
--- a/website/utils/nodeNftLoader.ts
+++ b/website/utils/nodeNftLoader.ts
@@ -3,7 +3,9 @@
* Used for build-time operations like blog generation
*/
-import { createNodePublicClient, nodeGenAiNFTContractConfig } from "./nodeChainUtils";
+import { createPublicClient, http } from "viem";
+import { getDefaultNetwork } from "./nodeChainUtils";
+import { getGenAiNFTAddress, GenImNFTv4ABI, getViemChain } from "@fretchen/chain-utils";
import { NFTMetadata } from "../types/BlogPost";
interface NFTMetadataJSON {
@@ -12,6 +14,18 @@ interface NFTMetadataJSON {
image?: string;
}
+/**
+ * Create a public client for the default network
+ */
+function createDefaultPublicClient() {
+ const network = getDefaultNetwork();
+ const chain = getViemChain(network);
+ return createPublicClient({
+ chain,
+ transport: http(),
+ });
+}
+
/**
* Node.js-specific NFT metadata loader using pure viem
*/
@@ -19,13 +33,14 @@ export async function loadNFTMetadataNode(tokenID: number): Promise
Date: Wed, 28 Jan 2026 22:23:13 +0100
Subject: [PATCH 29/46] Clean
---
website/components/AgentInfoPanel.tsx | 4 +-
website/components/EntryNftImage.tsx | 2 +-
website/components/ImageGenerator.tsx | 29 +++++-------
website/components/MyNFTList.tsx | 2 +-
website/components/NFTCard.tsx | 16 ++++++-
website/components/NFTFloatImage.tsx | 2 +-
website/components/NFTList.tsx | 2 +-
website/components/PublicNFTList.tsx | 2 +-
website/hooks/useAutoNetwork.ts | 68 +++++++++++++++++++++------
website/utils/getChain.ts | 63 ++-----------------------
website/utils/nodeChainUtils.ts | 40 ++--------------
11 files changed, 95 insertions(+), 135 deletions(-)
diff --git a/website/components/AgentInfoPanel.tsx b/website/components/AgentInfoPanel.tsx
index 95d92af94..276c0bde0 100644
--- a/website/components/AgentInfoPanel.tsx
+++ b/website/components/AgentInfoPanel.tsx
@@ -71,8 +71,8 @@ export function AgentInfoPanel({ service = "genimg", variant = "footer" }: Agent
const serviceHostname = serviceEndpoint ? new URL(serviceEndpoint).hostname : null;
// Get contract address based on service - use appropriate network list
- const genimgNetwork = useAutoNetwork(GENAI_NFT_NETWORKS);
- const llmNetwork = useAutoNetwork(LLM_V1_NETWORKS);
+ const { network: genimgNetwork } = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network: llmNetwork } = useAutoNetwork(LLM_V1_NETWORKS);
const contractAddress = service === "genimg" ? getGenAiNFTAddress(genimgNetwork) : getLLMv1Address(llmNetwork);
// Sidebar variant - vertical layout
diff --git a/website/components/EntryNftImage.tsx b/website/components/EntryNftImage.tsx
index 77ec8598d..c2bd32622 100644
--- a/website/components/EntryNftImage.tsx
+++ b/website/components/EntryNftImage.tsx
@@ -17,7 +17,7 @@ interface EntryNftImageProps {
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 { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
const contractAddress = getGenAiNFTAddress(network);
const publicClient = useConfiguredPublicClient(network);
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index fd175d073..6eda76e6f 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -1,5 +1,5 @@
import React, { useState } from "react";
-import { useAccount, useConnect, useSwitchChain, useChainId } from "wagmi";
+import { useAccount, useConnect } from "wagmi";
import { css } from "../styled-system/css";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
import { GENAI_NFT_NETWORKS, fromCAIP2, isTestnet, getViemChain } from "@fretchen/chain-utils";
@@ -111,11 +111,9 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
// Blockchain interaction
const { address, isConnected } = useAccount();
const { connectors, connect } = useConnect();
- const { switchChainAsync, isPending: isSwitchingChain } = useSwitchChain();
- const currentChainId = useChainId();
- // Determine target chain from useAutoNetwork
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ // Determine target chain from useAutoNetwork (no auto-switch, switch at interaction)
+ const { network, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS);
const targetChainId = fromCAIP2(network);
const targetChain = getViemChain(network);
const useTestnetFlag = isTestnet(network);
@@ -274,20 +272,15 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
return;
}
- // === Automatic Chain Switch ===
+ // === Chain Switch at Interaction ===
// Ensure user is on the correct chain before making payment
- if (currentChainId !== targetChainId) {
- console.log(`[x402] Chain mismatch: current=${currentChainId}, target=${targetChainId} (${targetChain.name})`);
- try {
- await switchChainAsync({ chainId: targetChainId });
- console.log(`[x402] Successfully switched to ${targetChain.name}`);
- } catch (switchError) {
- console.error("[x402] Chain switch failed:", switchError);
- const errorMsg = `${chainSwitchFailedText}: ${targetChain.name}`;
- setError(errorMsg);
- onError?.(errorMsg);
- return;
- }
+ const switched = await switchIfNeeded();
+ if (!switched) {
+ console.error("[x402] Chain switch rejected by user");
+ const errorMsg = `${chainSwitchFailedText}: ${targetChain.name}`;
+ setError(errorMsg);
+ onError?.(errorMsg);
+ return;
}
setIsLoading(true);
diff --git a/website/components/MyNFTList.tsx b/website/components/MyNFTList.tsx
index c6e766ec3..91d99fa8e 100644
--- a/website/components/MyNFTList.tsx
+++ b/website/components/MyNFTList.tsx
@@ -20,7 +20,7 @@ interface MyNFTListProps {
export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps) {
const { address, isConnected } = useAccount();
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 8d9f9fa64..45322c349 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -45,7 +45,7 @@ export function NFTCard({
const deleteLabel = useLocale({ label: "imagegen.delete" });
// Get network and contract address from chain-utils
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS);
const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
@@ -266,6 +266,13 @@ export function NFTCard({
setShowDeleteConfirmation(false);
try {
+ // Switch chain if needed before transaction
+ const switched = await switchIfNeeded();
+ if (!switched) {
+ showToast("Please switch to the correct network.", "error");
+ return;
+ }
+
await writeContract({
address: contractAddress,
abi: GenImNFTv4ABI,
@@ -285,6 +292,13 @@ export function NFTCard({
const statusText = newListedStatus ? "public" : "private";
try {
+ // Switch chain if needed before transaction
+ const switched = await switchIfNeeded();
+ if (!switched) {
+ showToast("Please switch to the correct network.", "error");
+ return;
+ }
+
// Update UI optimistically
onListedStatusChanged(nft.tokenId, newListedStatus);
diff --git a/website/components/NFTFloatImage.tsx b/website/components/NFTFloatImage.tsx
index 1aee3bf4b..7a7e5daf7 100644
--- a/website/components/NFTFloatImage.tsx
+++ b/website/components/NFTFloatImage.tsx
@@ -19,7 +19,7 @@ export function NFTFloatImage({ tokenId }: NFTFloatImageProps) {
const [error, setError] = useState(null);
// Get network and contract address from chain-utils
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
const contractAddress = getGenAiNFTAddress(network);
// Extract prompt from description for display (reusing utility function)
diff --git a/website/components/NFTList.tsx b/website/components/NFTList.tsx
index 82c87fe47..3fd1de9c3 100644
--- a/website/components/NFTList.tsx
+++ b/website/components/NFTList.tsx
@@ -25,7 +25,7 @@ export function NFTList({
const setActiveTab = onTabChange ?? setLocalActiveTab;
// Get network from useAutoNetwork
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
diff --git a/website/components/PublicNFTList.tsx b/website/components/PublicNFTList.tsx
index f7b6c5491..b096a40a4 100644
--- a/website/components/PublicNFTList.tsx
+++ b/website/components/PublicNFTList.tsx
@@ -9,7 +9,7 @@ import { ImageModal } from "./ImageModal";
export function PublicNFTList() {
// Get network and contract address from chain-utils
- const network = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
const contractAddress = getGenAiNFTAddress(network);
// Public NFTs state - now just store token IDs
diff --git a/website/hooks/useAutoNetwork.ts b/website/hooks/useAutoNetwork.ts
index 1689b8c9b..b25767522 100644
--- a/website/hooks/useAutoNetwork.ts
+++ b/website/hooks/useAutoNetwork.ts
@@ -1,44 +1,82 @@
/**
- * Hook for automatic network detection and switching.
+ * Hook for network detection with deferred switching.
*
- * Detects the user's connected wallet chain and returns the CAIP-2 network string.
- * If the chain is not in the supported networks, automatically switches to the default.
+ * Detects the user's connected wallet chain and returns the target CAIP-2 network.
+ * Does NOT auto-switch - instead provides a `switchIfNeeded()` function
+ * to call at interaction time (submit button).
+ *
+ * This pattern is more user-friendly:
+ * - No surprising wallet popups on connect
+ * - Switch happens in context of an action (expected by users)
+ * - Beginners don't need to understand chains
*/
import { useAccount, useChainId, useSwitchChain } from "wagmi";
-import { useEffect } from "react";
+import { useCallback } from "react";
import { toCAIP2, fromCAIP2 } from "@fretchen/chain-utils";
+interface UseAutoNetworkResult {
+ /** Target CAIP-2 network for transactions */
+ network: string;
+ /** Whether wallet is already on the target network */
+ isOnCorrectNetwork: boolean;
+ /** Call before submitting a transaction - switches chain if needed */
+ switchIfNeeded: () => Promise;
+}
+
/**
- * Returns the current CAIP-2 network if supported, otherwise switches to default.
+ * Returns the target CAIP-2 network and a function to switch when needed.
*
* @param supportedNetworks - Array of CAIP-2 network strings (e.g., ["eip155:10", "eip155:11155420"])
- * @returns Current CAIP-2 network string
+ * @returns Object with network, isOnCorrectNetwork, and switchIfNeeded()
*
* @example
* ```tsx
* import { GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
*
* function MyComponent() {
- * const network = useAutoNetwork(GENAI_NFT_NETWORKS);
- * // network is "eip155:10" or "eip155:11155420"
+ * const { network, isOnCorrectNetwork, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS);
+ *
+ * const handleSubmit = async () => {
+ * const switched = await switchIfNeeded();
+ * if (!switched) return; // User rejected switch
+ * // Proceed with transaction on `network`
+ * };
* }
* ```
*/
-export function useAutoNetwork(supportedNetworks: readonly string[]): string {
+export function useAutoNetwork(supportedNetworks: readonly string[]): UseAutoNetworkResult {
const chainId = useChainId();
const { isConnected } = useAccount();
- const { switchChain } = useSwitchChain();
+ const { switchChainAsync } = useSwitchChain();
const defaultNetwork = supportedNetworks[0];
const currentNetwork = toCAIP2(chainId);
const isSupported = supportedNetworks.includes(currentNetwork);
- useEffect(() => {
- if (isConnected && !isSupported && switchChain) {
- switchChain({ chainId: fromCAIP2(defaultNetwork) });
+ // Target network: current if supported, otherwise default
+ const network = isSupported ? currentNetwork : defaultNetwork;
+
+ const switchIfNeeded = useCallback(async (): Promise => {
+ // Already on correct network
+ if (isSupported) return true;
+
+ // Not connected - nothing to switch
+ if (!isConnected) return true;
+
+ // Try to switch
+ try {
+ await switchChainAsync({ chainId: fromCAIP2(defaultNetwork) });
+ return true;
+ } catch {
+ // User rejected or error
+ return false;
}
- }, [isConnected, isSupported, switchChain, defaultNetwork]);
+ }, [isSupported, isConnected, switchChainAsync, defaultNetwork]);
- return isSupported ? currentNetwork : defaultNetwork;
+ return {
+ network,
+ isOnCorrectNetwork: isSupported,
+ switchIfNeeded,
+ };
}
diff --git a/website/utils/getChain.ts b/website/utils/getChain.ts
index 0471acab0..ab7528145 100644
--- a/website/utils/getChain.ts
+++ b/website/utils/getChain.ts
@@ -1,54 +1,17 @@
import { sepolia, optimism, optimismSepolia, base, baseSepolia } from "wagmi/chains";
import type { Chain } from "wagmi/chains";
import CollectorNFTv1ABI from "../../eth/abi/contracts/CollectorNFTv1.json";
-import GenImNFTv3ABI from "../../eth/abi/contracts/GenImNFTv3.json";
import SupportV2ABI from "../../eth/abi/contracts/SupportV2.json";
import LLMv1ABI from "../../eth/abi/contracts/LLMv1.json";
// ═══════════════════════════════════════════════════════════════
-// Re-exports from @fretchen/chain-utils (PR 2a: Infrastructure)
-// These are the new patterns - use these in new code
-// ═══════════════════════════════════════════════════════════════
-export {
- // CAIP-2 utilities
- toCAIP2,
- fromCAIP2,
- isMainnet,
- isTestnet,
- isNetworkSupported,
- // Chain utilities
- getViemChain,
- // Contract addresses (GenAI NFT)
- getGenAiNFTAddress,
- tryGetGenAiNFTAddress,
- GENAI_NFT_NETWORKS,
- MAINNET_GENAI_NFT_ADDRESSES,
- TESTNET_GENAI_NFT_ADDRESSES,
- // Contract addresses (Collector NFT)
- getCollectorNFTAddress,
- tryGetCollectorNFTAddress,
- COLLECTOR_NFT_NETWORKS,
- MAINNET_COLLECTOR_NFT_ADDRESSES,
- // Contract addresses (LLMv1)
- getLLMv1Address,
- tryGetLLMv1Address,
- LLM_V1_NETWORKS,
- // ABIs
- GenImNFTv4ABI,
- LLMv1ABI as ChainUtilsLLMv1ABI,
- // Network lists
- ALL_NETWORKS,
- MAINNET_NETWORKS,
- TESTNET_NETWORKS,
-} from "@fretchen/chain-utils";
-
-// ═══════════════════════════════════════════════════════════════
-// Legacy exports (PR 2b will remove these)
+// Chain utilities are now in @fretchen/chain-utils
+// Import directly where needed:
+// import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
// ═══════════════════════════════════════════════════════════════
/**
* Get PUBLIC_ENV__CHAIN_NAME in Vite context (Browser)
- * Direct inline implementation for simplicity
*/
const CHAIN_NAME = import.meta.env?.PUBLIC_ENV__CHAIN_NAME || "optimism";
@@ -112,7 +75,7 @@ export function isSupportV2Chain(chainId: number): boolean {
// ═══════════════════════════════════════════════════════════════
// Legacy Contract Configurations
//
-// NOTE: genAiNFTContractConfig is DEPRECATED - use chain-utils instead:
+// GenAI NFT: MIGRATED to chain-utils - use:
// import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
// const network = useAutoNetwork(GENAI_NFT_NETWORKS);
// const address = getGenAiNFTAddress(network);
@@ -120,22 +83,6 @@ export function isSupportV2Chain(chainId: number): boolean {
// collectorNFTContractConfig and llmV1ContractConfig will be migrated in Phase 3
// ═══════════════════════════════════════════════════════════════
-// Create stable contract config references at module level - computed once when module loads
-/** @deprecated Use chain-utils getGenAiNFTAddress() instead */
-const STABLE_GENAI_NFT_CONTRACT_CONFIG = (() => {
- switch (CHAIN_NAME) {
- case "sepolia":
- return { address: "0xf18E3901D91D8a08380E37A466E6F7f6AA4BD4a6", abi: GenImNFTv3ABI } as const;
- case "optimismSepolia":
- // GenImNFTv4 deployed on 2025-12-24
- return { address: "0x10827cC42a09D0BAD2d43134C69F0e776D853D85", abi: GenImNFTv3ABI } as const;
- case "optimism":
- return { address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", abi: GenImNFTv3ABI } as const;
- default:
- return { address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb", abi: GenImNFTv3ABI } as const;
- }
-})();
-
const STABLE_COLLECTOR_NFT_CONTRACT_CONFIG = (() => {
switch (CHAIN_NAME) {
case "sepolia":
@@ -170,8 +117,6 @@ const STABLE_LLM_V1_CONTRACT_CONFIG = (() => {
})();
// Export stable references directly - these objects never change reference
-/** @deprecated Use chain-utils getGenAiNFTAddress() + GenImNFTv4ABI instead */
-export const genAiNFTContractConfig = STABLE_GENAI_NFT_CONTRACT_CONFIG;
/** Phase 3: Will be migrated to chain-utils */
export const collectorNFTContractConfig = STABLE_COLLECTOR_NFT_CONTRACT_CONFIG;
/** Out of scope: LLMv1 stays in legacy config */
diff --git a/website/utils/nodeChainUtils.ts b/website/utils/nodeChainUtils.ts
index 1b277999c..4eb6f65bb 100644
--- a/website/utils/nodeChainUtils.ts
+++ b/website/utils/nodeChainUtils.ts
@@ -1,28 +1,17 @@
/**
* Node.js-specific chain utilities using pure viem
* Used for build-time operations like blog generation
+ *
+ * Chain utilities are now in @fretchen/chain-utils - import directly where needed:
+ * import { getGenAiNFTAddress, GenImNFTv4ABI } from "@fretchen/chain-utils";
*/
import { createPublicClient, http } from "viem";
import { sepolia, optimism, optimismSepolia } from "viem/chains";
import type { Chain, PublicClient } from "viem";
import CollectorNFTv1ABI from "../../eth/abi/contracts/CollectorNFTv1.json";
-import GenImNFTv3ABI from "../../eth/abi/contracts/GenImNFTv3.json";
import LLMv1ABI from "../../eth/abi/contracts/LLMv1.json";
-// ═══════════════════════════════════════════════════════════════
-// Re-exports from @fretchen/chain-utils (PR 2a: Infrastructure)
-// ═══════════════════════════════════════════════════════════════
-export {
- getViemChain,
- getGenAiNFTAddress,
- tryGetGenAiNFTAddress,
- isMainnet,
- isTestnet,
- GenImNFTv4ABI,
- GENAI_NFT_NETWORKS,
-} from "@fretchen/chain-utils";
-
// ═══════════════════════════════════════════════════════════════
// SSR/Node.js specific utilities
// ═══════════════════════════════════════════════════════════════
@@ -83,25 +72,7 @@ export function createNodePublicClient(): PublicClient {
}
// Node.js contract configurations - computed once at module load
-const NODE_GENAI_NFT_CONTRACT_CONFIG = (() => {
- switch (NODE_CHAIN_NAME) {
- case "sepolia":
- return {
- address: "0xf18E3901D91D8a08380E37A466E6F7f6AA4BD4a6" as `0x${string}`,
- abi: GenImNFTv3ABI,
- } as const;
- case "optimism":
- return {
- address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb" as `0x${string}`,
- abi: GenImNFTv3ABI,
- } as const;
- default:
- return {
- address: "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb" as `0x${string}`,
- abi: GenImNFTv3ABI,
- } as const;
- }
-})();
+// GenAI NFT: MIGRATED to chain-utils - use getGenAiNFTAddress() + GenImNFTv4ABI
const NODE_COLLECTOR_NFT_CONTRACT_CONFIG = (() => {
switch (NODE_CHAIN_NAME) {
@@ -149,13 +120,12 @@ const NODE_LLM_V1_CONTRACT_CONFIG = (() => {
})();
// Export stable contract configurations for Node.js
+// GenAI NFT: Use chain-utils getGenAiNFTAddress() + GenImNFTv4ABI instead
export const nodeContractConfigs = {
- genAiNFT: NODE_GENAI_NFT_CONTRACT_CONFIG,
collectorNFT: NODE_COLLECTOR_NFT_CONTRACT_CONFIG,
llmV1: NODE_LLM_V1_CONTRACT_CONFIG,
} as const;
// Convenience exports for backward compatibility
-export const nodeGenAiNFTContractConfig = NODE_GENAI_NFT_CONTRACT_CONFIG;
export const nodeCollectorNFTContractConfig = NODE_COLLECTOR_NFT_CONTRACT_CONFIG;
export const nodeLlmV1ContractConfig = NODE_LLM_V1_CONTRACT_CONFIG;
From 5547cc515219c3532a449f499653f42420f5555d Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 29 Jan 2026 07:38:08 +0100
Subject: [PATCH 30/46] Cleaning
---
website/components/ImageGenerator.tsx | 6 +-
website/test/MyNFTList.test.tsx | 8 +-
website/test/setup.ts | 9 ++
website/test/useAutoNetwork.test.ts | 147 ++++++++++++++++----------
4 files changed, 108 insertions(+), 62 deletions(-)
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index 6eda76e6f..d2b88039f 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -150,7 +150,6 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const enterPromptErrorText = useLocale({ label: "imagegen.enterPromptError" });
const unknownErrorText = useLocale({ label: "imagegen.unknownError" });
const chainSwitchFailedText = useLocale({ label: "imagegen.chainSwitchFailed" });
- const switchingNetworkText = useLocale({ label: "imagegen.switchingNetwork" });
// x402 specific messages
const awaitingSignatureText = useLocale({ label: "imagegen.awaitingSignature" });
@@ -189,7 +188,6 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const checkGalleryText = useLocale({ label: "imagegen.checkGallery" });
const getButtonState = (): string => {
- if (isSwitchingChain) return "switching";
if (isLoading || x402Status === "awaiting-signature" || x402Status === "processing") return "loading";
if (!prompt.trim()) return "needsPrompt";
return "ready";
@@ -197,8 +195,6 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const getButtonText = (state: string) => {
switch (state) {
- case "switching":
- return switchingNetworkText;
case "loading":
if (x402Status === "awaiting-signature") return awaitingSignatureText;
if (x402Status === "processing") return processingPaymentText;
@@ -219,7 +215,7 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
// Button Components
const CreateArtworkButton = () => {
const isDisabled = buttonState === "needsPrompt";
- const isLoadingState = buttonState === "loading" || buttonState === "switching";
+ const isLoadingState = buttonState === "loading";
const handleClick = () => {
// Track create artwork attempt with context
diff --git a/website/test/MyNFTList.test.tsx b/website/test/MyNFTList.test.tsx
index 9392c5875..f64a82e94 100644
--- a/website/test/MyNFTList.test.tsx
+++ b/website/test/MyNFTList.test.tsx
@@ -25,9 +25,13 @@ vi.mock("wagmi/actions", () => ({
readContract: (...args: unknown[]) => mockReadContract(...args),
}));
-// Mock useAutoNetwork hook
+// Mock useAutoNetwork hook - returns object with network and switchIfNeeded
vi.mock("../hooks/useAutoNetwork", () => ({
- useAutoNetwork: vi.fn(() => "eip155:10"), // Default to Optimism mainnet
+ useAutoNetwork: vi.fn(() => ({
+ network: "eip155:10", // Default to Optimism mainnet
+ isOnCorrectNetwork: true,
+ switchIfNeeded: vi.fn(() => Promise.resolve(true)),
+ })),
}));
// Mock chain-utils
diff --git a/website/test/setup.ts b/website/test/setup.ts
index 8844b0b3a..7609fd86b 100644
--- a/website/test/setup.ts
+++ b/website/test/setup.ts
@@ -87,6 +87,15 @@ vi.mock("./hooks/useLocale", () => ({
useLocale: vi.fn(({ label }: { label: string }) => label),
}));
+// Mock useAutoNetwork hook - returns object with network and switchIfNeeded
+vi.mock("../hooks/useAutoNetwork", () => ({
+ useAutoNetwork: vi.fn(() => ({
+ network: "eip155:10", // Default to Optimism mainnet
+ isOnCorrectNetwork: true,
+ switchIfNeeded: vi.fn(() => Promise.resolve(true)),
+ })),
+}));
+
// Import wagmi at top level for mock utilities
import { useAccount } from "wagmi";
diff --git a/website/test/useAutoNetwork.test.ts b/website/test/useAutoNetwork.test.ts
index 50218092f..a0c8b9840 100644
--- a/website/test/useAutoNetwork.test.ts
+++ b/website/test/useAutoNetwork.test.ts
@@ -1,12 +1,12 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { renderHook } from "@testing-library/react";
+import { renderHook, act } from "@testing-library/react";
// Mock wagmi hooks
-const mockSwitchChain = vi.fn();
+const mockSwitchChainAsync = vi.fn();
vi.mock("wagmi", () => ({
useChainId: vi.fn(() => 10), // Default: Optimism mainnet
useAccount: vi.fn(() => ({ isConnected: true })),
- useSwitchChain: vi.fn(() => ({ switchChain: mockSwitchChain })),
+ useSwitchChain: vi.fn(() => ({ switchChainAsync: mockSwitchChainAsync })),
}));
// Mock chain-utils
@@ -21,6 +21,19 @@ import { useChainId, useAccount } from "wagmi";
describe("useAutoNetwork Hook", () => {
beforeEach(() => {
vi.clearAllMocks();
+ mockSwitchChainAsync.mockResolvedValue(undefined);
+ });
+
+ describe("return value structure", () => {
+ it("should return network, isOnCorrectNetwork, and switchIfNeeded", () => {
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ expect(result.current).toHaveProperty("network");
+ expect(result.current).toHaveProperty("isOnCorrectNetwork");
+ expect(result.current).toHaveProperty("switchIfNeeded");
+ expect(typeof result.current.switchIfNeeded).toBe("function");
+ });
});
describe("when on a supported network", () => {
@@ -31,8 +44,8 @@ describe("useAutoNetwork Hook", () => {
const supportedNetworks = ["eip155:10", "eip155:11155420"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(result.current).toBe("eip155:10");
- expect(mockSwitchChain).not.toHaveBeenCalled();
+ expect(result.current.network).toBe("eip155:10");
+ expect(result.current.isOnCorrectNetwork).toBe(true);
});
it("should return testnet network when on testnet", () => {
@@ -42,97 +55,109 @@ describe("useAutoNetwork Hook", () => {
const supportedNetworks = ["eip155:10", "eip155:11155420"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(result.current).toBe("eip155:11155420");
- expect(mockSwitchChain).not.toHaveBeenCalled();
+ expect(result.current.network).toBe("eip155:11155420");
+ expect(result.current.isOnCorrectNetwork).toBe(true);
});
- });
- describe("when on an unsupported network", () => {
- it("should switch to default network when connected", () => {
- vi.mocked(useChainId).mockReturnValue(1); // Ethereum mainnet (unsupported)
+ it("should NOT auto-switch - no popups on connect", () => {
+ vi.mocked(useChainId).mockReturnValue(10);
vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
const supportedNetworks = ["eip155:10", "eip155:11155420"];
renderHook(() => useAutoNetwork(supportedNetworks));
- expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 10 });
+ // Key change: NO auto-switch on connect
+ expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
+ });
- it("should return default network while switching", () => {
+ describe("when on an unsupported network", () => {
+ it("should NOT auto-switch - deferred to interaction", () => {
vi.mocked(useChainId).mockReturnValue(1); // Ethereum mainnet (unsupported)
vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
const supportedNetworks = ["eip155:10", "eip155:11155420"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+ renderHook(() => useAutoNetwork(supportedNetworks));
- // Returns default even though switch is pending
- expect(result.current).toBe("eip155:10");
+ // Key change: NO auto-switch
+ expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
- it("should switch to first network in list as default", () => {
- vi.mocked(useChainId).mockReturnValue(137); // Polygon (unsupported)
+ it("should return default network and isOnCorrectNetwork=false", () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Unsupported
vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
- // First network is testnet
- const supportedNetworks = ["eip155:11155420", "eip155:10"];
- renderHook(() => useAutoNetwork(supportedNetworks));
+ const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 11155420 });
+ expect(result.current.network).toBe("eip155:10"); // Default
+ expect(result.current.isOnCorrectNetwork).toBe(false);
});
});
- describe("when wallet is not connected", () => {
- it("should not attempt to switch chain", () => {
- vi.mocked(useChainId).mockReturnValue(1); // Unsupported chain
- vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
+ describe("switchIfNeeded() function", () => {
+ it("should return true immediately when already on correct network", async () => {
+ vi.mocked(useChainId).mockReturnValue(10); // Supported
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- renderHook(() => useAutoNetwork(supportedNetworks));
+ const supportedNetworks = ["eip155:10"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ let switched: boolean = false;
+ await act(async () => {
+ switched = await result.current.switchIfNeeded();
+ });
- expect(mockSwitchChain).not.toHaveBeenCalled();
+ expect(switched).toBe(true);
+ expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
- it("should return default network", () => {
- vi.mocked(useChainId).mockReturnValue(1); // Unsupported chain
- vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
+ it("should switch chain when on unsupported network", async () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Unsupported
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
+ const supportedNetworks = ["eip155:10"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(result.current).toBe("eip155:10");
+ let switched: boolean = false;
+ await act(async () => {
+ switched = await result.current.switchIfNeeded();
+ });
+
+ expect(switched).toBe(true);
+ expect(mockSwitchChainAsync).toHaveBeenCalledWith({ chainId: 10 });
});
- });
- describe("chain switching behavior", () => {
- it("should only switch once per render cycle", () => {
- vi.mocked(useChainId).mockReturnValue(1);
+ it("should return false when user rejects switch", async () => {
+ vi.mocked(useChainId).mockReturnValue(1); // Unsupported
vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+ mockSwitchChainAsync.mockRejectedValue(new Error("User rejected"));
const supportedNetworks = ["eip155:10"];
- const { rerender } = renderHook(() => useAutoNetwork(supportedNetworks));
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- // Initial switch
- expect(mockSwitchChain).toHaveBeenCalledTimes(1);
+ let switched: boolean = true;
+ await act(async () => {
+ switched = await result.current.switchIfNeeded();
+ });
- // Re-render with same props should not trigger another switch
- rerender();
- expect(mockSwitchChain).toHaveBeenCalledTimes(1);
+ expect(switched).toBe(false);
});
- it("should switch when user connects wallet on unsupported chain", () => {
+ it("should return true when not connected (nothing to switch)", async () => {
vi.mocked(useChainId).mockReturnValue(1);
vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
const supportedNetworks = ["eip155:10"];
- const { rerender } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- expect(mockSwitchChain).not.toHaveBeenCalled();
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- // User connects wallet
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
- rerender();
+ let switched: boolean = false;
+ await act(async () => {
+ switched = await result.current.switchIfNeeded();
+ });
- expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 10 });
+ expect(switched).toBe(true);
+ expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
});
@@ -144,7 +169,8 @@ describe("useAutoNetwork Hook", () => {
const supportedNetworks = ["eip155:10"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(result.current).toBe("eip155:10");
+ expect(result.current.network).toBe("eip155:10");
+ expect(result.current.isOnCorrectNetwork).toBe(true);
});
it("should handle Base network", () => {
@@ -154,8 +180,19 @@ describe("useAutoNetwork Hook", () => {
const supportedNetworks = ["eip155:10", "eip155:8453"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- expect(result.current).toBe("eip155:8453");
- expect(mockSwitchChain).not.toHaveBeenCalled();
+ expect(result.current.network).toBe("eip155:8453");
+ expect(result.current.isOnCorrectNetwork).toBe(true);
+ });
+
+ it("should use first network as default when on unsupported", () => {
+ vi.mocked(useChainId).mockReturnValue(137); // Polygon (unsupported)
+ vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+
+ const supportedNetworks = ["eip155:11155420", "eip155:10"];
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+
+ // First network is testnet - that's the default
+ expect(result.current.network).toBe("eip155:11155420");
});
});
});
From 5c86b14015d14fd9efe91fa27ab7f1af3239c0f8 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 29 Jan 2026 07:47:25 +0100
Subject: [PATCH 31/46] Add basic tests for useAutoNetwork
---
shared/chain-utils/src/abi/GenImNFTv4.ts | 46 +++++-
website/components/AgentInfoPanel.tsx | 7 +-
website/test/AgentInfoPanel.test.tsx | 200 +++++++++++++++++++++++
3 files changed, 249 insertions(+), 4 deletions(-)
create mode 100644 website/test/AgentInfoPanel.test.tsx
diff --git a/shared/chain-utils/src/abi/GenImNFTv4.ts b/shared/chain-utils/src/abi/GenImNFTv4.ts
index d4a17f256..3907910d4 100644
--- a/shared/chain-utils/src/abi/GenImNFTv4.ts
+++ b/shared/chain-utils/src/abi/GenImNFTv4.ts
@@ -1,7 +1,7 @@
/**
* GenImNFTv4 Minimal ABI
*
- * Contains only the functions used by scw_js and x402_facilitator.
+ * Contains the functions used by scw_js, x402_facilitator, and website frontend.
* Full ABI available in: eth/abi/contracts/GenImNFTv4.ts (after export)
*
* Used by:
@@ -9,6 +9,9 @@
* - scw_js/genimg_x402_token.js: safeMint, safeTransferFrom, mintPrice
* - scw_js/readhandler_v2.js: ownerOf, requestImageUpdate, mintPrice, isImageUpdated
* - x402_facilitator/x402_whitelist.js: isAuthorizedAgent
+ * - website/components/PublicNFTList.tsx: getAllPublicTokens
+ * - website/components/MyNFTList.tsx: balanceOf, tokenOfOwnerByIndex
+ * - website/components/NFTCard.tsx: setListed, burn
*/
export const GenImNFTv4ABI = [
@@ -58,6 +61,47 @@ export const GenImNFTv4ABI = [
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [{ name: "", type: "bool" }],
},
+ {
+ name: "getAllPublicTokens",
+ type: "function",
+ stateMutability: "view",
+ inputs: [],
+ outputs: [{ name: "", type: "uint256[]" }],
+ },
+ {
+ name: "balanceOf",
+ type: "function",
+ stateMutability: "view",
+ inputs: [{ name: "owner", type: "address" }],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ name: "tokenOfOwnerByIndex",
+ type: "function",
+ stateMutability: "view",
+ inputs: [
+ { name: "owner", type: "address" },
+ { name: "index", type: "uint256" },
+ ],
+ outputs: [{ name: "", type: "uint256" }],
+ },
+ {
+ name: "setListed",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "tokenId", type: "uint256" },
+ { name: "_isListed", type: "bool" },
+ ],
+ outputs: [],
+ },
+ {
+ name: "burn",
+ type: "function",
+ stateMutability: "nonpayable",
+ inputs: [{ name: "tokenId", type: "uint256" }],
+ outputs: [],
+ },
// ═══════════════════════════════════════════════════════════════
// Write Functions
diff --git a/website/components/AgentInfoPanel.tsx b/website/components/AgentInfoPanel.tsx
index 276c0bde0..2b0416429 100644
--- a/website/components/AgentInfoPanel.tsx
+++ b/website/components/AgentInfoPanel.tsx
@@ -35,6 +35,10 @@ export function AgentInfoPanel({ service = "genimg", variant = "footer" }: Agent
// Localized texts
const poweredByText = useLocale({ label: "imagegen.poweredBy" });
+ // Get contract address based on service - hooks must be called before early returns
+ const { network: genimgNetwork } = useAutoNetwork(GENAI_NFT_NETWORKS);
+ const { network: llmNetwork } = useAutoNetwork(LLM_V1_NETWORKS);
+
const isSidebar = variant === "sidebar";
if (isLoading) {
@@ -70,9 +74,6 @@ export function AgentInfoPanel({ service = "genimg", variant = "footer" }: Agent
const serviceEndpoint = service === "genimg" ? agent.genimgEndpoint : agent.llmEndpoint;
const serviceHostname = serviceEndpoint ? new URL(serviceEndpoint).hostname : null;
- // Get contract address based on service - use appropriate network list
- const { network: genimgNetwork } = useAutoNetwork(GENAI_NFT_NETWORKS);
- const { network: llmNetwork } = useAutoNetwork(LLM_V1_NETWORKS);
const contractAddress = service === "genimg" ? getGenAiNFTAddress(genimgNetwork) : getLLMv1Address(llmNetwork);
// Sidebar variant - vertical layout
diff --git a/website/test/AgentInfoPanel.test.tsx b/website/test/AgentInfoPanel.test.tsx
new file mode 100644
index 000000000..3563d2a52
--- /dev/null
+++ b/website/test/AgentInfoPanel.test.tsx
@@ -0,0 +1,200 @@
+/**
+ * AgentInfoPanel Component Tests
+ *
+ * Ensures the component renders correctly under all states (loading, error, success)
+ * without violating React hooks rules.
+ *
+ * Bug Prevention: This test catches the "Rendered more hooks than during previous render"
+ * error that occurs when hooks are called after early returns.
+ */
+
+import React from "react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { AgentInfoPanel } from "../components/AgentInfoPanel";
+
+// Mock useAgentInfo with different states
+const mockUseAgentInfo = vi.fn();
+vi.mock("../hooks/useAgentInfo", () => ({
+ useAgentInfo: () => mockUseAgentInfo(),
+}));
+
+// Mock useLocale
+vi.mock("../hooks/useLocale", () => ({
+ useLocale: vi.fn(() => "Powered by"),
+}));
+
+// Mock useAutoNetwork - must be called consistently regardless of early returns
+vi.mock("../hooks/useAutoNetwork", () => ({
+ useAutoNetwork: vi.fn(() => ({
+ network: "eip155:10",
+ isOnCorrectNetwork: true,
+ switchIfNeeded: vi.fn(() => Promise.resolve(true)),
+ })),
+}));
+
+// Mock chain-utils
+vi.mock("@fretchen/chain-utils", () => ({
+ getGenAiNFTAddress: vi.fn(() => "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"),
+ getLLMv1Address: vi.fn(() => "0x1234567890123456789012345678901234567890"),
+ GENAI_NFT_NETWORKS: ["eip155:10", "eip155:11155420"],
+ LLM_V1_NETWORKS: ["eip155:10", "eip155:11155420"],
+}));
+
+// Mock styles
+vi.mock("../styled-system/css", () => ({
+ css: () => "mock-css-class",
+}));
+
+describe("AgentInfoPanel Component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("Hooks Consistency (Bug Prevention)", () => {
+ /**
+ * This test specifically catches the bug where hooks were called after early returns.
+ * React requires hooks to be called in the same order on every render.
+ * By testing all three states sequentially, we ensure hooks are always called.
+ */
+ it("should render without hooks error in loading state", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {},
+ isLoading: true,
+ error: null,
+ });
+
+ // Should not throw "Rendered more hooks than during previous render"
+ expect(() => render()).not.toThrow();
+ expect(screen.getByText(/Powered by.*Optimism/)).toBeInTheDocument();
+ });
+
+ it("should render without hooks error in error state", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: { wallet: null },
+ isLoading: false,
+ error: new Error("Failed to fetch"),
+ });
+
+ expect(() => render()).not.toThrow();
+ expect(screen.getByText("Optimism")).toBeInTheDocument();
+ });
+
+ it("should render without hooks error in success state", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ name: "Test Agent",
+ genimgEndpoint: "https://genimg.example.com/api",
+ llmEndpoint: "https://llm.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+
+ expect(() => render()).not.toThrow();
+ });
+
+ it("should render all states in sequence without hooks error", () => {
+ // This test simulates what happens during re-renders with changing state
+ // If hooks are called after early returns, this will fail
+
+ // First: loading
+ mockUseAgentInfo.mockReturnValue({
+ agent: {},
+ isLoading: true,
+ error: null,
+ });
+ const { rerender } = render();
+
+ // Then: success
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ genimgEndpoint: "https://genimg.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+ expect(() => rerender()).not.toThrow();
+
+ // Then: error
+ mockUseAgentInfo.mockReturnValue({
+ agent: { wallet: null },
+ isLoading: false,
+ error: new Error("Network error"),
+ });
+ expect(() => rerender()).not.toThrow();
+
+ // Back to loading
+ mockUseAgentInfo.mockReturnValue({
+ agent: {},
+ isLoading: true,
+ error: null,
+ });
+ expect(() => rerender()).not.toThrow();
+ });
+ });
+
+ describe("Variants", () => {
+ it("should render footer variant", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ genimgEndpoint: "https://genimg.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+
+ expect(() => render()).not.toThrow();
+ });
+
+ it("should render sidebar variant", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ genimgEndpoint: "https://genimg.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+
+ expect(() => render()).not.toThrow();
+ });
+ });
+
+ describe("Service Types", () => {
+ it("should render genimg service", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ genimgEndpoint: "https://genimg.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+
+ expect(() => render()).not.toThrow();
+ });
+
+ it("should render llm service", () => {
+ mockUseAgentInfo.mockReturnValue({
+ agent: {
+ wallet: "0xAAEBC1441323B8ad6Bdf6793A8428166b510239C",
+ walletShort: "0xAAEB...239C",
+ llmEndpoint: "https://llm.example.com/api",
+ },
+ isLoading: false,
+ error: null,
+ });
+
+ expect(() => render()).not.toThrow();
+ });
+ });
+});
From 0a91e2fcc3b205f4c5b0cb181e9a525ba76f4c02 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 29 Jan 2026 07:49:36 +0100
Subject: [PATCH 32/46] Update PublicNFTList.test.tsx
---
website/test/PublicNFTList.test.tsx | 89 ++++++++++++++++++++++++++---
1 file changed, 81 insertions(+), 8 deletions(-)
diff --git a/website/test/PublicNFTList.test.tsx b/website/test/PublicNFTList.test.tsx
index 95610b87c..6159a5ec0 100644
--- a/website/test/PublicNFTList.test.tsx
+++ b/website/test/PublicNFTList.test.tsx
@@ -1,25 +1,40 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
import { PublicNFTList } from "../components/PublicNFTList";
/**
* Mock complex dependencies to focus on component logic
*/
-vi.mock("viem", () => ({
- createPublicClient: vi.fn(() => ({
- readContract: vi.fn(),
+
+// Mock useAutoNetwork - must return object with network
+vi.mock("../hooks/useAutoNetwork", () => ({
+ useAutoNetwork: vi.fn(() => ({
+ network: "eip155:10",
+ isOnCorrectNetwork: true,
+ switchIfNeeded: vi.fn(() => Promise.resolve(true)),
})),
- http: vi.fn(),
}));
-vi.mock("viem/chains", () => ({
- optimism: { id: 10, name: "Optimism" },
+// Mock useConfiguredPublicClient - returns a mock client
+const mockReadContract = vi.fn();
+vi.mock("../hooks/useConfiguredPublicClient", () => ({
+ useConfiguredPublicClient: vi.fn(() => ({
+ readContract: mockReadContract,
+ })),
}));
-// No need to mock getChain - it's just reading env vars and returning constants
+// Mock chain-utils
+vi.mock("@fretchen/chain-utils", () => ({
+ getGenAiNFTAddress: vi.fn(() => "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"),
+ GenImNFTv4ABI: [
+ { name: "getAllPublicTokens", type: "function", inputs: [], outputs: [{ type: "uint256[]" }] },
+ ],
+ GENAI_NFT_NETWORKS: ["eip155:10", "eip155:11155420"],
+}));
vi.mock("../components/NFTCard", () => ({
- NFTCard: vi.fn(() => Mock NFT Card
),
+ NFTCard: vi.fn(({ tokenId }) => NFT {tokenId.toString()}
),
}));
vi.mock("../components/ImageModal", () => ({
@@ -32,11 +47,16 @@ vi.mock("../layouts/styles", () => ({
loadingContainer: "loading-container",
emptyStateContainer: "empty-state-container",
emptyStateText: "empty-state-text",
+ galleryGrid: "gallery-grid",
grid: "nft-grid",
},
spinner: "spinner",
}));
+vi.mock("../hooks/useLocale", () => ({
+ useLocale: vi.fn(() => "Mocked text"),
+}));
+
global.fetch = vi.fn();
/**
@@ -46,6 +66,8 @@ global.fetch = vi.fn();
describe("PublicNFTList Component", () => {
beforeEach(() => {
vi.clearAllMocks();
+ // Default: return empty array (no public NFTs)
+ mockReadContract.mockResolvedValue([]);
});
/**
@@ -55,6 +77,57 @@ describe("PublicNFTList Component", () => {
expect(typeof PublicNFTList).toBe("function");
});
+ /**
+ * CRITICAL: Tests that component actually renders without errors
+ * This catches ABI mismatches like missing getAllPublicTokens function
+ */
+ describe("Rendering (Bug Prevention)", () => {
+ it("should render loading state initially", () => {
+ // Don't resolve the promise immediately
+ mockReadContract.mockReturnValue(new Promise(() => {}));
+
+ render();
+
+ expect(screen.getByText(/Loading public artworks/i)).toBeInTheDocument();
+ });
+
+ it("should render empty state when no public NFTs", async () => {
+ mockReadContract.mockResolvedValue([]);
+
+ render();
+
+ // Wait for loading to finish
+ await vi.waitFor(() => {
+ expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
+ });
+ });
+
+ it("should render NFT cards when public NFTs exist", async () => {
+ mockReadContract.mockResolvedValue([1n, 2n, 3n]);
+
+ render();
+
+ // Wait for NFTs to load
+ await vi.waitFor(() => {
+ expect(screen.getByTestId("nft-card-3")).toBeInTheDocument();
+ expect(screen.getByTestId("nft-card-2")).toBeInTheDocument();
+ expect(screen.getByTestId("nft-card-1")).toBeInTheDocument();
+ });
+ });
+
+ it("should handle contract errors gracefully", async () => {
+ mockReadContract.mockRejectedValue(new Error("Contract call failed"));
+
+ // Should not throw
+ expect(() => render()).not.toThrow();
+
+ // Wait for error handling
+ await vi.waitFor(() => {
+ expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
+ });
+ });
+ });
+
/**
* Tests component structure and React element creation
*/
From f56f7fb2f4a3835fcc3669b6cf72cccf946c0da7 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 29 Jan 2026 16:33:17 +0100
Subject: [PATCH 33/46] Cleaner tests
---
website/components/NFTCard.tsx | 2 +-
website/test/ImageGenerator.test.tsx | 7 +-
website/test/MyNFTList.test.tsx | 101 ++++++++-------
website/test/PublicNFTList.test.tsx | 99 +++++----------
website/test/useAutoNetwork.test.ts | 180 ++++++++-------------------
5 files changed, 141 insertions(+), 248 deletions(-)
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 45322c349..706682ad6 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -120,7 +120,7 @@ export function NFTCard({
const isListedResult = await readContract(config, {
address: contractAddress,
abi: GenImNFTv4ABI,
- functionName: "isTokenListed",
+ functionName: "isListed",
args: [tokenId],
chainId,
});
diff --git a/website/test/ImageGenerator.test.tsx b/website/test/ImageGenerator.test.tsx
index 53ac5c7f6..f1148cd2d 100644
--- a/website/test/ImageGenerator.test.tsx
+++ b/website/test/ImageGenerator.test.tsx
@@ -166,7 +166,12 @@ describe("ImageGenerator Component", () => {
});
});
- it("should call switchChain when user attempts to create artwork on wrong network", async () => {
+ // SKIPPED: This test has two issues:
+ // 1. vi.mocked(useChainId).mockReturnValue() doesn't work due to module caching
+ // (see useAutoNetwork.test.ts header for detailed explanation)
+ // 2. With "switch at interaction" pattern, switchChain is called via switchIfNeeded()
+ // inside the submit handler, not directly on button click
+ it.skip("should call switchChain when user attempts to create artwork on wrong network", async () => {
const mockSwitchChain = vi.fn().mockResolvedValue(undefined);
// Override the centralized mocks for this specific test
diff --git a/website/test/MyNFTList.test.tsx b/website/test/MyNFTList.test.tsx
index f64a82e94..4a09db723 100644
--- a/website/test/MyNFTList.test.tsx
+++ b/website/test/MyNFTList.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, waitFor, screen, cleanup } from "@testing-library/react";
import { MyNFTList } from "../components/MyNFTList";
/**
@@ -110,68 +110,65 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
});
});
- it("should show the bug is fixed with stable constants", async () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("should call balanceOf ABI function when loading NFTs", async () => {
// Render the component
render();
- // Wait a bit to let effects run
- await waitFor(
- () => {
- // With stable constants, only a few renders should occur
- expect(renderCount).toBeLessThanOrEqual(3);
- },
- { timeout: 2000 },
- );
-
- // Die instabile contractConfig Funktion sollte nicht mehr aufgerufen werden
- // da die Komponenten jetzt die stabile Konstante verwenden
- expect(contractConfigCallCount).toBe(0);
-
- console.log(`Total renders: ${renderCount}`);
- console.log(`Contract config calls: ${contractConfigCallCount}`);
+ // Wait for the component to load
+ await waitFor(() => {
+ // balanceOf should be called via useReadContract (mocked above)
+ expect(mockUseReadContract).toHaveBeenCalled();
+ });
});
- it("should show that chain-utils provides consistent addresses", async () => {
- // Import from chain-utils
- const { getGenAiNFTAddress } = await import("@fretchen/chain-utils");
+ it("should call tokenOfOwnerByIndex ABI function for each NFT", async () => {
+ // Render the component
+ render();
- // Multiple calls with same network should return the same address
- const address1 = getGenAiNFTAddress("eip155:10");
- const address2 = getGenAiNFTAddress("eip155:10");
+ // Wait for the component to load and fetch token IDs
+ await waitFor(() => {
+ // tokenOfOwnerByIndex should be called for each NFT in the balance
+ expect(mockReadContract).toHaveBeenCalled();
+ }, { timeout: 2000 });
+
+ // Verify tokenOfOwnerByIndex was called with correct function name
+ const calls = mockReadContract.mock.calls;
+ const tokenOfOwnerByIndexCalls = calls.filter(
+ (call: unknown[]) => {
+ const params = call[1] as { functionName?: string };
+ return params?.functionName === "tokenOfOwnerByIndex";
+ }
+ );
- // They should be exactly the same
- expect(address1).toEqual(address2);
- expect(address1).toBe(address2);
+ expect(tokenOfOwnerByIndexCalls.length).toBeGreaterThan(0);
});
- it("should demonstrate stable useAutoNetwork hook", async () => {
- let effectRunCount = 0;
-
- // Import useAutoNetwork at the top of the test
- const { useAutoNetwork } = await import("../hooks/useAutoNetwork");
-
- // Create a component that uses the useAutoNetwork hook
- function FixedComponent() {
- const network = useAutoNetwork(["eip155:10", "eip155:11155420"]);
-
- React.useEffect(() => {
- effectRunCount++;
- console.log(`Effect run #${effectRunCount}, network: ${network}`);
- }, [network]);
+ it("should render NFT cards after loading token IDs", async () => {
+ render();
- return Component on {network}
;
- }
+ // Wait for NFT cards to appear
+ await waitFor(() => {
+ expect(screen.getByTestId("nft-card-1")).toBeInTheDocument();
+ expect(screen.getByTestId("nft-card-2")).toBeInTheDocument();
+ }, { timeout: 2000 });
+ });
- render();
+ it("should show empty state when user has no NFTs", async () => {
+ // Mock no NFTs
+ mockUseReadContract.mockReturnValue({
+ data: 0n,
+ isLoading: false,
+ });
- await waitFor(
- () => {
- // With useAutoNetwork, the effect only runs once
- expect(effectRunCount).toBeLessThanOrEqual(2);
- },
- { timeout: 1000 },
- );
+ render();
- console.log(`Effect ran ${effectRunCount} times with useAutoNetwork`);
+ // Should not show any NFT cards
+ await waitFor(() => {
+ expect(screen.queryByTestId("nft-card-1")).not.toBeInTheDocument();
+ });
});
});
diff --git a/website/test/PublicNFTList.test.tsx b/website/test/PublicNFTList.test.tsx
index 6159a5ec0..f5a571c77 100644
--- a/website/test/PublicNFTList.test.tsx
+++ b/website/test/PublicNFTList.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen, waitFor, cleanup } from "@testing-library/react";
import { PublicNFTList } from "../components/PublicNFTList";
/**
@@ -70,6 +70,10 @@ describe("PublicNFTList Component", () => {
mockReadContract.mockResolvedValue([]);
});
+ afterEach(() => {
+ cleanup();
+ });
+
/**
* Tests component importability
*/
@@ -97,7 +101,7 @@ describe("PublicNFTList Component", () => {
render();
// Wait for loading to finish
- await vi.waitFor(() => {
+ await waitFor(() => {
expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
});
});
@@ -108,13 +112,28 @@ describe("PublicNFTList Component", () => {
render();
// Wait for NFTs to load
- await vi.waitFor(() => {
+ await waitFor(() => {
expect(screen.getByTestId("nft-card-3")).toBeInTheDocument();
expect(screen.getByTestId("nft-card-2")).toBeInTheDocument();
expect(screen.getByTestId("nft-card-1")).toBeInTheDocument();
});
});
+ it("should call getAllPublicTokens ABI function", async () => {
+ mockReadContract.mockResolvedValue([1n, 2n]);
+
+ render();
+
+ await waitFor(() => {
+ // Verify the correct ABI function is called
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.objectContaining({
+ functionName: "getAllPublicTokens",
+ })
+ );
+ });
+ });
+
it("should handle contract errors gracefully", async () => {
mockReadContract.mockRejectedValue(new Error("Contract call failed"));
@@ -122,7 +141,7 @@ describe("PublicNFTList Component", () => {
expect(() => render()).not.toThrow();
// Wait for error handling
- await vi.waitFor(() => {
+ await waitFor(() => {
expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
});
});
@@ -136,71 +155,15 @@ describe("PublicNFTList Component", () => {
expect(typeof PublicNFTList).toBe("function");
});
- /**
- * Tests props interface compatibility
- */
- it("should accept correct props interface", () => {
- expect(() => {
- const element = React.createElement(PublicNFTList, {});
- expect(element).toBeDefined();
- }).not.toThrow();
- });
-
/**
* Tests className prop from BaseComponentProps
*/
- it("should accept className prop", () => {
- expect(() => {
- const element = React.createElement(PublicNFTList, { className: "custom-class" });
- expect(element).toBeDefined();
- }).not.toThrow();
- });
-
- /**
- * Tests that component is designed for wallet independence
- */
- it("should be designed for wallet independence", () => {
- // PublicNFTList should work without wallet connection
- // This is tested by the fact that it can be created without wallet-related props
- expect(() => {
- const element = React.createElement(PublicNFTList, {});
- expect(element).toBeDefined();
- }).not.toThrow();
- });
-
- /**
- * Tests component with optional props
- */
- it("should handle optional props", () => {
- const propsWithOptional = {
- className: "custom-public-nft-list",
- };
-
- expect(() => {
- const element = React.createElement(PublicNFTList, propsWithOptional);
- expect(element).toBeDefined();
- }).not.toThrow();
- });
-
- /**
- * Tests architectural requirement: uses viem public client
- */
- it("should use viem public client architecture", async () => {
- // Verify that the component imports and could use createPublicClient
- const { createPublicClient } = await import("viem");
- expect(createPublicClient).toBeDefined();
- expect(typeof createPublicClient).toBe("function");
- });
-
- /**
- * Tests that component doesn't require wallet dependencies
- */
- it("should not depend on wagmi wallet hooks", () => {
- // The component should be importable and creatable without wallet hooks
- // This validates the architectural decision to use public clients only
- expect(() => {
- const element = React.createElement(PublicNFTList, {});
- expect(element).toBeDefined();
- }).not.toThrow();
+ it("should accept className prop", async () => {
+ render();
+ // Wait for async operations to complete
+ await waitFor(() => {
+ expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
+ });
});
});
+
diff --git a/website/test/useAutoNetwork.test.ts b/website/test/useAutoNetwork.test.ts
index a0c8b9840..12bfb428f 100644
--- a/website/test/useAutoNetwork.test.ts
+++ b/website/test/useAutoNetwork.test.ts
@@ -1,3 +1,38 @@
+/**
+ * Tests for useAutoNetwork hook
+ *
+ * ============================================================================
+ * KNOWN ISSUE: Dynamic chainId mocking does not work
+ * ============================================================================
+ *
+ * Problem: vi.mocked(useChainId).mockReturnValue(X) does not change the
+ * chainId that the hook sees. The hook always receives chainId=10 regardless
+ * of what we set in the test.
+ *
+ * Root cause analysis:
+ * 1. setup.ts defines a global vi.mock("wagmi") with useChainId: vi.fn(() => 10)
+ * 2. This test file also defines vi.mock("wagmi") but vi.mock() is hoisted
+ * 3. The module is cached after first import - subsequent mockReturnValue()
+ * calls don't affect the already-imported hook
+ * 4. vi.hoisted() pattern doesn't work because you can't export hoisted variables
+ * 5. vi.doMock() + vi.resetModules() also doesn't work due to module caching
+ *
+ * Attempted solutions that failed:
+ * - vi.mocked(useChainId).mockReturnValue() - ignored by cached module
+ * - vi.hoisted() with exported mocks - vitest syntax error
+ * - vi.resetModules() + vi.doMock() + dynamic import - still cached
+ * - Shared mutable mockState object - closure captures initial value
+ *
+ * Consequence:
+ * - Tests that require chainId !== 10 will fail
+ * - Only tests with chainId=10 (Optimism mainnet) work
+ *
+ * TODO: Investigate alternative approaches:
+ * - Separate test files per chainId scenario (each with own vi.mock)
+ * - Dependency injection pattern in the hook
+ * - Use real wagmi with test WagmiConfig wrapper
+ * ============================================================================
+ */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
@@ -16,7 +51,6 @@ vi.mock("@fretchen/chain-utils", () => ({
}));
import { useAutoNetwork } from "../hooks/useAutoNetwork";
-import { useChainId, useAccount } from "wagmi";
describe("useAutoNetwork Hook", () => {
beforeEach(() => {
@@ -36,11 +70,10 @@ describe("useAutoNetwork Hook", () => {
});
});
- describe("when on a supported network", () => {
- it("should return the current network as CAIP-2", () => {
- vi.mocked(useChainId).mockReturnValue(10); // Optimism
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+ describe("when on Optimism (chainId=10)", () => {
+ // Note: These tests work because the mock defaults to chainId=10
+ it("should return eip155:10 as current network", () => {
const supportedNetworks = ["eip155:10", "eip155:11155420"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
@@ -48,110 +81,18 @@ describe("useAutoNetwork Hook", () => {
expect(result.current.isOnCorrectNetwork).toBe(true);
});
- it("should return testnet network when on testnet", () => {
- vi.mocked(useChainId).mockReturnValue(11155420); // Optimism Sepolia
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- expect(result.current.network).toBe("eip155:11155420");
- expect(result.current.isOnCorrectNetwork).toBe(true);
- });
-
- it("should NOT auto-switch - no popups on connect", () => {
- vi.mocked(useChainId).mockReturnValue(10);
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- renderHook(() => useAutoNetwork(supportedNetworks));
-
- // Key change: NO auto-switch on connect
- expect(mockSwitchChainAsync).not.toHaveBeenCalled();
- });
- });
-
- describe("when on an unsupported network", () => {
- it("should NOT auto-switch - deferred to interaction", () => {
- vi.mocked(useChainId).mockReturnValue(1); // Ethereum mainnet (unsupported)
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
+ it("should NOT auto-switch on mount", () => {
const supportedNetworks = ["eip155:10", "eip155:11155420"];
renderHook(() => useAutoNetwork(supportedNetworks));
- // Key change: NO auto-switch
expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
- it("should return default network and isOnCorrectNetwork=false", () => {
- vi.mocked(useChainId).mockReturnValue(1); // Unsupported
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- expect(result.current.network).toBe("eip155:10"); // Default
- expect(result.current.isOnCorrectNetwork).toBe(false);
- });
- });
-
- describe("switchIfNeeded() function", () => {
- it("should return true immediately when already on correct network", async () => {
- vi.mocked(useChainId).mockReturnValue(10); // Supported
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
+ it("switchIfNeeded() should return true without switching", async () => {
const supportedNetworks = ["eip155:10"];
const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
- let switched: boolean = false;
- await act(async () => {
- switched = await result.current.switchIfNeeded();
- });
-
- expect(switched).toBe(true);
- expect(mockSwitchChainAsync).not.toHaveBeenCalled();
- });
-
- it("should switch chain when on unsupported network", async () => {
- vi.mocked(useChainId).mockReturnValue(1); // Unsupported
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- let switched: boolean = false;
- await act(async () => {
- switched = await result.current.switchIfNeeded();
- });
-
- expect(switched).toBe(true);
- expect(mockSwitchChainAsync).toHaveBeenCalledWith({ chainId: 10 });
- });
-
- it("should return false when user rejects switch", async () => {
- vi.mocked(useChainId).mockReturnValue(1); // Unsupported
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
- mockSwitchChainAsync.mockRejectedValue(new Error("User rejected"));
-
- const supportedNetworks = ["eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- let switched: boolean = true;
- await act(async () => {
- switched = await result.current.switchIfNeeded();
- });
-
- expect(switched).toBe(false);
- });
-
- it("should return true when not connected (nothing to switch)", async () => {
- vi.mocked(useChainId).mockReturnValue(1);
- vi.mocked(useAccount).mockReturnValue({ isConnected: false } as ReturnType);
-
- const supportedNetworks = ["eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- let switched: boolean = false;
+ let switched = false;
await act(async () => {
switched = await result.current.switchIfNeeded();
});
@@ -161,38 +102,25 @@ describe("useAutoNetwork Hook", () => {
});
});
- describe("edge cases", () => {
- it("should handle single network in list", () => {
- vi.mocked(useChainId).mockReturnValue(10);
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
+ // ============================================================================
+ // SKIPPED: These tests require dynamic chainId which doesn't work
+ // ============================================================================
- const supportedNetworks = ["eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- expect(result.current.network).toBe("eip155:10");
- expect(result.current.isOnCorrectNetwork).toBe(true);
+ describe.skip("when on other networks (SKIPPED - chainId mock issue)", () => {
+ it("should return testnet network when on Optimism Sepolia", () => {
+ // Would need: vi.mocked(useChainId).mockReturnValue(11155420)
});
- it("should handle Base network", () => {
- vi.mocked(useChainId).mockReturnValue(8453); // Base mainnet
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:10", "eip155:8453"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
-
- expect(result.current.network).toBe("eip155:8453");
- expect(result.current.isOnCorrectNetwork).toBe(true);
+ it("should return Base network when on Base", () => {
+ // Would need: vi.mocked(useChainId).mockReturnValue(8453)
});
- it("should use first network as default when on unsupported", () => {
- vi.mocked(useChainId).mockReturnValue(137); // Polygon (unsupported)
- vi.mocked(useAccount).mockReturnValue({ isConnected: true } as ReturnType);
-
- const supportedNetworks = ["eip155:11155420", "eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+ it("should return default when on unsupported network", () => {
+ // Would need: vi.mocked(useChainId).mockReturnValue(1)
+ });
- // First network is testnet - that's the default
- expect(result.current.network).toBe("eip155:11155420");
+ it("switchIfNeeded() should switch when on wrong network", () => {
+ // Would need: vi.mocked(useChainId).mockReturnValue(1)
});
});
});
From 92bbce8aa9b8653a08c3bba45adfec0bb534b315 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Thu, 29 Jan 2026 18:36:11 +0100
Subject: [PATCH 34/46] All test now run through
---
website/hooks/useAutoNetwork.ts | 4 +-
website/test/setup.ts | 142 +++++-----
website/test/useAutoNetwork.test.ts | 392 ++++++++++++++++++++++------
3 files changed, 391 insertions(+), 147 deletions(-)
diff --git a/website/hooks/useAutoNetwork.ts b/website/hooks/useAutoNetwork.ts
index b25767522..6843184d1 100644
--- a/website/hooks/useAutoNetwork.ts
+++ b/website/hooks/useAutoNetwork.ts
@@ -53,8 +53,8 @@ export function useAutoNetwork(supportedNetworks: readonly string[]): UseAutoNet
const defaultNetwork = supportedNetworks[0];
const currentNetwork = toCAIP2(chainId);
const isSupported = supportedNetworks.includes(currentNetwork);
-
- // Target network: current if supported, otherwise default
+
+ // Return the current network if supported, otherwise the default
const network = isSupported ? currentNetwork : defaultNetwork;
const switchIfNeeded = useCallback(async (): Promise => {
diff --git a/website/test/setup.ts b/website/test/setup.ts
index 7609fd86b..10ecc3c98 100644
--- a/website/test/setup.ts
+++ b/website/test/setup.ts
@@ -2,55 +2,61 @@ import "@testing-library/jest-dom";
import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
-// Mock wagmi hooks that are used in components
+// =============================================================================
+// EXPORTABLE MOCK FUNCTIONS
+// Tests can import these and configure them per test/describe block
+// =============================================================================
+
+// Chain & Network Mocks
+export const mockChainId = vi.fn(() => 10); // Default: Optimism
+export const mockSwitchChainAsync = vi.fn().mockResolvedValue(undefined);
+
+// Account Mocks
+export const mockAccountData = vi.fn(() => ({
+ address: "0x123456789abcdef" as `0x${string}`,
+ isConnected: false,
+ status: "disconnected" as const,
+ isConnecting: false,
+ isDisconnected: true,
+ isReconnecting: false,
+}));
+
+// Contract Mocks
+export const mockReadContractData = vi.fn(() => ({
+ data: undefined,
+ error: null,
+ isPending: false,
+ refetch: vi.fn(),
+}));
+
+export const mockWriteContractData = vi.fn(() => ({
+ writeContract: vi.fn(),
+ writeContractAsync: vi.fn(),
+ isPending: false,
+ error: null,
+}));
+
+// =============================================================================
+// WAGMI MOCK SETUP - Uses the exportable functions above
+// =============================================================================
+
vi.mock("wagmi", () => ({
- useAccount: vi.fn(() => ({
- address: "0x123456789abcdef",
- isConnected: false,
- })),
- useWalletClient: vi.fn(() => ({
- data: undefined,
- })),
- useSignMessage: vi.fn(() => ({
- signMessageAsync: vi.fn(),
- })),
- useReadContract: vi.fn(() => ({
- data: undefined,
- error: null,
- isPending: false,
- refetch: vi.fn(),
- })),
- useReadContracts: vi.fn(() => ({
- data: undefined,
- error: null,
- isPending: false,
- refetch: vi.fn(),
- })),
- useWriteContract: vi.fn(() => ({
- writeContract: vi.fn(),
- writeContractAsync: vi.fn(),
- isPending: false,
- error: null,
- })),
- useWaitForTransactionReceipt: vi.fn(() => ({
- isLoading: false,
- isSuccess: false,
- })),
- useChainId: vi.fn(() => 10),
+ useAccount: vi.fn(() => mockAccountData()),
+ useWalletClient: vi.fn(() => ({ data: undefined })),
+ useSignMessage: vi.fn(() => ({ signMessageAsync: vi.fn() })),
+ useReadContract: vi.fn(() => mockReadContractData()),
+ useReadContracts: vi.fn(() => mockReadContractData()),
+ useWriteContract: vi.fn(() => mockWriteContractData()),
+ useWaitForTransactionReceipt: vi.fn(() => ({ isLoading: false, isSuccess: false })),
+ useChainId: vi.fn(() => mockChainId()),
useSwitchChain: vi.fn(() => ({
switchChain: vi.fn(),
+ switchChainAsync: mockSwitchChainAsync,
chains: [],
})),
- useConnect: vi.fn(() => ({
- connectors: [],
- connect: vi.fn(),
- })),
- useDisconnect: vi.fn(() => ({
- disconnect: vi.fn(),
- })),
- useEnsName: vi.fn(() => ({
- data: null,
- })),
+ useConnect: vi.fn(() => ({ connectors: [], connect: vi.fn() })),
+ useDisconnect: vi.fn(() => ({ disconnect: vi.fn() })),
+ useEnsName: vi.fn(() => ({ data: null })),
createConfig: vi.fn(() => ({})),
http: vi.fn(),
WagmiProvider: vi.fn(({ children }) => children),
@@ -79,27 +85,21 @@ vi.mock("vike-react/usePageContext", () => ({
})),
}));
-// No need to mock utils/getChain - it's just reading env vars and returning constants
-// The real implementation works fine in tests and ensures we test realistic configurations
-
// Mock useLocale hook
vi.mock("./hooks/useLocale", () => ({
useLocale: vi.fn(({ label }: { label: string }) => label),
}));
-// Mock useAutoNetwork hook - returns object with network and switchIfNeeded
-vi.mock("../hooks/useAutoNetwork", () => ({
- useAutoNetwork: vi.fn(() => ({
- network: "eip155:10", // Default to Optimism mainnet
- isOnCorrectNetwork: true,
- switchIfNeeded: vi.fn(() => Promise.resolve(true)),
- })),
-}));
+// NOTE: useAutoNetwork is NOT mocked here anymore!
+// Tests that need specific useAutoNetwork behavior should:
+// 1. Configure mockChainId() for the desired chain
+// 2. Let the real hook run with mocked wagmi hooks
-// Import wagmi at top level for mock utilities
-import { useAccount } from "wagmi";
+// =============================================================================
+// HELPER FUNCTIONS FOR TESTS
+// =============================================================================
-// Reusable mock data for tests
+// Reusable mock data
export const MOCK_CONNECTED_ACCOUNT = {
address: "0x1234567890123456789012345678901234567890" as `0x${string}`,
isConnected: true,
@@ -107,7 +107,7 @@ export const MOCK_CONNECTED_ACCOUNT = {
isConnecting: false,
isDisconnected: false,
isReconnecting: false,
-} as ReturnType;
+};
export const MOCK_DISCONNECTED_ACCOUNT = {
address: undefined,
@@ -116,18 +116,34 @@ export const MOCK_DISCONNECTED_ACCOUNT = {
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
-} as ReturnType;
+};
+
+// Helper to set chain for a test
+export const mockChain = (chainId: number) => {
+ mockChainId.mockReturnValue(chainId);
+};
-// Reusable mock utilities for tests
+// Helper to set connected wallet
export const mockConnectedWallet = () => {
- vi.mocked(useAccount).mockReturnValue(MOCK_CONNECTED_ACCOUNT);
+ mockAccountData.mockReturnValue(MOCK_CONNECTED_ACCOUNT);
};
export const mockDisconnectedWallet = () => {
- vi.mocked(useAccount).mockReturnValue(MOCK_DISCONNECTED_ACCOUNT);
+ mockAccountData.mockReturnValue(MOCK_DISCONNECTED_ACCOUNT);
};
// Clean up after each test
afterEach(() => {
cleanup();
+ // Reset all mock return values to defaults
+ mockChainId.mockReturnValue(10);
+ mockAccountData.mockReturnValue({
+ address: "0x123456789abcdef" as `0x${string}`,
+ isConnected: false,
+ status: "disconnected" as const,
+ isConnecting: false,
+ isDisconnected: true,
+ isReconnecting: false,
+ });
+ mockSwitchChainAsync.mockResolvedValue(undefined);
});
diff --git a/website/test/useAutoNetwork.test.ts b/website/test/useAutoNetwork.test.ts
index 12bfb428f..3c7832e9e 100644
--- a/website/test/useAutoNetwork.test.ts
+++ b/website/test/useAutoNetwork.test.ts
@@ -1,67 +1,84 @@
/**
- * Tests for useAutoNetwork hook
+ * Tests for useAutoNetwork hook using real wagmi with mock connector
*
- * ============================================================================
- * KNOWN ISSUE: Dynamic chainId mocking does not work
- * ============================================================================
- *
- * Problem: vi.mocked(useChainId).mockReturnValue(X) does not change the
- * chainId that the hook sees. The hook always receives chainId=10 regardless
- * of what we set in the test.
- *
- * Root cause analysis:
- * 1. setup.ts defines a global vi.mock("wagmi") with useChainId: vi.fn(() => 10)
- * 2. This test file also defines vi.mock("wagmi") but vi.mock() is hoisted
- * 3. The module is cached after first import - subsequent mockReturnValue()
- * calls don't affect the already-imported hook
- * 4. vi.hoisted() pattern doesn't work because you can't export hoisted variables
- * 5. vi.doMock() + vi.resetModules() also doesn't work due to module caching
- *
- * Attempted solutions that failed:
- * - vi.mocked(useChainId).mockReturnValue() - ignored by cached module
- * - vi.hoisted() with exported mocks - vitest syntax error
- * - vi.resetModules() + vi.doMock() + dynamic import - still cached
- * - Shared mutable mockState object - closure captures initial value
- *
- * Consequence:
- * - Tests that require chainId !== 10 will fail
- * - Only tests with chainId=10 (Optimism mainnet) work
- *
- * TODO: Investigate alternative approaches:
- * - Separate test files per chainId scenario (each with own vi.mock)
- * - Dependency injection pattern in the hook
- * - Use real wagmi with test WagmiConfig wrapper
- * ============================================================================
+ * Uses wagmi's mock connector to test with different chainIds in one file.
+ * See: https://wagmi.sh/react/api/connectors/mock
+ *
+ * IMPORTANT: The mock connector shares state between tests, so each test
+ * must first reset to a known chain state before asserting.
*/
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { renderHook, act } from "@testing-library/react";
-
-// Mock wagmi hooks
-const mockSwitchChainAsync = vi.fn();
-vi.mock("wagmi", () => ({
- useChainId: vi.fn(() => 10), // Default: Optimism mainnet
- useAccount: vi.fn(() => ({ isConnected: true })),
- useSwitchChain: vi.fn(() => ({ switchChainAsync: mockSwitchChainAsync })),
-}));
-
-// Mock chain-utils
-vi.mock("@fretchen/chain-utils", () => ({
- toCAIP2: (chainId: number) => `eip155:${chainId}`,
- fromCAIP2: (network: string) => parseInt(network.split(":")[1]),
-}));
+import { describe, it, expect, vi } from "vitest";
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { createElement } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+// Disable global wagmi mocks from setup.ts
+vi.unmock("wagmi");
+vi.unmock("wagmi/connectors");
+import {
+ createConfig,
+ WagmiProvider,
+ http,
+ useChainId,
+ useSwitchChain,
+ useAccount,
+ useConnect,
+} from "wagmi";
+import { optimism, optimismSepolia, base, mainnet } from "wagmi/chains";
+import { mock } from "wagmi/connectors";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
-describe("useAutoNetwork Hook", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- mockSwitchChainAsync.mockResolvedValue(undefined);
+// Test networks in CAIP-2 format
+const OPTIMISM_MAINNET = "eip155:10";
+const OPTIMISM_SEPOLIA = "eip155:11155420";
+const BASE_MAINNET = "eip155:8453";
+
+// Shared config - state persists between tests
+const sharedConfig = createConfig({
+ chains: [optimism, optimismSepolia, base, mainnet],
+ connectors: [
+ mock({
+ accounts: ["0x1234567890123456789012345678901234567890"],
+ features: { defaultConnected: true },
+ }),
+ ],
+ transports: {
+ [optimism.id]: http(),
+ [optimismSepolia.id]: http(),
+ [base.id]: http(),
+ [mainnet.id]: http(),
+ },
+});
+
+// Create test wrapper with shared config
+function createTestWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
});
+ return ({ children }: { children: React.ReactNode }) =>
+ createElement(
+ QueryClientProvider,
+ { client: queryClient },
+ createElement(WagmiProvider, { config: sharedConfig }, children)
+ );
+}
+
+describe("useAutoNetwork Hook", () => {
describe("return value structure", () => {
- it("should return network, isOnCorrectNetwork, and switchIfNeeded", () => {
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+ it("should return network, isOnCorrectNetwork, and switchIfNeeded", async () => {
+ const wrapper = createTestWrapper();
+ const supportedNetworks = [OPTIMISM_MAINNET, OPTIMISM_SEPOLIA];
+
+ const { result } = renderHook(
+ () => useAutoNetwork(supportedNetworks),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.network).toBeDefined();
+ });
expect(result.current).toHaveProperty("network");
expect(result.current).toHaveProperty("isOnCorrectNetwork");
@@ -70,27 +87,35 @@ describe("useAutoNetwork Hook", () => {
});
});
- describe("when on Optimism (chainId=10)", () => {
- // Note: These tests work because the mock defaults to chainId=10
+ describe("when on Optimism mainnet (chainId=10)", () => {
+ it("should return eip155:10 as current network", async () => {
+ const wrapper = createTestWrapper();
+ const supportedNetworks = [OPTIMISM_MAINNET, OPTIMISM_SEPOLIA];
+
+ const { result } = renderHook(
+ () => useAutoNetwork(supportedNetworks),
+ { wrapper }
+ );
- it("should return eip155:10 as current network", () => {
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+ await waitFor(() => {
+ expect(result.current.network).toBe(OPTIMISM_MAINNET);
+ });
- expect(result.current.network).toBe("eip155:10");
expect(result.current.isOnCorrectNetwork).toBe(true);
});
- it("should NOT auto-switch on mount", () => {
- const supportedNetworks = ["eip155:10", "eip155:11155420"];
- renderHook(() => useAutoNetwork(supportedNetworks));
+ it("switchIfNeeded() should return true without switching", async () => {
+ const wrapper = createTestWrapper();
+ const supportedNetworks = [OPTIMISM_MAINNET];
- expect(mockSwitchChainAsync).not.toHaveBeenCalled();
- });
+ const { result } = renderHook(
+ () => useAutoNetwork(supportedNetworks),
+ { wrapper }
+ );
- it("switchIfNeeded() should return true without switching", async () => {
- const supportedNetworks = ["eip155:10"];
- const { result } = renderHook(() => useAutoNetwork(supportedNetworks));
+ await waitFor(() => {
+ expect(result.current.network).toBeDefined();
+ });
let switched = false;
await act(async () => {
@@ -98,29 +123,232 @@ describe("useAutoNetwork Hook", () => {
});
expect(switched).toBe(true);
- expect(mockSwitchChainAsync).not.toHaveBeenCalled();
});
});
- // ============================================================================
- // SKIPPED: These tests require dynamic chainId which doesn't work
- // ============================================================================
+ describe("when on Optimism Sepolia (chainId=11155420)", () => {
+ it("should return eip155:11155420 as current network", async () => {
+ const wrapper = createTestWrapper();
+
+ // ALL hooks in ONE renderHook - same React tree
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([OPTIMISM_MAINNET, OPTIMISM_SEPOLIA]),
+ chainId: useChainId(),
+ switchChain: useSwitchChain(),
+ }),
+ { wrapper }
+ );
+
+ // Wait for initial render
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(10);
+ });
+
+ // Switch to Sepolia
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 11155420 });
+ });
+
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(11155420);
+ });
+
+ expect(result.current.autoNetwork.network).toBe(OPTIMISM_SEPOLIA);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(true);
+ });
+ });
+
+ describe("when on Base mainnet (chainId=8453)", () => {
+ it("should return eip155:8453 as current network when Base is supported", async () => {
+ const wrapper = createTestWrapper();
+
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([BASE_MAINNET, OPTIMISM_MAINNET]),
+ switchChain: useSwitchChain(),
+ }),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBeDefined();
+ });
+
+ // Switch to Base
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 8453 });
+ });
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBe(BASE_MAINNET);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(true);
+ });
+ });
+ });
+
+ describe("when on unsupported network (chainId=1 Ethereum)", () => {
+ it("should return default network (first in list)", async () => {
+ const wrapper = createTestWrapper();
+
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([OPTIMISM_MAINNET, OPTIMISM_SEPOLIA]),
+ switchChain: useSwitchChain(),
+ }),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBeDefined();
+ });
+
+ // Switch to Ethereum mainnet (unsupported)
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 1 });
+ });
- describe.skip("when on other networks (SKIPPED - chainId mock issue)", () => {
- it("should return testnet network when on Optimism Sepolia", () => {
- // Would need: vi.mocked(useChainId).mockReturnValue(11155420)
+ await waitFor(() => {
+ // Should return default (first supported network)
+ expect(result.current.autoNetwork.network).toBe(OPTIMISM_MAINNET);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(false);
+ });
});
- it("should return Base network when on Base", () => {
- // Would need: vi.mocked(useChainId).mockReturnValue(8453)
+ it("switchIfNeeded() should switch to default network", async () => {
+ const wrapper = createTestWrapper();
+
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([OPTIMISM_MAINNET, OPTIMISM_SEPOLIA]),
+ switchChain: useSwitchChain(),
+ chainId: useChainId(),
+ account: useAccount(),
+ connect: useConnect(),
+ }),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBeDefined();
+ });
+
+ // First: connect (mock connector is not auto-connected)
+ await act(async () => {
+ result.current.connect.connect({ connector: result.current.connect.connectors[0] });
+ });
+ await waitFor(() => {
+ expect(result.current.account.isConnected).toBe(true);
+ });
+
+ // Reset to Optimism (in case previous tests left us elsewhere)
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 10 });
+ });
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(10);
+ });
+
+ // Switch to Ethereum mainnet (unsupported)
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 1 });
+ });
+
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(1);
+ });
+
+ // Now call switchIfNeeded - should switch back to Optimism
+ await act(async () => {
+ const switched = await result.current.autoNetwork.switchIfNeeded();
+ expect(switched).toBe(true);
+ });
+
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(10); // Optimism
+ });
});
+ });
+
+ describe("network switching behavior", () => {
+ it("should NOT auto-switch on mount", async () => {
+ const wrapper = createTestWrapper();
- it("should return default when on unsupported network", () => {
- // Would need: vi.mocked(useChainId).mockReturnValue(1)
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([OPTIMISM_SEPOLIA]), // Only Sepolia supported
+ switchChain: useSwitchChain(),
+ chainId: useChainId(),
+ }),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBeDefined();
+ });
+
+ // Reset to Optimism (in case previous tests left us elsewhere)
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 10 });
+ });
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(10);
+ });
+
+ // Should still be on Optimism mainnet (10), NOT auto-switched
+ expect(result.current.chainId).toBe(10);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(false);
});
- it("switchIfNeeded() should switch when on wrong network", () => {
- // Would need: vi.mocked(useChainId).mockReturnValue(1)
+ it("switchIfNeeded() should switch when on wrong network", async () => {
+ const wrapper = createTestWrapper();
+
+ const { result } = renderHook(
+ () => ({
+ autoNetwork: useAutoNetwork([OPTIMISM_SEPOLIA]), // Only Sepolia supported
+ switchChain: useSwitchChain(),
+ chainId: useChainId(),
+ account: useAccount(),
+ connect: useConnect(),
+ }),
+ { wrapper }
+ );
+
+ await waitFor(() => {
+ expect(result.current.autoNetwork.network).toBeDefined();
+ });
+
+ // First: connect (mock connector is not auto-connected)
+ await act(async () => {
+ result.current.connect.connect({ connector: result.current.connect.connectors[0] });
+ });
+ await waitFor(() => {
+ expect(result.current.account.isConnected).toBe(true);
+ });
+
+ // Reset to Optimism (in case previous tests left us elsewhere)
+ await act(async () => {
+ result.current.switchChain.switchChain({ chainId: 10 });
+ });
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(10);
+ });
+
+ // Currently on Optimism (10), but only Sepolia supported
+ expect(result.current.chainId).toBe(10);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(false);
+
+ // Call switchIfNeeded
+ await act(async () => {
+ const switched = await result.current.autoNetwork.switchIfNeeded();
+ expect(switched).toBe(true);
+ });
+
+ // Should now be on Sepolia
+ await waitFor(() => {
+ expect(result.current.chainId).toBe(11155420);
+ expect(result.current.autoNetwork.isOnCorrectNetwork).toBe(true);
+ });
});
});
});
From 512da775fc03d0fcca1859ade2aeb51d3166c289 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 13:38:44 +0100
Subject: [PATCH 35/46] fix the testing
---
website/components/NFTCard.tsx | 45 ++--
website/hooks/useNFTListedStatus.ts | 107 +++++++++
website/test/useNFTListedStatus.test.ts | 286 ++++++++++++++++++++++++
3 files changed, 413 insertions(+), 25 deletions(-)
create mode 100644 website/hooks/useNFTListedStatus.ts
create mode 100644 website/test/useNFTListedStatus.test.ts
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 706682ad6..15651015c 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -1,8 +1,7 @@
import React, { useState, useEffect } from "react";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
-import { readContract } from "wagmi/actions";
-import { config } from "../wagmi.config";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
+import { useNFTListedStatus } from "../hooks/useNFTListedStatus";
import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2, isMainnet } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { NFTCardProps, NFT, NFTMetadata } from "../types/components";
@@ -49,6 +48,16 @@ export function NFTCard({
const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
+ // Use the extracted hook for listing status (only in private view)
+ const {
+ isListed,
+ setOptimisticListed,
+ } = useNFTListedStatus({
+ tokenId,
+ network,
+ enabled: !isPublicView,
+ });
+
// Use the custom hook for a stable public client reference
const publicClient = useConfiguredPublicClient(network);
@@ -113,22 +122,7 @@ export function NFTCard({
setOwner(nftOwner);
}
- // Get listing status if not public view
- let isListed: boolean | undefined;
- if (!isPublicView) {
- try {
- const isListedResult = await readContract(config, {
- address: contractAddress,
- abi: GenImNFTv4ABI,
- functionName: "isListed",
- args: [tokenId],
- chainId,
- });
- isListed = isListedResult as boolean;
- } catch (error) {
- console.warn("Could not load listing status:", error);
- }
- }
+ // Note: isListed is now handled by useNFTListedStatus hook
// Fetch metadata only if tokenURI is available
let metadata = preloadedMetadata; // Keep preloaded metadata as fallback
@@ -148,7 +142,6 @@ export function NFTCard({
metadata,
imageUrl: finalImageUrl,
isLoading: false,
- isListed,
});
} catch (error) {
console.error(`Error loading NFT ${tokenId}:`, error);
@@ -288,7 +281,7 @@ export function NFTCard({
const handleToggleListing = async () => {
if (!onListedStatusChanged) return;
- const newListedStatus = !nft.isListed;
+ const newListedStatus = !isListed;
const statusText = newListedStatus ? "public" : "private";
try {
@@ -299,7 +292,8 @@ export function NFTCard({
return;
}
- // Update UI optimistically
+ // Update UI optimistically (both local state and parent)
+ setOptimisticListed(newListedStatus);
onListedStatusChanged(nft.tokenId, newListedStatus);
// Call contract
@@ -312,6 +306,7 @@ export function NFTCard({
} catch (error) {
console.error("Failed to update listing status:", error);
// Revert optimistic update on error
+ setOptimisticListed(!newListedStatus);
onListedStatusChanged(nft.tokenId, !newListedStatus);
showToast(`Failed to set artwork as ${statusText}. Please try again.`, "error");
}
@@ -361,7 +356,7 @@ export function NFTCard({
#{nft.tokenId.toString()}
{/* Listed Badge (nur wenn listed) */}
- {!isPublicView && nft.isListed && ✓ {listedLabel}
}
+ {!isPublicView && isListed && ✓ {listedLabel}
}
{/* Owner Badge (nur in Public View) */}
{isPublicView && owner && (
@@ -415,7 +410,7 @@ export function NFTCard({
{/* Listed Toggle (nur private view) */}
- {!isPublicView && onListedStatusChanged && nft.isListed !== undefined && (
+ {!isPublicView && onListedStatusChanged && isListed !== undefined && (
)}
diff --git a/website/hooks/useNFTListedStatus.ts b/website/hooks/useNFTListedStatus.ts
new file mode 100644
index 000000000..08afaeb19
--- /dev/null
+++ b/website/hooks/useNFTListedStatus.ts
@@ -0,0 +1,107 @@
+/**
+ * Hook to fetch and manage the listing status of an NFT.
+ *
+ * This hook encapsulates the logic for checking if an NFT is publicly listed
+ * via the smart contract's isListed function.
+ *
+ * @example
+ * ```tsx
+ * const { isListed, isLoading, error, refetch } = useNFTListedStatus({
+ * tokenId: BigInt(42),
+ * network: "eip155:10",
+ * enabled: !isPublicView, // Only fetch in private view
+ * });
+ * ```
+ */
+
+import { useState, useEffect, useCallback } from "react";
+import { readContract } from "wagmi/actions";
+import { config } from "../wagmi.config";
+import { getGenAiNFTAddress, GenImNFTv4ABI, fromCAIP2 } from "@fretchen/chain-utils";
+
+export interface UseNFTListedStatusOptions {
+ /** The token ID to check */
+ tokenId: bigint;
+ /** The CAIP-2 network string (e.g., "eip155:10") */
+ network: string;
+ /** Whether to enable the query (default: true) */
+ enabled?: boolean;
+}
+
+export interface UseNFTListedStatusResult {
+ /** Whether the NFT is listed (undefined if not yet loaded) */
+ isListed: boolean | undefined;
+ /** Whether the query is currently loading */
+ isLoading: boolean;
+ /** Error message if the query failed */
+ error: string | undefined;
+ /** Function to manually refetch the listing status */
+ refetch: () => Promise;
+ /** Optimistically update the listing status (for UI feedback) */
+ setOptimisticListed: (value: boolean) => void;
+}
+
+/**
+ * Fetches and manages the listing status of an NFT from the smart contract.
+ *
+ * The isListed status determines whether an NFT appears in the public gallery.
+ * This hook only fetches in private view (when enabled=true).
+ */
+export function useNFTListedStatus({
+ tokenId,
+ network,
+ enabled = true,
+}: UseNFTListedStatusOptions): UseNFTListedStatusResult {
+ const [isListed, setIsListed] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ const chainId = fromCAIP2(network);
+ const contractAddress = getGenAiNFTAddress(network);
+
+ const fetchListedStatus = useCallback(async () => {
+ if (!enabled) {
+ setIsListed(undefined);
+ return;
+ }
+
+ setIsLoading(true);
+ setError(undefined);
+
+ try {
+ const result = await readContract(config, {
+ address: contractAddress,
+ abi: GenImNFTv4ABI,
+ functionName: "isListed",
+ args: [tokenId],
+ chainId,
+ });
+
+ setIsListed(result as boolean);
+ } catch (err) {
+ console.warn("Could not load listing status:", err);
+ setError(err instanceof Error ? err.message : "Failed to load listing status");
+ // Keep isListed as undefined on error
+ } finally {
+ setIsLoading(false);
+ }
+ }, [tokenId, network, enabled, contractAddress, chainId]);
+
+ // Fetch on mount and when dependencies change
+ useEffect(() => {
+ fetchListedStatus();
+ }, [fetchListedStatus]);
+
+ // Optimistic update for immediate UI feedback
+ const setOptimisticListed = useCallback((value: boolean) => {
+ setIsListed(value);
+ }, []);
+
+ return {
+ isListed,
+ isLoading,
+ error,
+ refetch: fetchListedStatus,
+ setOptimisticListed,
+ };
+}
diff --git a/website/test/useNFTListedStatus.test.ts b/website/test/useNFTListedStatus.test.ts
new file mode 100644
index 000000000..acf21aee6
--- /dev/null
+++ b/website/test/useNFTListedStatus.test.ts
@@ -0,0 +1,286 @@
+/**
+ * Tests for useNFTListedStatus hook
+ *
+ * This hook fetches the isListed status from the smart contract.
+ * Tests verify the contract call logic and state management.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { useNFTListedStatus } from "../hooks/useNFTListedStatus";
+
+// Mock the readContract function from wagmi/actions
+const mockReadContract = vi.fn();
+
+vi.mock("wagmi/actions", () => ({
+ readContract: (...args: unknown[]) => mockReadContract(...args),
+}));
+
+// Mock the wagmi config
+vi.mock("../wagmi.config", () => ({
+ config: { testConfig: true },
+}));
+
+// Mock chain-utils
+vi.mock("@fretchen/chain-utils", () => ({
+ getGenAiNFTAddress: vi.fn((network: string) => {
+ if (network === "eip155:10") return "0xOptimismContract";
+ if (network === "eip155:11155420") return "0xSepoliaContract";
+ return "0xUnknownContract";
+ }),
+ GenImNFTv4ABI: [{ name: "isListed", type: "function" }],
+ fromCAIP2: vi.fn((network: string) => {
+ if (network === "eip155:10") return 10;
+ if (network === "eip155:11155420") return 11155420;
+ return 1;
+ }),
+}));
+
+describe("useNFTListedStatus", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("Contract Call Behavior", () => {
+ it("should call isListed contract function when enabled", async () => {
+ mockReadContract.mockResolvedValue(true);
+
+ renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ enabled: true,
+ })
+ );
+
+ await waitFor(() => {
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.anything(), // config
+ expect.objectContaining({
+ address: "0xOptimismContract",
+ functionName: "isListed",
+ args: [BigInt(42)],
+ chainId: 10,
+ })
+ );
+ });
+ });
+
+ it("should NOT call isListed when enabled=false", async () => {
+ renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ enabled: false,
+ })
+ );
+
+ // Wait a tick to ensure no async calls are made
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ });
+
+ expect(mockReadContract).not.toHaveBeenCalled();
+ });
+
+ it("should use correct contract address for different networks", async () => {
+ mockReadContract.mockResolvedValue(false);
+
+ // Test Optimism
+ const { unmount: unmount1 } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(1),
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ address: "0xOptimismContract",
+ chainId: 10,
+ })
+ );
+ });
+
+ unmount1();
+ vi.clearAllMocks();
+
+ // Test Sepolia
+ renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(1),
+ network: "eip155:11155420",
+ })
+ );
+
+ await waitFor(() => {
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ address: "0xSepoliaContract",
+ chainId: 11155420,
+ })
+ );
+ });
+ });
+ });
+
+ describe("Return Values", () => {
+ it("should return isListed=true when contract returns true", async () => {
+ mockReadContract.mockResolvedValue(true);
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ })
+ );
+
+ // Initially loading
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.isListed).toBeUndefined();
+
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(true);
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.error).toBeUndefined();
+ });
+ });
+
+ it("should return isListed=false when contract returns false", async () => {
+ mockReadContract.mockResolvedValue(false);
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(false);
+ expect(result.current.isLoading).toBe(false);
+ });
+ });
+
+ it("should return error when contract call fails", async () => {
+ mockReadContract.mockRejectedValue(new Error("Contract call failed"));
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(result.current.error).toBe("Contract call failed");
+ expect(result.current.isListed).toBeUndefined();
+ expect(result.current.isLoading).toBe(false);
+ });
+ });
+
+ it("should return undefined isListed when disabled", async () => {
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ enabled: false,
+ })
+ );
+
+ // Should immediately be undefined, not loading
+ expect(result.current.isListed).toBeUndefined();
+ expect(result.current.isLoading).toBe(false);
+ });
+ });
+
+ describe("Optimistic Updates", () => {
+ it("should allow optimistic update via setOptimisticListed", async () => {
+ mockReadContract.mockResolvedValue(false);
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(false);
+ });
+
+ // Optimistically set to true (simulating user toggle)
+ act(() => {
+ result.current.setOptimisticListed(true);
+ });
+
+ expect(result.current.isListed).toBe(true);
+ });
+ });
+
+ describe("Refetch", () => {
+ it("should refetch when refetch() is called", async () => {
+ mockReadContract.mockResolvedValue(false);
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(false);
+ });
+
+ expect(mockReadContract).toHaveBeenCalledTimes(1);
+
+ // Change the mock return value
+ mockReadContract.mockResolvedValue(true);
+
+ // Refetch
+ await act(async () => {
+ await result.current.refetch();
+ });
+
+ expect(mockReadContract).toHaveBeenCalledTimes(2);
+ expect(result.current.isListed).toBe(true);
+ });
+ });
+
+ describe("Dependency Changes", () => {
+ it("should refetch when tokenId changes", async () => {
+ mockReadContract.mockResolvedValue(true);
+
+ const { result, rerender } = renderHook(
+ ({ tokenId }) =>
+ useNFTListedStatus({
+ tokenId,
+ network: "eip155:10",
+ }),
+ { initialProps: { tokenId: BigInt(1) } }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(true);
+ });
+
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ args: [BigInt(1)] })
+ );
+
+ // Change tokenId
+ rerender({ tokenId: BigInt(2) });
+
+ await waitFor(() => {
+ expect(mockReadContract).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ args: [BigInt(2)] })
+ );
+ });
+ });
+ });
+});
From 7f20abe8971f745567fa0128ddec2289b0803bc7 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 14:21:56 +0100
Subject: [PATCH 36/46] Test the problems.
---
website/components/NFTCard.tsx | 9 ++++++--
website/hooks/useNFTListedStatus.ts | 20 ++++++++++++++---
website/test/useNFTListedStatus.test.ts | 30 +++++++++++++++++++++++++
3 files changed, 54 insertions(+), 5 deletions(-)
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 15651015c..9d3e28909 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -48,14 +48,19 @@ export function NFTCard({
const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
- // Use the extracted hook for listing status (only in private view)
+ // Use the extracted hook for listing status
+ // Only enabled when:
+ // 1. Not public view (owners can toggle listing)
+ // 2. Token data loaded successfully (no error, not loading)
+ // This prevents contract calls for non-existent/burned tokens
+ const tokenDataLoaded = !nft.isLoading && !nft.error;
const {
isListed,
setOptimisticListed,
} = useNFTListedStatus({
tokenId,
network,
- enabled: !isPublicView,
+ enabled: !isPublicView && tokenDataLoaded,
});
// Use the custom hook for a stable public client reference
diff --git a/website/hooks/useNFTListedStatus.ts b/website/hooks/useNFTListedStatus.ts
index 08afaeb19..f6d4556ab 100644
--- a/website/hooks/useNFTListedStatus.ts
+++ b/website/hooks/useNFTListedStatus.ts
@@ -79,9 +79,23 @@ export function useNFTListedStatus({
setIsListed(result as boolean);
} catch (err) {
- console.warn("Could not load listing status:", err);
- setError(err instanceof Error ? err.message : "Failed to load listing status");
- // Keep isListed as undefined on error
+ // Distinguish between contract reverts (legacy tokens) and real errors
+ const isContractRevert =
+ err instanceof Error &&
+ (err.message.includes("reverted") ||
+ err.name.includes("ContractFunctionRevertedError") ||
+ err.name.includes("ContractFunctionExecutionError"));
+
+ if (isContractRevert) {
+ // Legacy tokens minted before isListed feature - not an error, just not supported
+ // Keep isListed as undefined - UI will hide the toggle for these tokens
+ setError("isListed not available for this token");
+ } else {
+ // Real errors (network issues, etc.) - log and set error
+ console.error("Failed to load listing status:", err);
+ setError(err instanceof Error ? err.message : "Failed to load listing status");
+ }
+ // In both cases, isListed remains undefined
} finally {
setIsLoading(false);
}
diff --git a/website/test/useNFTListedStatus.test.ts b/website/test/useNFTListedStatus.test.ts
index acf21aee6..72b12284a 100644
--- a/website/test/useNFTListedStatus.test.ts
+++ b/website/test/useNFTListedStatus.test.ts
@@ -181,6 +181,36 @@ describe("useNFTListedStatus", () => {
});
});
+ it("should handle legacy tokens (contract reverts) gracefully without console.error", async () => {
+ // Simulate a contract revert error (legacy token without isListed support)
+ const revertError = new Error("The contract function \"isListed\" reverted.");
+ revertError.name = "ContractFunctionExecutionError";
+ mockReadContract.mockRejectedValue(revertError);
+
+ // Spy on console.error to verify it's NOT called for reverts
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ const { result } = renderHook(() =>
+ useNFTListedStatus({
+ tokenId: BigInt(2), // Legacy token ID
+ network: "eip155:10",
+ })
+ );
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ // Should set specific error message for legacy tokens
+ expect(result.current.error).toBe("isListed not available for this token");
+ // isListed should remain undefined (not an error state, just not supported)
+ expect(result.current.isListed).toBeUndefined();
+ // Should NOT log to console.error for expected contract reverts
+ expect(consoleSpy).not.toHaveBeenCalled();
+
+ consoleSpy.mockRestore();
+ });
+
it("should return undefined isListed when disabled", async () => {
const { result } = renderHook(() =>
useNFTListedStatus({
From a3a3e885eaf573283834f415aad82f896a9ae1e8 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 14:26:40 +0100
Subject: [PATCH 37/46] Update ImageGenerator.tsx
---
website/components/ImageGenerator.tsx | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index d2b88039f..04fd3683e 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { useAccount, useConnect } from "wagmi";
import { css } from "../styled-system/css";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
@@ -101,6 +101,12 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const [generatedImageUrl, setGeneratedImageUrl] = useState();
const [tokenId, setTokenId] = useState();
+ // Hydration safety: prevent SSR/client mismatch by showing collapsed state until mounted
+ const [hasMounted, setHasMounted] = useState(false);
+ useEffect(() => {
+ setHasMounted(true);
+ }, []);
+
// Preview area state
const [currentPreviewImage, setCurrentPreviewImage] = useState();
@@ -112,6 +118,10 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const { address, isConnected } = useAccount();
const { connectors, connect } = useConnect();
+ // Effective connection state: only true after client has mounted
+ // This prevents hydration mismatch between SSR (always false) and client (may be true)
+ const isEffectivelyConnected = hasMounted && isConnected;
+
// Determine target chain from useAutoNetwork (no auto-switch, switch at interaction)
const { network, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS);
const targetChainId = fromCAIP2(network);
@@ -515,8 +525,8 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
AI Image Generation is currently under maintenance. Please check back later.
- ) : !isConnected ? (
- // Collapsed State - Clean & Simple
+ ) : !isEffectivelyConnected ? (
+ // Collapsed State - Clean & Simple (also shown during SSR before hydration)
Date: Fri, 30 Jan 2026 14:29:31 +0100
Subject: [PATCH 38/46] clean console logs
---
website/components/ImageGenerator.tsx | 9 +--------
website/components/MyNFTList.tsx | 5 ++---
2 files changed, 3 insertions(+), 11 deletions(-)
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index 04fd3683e..e839e4f44 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -312,8 +312,6 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
isListed,
});
- console.log("[x402] Image generation completed:", result);
-
// Update state with results
const newTokenId = BigInt(result.tokenId);
const imageUrl = result.image_url;
@@ -414,12 +412,7 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
setReferenceImageMimeType(compressedResult.mimeType);
setPreviewState("reference");
- // Zeige Erfolg-Feedback
- const originalSizeKB = Math.round(file.size / 1024);
- const compressedSizeKB = Math.round((compressedResult.base64.length * 0.75) / 1024);
- console.log(
- `Image compressed: ${originalSizeKB}KB → ${compressedSizeKB}KB (${file.type} → ${compressedResult.mimeType})`,
- );
+ // Image compression succeeded - no logging needed in production
} catch (err) {
console.error("Image compression failed:", err);
const errorMsg = err instanceof Error ? err.message : failedToProcessImageText;
diff --git a/website/components/MyNFTList.tsx b/website/components/MyNFTList.tsx
index 91d99fa8e..b15edc745 100644
--- a/website/components/MyNFTList.tsx
+++ b/website/components/MyNFTList.tsx
@@ -144,10 +144,9 @@ export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps
);
// Handle listing status changes
- const handleListedStatusChanged = useCallback((tokenId: bigint, isListed: boolean) => {
- // This function can be used to update local state if needed
+ const handleListedStatusChanged = useCallback((_tokenId: bigint, _isListed: boolean) => {
+ // This callback can be used to update local state if needed
// For now, the NFTCard handles the blockchain state itself
- console.log(`NFT ${tokenId} listing status changed to: ${isListed}`);
}, []);
// Load data when component mounts or dependencies change
From 5c36bfd3b81b19c15285108414ed946d005fc254 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 14:45:42 +0100
Subject: [PATCH 39/46] Further cleaning
---
eth/DEPLOY_SUPPORT_V2_GUIDE.md | 37 +-
eth/SUPPORT_V2_PROPOSAL.md | 230 ++--
eth/abi/contracts/CollectorNFT-summary.md | 2 -
eth/abi/contracts/CollectorNFT.ts | 1 -
eth/abi/contracts/CollectorNFTv1-summary.md | 2 -
eth/abi/contracts/CollectorNFTv1.ts | 1 -
.../contracts/EIP3009SplitterV1-summary.md | 2 -
eth/abi/contracts/EIP3009SplitterV1.ts | 1 -
eth/abi/contracts/GenImNFTv3-summary.md | 2 -
eth/abi/contracts/GenImNFTv3.ts | 1 -
eth/abi/contracts/GenImNFTv4-summary.md | 87 ++
eth/abi/contracts/GenImNFTv4.json | 1125 ++++++++++++++++
eth/abi/contracts/GenImNFTv4.ts | 1130 +++++++++++++++++
eth/abi/contracts/LLMv1-summary.md | 2 -
eth/abi/contracts/LLMv1.ts | 1 -
eth/abi/contracts/Support-summary.md | 2 -
eth/abi/contracts/Support.ts | 1 -
eth/abi/contracts/SupportV2-summary.md | 2 -
eth/abi/contracts/SupportV2.ts | 1 -
.../support-v2-optimisticEthereum.json | 2 +-
eth/deployments/support-v2-optsepolia.json | 2 +-
eth/scripts/deploy-support-v2.config.json | 2 +-
eth/scripts/export-abi.ts | 9 +-
eth/test/SupportV2_Deployment.ts | 25 +-
shared/chain-utils/src/abi/GenImNFTv4.ts | 6 +-
website/hooks/useNFTListedStatus.ts | 7 +-
website/test/useNFTListedStatus.test.ts | 33 +-
27 files changed, 2528 insertions(+), 188 deletions(-)
create mode 100644 eth/abi/contracts/GenImNFTv4-summary.md
create mode 100644 eth/abi/contracts/GenImNFTv4.json
create mode 100644 eth/abi/contracts/GenImNFTv4.ts
diff --git a/eth/DEPLOY_SUPPORT_V2_GUIDE.md b/eth/DEPLOY_SUPPORT_V2_GUIDE.md
index ee2caffc0..9acf10ab5 100644
--- a/eth/DEPLOY_SUPPORT_V2_GUIDE.md
+++ b/eth/DEPLOY_SUPPORT_V2_GUIDE.md
@@ -14,8 +14,8 @@ This guide explains how to deploy the SupportV2 contract to Optimism and Base ma
SupportV2 is an evolution of the original Support contract with these key changes:
-| Feature | Support (v1) | SupportV2 |
-|---------|--------------|-----------|
+| Feature | Support (v1) | SupportV2 |
+| -------------------- | ------------- | ------------------------ |
| `donate()` signature | `donate(url)` | `donate(url, recipient)` |
| Upgradeability | None | UUPS proxy |
@@ -82,17 +82,17 @@ networks: {
### Testnets ✅
-| Network | Proxy Address | Implementation | Verified |
-|---------|---------------|----------------|----------|
-| Optimism Sepolia | `0x9859431b682e861b19e87Db14a04944BC747AB6d` | - | ✅ |
-| Base Sepolia | `0xaB44BE78499721b593a0f4BE2099b246e9C53B57` | - | ✅ |
+| Network | Proxy Address | Implementation | Verified |
+| ---------------- | -------------------------------------------- | -------------- | -------- |
+| Optimism Sepolia | `0x9859431b682e861b19e87Db14a04944BC747AB6d` | - | ✅ |
+| Base Sepolia | `0xaB44BE78499721b593a0f4BE2099b246e9C53B57` | - | ✅ |
### Mainnets ✅
-| Network | Proxy Address | Implementation | Verified |
-|---------|---------------|----------------|----------|
-| Optimism | `0x4ca63f8A4Cd56287E854f53E18ca482D74391316` | `0x011881999565F10aB2C62912878050Fb5deC10ac` | ✅ |
-| Base | `0xB70EA4d714Fed01ce20E93F9033008BadA1c8694` | `0x314B07fBd33A7343479e99E6682D5Ee1da7F17c1` | ✅ |
+| Network | Proxy Address | Implementation | Verified |
+| -------- | -------------------------------------------- | -------------------------------------------- | -------- |
+| Optimism | `0x4ca63f8A4Cd56287E854f53E18ca482D74391316` | `0x011881999565F10aB2C62912878050Fb5deC10ac` | ✅ |
+| Base | `0xB70EA4d714Fed01ce20E93F9033008BadA1c8694` | `0x314B07fBd33A7343479e99E6682D5Ee1da7F17c1` | ✅ |
## Configuration File
@@ -135,10 +135,13 @@ Edit `scripts/deploy-support-v2.config.json`:
}
}
```
+
"environment": "mainnet"
- }
+
}
-```
+}
+
+````
### Configuration Options
@@ -175,9 +178,10 @@ cd eth
# Edit config: set validateOnly: true
npx hardhat run scripts/deploy-support-v2.ts --network optimisticEthereum
-```
+````
**What happens:**
+
- ✅ Validates contract compiles successfully
- ✅ Checks OpenZeppelin upgrade patterns
- ✅ Verifies UUPS proxy compatibility
@@ -192,6 +196,7 @@ npx hardhat run scripts/deploy-support-v2.ts --network optimisticEthereum
```
**What happens:**
+
- ✅ Shows deployment parameters
- ✅ Validates configuration
- ✅ Simulates deployment flow
@@ -205,6 +210,7 @@ npx hardhat run scripts/deploy-support-v2.ts --network optimisticEthereum
```
**For Base Mainnet:**
+
```bash
npx hardhat run scripts/deploy-support-v2.ts --network base
```
@@ -282,11 +288,13 @@ npx hardhat run scripts/verify-contract.ts --network base
```
The script will:
+
1. Verify the implementation contract
2. Attempt proxy verification with multiple strategies
3. Handle "Already Verified" gracefully
**Alternative (simple):**
+
```bash
npx hardhat verify --network optimisticEthereum
```
@@ -302,7 +310,7 @@ const SUPPORT_V2_ADDRESSES: Record = {
[baseSepolia.id]: "0xaB44BE78499721b593a0f4BE2099b246e9C53B57",
// Mainnets - ADD THESE AFTER DEPLOYMENT
[optimism.id]: "0x...", // ← Optimism Mainnet proxy address
- [base.id]: "0x...", // ← Base Mainnet proxy address
+ [base.id]: "0x...", // ← Base Mainnet proxy address
};
// Update default chain for production
@@ -369,6 +377,7 @@ Ensure `scripts/deploy-support-v2.config.json` exists.
### "OpenZeppelin upgrade validation failed"
Check the contract for:
+
- Missing `__gap` arrays in base contracts
- Storage layout incompatibilities
diff --git a/eth/SUPPORT_V2_PROPOSAL.md b/eth/SUPPORT_V2_PROPOSAL.md
index d33ec6b81..c441a7eaf 100644
--- a/eth/SUPPORT_V2_PROPOSAL.md
+++ b/eth/SUPPORT_V2_PROPOSAL.md
@@ -7,40 +7,40 @@
## 0. Implementierungs-Übersicht
-| Phase | Beschreibung | Status |
-|-------|--------------|--------|
-| Phase 1 | Contract, Tests, Deployment Script | ✅ ABGESCHLOSSEN |
-| Phase 2 | Multi-Chain & Testing | ✅ ABGESCHLOSSEN |
-| Phase 3 | Frontend Integration | ✅ ABGESCHLOSSEN (ETH only) |
-| Phase 4 | Production Deployment | ✅ ABGESCHLOSSEN |
+| Phase | Beschreibung | Status |
+| ------- | ---------------------------------- | --------------------------- |
+| Phase 1 | Contract, Tests, Deployment Script | ✅ ABGESCHLOSSEN |
+| Phase 2 | Multi-Chain & Testing | ✅ ABGESCHLOSSEN |
+| Phase 3 | Frontend Integration | ✅ ABGESCHLOSSEN (ETH only) |
+| Phase 4 | Production Deployment | ✅ ABGESCHLOSSEN |
### Deployment Adressen
#### Mainnets
-| Chain | Proxy Address | Deployed |
-|-------|---------------|----------|
+| Chain | Proxy Address | Deployed |
+| -------- | -------------------------------------------- | ---------- |
| Optimism | `0x4ca63f8A4Cd56287E854f53E18ca482D74391316` | 24.01.2026 |
-| Base | `0xB70EA4d714Fed01ce20E93F9033008BadA1c8694` | 25.01.2026 |
+| Base | `0xB70EA4d714Fed01ce20E93F9033008BadA1c8694` | 25.01.2026 |
#### Testnets
-| Chain | Proxy Address | Deployed |
-|-------|---------------|----------|
+| Chain | Proxy Address | Deployed |
+| ---------------- | -------------------------------------------- | ---------- |
| Optimism Sepolia | `0x9859431b682e861b19e87Db14a04944BC747AB6d` | 20.01.2026 |
-| Base Sepolia | `0xaB44BE78499721b593a0f4BE2099b246e9C53B57` | 21.01.2026 |
+| Base Sepolia | `0xaB44BE78499721b593a0f4BE2099b246e9C53B57` | 21.01.2026 |
---
## 1. Feature-Übersicht
-| Feature | Beschreibung |
-|---------|--------------|
-| 🔄 UUPS Upgradeable | Proxy-Architektur für spätere Updates |
-| 💰 ETH Donations | `donate(url, recipient)` |
-| 🪙 EIP-3009 Tokens | `donateToken(...)` für USDC und kompatible Tokens (permissionless) |
-| 📊 Like-Counting | On-chain `urlLikes` Mapping |
-| 🌐 Multi-Chain | Optimism + Base |
+| Feature | Beschreibung |
+| ------------------- | ------------------------------------------------------------------ |
+| 🔄 UUPS Upgradeable | Proxy-Architektur für spätere Updates |
+| 💰 ETH Donations | `donate(url, recipient)` |
+| 🪙 EIP-3009 Tokens | `donateToken(...)` für USDC und kompatible Tokens (permissionless) |
+| 📊 Like-Counting | On-chain `urlLikes` Mapping |
+| 🌐 Multi-Chain | Optimism + Base |
---
@@ -124,12 +124,12 @@ import "./interfaces/IEIP3009.sol";
* Multi-chain: Deploy on Optimism + Base
*/
contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable {
-
+
/// @notice Contract version for upgrade tracking
uint256 public constant VERSION = 1;
-
+
mapping(bytes32 => uint256) public urlLikes;
-
+
event Donation(
address indexed from,
address indexed recipient,
@@ -138,35 +138,35 @@ contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgrad
uint256 amount,
address token // address(0) = ETH
);
-
+
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
-
+
function initialize(address _owner) public initializer {
__Ownable_init(_owner);
__ReentrancyGuard_init();
__UUPSUpgradeable_init();
}
-
+
// ETH Donation
function donate(
- string calldata _url,
+ string calldata _url,
address _recipient
) external payable nonReentrant {
require(msg.value > 0, "No ETH sent");
require(_recipient != address(0), "Invalid recipient");
-
+
bytes32 urlHash = keccak256(bytes(_url));
urlLikes[urlHash]++;
-
+
(bool success, ) = payable(_recipient).call{value: msg.value}("");
require(success, "Transfer failed");
-
+
emit Donation(msg.sender, _recipient, urlHash, _url, msg.value, address(0));
}
-
+
// EIP-3009 Token Donation (v,r,s format) - Permissionless
function donateToken(
string calldata _url,
@@ -183,7 +183,7 @@ contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgrad
require(_recipient != address(0), "Invalid recipient");
require(_amount > 0, "Amount must be > 0");
require(_token != address(0), "Invalid token");
-
+
IEIP3009(_token).transferWithAuthorization(
msg.sender,
_recipient,
@@ -195,17 +195,17 @@ contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgrad
_r,
_s
);
-
+
bytes32 urlHash = keccak256(bytes(_url));
urlLikes[urlHash]++;
-
+
emit Donation(msg.sender, _recipient, urlHash, _url, _amount, _token);
}
-
+
function getLikesForUrl(string calldata _url) external view returns (uint256) {
return urlLikes[keccak256(bytes(_url))];
}
-
+
function _authorizeUpgrade(address) internal override onlyOwner {}
}
```
@@ -216,12 +216,12 @@ contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgrad
## 3. Token-Adressen
-| Token | Chain | Adresse |
-|-------|-------|---------|
-| USDC | Optimism | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
-| USDC | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
-| USDC | Optimism Sepolia | `0x5fd84259d66Cd46123540766Be93DFE6D43130D7` |
-| USDC | Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
+| Token | Chain | Adresse |
+| ----- | ---------------- | -------------------------------------------- |
+| USDC | Optimism | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
+| USDC | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
+| USDC | Optimism Sepolia | `0x5fd84259d66Cd46123540766Be93DFE6D43130D7` |
+| USDC | Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
---
@@ -230,42 +230,37 @@ contract SupportV2 is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgrad
### 4.1 EIP-3009 Signatur (v,r,s Format)
```typescript
-import { hexToSignature } from 'viem';
-
-async function signEIP3009(
- token: Address,
- recipient: Address,
- amount: bigint,
- chainId: number
-) {
- const nonce = `0x${crypto.randomUUID().replace(/-/g, '')}` as `0x${string}`;
+import { hexToSignature } from "viem";
+
+async function signEIP3009(token: Address, recipient: Address, amount: bigint, chainId: number) {
+ const nonce = `0x${crypto.randomUUID().replace(/-/g, "")}` as `0x${string}`;
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600);
-
+
const signature = await walletClient.signTypedData({
domain: {
- name: 'USD Coin',
- version: '2',
+ name: "USD Coin",
+ version: "2",
chainId,
verifyingContract: token,
},
types: {
TransferWithAuthorization: [
- { name: 'from', type: 'address' },
- { name: 'to', type: 'address' },
- { name: 'value', type: 'uint256' },
- { name: 'validAfter', type: 'uint256' },
- { name: 'validBefore', type: 'uint256' },
- { name: 'nonce', type: 'bytes32' },
+ { name: "from", type: "address" },
+ { name: "to", type: "address" },
+ { name: "value", type: "uint256" },
+ { name: "validAfter", type: "uint256" },
+ { name: "validBefore", type: "uint256" },
+ { name: "nonce", type: "bytes32" },
],
},
- primaryType: 'TransferWithAuthorization',
+ primaryType: "TransferWithAuthorization",
message: { from: userAddress, to: recipient, value: amount, validAfter, validBefore, nonce },
});
-
+
// Split signature into v, r, s for contract
const { v, r, s } = hexToSignature(signature);
-
+
return { v: Number(v), r, s, nonce, validAfter, validBefore };
}
```
@@ -276,7 +271,7 @@ async function signEIP3009(
// ETH
await writeContract({
...supportV2Config,
- functionName: 'donate',
+ functionName: "donate",
args: [url, recipient],
value: amount,
});
@@ -285,7 +280,7 @@ await writeContract({
const sig = await signEIP3009(USDC_ADDRESS, recipient, amount, chainId);
await writeContract({
...supportV2Config,
- functionName: 'donateToken',
+ functionName: "donateToken",
args: [url, recipient, USDC_ADDRESS, amount, sig.validAfter, sig.validBefore, sig.nonce, sig.v, sig.r, sig.s],
});
```
@@ -296,58 +291,58 @@ await writeContract({
### Phase 1: Contract ✅ ABGESCHLOSSEN
-| Schritt | Beschreibung | Status |
-|---------|--------------|--------|
-| 1.1 | `contracts/interfaces/IEIP3009.sol` | ✅ |
-| 1.2 | `contracts/SupportV2.sol` | ✅ |
-| 1.3 | `test/SupportV2_Functional.ts` + `test/SupportV2_Deployment.ts` | ✅ |
-| 1.4 | `scripts/deploy-support-v2.ts` + Config | ✅ |
+| Schritt | Beschreibung | Status |
+| ------- | --------------------------------------------------------------- | ------ |
+| 1.1 | `contracts/interfaces/IEIP3009.sol` | ✅ |
+| 1.2 | `contracts/SupportV2.sol` | ✅ |
+| 1.3 | `test/SupportV2_Functional.ts` + `test/SupportV2_Deployment.ts` | ✅ |
+| 1.4 | `scripts/deploy-support-v2.ts` + Config | ✅ |
### Phase 2: Multi-Chain & Testing ✅ ABGESCHLOSSEN
-| Schritt | Beschreibung | Status |
-|---------|--------------|--------|
-| 2.1 | Base + Base Sepolia zu `hardhat.config.ts` hinzufügen | ✅ |
-| 2.2 | Deploy auf Optimism Sepolia + Base Sepolia | ✅ |
-| 2.3 | ABI Export (`abi/contracts/SupportV2.ts`) | ✅ |
-| 2.4 | `notebooks/support_v2_demo.ipynb` — Deno TypeScript Notebook | ✅ |
+| Schritt | Beschreibung | Status |
+| ------- | ------------------------------------------------------------ | ------ |
+| 2.1 | Base + Base Sepolia zu `hardhat.config.ts` hinzufügen | ✅ |
+| 2.2 | Deploy auf Optimism Sepolia + Base Sepolia | ✅ |
+| 2.3 | ABI Export (`abi/contracts/SupportV2.ts`) | ✅ |
+| 2.4 | `notebooks/support_v2_demo.ipynb` — Deno TypeScript Notebook | ✅ |
### Phase 3: Frontend ✅ ABGESCHLOSSEN (ETH only)
-| Schritt | Beschreibung | Status |
-|---------|--------------|--------|
-| 3.1 | `wagmi.config.ts` — Base + Base Sepolia Chains hinzufügen | ✅ |
-| 3.2 | `getChain.ts` — SupportV2 Multi-Chain Config | ✅ |
-| 3.3 | ~~EIP-3009 Signatur-Helper~~ (deprioritisiert) | ⏸️ |
-| 3.4 | ~~Token-Auswahl UI (ETH / USDC)~~ (deprioritisiert) | ⏸️ |
-| 3.5 | `useSupportAction.ts` — Multi-Chain Hook mit Auto-Switch | ✅ |
-| 3.6 | Legacy Support Config entfernt (`supportContractConfig`) | ✅ |
-| 3.7 | Unit Tests für `useSupportAction` | ✅ |
-| 3.8 | `VITE_USE_TESTNET` Env-Variable für Testnet-Modus | ✅ |
-| 3.9 | Aggregierte Likes von beiden Chains | ✅ |
+| Schritt | Beschreibung | Status |
+| ------- | --------------------------------------------------------- | ------ |
+| 3.1 | `wagmi.config.ts` — Base + Base Sepolia Chains hinzufügen | ✅ |
+| 3.2 | `getChain.ts` — SupportV2 Multi-Chain Config | ✅ |
+| 3.3 | ~~EIP-3009 Signatur-Helper~~ (deprioritisiert) | ⏸️ |
+| 3.4 | ~~Token-Auswahl UI (ETH / USDC)~~ (deprioritisiert) | ⏸️ |
+| 3.5 | `useSupportAction.ts` — Multi-Chain Hook mit Auto-Switch | ✅ |
+| 3.6 | Legacy Support Config entfernt (`supportContractConfig`) | ✅ |
+| 3.7 | Unit Tests für `useSupportAction` | ✅ |
+| 3.8 | `VITE_USE_TESTNET` Env-Variable für Testnet-Modus | ✅ |
+| 3.9 | Aggregierte Likes von beiden Chains | ✅ |
**Frontend-Änderungen (25. Januar 2026):**
- `website/.env`: `VITE_USE_TESTNET` Variable hinzugefügt (default: mainnet)
- `website/wagmi.config.ts`: Base + Base Sepolia zu Chains hinzugefügt
-- `website/utils/getChain.ts`:
+- `website/utils/getChain.ts`:
- `VITE_USE_TESTNET` steuert Mainnet/Testnet Modus
- `SUPPORT_V2_CHAINS` exportiert aktive Chains basierend auf Modus
- `getSupportV2Config()`, `isSupportV2Chain()`, `DEFAULT_SUPPORT_CHAIN`
- Legacy `supportContractConfig` entfernt
-- `website/hooks/useSupportAction.ts`:
+- `website/hooks/useSupportAction.ts`:
- Multi-Chain mit automatischem Chain-Switch
- Aggregierte Likes von beiden Chains im aktiven Modus
- `website/test/useSupportAction.test.ts`: 17 Unit Tests
### Phase 4: Production Deployment ✅ ABGESCHLOSSEN
-| Schritt | Beschreibung | Status |
-|---------|--------------|--------|
-| 4.1 | Deploy auf Optimism Mainnet | ✅ |
-| 4.2 | Deploy auf Base Mainnet | ✅ |
-| 4.3 | `getChain.ts` — Mainnet Adressen eintragen | ✅ |
-| 4.4 | `DEFAULT_SUPPORT_CHAIN` auf Mainnet umstellen | ✅ |
+| Schritt | Beschreibung | Status |
+| ------- | --------------------------------------------- | ------ |
+| 4.1 | Deploy auf Optimism Mainnet | ✅ |
+| 4.2 | Deploy auf Base Mainnet | ✅ |
+| 4.3 | `getChain.ts` — Mainnet Adressen eintragen | ✅ |
+| 4.4 | `DEFAULT_SUPPORT_CHAIN` auf Mainnet umstellen | ✅ |
---
@@ -412,12 +407,12 @@ function donate(string calldata _url, address _recipient) external payable
#### Wo passiert was?
-| Aktion | Wo | Code |
-|--------|-----|------|
-| Likes lesen | `useSupportAction` Hook | `useReadContract` mit `DEFAULT_READ_CHAIN.id` |
-| Chain prüfen | `handleSupport()` | `if (!isSupported)` |
-| Chain wechseln | `handleSupport()` | `await switchChainAsync({ chainId: DEFAULT_READ_CHAIN.id })` |
-| Donation senden | `handleSupport()` | `writeContract({ ...activeConfig, ... })` |
+| Aktion | Wo | Code |
+| --------------- | ----------------------- | ------------------------------------------------------------ |
+| Likes lesen | `useSupportAction` Hook | `useReadContract` mit `DEFAULT_READ_CHAIN.id` |
+| Chain prüfen | `handleSupport()` | `if (!isSupported)` |
+| Chain wechseln | `handleSupport()` | `await switchChainAsync({ chainId: DEFAULT_READ_CHAIN.id })` |
+| Donation senden | `handleSupport()` | `writeContract({ ...activeConfig, ... })` |
#### Schritt 1: `wagmi.config.ts` — Base Chains hinzufügen
@@ -474,7 +469,7 @@ export const RECIPIENT_ADDRESS = "0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20" as
export function getSupportV2Config(chainId: number) {
const address = SUPPORT_V2_ADDRESSES[chainId];
if (!address) return null;
-
+
return {
address,
abi: SupportV2ABI,
@@ -496,12 +491,7 @@ import * as React from "react";
import { useAccount, useWriteContract, useWaitForTransactionReceipt, useChainId, useSwitchChain } from "wagmi";
import { parseEther } from "viem";
import { useReadContract } from "wagmi";
-import {
- getSupportV2Config,
- isSupportV2Chain,
- RECIPIENT_ADDRESS,
- DEFAULT_READ_CHAIN
-} from "../utils/getChain";
+import { getSupportV2Config, isSupportV2Chain, RECIPIENT_ADDRESS, DEFAULT_READ_CHAIN } from "../utils/getChain";
import { trackEvent } from "../utils/analytics";
/**
@@ -514,10 +504,10 @@ export function useSupportAction(url: string) {
const [fullUrl, setFullUrl] = React.useState(url);
const { isConnected } = useAccount();
- const chainId = useChainId(); // ← Aktuelle Chain des Users
- const { switchChainAsync } = useSwitchChain(); // ← Async Version für await
+ const chainId = useChainId(); // ← Aktuelle Chain des Users
+ const { switchChainAsync } = useSwitchChain(); // ← Async Version für await
const donationAmount = parseEther("0.0002");
-
+
const { writeContract, isPending, data: hash, error: writeError } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
@@ -533,13 +523,13 @@ export function useSupportAction(url: string) {
// ═══════════════════════════════════════════════════════════════
// HIER WIRD GEPRÜFT: Ist die Chain unterstützt?
// ═══════════════════════════════════════════════════════════════
-
+
// Contract Config für aktuelle Chain holen (null wenn nicht unterstützt)
const contractConfig = React.useMemo(() => getSupportV2Config(chainId), [chainId]);
-
+
// Boolean: Ist die aktuelle Chain unterstützt?
const isSupported = isSupportV2Chain(chainId);
-
+
// Chain für Read-Operationen: User's Chain wenn unterstützt, sonst Default
const readChainId = isSupported ? chainId : DEFAULT_READ_CHAIN.id;
const readConfig = getSupportV2Config(readChainId)!;
@@ -556,7 +546,7 @@ export function useSupportAction(url: string) {
...readConfig,
functionName: "getLikesForUrl",
args: [fullUrl],
- chainId: readChainId, // ← Liest von Default Chain wenn User's Chain nicht unterstützt
+ chainId: readChainId, // ← Liest von Default Chain wenn User's Chain nicht unterstützt
query: { enabled: !!fullUrl },
});
@@ -598,7 +588,7 @@ export function useSupportAction(url: string) {
writeContract({
...activeConfig,
functionName: "donate",
- args: [fullUrl, RECIPIENT_ADDRESS], // ← Neuer recipient Parameter
+ args: [fullUrl, RECIPIENT_ADDRESS], // ← Neuer recipient Parameter
value: donationAmount,
});
}, [fullUrl, isSupported, chainId, switchChainAsync, writeContract, donationAmount]);
@@ -618,9 +608,9 @@ export function useSupportAction(url: string) {
}, [isSuccess, writeError, refetch, fullUrl, chainId]);
// Warning message
- const warningMessage = errorMessage || (!isSupported && isConnected
- ? `Wechsle zu ${SUPPORTED_CHAINS.map(c => c.name).join(" oder ")}`
- : null);
+ const warningMessage =
+ errorMessage ||
+ (!isSupported && isConnected ? `Wechsle zu ${SUPPORTED_CHAINS.map((c) => c.name).join(" oder ")}` : null);
return {
supportCount: supportCount?.toString() || "0",
@@ -651,7 +641,7 @@ const allCounts = await Promise.all(
args: [fullUrl],
});
return count;
- })
+ }),
);
const totalLikes = allCounts.reduce((sum, c) => sum + c, 0n);
```
diff --git a/eth/abi/contracts/CollectorNFT-summary.md b/eth/abi/contracts/CollectorNFT-summary.md
index aff967803..ab8961b3b 100644
--- a/eth/abi/contracts/CollectorNFT-summary.md
+++ b/eth/abi/contracts/CollectorNFT-summary.md
@@ -2,8 +2,6 @@
NFT collection based on GenImNFT tokens
-Generated on: 2026-01-20T20:33:28.414Z
-
## Contract Information
- **Name**: CollectorNFT
diff --git a/eth/abi/contracts/CollectorNFT.ts b/eth/abi/contracts/CollectorNFT.ts
index 1883c3f04..4859c0cd9 100644
--- a/eth/abi/contracts/CollectorNFT.ts
+++ b/eth/abi/contracts/CollectorNFT.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for CollectorNFT
// NFT collection based on GenImNFT tokens
-// Generated on: 2026-01-20T20:33:28.413Z
export const CollectorNFTABI = [
{
diff --git a/eth/abi/contracts/CollectorNFTv1-summary.md b/eth/abi/contracts/CollectorNFTv1-summary.md
index 7d00c0c20..e332634cc 100644
--- a/eth/abi/contracts/CollectorNFTv1-summary.md
+++ b/eth/abi/contracts/CollectorNFTv1-summary.md
@@ -2,8 +2,6 @@
CollectorNFT Version 1 with upgraded features and UUPS proxy pattern
-Generated on: 2026-01-20T20:33:28.415Z
-
## Contract Information
- **Name**: CollectorNFTv1
diff --git a/eth/abi/contracts/CollectorNFTv1.ts b/eth/abi/contracts/CollectorNFTv1.ts
index f812f15a5..2d02e51c3 100644
--- a/eth/abi/contracts/CollectorNFTv1.ts
+++ b/eth/abi/contracts/CollectorNFTv1.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for CollectorNFTv1
// CollectorNFT Version 1 with upgraded features and UUPS proxy pattern
-// Generated on: 2026-01-20T20:33:28.415Z
export const CollectorNFTv1ABI = [
{
diff --git a/eth/abi/contracts/EIP3009SplitterV1-summary.md b/eth/abi/contracts/EIP3009SplitterV1-summary.md
index 5c2ac2707..5970fbb36 100644
--- a/eth/abi/contracts/EIP3009SplitterV1-summary.md
+++ b/eth/abi/contracts/EIP3009SplitterV1-summary.md
@@ -2,8 +2,6 @@
EIP-3009 payment splitter with fixed facilitator fee
-Generated on: 2026-01-20T20:33:28.418Z
-
## Contract Information
- **Name**: EIP3009SplitterV1
diff --git a/eth/abi/contracts/EIP3009SplitterV1.ts b/eth/abi/contracts/EIP3009SplitterV1.ts
index d8372bd46..e5eb6b953 100644
--- a/eth/abi/contracts/EIP3009SplitterV1.ts
+++ b/eth/abi/contracts/EIP3009SplitterV1.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for EIP3009SplitterV1
// EIP-3009 payment splitter with fixed facilitator fee
-// Generated on: 2026-01-20T20:33:28.418Z
export const EIP3009SplitterV1ABI = [
{
diff --git a/eth/abi/contracts/GenImNFTv3-summary.md b/eth/abi/contracts/GenImNFTv3-summary.md
index 6b76f112e..dfdfa6166 100644
--- a/eth/abi/contracts/GenImNFTv3-summary.md
+++ b/eth/abi/contracts/GenImNFTv3-summary.md
@@ -2,8 +2,6 @@
GenImNFT Version 3 with listing functionality
-Generated on: 2026-01-20T20:33:28.408Z
-
## Contract Information
- **Name**: GenImNFTv3
diff --git a/eth/abi/contracts/GenImNFTv3.ts b/eth/abi/contracts/GenImNFTv3.ts
index 64aa58689..72195d26c 100644
--- a/eth/abi/contracts/GenImNFTv3.ts
+++ b/eth/abi/contracts/GenImNFTv3.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for GenImNFTv3
// GenImNFT Version 3 with listing functionality
-// Generated on: 2026-01-20T20:33:28.405Z
export const GenImNFTv3ABI = [
{
diff --git a/eth/abi/contracts/GenImNFTv4-summary.md b/eth/abi/contracts/GenImNFTv4-summary.md
new file mode 100644
index 000000000..d8516ef45
--- /dev/null
+++ b/eth/abi/contracts/GenImNFTv4-summary.md
@@ -0,0 +1,87 @@
+# GenImNFTv4 Contract Summary
+
+GenImNFT Version 4 with EIP-8004 agent whitelist (CVE-2025-11-26 fix)
+
+## Contract Information
+
+- **Name**: GenImNFTv4
+- **Functions**: 41
+- **Events**: 13
+- **Errors**: 20
+
+## All Functions
+
+- `UPGRADE_INTERFACE_VERSION()`
+- `approve(address to, uint256 tokenId)`
+- `authorizeAgentWallet(address agentWallet)`
+- `balanceOf(address owner)`
+- `burn(uint256 tokenId)`
+- `getAllPublicTokens()`
+- `getApproved(uint256 tokenId)`
+- `getPublicTokensOfOwner(address owner)`
+- `initialize()`
+- `isApprovedForAll(address owner, address operator)`
+- `isAuthorizedAgent(address agentWallet)`
+- `isImageUpdated(uint256 tokenId)`
+- `isTokenListed(uint256 tokenId)`
+- `mintPrice()`
+- `name()`
+- `owner()`
+- `ownerOf(uint256 tokenId)`
+- `proxiableUUID()`
+- `reinitializeV3()`
+- `reinitializeV4()`
+- `renounceOwnership()`
+- `requestImageUpdate(uint256 tokenId, string imageUrl)`
+- `revokeAgentWallet(address agentWallet)`
+- `safeMint(string uri, bool isListed)`
+- `safeMint(string uri)`
+- `safeTransferFrom(address from, address to, uint256 tokenId)`
+- `safeTransferFrom(address from, address to, uint256 tokenId, bytes data)`
+- `setApprovalForAll(address operator, bool approved)`
+- `setMintPrice(uint256 newPrice)`
+- `setMultipleTokensListed(uint256[] tokenIds, bool isListed)`
+- `setTokenListed(uint256 tokenId, bool isListed)`
+- `supportsInterface(bytes4 interfaceId)`
+- `symbol()`
+- `tokenByIndex(uint256 index)`
+- `tokenOfOwnerByIndex(address owner, uint256 index)`
+- `tokenURI(uint256 tokenId)`
+- `totalSupply()`
+- `transferFrom(address from, address to, uint256 tokenId)`
+- `transferOwnership(address newOwner)`
+- `upgradeToAndCall(address newImplementation, bytes data)`
+- `withdraw()`
+
+## Events
+
+- `AgentWalletAuthorized(indexed address agentWallet)`
+- `AgentWalletRevoked(indexed address agentWallet)`
+- `Approval(indexed address owner, indexed address approved, indexed uint256 tokenId)`
+- `ApprovalForAll(indexed address owner, indexed address operator, bool approved)`
+- `BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId)`
+- `ImageUpdateRequested(indexed uint256 tokenId, indexed address updater, string imageUrl)`
+- `Initialized(uint64 version)`
+- `MetadataUpdate(uint256 _tokenId)`
+- `OwnershipTransferred(indexed address previousOwner, indexed address newOwner)`
+- `TokenListingChanged(indexed uint256 tokenId, bool isListed)`
+- `Transfer(indexed address from, indexed address to, indexed uint256 tokenId)`
+- `UpdaterPaid(indexed uint256 tokenId, indexed address updater, uint256 amount)`
+- `Upgraded(indexed address implementation)`
+
+## Usage
+
+### TypeScript/JavaScript ES Modules
+
+```typescript
+import { GenImNFTv4ABI } from "./GenImNFTv4";
+// Use with ethers, web3, viem, etc.
+```
+
+### JSON (Direct import)
+
+```javascript
+import abi from "./GenImNFTv4.json";
+// Or for Node.js/CommonJS environments:
+const abi = require("./GenImNFTv4.json");
+```
diff --git a/eth/abi/contracts/GenImNFTv4.json b/eth/abi/contracts/GenImNFTv4.json
new file mode 100644
index 000000000..23fbd1c76
--- /dev/null
+++ b/eth/abi/contracts/GenImNFTv4.json
@@ -0,0 +1,1125 @@
+[
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "target",
+ "type": "address"
+ }
+ ],
+ "name": "AddressEmptyCode",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "implementation",
+ "type": "address"
+ }
+ ],
+ "name": "ERC1967InvalidImplementation",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "ERC1967NonPayable",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "ERC721EnumerableForbiddenBatchMint",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "sender",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721IncorrectOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ERC721InsufficientApproval",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "approver",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidApprover",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidOperator",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "receiver",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidReceiver",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "sender",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidSender",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ERC721NonexistentToken",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "index",
+ "type": "uint256"
+ }
+ ],
+ "name": "ERC721OutOfBoundsIndex",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "FailedCall",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "InvalidInitialization",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "NotInitializing",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "OwnableInvalidOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "account",
+ "type": "address"
+ }
+ ],
+ "name": "OwnableUnauthorizedAccount",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "UUPSUnauthorizedCallContext",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes32",
+ "name": "slot",
+ "type": "bytes32"
+ }
+ ],
+ "name": "UUPSUnsupportedProxiableUUID",
+ "type": "error"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "agentWallet",
+ "type": "address"
+ }
+ ],
+ "name": "AgentWalletAuthorized",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "agentWallet",
+ "type": "address"
+ }
+ ],
+ "name": "AgentWalletRevoked",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "ApprovalForAll",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "_fromTokenId",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "_toTokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "BatchMetadataUpdate",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "updater",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "imageUrl",
+ "type": "string"
+ }
+ ],
+ "name": "ImageUpdateRequested",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "uint64",
+ "name": "version",
+ "type": "uint64"
+ }
+ ],
+ "name": "Initialized",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "_tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "MetadataUpdate",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "previousOwner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "OwnershipTransferred",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "isListed",
+ "type": "bool"
+ }
+ ],
+ "name": "TokenListingChanged",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "updater",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "amount",
+ "type": "uint256"
+ }
+ ],
+ "name": "UpdaterPaid",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "implementation",
+ "type": "address"
+ }
+ ],
+ "name": "Upgraded",
+ "type": "event"
+ },
+ {
+ "inputs": [],
+ "name": "UPGRADE_INTERFACE_VERSION",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "agentWallet",
+ "type": "address"
+ }
+ ],
+ "name": "authorizeAgentWallet",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "burn",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "getAllPublicTokens",
+ "outputs": [
+ {
+ "internalType": "uint256[]",
+ "name": "",
+ "type": "uint256[]"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "getPublicTokensOfOwner",
+ "outputs": [
+ {
+ "internalType": "uint256[]",
+ "name": "",
+ "type": "uint256[]"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "initialize",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "agentWallet",
+ "type": "address"
+ }
+ ],
+ "name": "isAuthorizedAgent",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "isImageUpdated",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "isTokenListed",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "mintPrice",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "owner",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "proxiableUUID",
+ "outputs": [
+ {
+ "internalType": "bytes32",
+ "name": "",
+ "type": "bytes32"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "reinitializeV3",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "reinitializeV4",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "renounceOwnership",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "string",
+ "name": "imageUrl",
+ "type": "string"
+ }
+ ],
+ "name": "requestImageUpdate",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "agentWallet",
+ "type": "address"
+ }
+ ],
+ "name": "revokeAgentWallet",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "string",
+ "name": "uri",
+ "type": "string"
+ },
+ {
+ "internalType": "bool",
+ "name": "isListed",
+ "type": "bool"
+ }
+ ],
+ "name": "safeMint",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "string",
+ "name": "uri",
+ "type": "string"
+ }
+ ],
+ "name": "safeMint",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes",
+ "name": "data",
+ "type": "bytes"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "newPrice",
+ "type": "uint256"
+ }
+ ],
+ "name": "setMintPrice",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256[]",
+ "name": "tokenIds",
+ "type": "uint256[]"
+ },
+ {
+ "internalType": "bool",
+ "name": "isListed",
+ "type": "bool"
+ }
+ ],
+ "name": "setMultipleTokensListed",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bool",
+ "name": "isListed",
+ "type": "bool"
+ }
+ ],
+ "name": "setTokenListed",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4"
+ }
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "index",
+ "type": "uint256"
+ }
+ ],
+ "name": "tokenByIndex",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "index",
+ "type": "uint256"
+ }
+ ],
+ "name": "tokenOfOwnerByIndex",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "transferOwnership",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "newImplementation",
+ "type": "address"
+ },
+ {
+ "internalType": "bytes",
+ "name": "data",
+ "type": "bytes"
+ }
+ ],
+ "name": "upgradeToAndCall",
+ "outputs": [],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "withdraw",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+]
diff --git a/eth/abi/contracts/GenImNFTv4.ts b/eth/abi/contracts/GenImNFTv4.ts
new file mode 100644
index 000000000..cd682a7ad
--- /dev/null
+++ b/eth/abi/contracts/GenImNFTv4.ts
@@ -0,0 +1,1130 @@
+// Auto-generated ABI for GenImNFTv4
+// GenImNFT Version 4 with EIP-8004 agent whitelist (CVE-2025-11-26 fix)
+
+export const GenImNFTv4ABI = [
+ {
+ inputs: [],
+ stateMutability: "nonpayable",
+ type: "constructor",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "target",
+ type: "address",
+ },
+ ],
+ name: "AddressEmptyCode",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "implementation",
+ type: "address",
+ },
+ ],
+ name: "ERC1967InvalidImplementation",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "ERC1967NonPayable",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "ERC721EnumerableForbiddenBatchMint",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "sender",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "ERC721IncorrectOwner",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "ERC721InsufficientApproval",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "approver",
+ type: "address",
+ },
+ ],
+ name: "ERC721InvalidApprover",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ ],
+ name: "ERC721InvalidOperator",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "ERC721InvalidOwner",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "receiver",
+ type: "address",
+ },
+ ],
+ name: "ERC721InvalidReceiver",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "sender",
+ type: "address",
+ },
+ ],
+ name: "ERC721InvalidSender",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "ERC721NonexistentToken",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "ERC721OutOfBoundsIndex",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "FailedCall",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "InvalidInitialization",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "NotInitializing",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "OwnableInvalidOwner",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "account",
+ type: "address",
+ },
+ ],
+ name: "OwnableUnauthorizedAccount",
+ type: "error",
+ },
+ {
+ inputs: [],
+ name: "UUPSUnauthorizedCallContext",
+ type: "error",
+ },
+ {
+ inputs: [
+ {
+ internalType: "bytes32",
+ name: "slot",
+ type: "bytes32",
+ },
+ ],
+ name: "UUPSUnsupportedProxiableUUID",
+ type: "error",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "agentWallet",
+ type: "address",
+ },
+ ],
+ name: "AgentWalletAuthorized",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "agentWallet",
+ type: "address",
+ },
+ ],
+ name: "AgentWalletRevoked",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "approved",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "Approval",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "bool",
+ name: "approved",
+ type: "bool",
+ },
+ ],
+ name: "ApprovalForAll",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "_fromTokenId",
+ type: "uint256",
+ },
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "_toTokenId",
+ type: "uint256",
+ },
+ ],
+ name: "BatchMetadataUpdate",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "updater",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "string",
+ name: "imageUrl",
+ type: "string",
+ },
+ ],
+ name: "ImageUpdateRequested",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: "uint64",
+ name: "version",
+ type: "uint64",
+ },
+ ],
+ name: "Initialized",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "_tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "MetadataUpdate",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "previousOwner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "newOwner",
+ type: "address",
+ },
+ ],
+ name: "OwnershipTransferred",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ indexed: false,
+ internalType: "bool",
+ name: "isListed",
+ type: "bool",
+ },
+ ],
+ name: "TokenListingChanged",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "Transfer",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ indexed: true,
+ internalType: "address",
+ name: "updater",
+ type: "address",
+ },
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "amount",
+ type: "uint256",
+ },
+ ],
+ name: "UpdaterPaid",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: "address",
+ name: "implementation",
+ type: "address",
+ },
+ ],
+ name: "Upgraded",
+ type: "event",
+ },
+ {
+ inputs: [],
+ name: "UPGRADE_INTERFACE_VERSION",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "approve",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "agentWallet",
+ type: "address",
+ },
+ ],
+ name: "authorizeAgentWallet",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "balanceOf",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "burn",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "getAllPublicTokens",
+ outputs: [
+ {
+ internalType: "uint256[]",
+ name: "",
+ type: "uint256[]",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "getApproved",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "getPublicTokensOfOwner",
+ outputs: [
+ {
+ internalType: "uint256[]",
+ name: "",
+ type: "uint256[]",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "initialize",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ ],
+ name: "isApprovedForAll",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "agentWallet",
+ type: "address",
+ },
+ ],
+ name: "isAuthorizedAgent",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "isImageUpdated",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "isTokenListed",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "mintPrice",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "name",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "owner",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "ownerOf",
+ outputs: [
+ {
+ internalType: "address",
+ name: "",
+ type: "address",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "proxiableUUID",
+ outputs: [
+ {
+ internalType: "bytes32",
+ name: "",
+ type: "bytes32",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "reinitializeV3",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "reinitializeV4",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "renounceOwnership",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ internalType: "string",
+ name: "imageUrl",
+ type: "string",
+ },
+ ],
+ name: "requestImageUpdate",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "agentWallet",
+ type: "address",
+ },
+ ],
+ name: "revokeAgentWallet",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "string",
+ name: "uri",
+ type: "string",
+ },
+ {
+ internalType: "bool",
+ name: "isListed",
+ type: "bool",
+ },
+ ],
+ name: "safeMint",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "payable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "string",
+ name: "uri",
+ type: "string",
+ },
+ ],
+ name: "safeMint",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "payable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "safeTransferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ internalType: "bytes",
+ name: "data",
+ type: "bytes",
+ },
+ ],
+ name: "safeTransferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "operator",
+ type: "address",
+ },
+ {
+ internalType: "bool",
+ name: "approved",
+ type: "bool",
+ },
+ ],
+ name: "setApprovalForAll",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "newPrice",
+ type: "uint256",
+ },
+ ],
+ name: "setMintPrice",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256[]",
+ name: "tokenIds",
+ type: "uint256[]",
+ },
+ {
+ internalType: "bool",
+ name: "isListed",
+ type: "bool",
+ },
+ ],
+ name: "setMultipleTokensListed",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ {
+ internalType: "bool",
+ name: "isListed",
+ type: "bool",
+ },
+ ],
+ name: "setTokenListed",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "bytes4",
+ name: "interfaceId",
+ type: "bytes4",
+ },
+ ],
+ name: "supportsInterface",
+ outputs: [
+ {
+ internalType: "bool",
+ name: "",
+ type: "bool",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "symbol",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "tokenByIndex",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "tokenOfOwnerByIndex",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "tokenURI",
+ outputs: [
+ {
+ internalType: "string",
+ name: "",
+ type: "string",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "totalSupply",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "from",
+ type: "address",
+ },
+ {
+ internalType: "address",
+ name: "to",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "tokenId",
+ type: "uint256",
+ },
+ ],
+ name: "transferFrom",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "newOwner",
+ type: "address",
+ },
+ ],
+ name: "transferOwnership",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "newImplementation",
+ type: "address",
+ },
+ {
+ internalType: "bytes",
+ name: "data",
+ type: "bytes",
+ },
+ ],
+ name: "upgradeToAndCall",
+ outputs: [],
+ stateMutability: "payable",
+ type: "function",
+ },
+ {
+ inputs: [],
+ name: "withdraw",
+ outputs: [],
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+] as const;
+
+export type GenImNFTv4ABI = typeof GenImNFTv4ABI;
diff --git a/eth/abi/contracts/LLMv1-summary.md b/eth/abi/contracts/LLMv1-summary.md
index 487253bcd..c7aebc153 100644
--- a/eth/abi/contracts/LLMv1-summary.md
+++ b/eth/abi/contracts/LLMv1-summary.md
@@ -2,8 +2,6 @@
LLM Version 1 to interact with LLMs
-Generated on: 2026-01-20T20:33:28.417Z
-
## Contract Information
- **Name**: LLMv1
diff --git a/eth/abi/contracts/LLMv1.ts b/eth/abi/contracts/LLMv1.ts
index 64db0eb45..7faec138e 100644
--- a/eth/abi/contracts/LLMv1.ts
+++ b/eth/abi/contracts/LLMv1.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for LLMv1
// LLM Version 1 to interact with LLMs
-// Generated on: 2026-01-20T20:33:28.417Z
export const LLMv1ABI = [
{
diff --git a/eth/abi/contracts/Support-summary.md b/eth/abi/contracts/Support-summary.md
index 3ab286738..7cf07d2ec 100644
--- a/eth/abi/contracts/Support-summary.md
+++ b/eth/abi/contracts/Support-summary.md
@@ -2,8 +2,6 @@
Support contract for donations and likes functionality
-Generated on: 2026-01-20T20:33:28.416Z
-
## Contract Information
- **Name**: Support
diff --git a/eth/abi/contracts/Support.ts b/eth/abi/contracts/Support.ts
index 9ca345c0e..61bdc9411 100644
--- a/eth/abi/contracts/Support.ts
+++ b/eth/abi/contracts/Support.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for Support
// Support contract for donations and likes functionality
-// Generated on: 2026-01-20T20:33:28.416Z
export const SupportABI = [
{
diff --git a/eth/abi/contracts/SupportV2-summary.md b/eth/abi/contracts/SupportV2-summary.md
index 4251cce17..4838d03c8 100644
--- a/eth/abi/contracts/SupportV2-summary.md
+++ b/eth/abi/contracts/SupportV2-summary.md
@@ -2,8 +2,6 @@
Support contract V2 with ETH and EIP-3009 token donations
-Generated on: 2026-01-20T20:33:28.419Z
-
## Contract Information
- **Name**: SupportV2
diff --git a/eth/abi/contracts/SupportV2.ts b/eth/abi/contracts/SupportV2.ts
index fef4ed318..0676ae90e 100644
--- a/eth/abi/contracts/SupportV2.ts
+++ b/eth/abi/contracts/SupportV2.ts
@@ -1,6 +1,5 @@
// Auto-generated ABI for SupportV2
// Support contract V2 with ETH and EIP-3009 token donations
-// Generated on: 2026-01-20T20:33:28.419Z
export const SupportV2ABI = [
{
diff --git a/eth/deployments/support-v2-optimisticEthereum.json b/eth/deployments/support-v2-optimisticEthereum.json
index 345fa372e..7bc2f4cf1 100644
--- a/eth/deployments/support-v2-optimisticEthereum.json
+++ b/eth/deployments/support-v2-optimisticEthereum.json
@@ -5,4 +5,4 @@
"owner": "0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20",
"deployedAt": "2026-01-24T17:05:07.947Z",
"version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/eth/deployments/support-v2-optsepolia.json b/eth/deployments/support-v2-optsepolia.json
index 1c9c009db..213863795 100644
--- a/eth/deployments/support-v2-optsepolia.json
+++ b/eth/deployments/support-v2-optsepolia.json
@@ -5,4 +5,4 @@
"owner": "0x073f26F0C3FC100e7b075C3DC3cDE0A777497D20",
"deployedAt": "2026-01-20T20:31:42.392Z",
"version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/eth/scripts/deploy-support-v2.config.json b/eth/scripts/deploy-support-v2.config.json
index 570ecae80..c0ba7ecd8 100644
--- a/eth/scripts/deploy-support-v2.config.json
+++ b/eth/scripts/deploy-support-v2.config.json
@@ -11,4 +11,4 @@
"version": "1.0.0",
"environment": "production"
}
-}
\ No newline at end of file
+}
diff --git a/eth/scripts/export-abi.ts b/eth/scripts/export-abi.ts
index a73cd0d87..b5114d26e 100644
--- a/eth/scripts/export-abi.ts
+++ b/eth/scripts/export-abi.ts
@@ -31,6 +31,12 @@ const contracts: ContractConfig[] = [
contractName: "GenImNFTv3",
description: "GenImNFT Version 3 with listing functionality",
},
+ {
+ name: "GenImNFTv4",
+ contractFile: "GenImNFTv4.sol",
+ contractName: "GenImNFTv4",
+ description: "GenImNFT Version 4 with EIP-8004 agent whitelist (CVE-2025-11-26 fix)",
+ },
{
name: "CollectorNFT",
contractFile: "CollectorNFT.sol",
@@ -102,7 +108,6 @@ async function exportContractABI(config: ContractConfig) {
const abiTsPath = path.join(exportDir, `${config.name}.ts`);
const tsContent = `// Auto-generated ABI for ${config.name}
// ${config.description}
-// Generated on: ${new Date().toISOString()}
export const ${config.name}ABI = ${JSON.stringify(abi, null, 2)} as const;
@@ -171,8 +176,6 @@ export type ${config.name}ABI = typeof ${config.name}ABI;
${config.description}
-Generated on: ${new Date().toISOString()}
-
## Contract Information
- **Name**: ${config.name}
- **Functions**: ${functions.length}
diff --git a/eth/test/SupportV2_Deployment.ts b/eth/test/SupportV2_Deployment.ts
index b3e700be8..ab6a6518d 100644
--- a/eth/test/SupportV2_Deployment.ts
+++ b/eth/test/SupportV2_Deployment.ts
@@ -54,11 +54,7 @@ describe("SupportV2 - Deployment Tests", function () {
/**
* Helper: Run test with temporary config, then restore original
*/
- async function withTempConfig(
- options: SupportV2ConfigOptions,
- testFn: () => Promise,
- owner?: string,
- ) {
+ async function withTempConfig(options: SupportV2ConfigOptions, testFn: () => Promise, owner?: string) {
await createTempConfig(options, owner);
try {
@@ -177,10 +173,7 @@ describe("SupportV2 - Deployment Tests", function () {
describe("Configuration Validation", function () {
it("should reject invalid owner address format", async function () {
- const invalidConfigPath = path.join(
- __dirname,
- "../scripts/deploy-support-v2.config-invalid.json",
- );
+ const invalidConfigPath = path.join(__dirname, "../scripts/deploy-support-v2.config-invalid.json");
const invalidConfig = {
parameters: {
@@ -277,16 +270,10 @@ describe("SupportV2 - Deployment Tests", function () {
expect(result).to.not.be.undefined;
// Verify proxy implementation slot is set
- const implementationSlot =
- "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
- const implementation = await hre.ethers.provider.getStorage(
- result!.proxyAddress,
- implementationSlot,
- );
-
- expect(implementation).to.not.equal(
- "0x0000000000000000000000000000000000000000000000000000000000000000",
- );
+ const implementationSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
+ const implementation = await hre.ethers.provider.getStorage(result!.proxyAddress, implementationSlot);
+
+ expect(implementation).to.not.equal("0x0000000000000000000000000000000000000000000000000000000000000000");
});
});
diff --git a/shared/chain-utils/src/abi/GenImNFTv4.ts b/shared/chain-utils/src/abi/GenImNFTv4.ts
index 3907910d4..ec7a1bb46 100644
--- a/shared/chain-utils/src/abi/GenImNFTv4.ts
+++ b/shared/chain-utils/src/abi/GenImNFTv4.ts
@@ -11,7 +11,7 @@
* - x402_facilitator/x402_whitelist.js: isAuthorizedAgent
* - website/components/PublicNFTList.tsx: getAllPublicTokens
* - website/components/MyNFTList.tsx: balanceOf, tokenOfOwnerByIndex
- * - website/components/NFTCard.tsx: setListed, burn
+ * - website/components/NFTCard.tsx: setTokenListed, isTokenListed, burn
*/
export const GenImNFTv4ABI = [
@@ -55,7 +55,7 @@ export const GenImNFTv4ABI = [
outputs: [{ name: "", type: "string" }],
},
{
- name: "isListed",
+ name: "isTokenListed",
type: "function",
stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }],
@@ -86,7 +86,7 @@ export const GenImNFTv4ABI = [
outputs: [{ name: "", type: "uint256" }],
},
{
- name: "setListed",
+ name: "setTokenListed",
type: "function",
stateMutability: "nonpayable",
inputs: [
diff --git a/website/hooks/useNFTListedStatus.ts b/website/hooks/useNFTListedStatus.ts
index f6d4556ab..0521ead91 100644
--- a/website/hooks/useNFTListedStatus.ts
+++ b/website/hooks/useNFTListedStatus.ts
@@ -2,7 +2,7 @@
* Hook to fetch and manage the listing status of an NFT.
*
* This hook encapsulates the logic for checking if an NFT is publicly listed
- * via the smart contract's isListed function.
+ * via the smart contract's isTokenListed function.
*
* @example
* ```tsx
@@ -61,7 +61,8 @@ export function useNFTListedStatus({
const fetchListedStatus = useCallback(async () => {
if (!enabled) {
- setIsListed(undefined);
+ // Don't reset isListed when disabled - keep the last known value
+ // This prevents flickering when enabled toggles temporarily
return;
}
@@ -72,7 +73,7 @@ export function useNFTListedStatus({
const result = await readContract(config, {
address: contractAddress,
abi: GenImNFTv4ABI,
- functionName: "isListed",
+ functionName: "isTokenListed",
args: [tokenId],
chainId,
});
diff --git a/website/test/useNFTListedStatus.test.ts b/website/test/useNFTListedStatus.test.ts
index 72b12284a..a0d1a2d66 100644
--- a/website/test/useNFTListedStatus.test.ts
+++ b/website/test/useNFTListedStatus.test.ts
@@ -27,7 +27,7 @@ vi.mock("@fretchen/chain-utils", () => ({
if (network === "eip155:11155420") return "0xSepoliaContract";
return "0xUnknownContract";
}),
- GenImNFTv4ABI: [{ name: "isListed", type: "function" }],
+ GenImNFTv4ABI: [{ name: "isTokenListed", type: "function" }],
fromCAIP2: vi.fn((network: string) => {
if (network === "eip155:10") return 10;
if (network === "eip155:11155420") return 11155420;
@@ -57,7 +57,7 @@ describe("useNFTListedStatus", () => {
expect.anything(), // config
expect.objectContaining({
address: "0xOptimismContract",
- functionName: "isListed",
+ functionName: "isTokenListed",
args: [BigInt(42)],
chainId: 10,
})
@@ -312,5 +312,34 @@ describe("useNFTListedStatus", () => {
);
});
});
+
+ it("should fetch when enabled changes from false to true", async () => {
+ mockReadContract.mockResolvedValue(true);
+
+ // Start with enabled=false (simulating isLoading state)
+ const { result, rerender } = renderHook(
+ ({ enabled }) =>
+ useNFTListedStatus({
+ tokenId: BigInt(42),
+ network: "eip155:10",
+ enabled,
+ }),
+ { initialProps: { enabled: false } }
+ );
+
+ // Should not have called contract yet
+ expect(mockReadContract).not.toHaveBeenCalled();
+ expect(result.current.isListed).toBeUndefined();
+
+ // Simulate NFT data loaded - enabled becomes true
+ rerender({ enabled: true });
+
+ // Should now fetch and return isListed
+ await waitFor(() => {
+ expect(result.current.isListed).toBe(true);
+ });
+
+ expect(mockReadContract).toHaveBeenCalledTimes(1);
+ });
});
});
From c01819e89658cfc0859c562940f194e8893048a1 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 14:54:20 +0100
Subject: [PATCH 40/46] Lint
---
website/components/NFTCard.tsx | 8 ++---
website/hooks/useAutoNetwork.ts | 2 +-
website/test/MyNFTList.test.tsx | 35 +++++++++----------
website/test/PublicNFTList.test.tsx | 27 +++++++--------
website/test/useAutoNetwork.test.ts | 45 +++++++++----------------
website/test/useNFTListedStatus.test.ts | 41 +++++++++++-----------
6 files changed, 67 insertions(+), 91 deletions(-)
diff --git a/website/components/NFTCard.tsx b/website/components/NFTCard.tsx
index 9d3e28909..1cdcc5dcb 100644
--- a/website/components/NFTCard.tsx
+++ b/website/components/NFTCard.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
import { useNFTListedStatus } from "../hooks/useNFTListedStatus";
-import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2, isMainnet } from "@fretchen/chain-utils";
+import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, isMainnet } from "@fretchen/chain-utils";
import { useConfiguredPublicClient } from "../hooks/useConfiguredPublicClient";
import { NFTCardProps, NFT, NFTMetadata } from "../types/components";
import { useToast } from "./Toast";
@@ -45,7 +45,6 @@ export function NFTCard({
// Get network and contract address from chain-utils
const { network, switchIfNeeded } = useAutoNetwork(GENAI_NFT_NETWORKS);
- const chainId = fromCAIP2(network);
const contractAddress = getGenAiNFTAddress(network);
// Use the extracted hook for listing status
@@ -54,10 +53,7 @@ export function NFTCard({
// 2. Token data loaded successfully (no error, not loading)
// This prevents contract calls for non-existent/burned tokens
const tokenDataLoaded = !nft.isLoading && !nft.error;
- const {
- isListed,
- setOptimisticListed,
- } = useNFTListedStatus({
+ const { isListed, setOptimisticListed } = useNFTListedStatus({
tokenId,
network,
enabled: !isPublicView && tokenDataLoaded,
diff --git a/website/hooks/useAutoNetwork.ts b/website/hooks/useAutoNetwork.ts
index 6843184d1..facaf8a09 100644
--- a/website/hooks/useAutoNetwork.ts
+++ b/website/hooks/useAutoNetwork.ts
@@ -53,7 +53,7 @@ export function useAutoNetwork(supportedNetworks: readonly string[]): UseAutoNet
const defaultNetwork = supportedNetworks[0];
const currentNetwork = toCAIP2(chainId);
const isSupported = supportedNetworks.includes(currentNetwork);
-
+
// Return the current network if supported, otherwise the default
const network = isSupported ? currentNetwork : defaultNetwork;
diff --git a/website/test/MyNFTList.test.tsx b/website/test/MyNFTList.test.tsx
index 4a09db723..8ac04646d 100644
--- a/website/test/MyNFTList.test.tsx
+++ b/website/test/MyNFTList.test.tsx
@@ -8,9 +8,6 @@ import { MyNFTList } from "../components/MyNFTList";
* in useEffect dependencies after the "Lint hooks" commit.
*/
-let renderCount = 0;
-let contractConfigCallCount = 0;
-
// Mock wagmi functions
const mockUseAccount = vi.fn();
const mockUseReadContract = vi.fn();
@@ -130,19 +127,20 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
render();
// Wait for the component to load and fetch token IDs
- await waitFor(() => {
- // tokenOfOwnerByIndex should be called for each NFT in the balance
- expect(mockReadContract).toHaveBeenCalled();
- }, { timeout: 2000 });
+ await waitFor(
+ () => {
+ // tokenOfOwnerByIndex should be called for each NFT in the balance
+ expect(mockReadContract).toHaveBeenCalled();
+ },
+ { timeout: 2000 },
+ );
// Verify tokenOfOwnerByIndex was called with correct function name
const calls = mockReadContract.mock.calls;
- const tokenOfOwnerByIndexCalls = calls.filter(
- (call: unknown[]) => {
- const params = call[1] as { functionName?: string };
- return params?.functionName === "tokenOfOwnerByIndex";
- }
- );
+ const tokenOfOwnerByIndexCalls = calls.filter((call: unknown[]) => {
+ const params = call[1] as { functionName?: string };
+ return params?.functionName === "tokenOfOwnerByIndex";
+ });
expect(tokenOfOwnerByIndexCalls.length).toBeGreaterThan(0);
});
@@ -151,10 +149,13 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
render();
// Wait for NFT cards to appear
- await waitFor(() => {
- expect(screen.getByTestId("nft-card-1")).toBeInTheDocument();
- expect(screen.getByTestId("nft-card-2")).toBeInTheDocument();
- }, { timeout: 2000 });
+ await waitFor(
+ () => {
+ expect(screen.getByTestId("nft-card-1")).toBeInTheDocument();
+ expect(screen.getByTestId("nft-card-2")).toBeInTheDocument();
+ },
+ { timeout: 2000 },
+ );
});
it("should show empty state when user has no NFTs", async () => {
diff --git a/website/test/PublicNFTList.test.tsx b/website/test/PublicNFTList.test.tsx
index f5a571c77..d485dcafc 100644
--- a/website/test/PublicNFTList.test.tsx
+++ b/website/test/PublicNFTList.test.tsx
@@ -27,9 +27,7 @@ vi.mock("../hooks/useConfiguredPublicClient", () => ({
// Mock chain-utils
vi.mock("@fretchen/chain-utils", () => ({
getGenAiNFTAddress: vi.fn(() => "0x80f95d330417a4acEfEA415FE9eE28db7A0A1Cdb"),
- GenImNFTv4ABI: [
- { name: "getAllPublicTokens", type: "function", inputs: [], outputs: [{ type: "uint256[]" }] },
- ],
+ GenImNFTv4ABI: [{ name: "getAllPublicTokens", type: "function", inputs: [], outputs: [{ type: "uint256[]" }] }],
GENAI_NFT_NETWORKS: ["eip155:10", "eip155:11155420"],
}));
@@ -89,17 +87,17 @@ describe("PublicNFTList Component", () => {
it("should render loading state initially", () => {
// Don't resolve the promise immediately
mockReadContract.mockReturnValue(new Promise(() => {}));
-
+
render();
-
+
expect(screen.getByText(/Loading public artworks/i)).toBeInTheDocument();
});
it("should render empty state when no public NFTs", async () => {
mockReadContract.mockResolvedValue([]);
-
+
render();
-
+
// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
@@ -108,9 +106,9 @@ describe("PublicNFTList Component", () => {
it("should render NFT cards when public NFTs exist", async () => {
mockReadContract.mockResolvedValue([1n, 2n, 3n]);
-
+
render();
-
+
// Wait for NFTs to load
await waitFor(() => {
expect(screen.getByTestId("nft-card-3")).toBeInTheDocument();
@@ -121,25 +119,25 @@ describe("PublicNFTList Component", () => {
it("should call getAllPublicTokens ABI function", async () => {
mockReadContract.mockResolvedValue([1n, 2n]);
-
+
render();
-
+
await waitFor(() => {
// Verify the correct ABI function is called
expect(mockReadContract).toHaveBeenCalledWith(
expect.objectContaining({
functionName: "getAllPublicTokens",
- })
+ }),
);
});
});
it("should handle contract errors gracefully", async () => {
mockReadContract.mockRejectedValue(new Error("Contract call failed"));
-
+
// Should not throw
expect(() => render()).not.toThrow();
-
+
// Wait for error handling
await waitFor(() => {
expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument();
@@ -166,4 +164,3 @@ describe("PublicNFTList Component", () => {
});
});
});
-
diff --git a/website/test/useAutoNetwork.test.ts b/website/test/useAutoNetwork.test.ts
index 3c7832e9e..09312ef53 100644
--- a/website/test/useAutoNetwork.test.ts
+++ b/website/test/useAutoNetwork.test.ts
@@ -3,7 +3,7 @@
*
* Uses wagmi's mock connector to test with different chainIds in one file.
* See: https://wagmi.sh/react/api/connectors/mock
- *
+ *
* IMPORTANT: The mock connector shares state between tests, so each test
* must first reset to a known chain state before asserting.
*/
@@ -16,15 +16,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
vi.unmock("wagmi");
vi.unmock("wagmi/connectors");
-import {
- createConfig,
- WagmiProvider,
- http,
- useChainId,
- useSwitchChain,
- useAccount,
- useConnect,
-} from "wagmi";
+import { createConfig, WagmiProvider, http, useChainId, useSwitchChain, useAccount, useConnect } from "wagmi";
import { optimism, optimismSepolia, base, mainnet } from "wagmi/chains";
import { mock } from "wagmi/connectors";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
@@ -57,12 +49,14 @@ function createTestWrapper() {
defaultOptions: { queries: { retry: false } },
});
- return ({ children }: { children: React.ReactNode }) =>
+ const TestWrapper = ({ children }: { children: React.ReactNode }) =>
createElement(
QueryClientProvider,
{ client: queryClient },
- createElement(WagmiProvider, { config: sharedConfig }, children)
+ createElement(WagmiProvider, { config: sharedConfig }, children),
);
+ TestWrapper.displayName = "TestWrapper";
+ return TestWrapper;
}
describe("useAutoNetwork Hook", () => {
@@ -71,10 +65,7 @@ describe("useAutoNetwork Hook", () => {
const wrapper = createTestWrapper();
const supportedNetworks = [OPTIMISM_MAINNET, OPTIMISM_SEPOLIA];
- const { result } = renderHook(
- () => useAutoNetwork(supportedNetworks),
- { wrapper }
- );
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks), { wrapper });
await waitFor(() => {
expect(result.current.network).toBeDefined();
@@ -92,10 +83,7 @@ describe("useAutoNetwork Hook", () => {
const wrapper = createTestWrapper();
const supportedNetworks = [OPTIMISM_MAINNET, OPTIMISM_SEPOLIA];
- const { result } = renderHook(
- () => useAutoNetwork(supportedNetworks),
- { wrapper }
- );
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks), { wrapper });
await waitFor(() => {
expect(result.current.network).toBe(OPTIMISM_MAINNET);
@@ -108,10 +96,7 @@ describe("useAutoNetwork Hook", () => {
const wrapper = createTestWrapper();
const supportedNetworks = [OPTIMISM_MAINNET];
- const { result } = renderHook(
- () => useAutoNetwork(supportedNetworks),
- { wrapper }
- );
+ const { result } = renderHook(() => useAutoNetwork(supportedNetworks), { wrapper });
await waitFor(() => {
expect(result.current.network).toBeDefined();
@@ -137,7 +122,7 @@ describe("useAutoNetwork Hook", () => {
chainId: useChainId(),
switchChain: useSwitchChain(),
}),
- { wrapper }
+ { wrapper },
);
// Wait for initial render
@@ -168,7 +153,7 @@ describe("useAutoNetwork Hook", () => {
autoNetwork: useAutoNetwork([BASE_MAINNET, OPTIMISM_MAINNET]),
switchChain: useSwitchChain(),
}),
- { wrapper }
+ { wrapper },
);
await waitFor(() => {
@@ -196,7 +181,7 @@ describe("useAutoNetwork Hook", () => {
autoNetwork: useAutoNetwork([OPTIMISM_MAINNET, OPTIMISM_SEPOLIA]),
switchChain: useSwitchChain(),
}),
- { wrapper }
+ { wrapper },
);
await waitFor(() => {
@@ -226,7 +211,7 @@ describe("useAutoNetwork Hook", () => {
account: useAccount(),
connect: useConnect(),
}),
- { wrapper }
+ { wrapper },
);
await waitFor(() => {
@@ -280,7 +265,7 @@ describe("useAutoNetwork Hook", () => {
switchChain: useSwitchChain(),
chainId: useChainId(),
}),
- { wrapper }
+ { wrapper },
);
await waitFor(() => {
@@ -311,7 +296,7 @@ describe("useAutoNetwork Hook", () => {
account: useAccount(),
connect: useConnect(),
}),
- { wrapper }
+ { wrapper },
);
await waitFor(() => {
diff --git a/website/test/useNFTListedStatus.test.ts b/website/test/useNFTListedStatus.test.ts
index a0d1a2d66..90cd87c5a 100644
--- a/website/test/useNFTListedStatus.test.ts
+++ b/website/test/useNFTListedStatus.test.ts
@@ -49,7 +49,7 @@ describe("useNFTListedStatus", () => {
tokenId: BigInt(42),
network: "eip155:10",
enabled: true,
- })
+ }),
);
await waitFor(() => {
@@ -60,7 +60,7 @@ describe("useNFTListedStatus", () => {
functionName: "isTokenListed",
args: [BigInt(42)],
chainId: 10,
- })
+ }),
);
});
});
@@ -71,7 +71,7 @@ describe("useNFTListedStatus", () => {
tokenId: BigInt(42),
network: "eip155:10",
enabled: false,
- })
+ }),
);
// Wait a tick to ensure no async calls are made
@@ -90,7 +90,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(1),
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -99,7 +99,7 @@ describe("useNFTListedStatus", () => {
expect.objectContaining({
address: "0xOptimismContract",
chainId: 10,
- })
+ }),
);
});
@@ -111,7 +111,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(1),
network: "eip155:11155420",
- })
+ }),
);
await waitFor(() => {
@@ -120,7 +120,7 @@ describe("useNFTListedStatus", () => {
expect.objectContaining({
address: "0xSepoliaContract",
chainId: 11155420,
- })
+ }),
);
});
});
@@ -134,7 +134,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(42),
network: "eip155:10",
- })
+ }),
);
// Initially loading
@@ -155,7 +155,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(42),
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -171,7 +171,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(42),
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -183,7 +183,7 @@ describe("useNFTListedStatus", () => {
it("should handle legacy tokens (contract reverts) gracefully without console.error", async () => {
// Simulate a contract revert error (legacy token without isListed support)
- const revertError = new Error("The contract function \"isListed\" reverted.");
+ const revertError = new Error('The contract function "isListed" reverted.');
revertError.name = "ContractFunctionExecutionError";
mockReadContract.mockRejectedValue(revertError);
@@ -194,7 +194,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(2), // Legacy token ID
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -217,7 +217,7 @@ describe("useNFTListedStatus", () => {
tokenId: BigInt(42),
network: "eip155:10",
enabled: false,
- })
+ }),
);
// Should immediately be undefined, not loading
@@ -234,7 +234,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(42),
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -258,7 +258,7 @@ describe("useNFTListedStatus", () => {
useNFTListedStatus({
tokenId: BigInt(42),
network: "eip155:10",
- })
+ }),
);
await waitFor(() => {
@@ -290,17 +290,14 @@ describe("useNFTListedStatus", () => {
tokenId,
network: "eip155:10",
}),
- { initialProps: { tokenId: BigInt(1) } }
+ { initialProps: { tokenId: BigInt(1) } },
);
await waitFor(() => {
expect(result.current.isListed).toBe(true);
});
- expect(mockReadContract).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({ args: [BigInt(1)] })
- );
+ expect(mockReadContract).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ args: [BigInt(1)] }));
// Change tokenId
rerender({ tokenId: BigInt(2) });
@@ -308,7 +305,7 @@ describe("useNFTListedStatus", () => {
await waitFor(() => {
expect(mockReadContract).toHaveBeenCalledWith(
expect.anything(),
- expect.objectContaining({ args: [BigInt(2)] })
+ expect.objectContaining({ args: [BigInt(2)] }),
);
});
});
@@ -324,7 +321,7 @@ describe("useNFTListedStatus", () => {
network: "eip155:10",
enabled,
}),
- { initialProps: { enabled: false } }
+ { initialProps: { enabled: false } },
);
// Should not have called contract yet
From b6fc8ce4e3ab1fc8657394de76d349229c04994a Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 14:56:56 +0100
Subject: [PATCH 41/46] Proper build
---
.github/workflows/ci.yml | 7 +++++++
.github/workflows/pages.yml | 6 ++++++
2 files changed, 13 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c16738dbb..b03db3cdf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,13 @@ jobs:
with:
node-version: "22"
+ # Build shared chain-utils first (required by website)
+ - name: Build chain-utils
+ run: |
+ cd shared/chain-utils
+ npm ci
+ npm run build
+
# Install website dependencies
- name: Install website dependencies
run: |
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index cce40a6ac..9598ebc3e 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -14,6 +14,12 @@ jobs:
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
run: |
+ # Build shared chain-utils first (required by website)
+ cd shared/chain-utils
+ npm ci
+ npm run build
+ cd ../..
+ # Build website
cd website
npm ci
npm run build
From 97b435231fcb7ed6383c7ddff2ddf21188d138ba Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 15:04:22 +0100
Subject: [PATCH 42/46] Update ImageGenerator.test.tsx
---
website/test/ImageGenerator.test.tsx | 82 ++++------------------------
1 file changed, 12 insertions(+), 70 deletions(-)
diff --git a/website/test/ImageGenerator.test.tsx b/website/test/ImageGenerator.test.tsx
index f1148cd2d..ea2c9cd44 100644
--- a/website/test/ImageGenerator.test.tsx
+++ b/website/test/ImageGenerator.test.tsx
@@ -33,7 +33,7 @@ import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ImageGenerator } from "../components/ImageGenerator";
-import { useAccount, useSwitchChain, useChainId, useConnect, useWalletClient } from "wagmi";
+import { useAccount, useConnect, useWalletClient } from "wagmi";
// No need to mock getChain - it's just reading env vars and returning constants
// The real implementation works fine in tests
@@ -107,18 +107,20 @@ describe("ImageGenerator Component", () => {
it("should trigger wallet connection when expand button is clicked", async () => {
// Start in disconnected state (collapsed)
+ // Create a new mock that we can track
const mockConnect = vi.fn();
- vi.mocked(useConnect).mockReturnValueOnce({
- connect: mockConnect,
- connectors: [{ id: "mockConnector", name: "Mock Wallet" }],
- } as unknown as ReturnType);
-
- vi.mocked(useAccount).mockReturnValueOnce({
+
+ vi.mocked(useAccount).mockReturnValue({
address: undefined,
isConnected: false,
status: "disconnected",
} as ReturnType);
+ vi.mocked(useConnect).mockReturnValue({
+ connect: mockConnect,
+ connectors: [{ id: "mockConnector", name: "Mock Wallet" }],
+ } as unknown as ReturnType);
+
render();
// Verify we start in collapsed state
@@ -166,67 +168,7 @@ describe("ImageGenerator Component", () => {
});
});
- // SKIPPED: This test has two issues:
- // 1. vi.mocked(useChainId).mockReturnValue() doesn't work due to module caching
- // (see useAutoNetwork.test.ts header for detailed explanation)
- // 2. With "switch at interaction" pattern, switchChain is called via switchIfNeeded()
- // inside the submit handler, not directly on button click
- it.skip("should call switchChain when user attempts to create artwork on wrong network", async () => {
- const mockSwitchChain = vi.fn().mockResolvedValue(undefined);
-
- // Override the centralized mocks for this specific test
- // Mock a connected wallet on the wrong chain (Ethereum mainnet = 1)
- vi.mocked(useAccount).mockReturnValue({
- address: "0x1234567890123456789012345678901234567890" as `0x${string}`,
- isConnected: true,
- status: "connected",
- } as ReturnType);
-
- // Mock wallet client for x402 hook
-
- vi.mocked(useWalletClient).mockReturnValue({
- data: {
- account: { address: "0x1234567890123456789012345678901234567890" },
- signTypedData: vi.fn(),
- },
- } as ReturnType);
-
- // Mock that user is on wrong chain (Ethereum mainnet instead of Optimism)
- vi.mocked(useChainId).mockReturnValue(1);
-
- // Mock the switchChainAsync function (used by the x402 flow)
- vi.mocked(useSwitchChain).mockReturnValue({
- switchChain: mockSwitchChain,
- switchChainAsync: mockSwitchChain,
- isPending: false,
- chains: [{ id: 10, name: "Optimism" }],
- } as unknown as ReturnType);
-
- render();
-
- // Fill in the prompt
- const textarea = screen.getByPlaceholderText("mocked-imagegen.promptPlaceholder");
- fireEvent.change(textarea, { target: { value: "Test artwork prompt" } });
-
- // Find and click the create artwork button
- // The button text might vary based on wallet state, so we look for common patterns
- const buttons = screen.getAllByRole("button");
- const createButton = buttons.find(
- (button) =>
- button.textContent?.toLowerCase().includes("create") ||
- button.textContent?.toLowerCase().includes("artwork") ||
- button.textContent?.toLowerCase().includes("mint"),
- );
-
- if (createButton) {
- fireEvent.click(createButton);
-
- // Verify that switchChainAsync was called with Optimism chain ID (10)
- expect(mockSwitchChain).toHaveBeenCalledWith({ chainId: 10 });
- } else {
- // If no create button is found, at least verify the mock is set up correctly
- expect(mockSwitchChain).toBeDefined();
- // This is a fallback test - the real test depends on the component's current state logic
- }
- });
+ // Note: Chain switching behavior is tested in useAutoNetwork.test.ts
+ // The switchIfNeeded() function is called before transactions and is
+ // thoroughly covered there (see "switchIfNeeded() should switch when on wrong network")
});
From 99f1b7f9ad9f1296b685602b8b7ecdda953908b0 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 15:11:37 +0100
Subject: [PATCH 43/46] Clean it up
---
website/components/ImageGenerator.tsx | 10 ++++++----
website/components/MyNFTList.tsx | 7 +++++--
website/test/setup.ts | 2 +-
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/website/components/ImageGenerator.tsx b/website/components/ImageGenerator.tsx
index e839e4f44..5da70c247 100644
--- a/website/components/ImageGenerator.tsx
+++ b/website/components/ImageGenerator.tsx
@@ -224,8 +224,8 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
// Button Components
const CreateArtworkButton = () => {
- const isDisabled = buttonState === "needsPrompt";
const isLoadingState = buttonState === "loading";
+ const isDisabled = buttonState === "needsPrompt" || isLoadingState;
const handleClick = () => {
// Track create artwork attempt with context
@@ -278,6 +278,10 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
return;
}
+ // Enter loading state immediately to prevent double-clicks
+ setIsLoading(true);
+ setError(null);
+
// === Chain Switch at Interaction ===
// Ensure user is on the correct chain before making payment
const switched = await switchIfNeeded();
@@ -286,12 +290,10 @@ export function ImageGenerator({ onSuccess, onError }: ImageGeneratorProps) {
const errorMsg = `${chainSwitchFailedText}: ${targetChain.name}`;
setError(errorMsg);
onError?.(errorMsg);
+ setIsLoading(false);
return;
}
- setIsLoading(true);
- setError(null);
-
try {
// Determine mode
const isEditMode = referenceImageBase64 !== null;
diff --git a/website/components/MyNFTList.tsx b/website/components/MyNFTList.tsx
index b15edc745..dc047c401 100644
--- a/website/components/MyNFTList.tsx
+++ b/website/components/MyNFTList.tsx
@@ -4,7 +4,10 @@ import { readContract } from "wagmi/actions";
import { config } from "../wagmi.config";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
import { getGenAiNFTAddress, GenImNFTv4ABI, GENAI_NFT_NETWORKS, fromCAIP2 } from "@fretchen/chain-utils";
+import type { config } from "../wagmi.config";
import { NFTMetadata, ModalImageData } from "../types/components";
+
+type SupportedChainId = (typeof config)["chains"][number]["id"];
import * as styles from "../layouts/styles";
import { NFTCard } from "./NFTCard";
import { ImageModal } from "./ImageModal";
@@ -21,7 +24,7 @@ interface MyNFTListProps {
export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps) {
const { address, isConnected } = useAccount();
const { network } = useAutoNetwork(GENAI_NFT_NETWORKS);
- const chainId = fromCAIP2(network);
+ const chainId = fromCAIP2(network) as SupportedChainId;
const contractAddress = getGenAiNFTAddress(network);
// My NFTs state - now just store token IDs
@@ -117,7 +120,7 @@ export function MyNFTList({ newlyCreatedNFT, onNewNFTDisplayed }: MyNFTListProps
} finally {
setIsLoadingTokenIds(false);
}
- }, [isConnected, address]); // Removed genAiNFTContractConfig as it's now stable
+ }, [isConnected, address, contractAddress, chainId]);
// Handle newly created NFT - just add to token list
const handleNewlyCreatedNFT = useCallback(
diff --git a/website/test/setup.ts b/website/test/setup.ts
index 10ecc3c98..027187b6d 100644
--- a/website/test/setup.ts
+++ b/website/test/setup.ts
@@ -86,7 +86,7 @@ vi.mock("vike-react/usePageContext", () => ({
}));
// Mock useLocale hook
-vi.mock("./hooks/useLocale", () => ({
+vi.mock("../hooks/useLocale", () => ({
useLocale: vi.fn(({ label }: { label: string }) => label),
}));
From d368b7c01da82ba02a98b1e3093bb7ae996afc9c Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 15:17:46 +0100
Subject: [PATCH 44/46] Clean the tests
---
.../test/ImageGenerator.fileUpload.test.tsx | 38 +++++++++----------
website/test/MyNFTList.test.tsx | 14 ++-----
website/test/WalletOptions.test.tsx | 2 +-
3 files changed, 23 insertions(+), 31 deletions(-)
diff --git a/website/test/ImageGenerator.fileUpload.test.tsx b/website/test/ImageGenerator.fileUpload.test.tsx
index 4f72537b4..2a7a80798 100644
--- a/website/test/ImageGenerator.fileUpload.test.tsx
+++ b/website/test/ImageGenerator.fileUpload.test.tsx
@@ -52,10 +52,10 @@ describe("ImageGenerator Reference Image Integration", () => {
it("sollte ImageGenerator Component rendern können", () => {
render();
- // Prüfe dass wichtige Elemente vorhanden sind
+ // LocaleText zeigt echte übersetzte Texte, useLocale gibt Label-Keys zurück
expect(screen.getByText("Create Collectible AI Art • 10¢")).toBeInTheDocument();
expect(screen.getByTestId("reference-image-input")).toBeInTheDocument();
- expect(screen.getByPlaceholderText("Describe your image in detail...")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("imagegen.promptPlaceholder")).toBeInTheDocument();
});
it("sollte File Input korrekt konfiguriert haben", () => {
@@ -72,9 +72,8 @@ describe("ImageGenerator Reference Image Integration", () => {
const dropZone = screen.getByTestId("drop-zone");
expect(dropZone).toBeInTheDocument();
- // Text is now split across elements, so check for parts
- expect(screen.getByText(/Drag & drop an image here/)).toBeInTheDocument();
- expect(screen.getByText(/click to browse/)).toBeInTheDocument();
+ // useLocale Mock gibt Label-Keys zurück
+ expect(screen.getByText("imagegen.uploadReferenceImage")).toBeInTheDocument();
});
it("sollte verschiedene Image Sizes unterstützen", () => {
@@ -83,8 +82,9 @@ describe("ImageGenerator Reference Image Integration", () => {
const sizeSelect = screen.getByLabelText("Select image format for your artwork");
expect(sizeSelect).toBeInTheDocument();
- expect(screen.getByText("◼ Square")).toBeInTheDocument();
- expect(screen.getByText("▬ Wide")).toBeInTheDocument();
+ // useLocale Mock gibt Label-Keys zurück
+ expect(screen.getByText("imagegen.square")).toBeInTheDocument();
+ expect(screen.getByText("imagegen.wide")).toBeInTheDocument();
});
});
@@ -134,14 +134,15 @@ describe("ImageGenerator Reference Image Integration", () => {
it("sollte initial Create Artwork Button zeigen", () => {
render();
- const button = screen.getByRole("button", { name: /Enter a prompt to create/ });
+ // useLocale Mock gibt Label-Keys zurück
+ const button = screen.getByRole("button", { name: /imagegen.enterPrompt/ });
expect(button).toBeInTheDocument();
});
it("sollte Textarea für Prompt haben", () => {
render();
- const textarea = screen.getByPlaceholderText("Describe your image in detail...");
+ const textarea = screen.getByPlaceholderText("imagegen.promptPlaceholder");
expect(textarea).toBeInTheDocument();
expect(textarea.tagName.toLowerCase()).toBe("textarea");
});
@@ -171,31 +172,30 @@ describe("ImageGenerator Reference Image Integration", () => {
it("sollte Standard-Placeholder zeigen wenn kein Reference Image hochgeladen ist", () => {
render();
- // Sollte Standard-Placeholder zeigen (tatsächlicher Text aus Locale)
- const textarea = screen.getByPlaceholderText("Describe your image in detail...");
+ // useLocale Mock gibt Label-Keys zurück
+ const textarea = screen.getByPlaceholderText("imagegen.promptPlaceholder");
expect(textarea).toBeInTheDocument();
// Sollte NICHT Edit-Placeholder zeigen
expect(
- screen.queryByPlaceholderText("Describe changes you want to make to the image..."),
+ screen.queryByPlaceholderText("imagegen.editPromptPlaceholder"),
).not.toBeInTheDocument();
});
it("sollte Button Text korrekt ändern basierend auf previewState", () => {
render();
- // Initial: "Enter a prompt to create" Button (wenn previewState "empty" ist und kein prompt)
- const button = screen.getByRole("button", { name: /Enter a prompt to create/ });
+ // useLocale Mock gibt Label-Keys zurück
+ const button = screen.getByRole("button", { name: /imagegen.enterPrompt/ });
expect(button).toBeInTheDocument();
});
it("sollte Edit-Mode Locale Keys korrekt geladen haben", () => {
- // Test dass die Locale Keys verfügbar sind durch tatsächliche Werte
render();
- // Verifiziere dass Standard-Placeholder korrekt geladen ist
- const textarea = screen.getByPlaceholderText("Describe your image in detail...");
- expect(textarea).toHaveAttribute("placeholder", "Describe your image in detail...");
+ // useLocale Mock gibt Label-Keys zurück
+ const textarea = screen.getByPlaceholderText("imagegen.promptPlaceholder");
+ expect(textarea).toHaveAttribute("placeholder", "imagegen.promptPlaceholder");
// Die dynamische Placeholder-Logik ist implementiert (wird bei previewState === "reference" aktiv)
expect(textarea).toBeInTheDocument();
@@ -210,7 +210,7 @@ describe("ImageGenerator Reference Image Integration", () => {
expect(dropZone).toBeInTheDocument();
// Referenz Image Upload Bereich sollte vorhanden sein
- const uploadSection = screen.getByText("Upload Reference Image (Optional)");
+ const uploadSection = screen.getByText("imagegen.uploadReferenceImage");
expect(uploadSection).toBeInTheDocument();
});
});
diff --git a/website/test/MyNFTList.test.tsx b/website/test/MyNFTList.test.tsx
index 8ac04646d..5e4a382c5 100644
--- a/website/test/MyNFTList.test.tsx
+++ b/website/test/MyNFTList.test.tsx
@@ -72,17 +72,9 @@ vi.mock("../hooks/useLocale", () => ({
useLocale: vi.fn(() => "Mocked text"),
}));
-// Create a wrapper component to count renders
-function MyNFTListWrapper(props: Record) {
- renderCount++;
- return ;
-}
-
describe("MyNFTList Re-render Bug Reproduction", () => {
beforeEach(() => {
vi.clearAllMocks();
- renderCount = 0;
- contractConfigCallCount = 0;
// Setup default mock returns
mockUseAccount.mockReturnValue({
@@ -113,7 +105,7 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
it("should call balanceOf ABI function when loading NFTs", async () => {
// Render the component
- render();
+ render();
// Wait for the component to load
await waitFor(() => {
@@ -124,7 +116,7 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
it("should call tokenOfOwnerByIndex ABI function for each NFT", async () => {
// Render the component
- render();
+ render();
// Wait for the component to load and fetch token IDs
await waitFor(
@@ -146,7 +138,7 @@ describe("MyNFTList Re-render Bug Reproduction", () => {
});
it("should render NFT cards after loading token IDs", async () => {
- render();
+ render();
// Wait for NFT cards to appear
await waitFor(
diff --git a/website/test/WalletOptions.test.tsx b/website/test/WalletOptions.test.tsx
index d9add3db9..313a5bd11 100644
--- a/website/test/WalletOptions.test.tsx
+++ b/website/test/WalletOptions.test.tsx
@@ -112,7 +112,7 @@ describe("WalletOptions Component", () => {
it("always shows 'Connect Account' initially (hydration safety)", () => {
render();
- expect(screen.getByRole("button")).toHaveTextContent("Connect Account");
+ expect(screen.getByRole("button")).toHaveTextContent("walletoptions.connectAccount");
});
it("prevents hydration mismatch when wallet is connected", async () => {
From a5c331e410bb83e64bce6e514095d55c0527926c Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 15:18:20 +0100
Subject: [PATCH 45/46] Update codeql.yml
---
.github/workflows/codeql.yml | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 459b82ec5..82a566184 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -39,9 +39,16 @@ jobs:
node-version: "22"
cache: "npm"
cache-dependency-path: |
+ shared/chain-utils/package-lock.json
website/package-lock.json
scw_js/package-lock.json
+ - name: Build chain-utils (local dependency)
+ working-directory: ./shared/chain-utils
+ run: |
+ npm ci
+ npm run build
+
- name: Install dependencies (website)
working-directory: ./website
run: npm ci
From 7463db6ef082e97e18d444bd953c380d647567c1 Mon Sep 17 00:00:00 2001
From: fretchen
Date: Fri, 30 Jan 2026 15:20:11 +0100
Subject: [PATCH 46/46] Prettier
---
website/test/ImageGenerator.fileUpload.test.tsx | 4 +---
website/test/ImageGenerator.test.tsx | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/website/test/ImageGenerator.fileUpload.test.tsx b/website/test/ImageGenerator.fileUpload.test.tsx
index 2a7a80798..ea5b02a77 100644
--- a/website/test/ImageGenerator.fileUpload.test.tsx
+++ b/website/test/ImageGenerator.fileUpload.test.tsx
@@ -177,9 +177,7 @@ describe("ImageGenerator Reference Image Integration", () => {
expect(textarea).toBeInTheDocument();
// Sollte NICHT Edit-Placeholder zeigen
- expect(
- screen.queryByPlaceholderText("imagegen.editPromptPlaceholder"),
- ).not.toBeInTheDocument();
+ expect(screen.queryByPlaceholderText("imagegen.editPromptPlaceholder")).not.toBeInTheDocument();
});
it("sollte Button Text korrekt ändern basierend auf previewState", () => {
diff --git a/website/test/ImageGenerator.test.tsx b/website/test/ImageGenerator.test.tsx
index ea2c9cd44..ac5079d71 100644
--- a/website/test/ImageGenerator.test.tsx
+++ b/website/test/ImageGenerator.test.tsx
@@ -109,7 +109,7 @@ describe("ImageGenerator Component", () => {
// Start in disconnected state (collapsed)
// Create a new mock that we can track
const mockConnect = vi.fn();
-
+
vi.mocked(useAccount).mockReturnValue({
address: undefined,
isConnected: false,