{"path":"scripts/e2e-devnet.ts","size":9945,"locked":false,"binary":false,"truncated":false,"content":"/**\n * Mappers Protocol — End-to-End Devnet Test Script\n *\n * What this does:\n *   1. Loads client + oracle keypairs\n *   2. Creates a real job escrow on devnet\n *   3. Waits for the oracle to detect it via gRPC\n *   4. Posts a test submission to the oracle HTTP server\n *   5. Polls until the oracle resolves it on-chain\n *   6. Prints the final transaction signature\n *\n * Run: ts-node scripts/e2e-devnet.ts\n * Make sure the oracle is already running: cd oracle && npm run dev\n */\n\nimport * as anchor from \"@coral-xyz/anchor\";\nimport {\n  Connection,\n  Keypair,\n  PublicKey,\n  SystemProgram,\n  LAMPORTS_PER_SOL,\n} from \"@solana/web3.js\";\nimport { BN } from \"@coral-xyz/anchor\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { deriveEscrowPda, deriveVaultPda } from \"../shared/pda\";\n\n// ─── CONFIG ──────────────────────────────────────────────────────────────────\n\nconst RPC_URL       = process.env.SOLANA_RPC_URL || \"https://api.devnet.solana.com\";\nconst ORACLE_URL    = process.env.ORACLE_URL      || \"http://localhost:3001\";\nconst PROGRAM_ID    = new PublicKey(\"52yt1gCbPeiKP4JYjUVKmMJSgBMMcUx8xRGqozMKX2Mu\");\nconst JOB_AMOUNT    = new BN(0.01 * LAMPORTS_PER_SOL); // 0.01 SOL — minimal devnet test\nconst JOB_ID        = `e2e-test-${Date.now()}`;\nconst POLL_INTERVAL = 3000;  // ms between oracle polls\nconst MAX_POLLS     = 20;    // 60 second timeout\n\n// ─── KEYPAIR LOADER ───────────────────────────────────────────────────────────\n\nfunction loadKeypair(envKey: string, fileFallback: string): Keypair {\n  if (process.env[envKey]) {\n    const bs58 = require(\"bs58\");\n    return Keypair.fromSecretKey(bs58.decode(process.env[envKey]!));\n  }\n  if (fs.existsSync(fileFallback)) {\n    const raw = JSON.parse(fs.readFileSync(fileFallback, \"utf-8\"));\n    return Keypair.fromSecretKey(Uint8Array.from(raw));\n  }\n  throw new Error(\n    `Could not load keypair. Set env var ${envKey} or provide file at ${fileFallback}`\n  );\n}\n\n// ─── ORACLE HTTP HELPERS ──────────────────────────────────────────────────────\n\nasync function checkOracleHealth(): Promise<boolean> {\n  try {\n    const res = await fetch(`${ORACLE_URL}/health`);\n    return res.ok;\n  } catch (err: unknown) {\n    const message = err instanceof Error ? err.message : String(err);\n    console.warn(`[health] Oracle health check failed: ${message}`);\n    return false;\n  }\n}\n\nasync function checkJobInOracle(jobId: string): Promise<boolean> {\n  try {\n    const res = await fetch(`${ORACLE_URL}/jobs/${jobId}`);\n    return res.ok;\n  } catch (err: unknown) {\n    const message = err instanceof Error ? err.message : String(err);\n    console.warn(`[poll] Job poll failed for ${jobId}: ${message}`);\n    return false;\n  }\n}\n\nasync function submitToOracle(jobId: string): Promise<{\n  success: boolean;\n  outcome?: string;\n  txSig?: string;\n  error?: string;\n}> {\n  const res = await fetch(`${ORACLE_URL}/submit`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      jobId,\n      description:\n        \"Write a TypeScript function that takes an array of numbers and returns their sum.\",\n      acceptanceCriteria: [\n        \"Function must be written in TypeScript\",\n        \"Function must accept an array of numbers as its only parameter\",\n        \"Function must return a number\",\n        \"Function must correctly compute the sum\",\n      ],\n      deliverable: `\nfunction sumArray(numbers: number[]): number {\n  return numbers.reduce((acc, n) => acc + n, 0);\n}\n\n// Tests\nconsole.log(sumArray([1, 2, 3]));       // 6\nconsole.log(sumArray([10, -5, 3]));     // 8\nconsole.log(sumArray([]));              // 0\n      `.trim(),\n      deliverableType: \"text\",\n    }),\n  });\n\n  if (!res.ok) {\n    const body = await res.text();\n    throw new Error(`Oracle /submit returned HTTP ${res.status}: ${body}`);\n  }\n\n  return res.json();\n}\n\n// ─── MAIN ─────────────────────────────────────────────────────────────────────\n\nasync function main() {\n  console.log(\"\\n🚀 Mappers Protocol — End-to-End Devnet Test\");\n  console.log(\"═\".repeat(50));\n\n  // 1. Load keypairs\n  const clientKeypair = loadKeypair(\n    \"CLIENT_PRIVATE_KEY\",\n    path.join(process.env.HOME || \"~\", \".config/solana/id.json\")\n  );\n  const oracleKeypair = loadKeypair(\n    \"ORACLE_PRIVATE_KEY\",\n    \"./oracle-keypair.json\"\n  );\n\n  console.log(`\\n📋 Test Config`);\n  console.log(`   Job ID:    ${JOB_ID}`);\n  console.log(`   Client:    ${clientKeypair.publicKey.toBase58()}`);\n  console.log(`   Oracle:    ${oracleKeypair.publicKey.toBase58()}`);\n  console.log(`   Amount:    ${JOB_AMOUNT.toNumber() / LAMPORTS_PER_SOL} SOL`);\n\n  // 2. Check oracle is running\n  console.log(`\\n⏳ Checking oracle at ${ORACLE_URL}...`);\n  const oracleUp = await checkOracleHealth();\n  if (!oracleUp) {\n    console.error(\"❌ Oracle is not running. Start it with: cd oracle && npm run dev\");\n    process.exit(1);\n  }\n  console.log(\"✅ Oracle is up\");\n\n  // 3. Set up Anchor program\n  const connection = new Connection(RPC_URL, \"confirmed\");\n  const wallet     = new anchor.Wallet(clientKeypair);\n  const provider   = new anchor.AnchorProvider(connection, wallet, {\n    commitment: \"confirmed\",\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-var-requires\n  const idl = require(\"../oracle/idl.json\");\n  const idlWithAddress = { ...idl, address: PROGRAM_ID.toBase58() };\n  const program = new anchor.Program(idlWithAddress as anchor.Idl, provider);\n\n  // 4. Check client balance\n  const balance = await connection.getBalance(clientKeypair.publicKey);\n  console.log(`\\n💰 Client balance: ${(balance / LAMPORTS_PER_SOL).toFixed(4)} SOL`);\n  if (balance < JOB_AMOUNT.toNumber() + 0.01 * LAMPORTS_PER_SOL) {\n    console.error(\"❌ Insufficient balance. Airdrop some devnet SOL:\");\n    console.error(`   solana airdrop 1 ${clientKeypair.publicKey.toBase58()} --url devnet`);\n    process.exit(1);\n  }\n\n  // 5. Create escrow on-chain\n  const freelancer = Keypair.generate(); // random for test\n  const [escrowPda] = deriveEscrowPda(clientKeypair.publicKey, JOB_ID, PROGRAM_ID);\n  const [vaultPda]  = deriveVaultPda(clientKeypair.publicKey, JOB_ID, PROGRAM_ID);\n\n  console.log(`\\n📝 Creating escrow on devnet...`);\n  console.log(`   Escrow PDA: ${escrowPda.toBase58()}`);\n  console.log(`   Vault PDA:  ${vaultPda.toBase58()}`);\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const initTx = await (program.methods as any)\n    .initializeJob(JOB_ID, JOB_AMOUNT)\n    .accounts({\n      client:        clientKeypair.publicKey,\n      freelancer:    freelancer.publicKey,\n      oracle:        oracleKeypair.publicKey,\n      escrowAccount: escrowPda,\n      vaultAccount:  vaultPda,\n      systemProgram: SystemProgram.programId,\n    })\n    .signers([clientKeypair])\n    .rpc({ commitment: \"confirmed\" });\n\n  console.log(`✅ Escrow created`);\n  console.log(`   Tx: https://explorer.solana.com/tx/${initTx}?cluster=devnet`);\n\n  // 6. Wait for oracle to detect the job via gRPC\n  console.log(`\\n⏳ Waiting for oracle to detect job via gRPC stream...`);\n  let detected = false;\n  for (let i = 0; i < 10; i++) {\n    await new Promise((r) => setTimeout(r, 2000));\n    detected = await checkJobInOracle(JOB_ID);\n    if (detected) break;\n    process.stdout.write(\".\");\n  }\n\n  if (!detected) {\n    console.log(\"\\n⚠️  Oracle hasn't picked up the job via gRPC yet.\");\n    console.log(\"   This can happen on slow devnet. Submitting anyway...\");\n  } else {\n    console.log(`\\n✅ Oracle detected job: ${JOB_ID}`);\n  }\n\n  // 7. Submit deliverable to oracle\n  console.log(`\\n🤖 Submitting deliverable for AI verification...`);\n  console.log(\"   Running Gemini + Claude consensus pipeline...\");\n\n  const result = await submitToOracle(JOB_ID);\n\n  console.log(`\\n📊 Verification Result`);\n  console.log(`   Success:  ${result.success}`);\n  console.log(`   Outcome:  ${result.outcome}`);\n\n  if (result.txSig) {\n    console.log(`   Tx:       https://explorer.solana.com/tx/${result.txSig}?cluster=devnet`);\n  }\n  if (result.error) {\n    console.log(`   Error:    ${result.error}`);\n  }\n\n  // 8. Final on-chain state check\n  if (result.outcome === \"RELEASE\" || result.outcome === \"REFUND\") {\n    try {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      await (program.account as any).gigEscrow.fetch(escrowPda);\n      console.log(\"\\n⚠️  Escrow account still exists — close may be pending\");\n    } catch (err: unknown) {\n      const message = err instanceof Error ? err.message : String(err);\n      const isAccountGone =\n        message.includes(\"Account does not exist\") ||\n        message.includes(\"could not find account\");\n      if (isAccountGone) {\n        console.log(\"\\n✅ Escrow account closed — rent returned to client\");\n      } else {\n        console.error(`\\n⚠️  Failed to verify escrow state: ${message}`);\n      }\n    }\n  }\n\n  console.log(\"\\n\" + \"═\".repeat(50));\n  if (result.success && result.outcome !== \"ESCALATE\") {\n    console.log(\"✅ END-TO-END TEST PASSED\");\n  } else if (result.outcome === \"ESCALATE\") {\n    console.log(\"⚠️  TEST COMPLETE — Oracle escalated to human arbitration\");\n    console.log(\"   (Models disagreed — expected occasionally on test data)\");\n  } else {\n    console.log(\"❌ TEST FAILED — Check oracle logs above\");\n  }\n  console.log(\"═\".repeat(50) + \"\\n\");\n}\n\nmain().catch((err) => {\n  console.error(\"\\n❌ Fatal error:\", err.message);\n  process.exit(1);\n});\n"}