{"path":"scripts/devnet-test.js","size":12159,"locked":false,"binary":false,"truncated":false,"content":"/**\n * devnet-test.js — Standalone, throwaway verification script.\n *\n * NOT part of the Anchr CLI (MVP scope is locked to `init`/`deploy` only —\n * see docs/ROADMAP.md). This exists purely to verify the SNS write path\n * actually works before trusting lib/sns.js or turning deploy.yml's\n * auto-trigger on. Run manually with:\n *\n *   node scripts/devnet-test.js\n *\n * Uses Solana DEVNET only. Devnet SOL is free and worthless — this is\n * safe to run as many times as needed, unlike anything touching mainnet.\n *\n * STATUS OF WHAT'S VERIFIED vs GUESSED:\n *  - Devnet domain registration (devnet.bindings.registerDomainNameV2) —\n *    CONFIRMED against Bonfida's own docs.\n *  - Devnet program ID for the SNS Registrar — CONFIRMED against\n *    Bonfida/sns-registrar's own GitHub README.\n *  - Domain registration requires payment in WRAPPED SOL, not native SOL —\n *    CONFIRMED (stated explicitly in Bonfida's registration docs).\n *  - The exact function name for *writing* an IPFS record on devnet —\n *    CONFIRMED via a live run of this script: devnet.bindings.updateNameRegistryData\n *    exists (separate from the top-level mainnet function), and so does\n *    devnet.utils.getDomainKeySync for correctly deriving the devnet PDA.\n *  - BONUS FIND: devnet.bindings also exposes createRecordV2Instruction /\n *    updateRecordV2Instruction — meaning V2 record support (marked as an\n *    unresolved TODO in lib/sns.js) may genuinely exist. Not yet tested —\n *    this script only exercises the V1 path so far.\n *  - NOT YET CONFIRMED: whether an actual write+readback round-trip\n *    succeeds end-to-end (this run failed earlier at the airdrop step,\n *    before reaching the write). That's what running this script to\n *    completion will confirm.\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { inspect } from 'node:util';\nimport {\n  Connection,\n  Keypair,\n  Transaction,\n  SystemProgram\n} from '@solana/web3.js';\nimport {\n  getAssociatedTokenAddressSync,\n  createAssociatedTokenAccountInstruction,\n  createSyncNativeInstruction,\n  NATIVE_MINT\n} from '@solana/spl-token';\nimport {\n  devnet,\n  NameRegistryState\n} from '@bonfida/spl-name-service';\nimport { sendAndConfirm } from '../lib/solana-confirm.js';\n\n// Defaults to the public devnet RPC, but that's shared across every Replit\n// user (and everyone else) hitting it at once, so it's prone to sustained\n// 429s that have nothing to do with your own request volume. Set\n// ANCHR_DEVNET_RPC_URL to a free dedicated endpoint (Helius, QuickNode,\n// Syndica, Alchemy, etc.) to get a quota that's actually yours alone.\nconst DEVNET_RPC = process.env.ANCHR_DEVNET_RPC_URL || 'https://api.devnet.solana.com';\nconst TEST_DOMAIN = `anchr-test-${Date.now()}`; // unique per run, avoids domain collisions\nconst WRAP_AMOUNT_SOL = 0.05; // covers registration fee + rent; bump if it fails\nconst FAKE_CID = 'bafybeigdyrztestcidxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';\nconst WALLET_FILE = '.devnet-test-wallet.json';\n\n/**\n * The wallet is the thing that needs devnet SOL — unlike TEST_DOMAIN, it\n * must stay THE SAME across runs when the automated airdrop fails and\n * you're funding it manually via faucet.solana.com. Without this, every\n * re-run would generate a fresh throwaway address, abandoning whatever\n * you just funded. Delete this file if you ever want a clean new wallet.\n */\nfunction loadOrCreateWallet() {\n  if (existsSync(WALLET_FILE)) {\n    const secretKey = Uint8Array.from(JSON.parse(readFileSync(WALLET_FILE, 'utf-8')));\n    return { wallet: Keypair.fromSecretKey(secretKey), reused: true };\n  }\n  const wallet = Keypair.generate();\n  writeFileSync(WALLET_FILE, JSON.stringify(Array.from(wallet.secretKey)));\n  return { wallet, reused: false };\n}\n\nasync function main() {\n  const connection = new Connection(DEVNET_RPC, 'confirmed');\n  console.log('Using RPC:', DEVNET_RPC);\n  console.log('');\n\n  console.log('Available devnet.bindings functions:', Object.keys(devnet.bindings));\n  console.log('Available devnet.utils functions:', Object.keys(devnet.utils));\n  console.log('');\n\n  // 1. Reuse the same throwaway wallet across runs (see loadOrCreateWallet\n  //    comment above) — never reuse a REAL wallet for this, only this\n  //    disposable devnet-only one.\n  const { wallet, reused } = loadOrCreateWallet();\n  console.log(`Test wallet (${reused ? 'reused from last run' : 'newly generated'}):`, wallet.publicKey.toBase58());\n\n  // 2. Airdrop devnet SOL — skip if this wallet already has enough\n  const existingBalance = await connection.getBalance(wallet.publicKey);\n  if (existingBalance >= 0.02 * 1_000_000_000) {\n    console.log(`Wallet already funded (${existingBalance / 1_000_000_000} SOL) — skipping airdrop.`);\n  } else {\n    console.log('Requesting devnet airdrop (1 SOL)...');\n    try {\n      const airdropSig = await connection.requestAirdrop(wallet.publicKey, 1_000_000_000);\n      const start = Date.now();\n      let confirmed = false;\n      while (Date.now() - start < 30_000) {\n        const { value } = await connection.getSignatureStatus(airdropSig);\n        if (value?.confirmationStatus === 'confirmed' || value?.confirmationStatus === 'finalized') {\n          confirmed = true;\n          break;\n        }\n        await new Promise((resolve) => setTimeout(resolve, 1500));\n      }\n      if (!confirmed) throw new Error('Airdrop confirmation timed out');\n      console.log('Airdrop confirmed.');\n    } catch (err) {\n      console.error(\n        'Airdrop failed (devnet faucet is often rate-limited). Manually fund this address ' +\n        `at https://faucet.solana.com, then re-run — this SAME wallet will be reused ` +\n        `next time, so funding it once is enough.\\nAddress: ${wallet.publicKey.toBase58()}`\n      );\n      throw err;\n    }\n  }\n\n  // 3. Wrap SOL into wSOL — but only if this wallet doesn't already have a\n  //    wrapped-SOL account from a previous run. Re-running the create\n  //    instruction on an already-existing account fails with \"Provided\n  //    owner is not allowed\" — same reused-wallet issue as the airdrop\n  //    check above, just one step later.\n  const ata = getAssociatedTokenAddressSync(NATIVE_MINT, wallet.publicKey);\n  const ataInfo = await connection.getAccountInfo(ata);\n  if (ataInfo) {\n    console.log('Wrapped SOL account already exists from a previous run — skipping wrap step.');\n  } else {\n    console.log(`Wrapping ${WRAP_AMOUNT_SOL} SOL...`);\n    const wrapTx = new Transaction().add(\n      createAssociatedTokenAccountInstruction(wallet.publicKey, ata, wallet.publicKey, NATIVE_MINT),\n      SystemProgram.transfer({\n        fromPubkey: wallet.publicKey,\n        toPubkey: ata,\n        lamports: Math.round(WRAP_AMOUNT_SOL * 1_000_000_000)\n      }),\n      createSyncNativeInstruction(ata)\n    );\n    await sendAndConfirm(connection, wrapTx, [wallet]);\n    console.log('Wrapped.');\n  }\n\n  // 4. Register a throwaway devnet domain — CONFIRMED pattern for the call\n  //    itself, but the exact SHAPE of what it returns (single instruction\n  //    vs an array of instructions) wasn't independently verified. Logging\n  //    it and handling both shapes rather than guessing.\n  console.log(`\\nRegistering devnet domain: ${TEST_DOMAIN}`);\n  const registerIx = await devnet.bindings.registerDomainNameV2(\n    connection,\n    TEST_DOMAIN,\n    1_000,\n    wallet.publicKey,\n    ata,\n    NATIVE_MINT\n  );\n  console.log('registerDomainNameV2 returned:', Array.isArray(registerIx) ? `array of ${registerIx.length}` : typeof registerIx);\n\n  const registerTx = new Transaction();\n  if (Array.isArray(registerIx)) {\n    registerTx.add(...registerIx);\n  } else {\n    registerTx.add(registerIx);\n  }\n  const registerSig = await sendAndConfirm(connection, registerTx, [wallet]);\n  console.log('Domain registered. Tx:', `https://explorer.solana.com/tx/${registerSig}?cluster=devnet`);\n\n  // 5. Write a fake IPFS record — THIS is the part we're actually verifying.\n  //    CONFIRMED via a live run: the record account must be CREATED before\n  //    it can be UPDATED — updateNameRegistryData alone fails with\n  //    \"AccountDoesNotExist\" on a brand-new domain's record. Attempting\n  //    devnet.bindings.createNameRegistry first, based on the standard\n  //    SPL Name Service Create instruction shape (name, space, payer,\n  //    nameOwner, nameClass, nameParent) — NOT independently confirmed\n  //    for this exact JS wrapper's parameter order, so if this throws,\n  //    the error message is the next real signal to go on.\n  console.log('\\nCreating record account (if it does not already exist)...');\n  const recordName = `IPFS.${TEST_DOMAIN}`;\n  const data = Buffer.from(FAKE_CID, 'utf-8');\n  const RECORD_SPACE = 1_000; // bytes to allocate — generous for a CID string\n\n  try {\n    const createIx = await devnet.bindings.createNameRegistry(\n      connection,\n      recordName,\n      RECORD_SPACE,\n      wallet.publicKey, // payer\n      wallet.publicKey, // name owner\n      undefined,        // lamports (let it compute rent-exemption automatically)\n      undefined,        // nameClass\n      devnet.constants.ROOT_DOMAIN_ACCOUNT // nameParent\n    );\n    console.log(\n      'createNameRegistry returned:',\n      Array.isArray(createIx)\n        ? `array of ${createIx.length}`\n        : `object with keys: ${Object.keys(createIx || {}).join(', ')}`\n    );\n    if (!Array.isArray(createIx) && !createIx?.programId) {\n      // Doesn't look like a plain TransactionInstruction — likely a\n      // compound object (e.g. instruction + a generated signer). Full\n      // dump to see exactly what's actually in it.\n      console.log('Full structure:', inspect(createIx, { depth: 4 }));\n    }\n    const createTx = new Transaction();\n    if (Array.isArray(createIx)) {\n      createTx.add(...createIx);\n    } else {\n      createTx.add(createIx);\n    }\n    const createSig = await sendAndConfirm(connection, createTx, [wallet]);\n    console.log('Record account created. Tx:', `https://explorer.solana.com/tx/${createSig}?cluster=devnet`);\n  } catch (err) {\n    console.log('createNameRegistry failed (may already exist, or the guessed params above are wrong):');\n    console.log(err.message || err);\n    console.log('Proceeding to the update step regardless — if that also fails, this create step needs fixing first.');\n  }\n\n  console.log('\\nAttempting IPFS record write...');\n  const writeIx = await devnet.bindings.updateNameRegistryData(\n    connection,\n    recordName,\n    0,\n    data,\n    undefined,\n    devnet.constants.ROOT_DOMAIN_ACCOUNT\n  );\n  console.log('updateNameRegistryData returned:', Array.isArray(writeIx) ? `array of ${writeIx.length}` : typeof writeIx);\n\n  const writeTx = new Transaction();\n  if (Array.isArray(writeIx)) {\n    writeTx.add(...writeIx);\n  } else {\n    writeTx.add(writeIx);\n  }\n  const writeSig = await sendAndConfirm(connection, writeTx, [wallet]);\n  console.log('Write tx sent:', `https://explorer.solana.com/tx/${writeSig}?cluster=devnet`);\n\n  // 6. Read it back to confirm the write actually landed correctly.\n  //    Using devnet.utils.getDomainKeySync (confirmed to exist separately\n  //    from the top-level version) so the derived pubkey actually matches\n  //    devnet's program deployment.\n  const { pubkey } = devnet.utils.getDomainKeySync(recordName, true);\n  const { registry } = await NameRegistryState.retrieve(connection, pubkey);\n  const readBack = registry.data ? registry.data.toString('utf-8').replace(/\\0/g, '') : null;\n\n  console.log('\\n--- RESULT ---');\n  console.log('Wrote:     ', FAKE_CID);\n  console.log('Read back: ', readBack);\n  console.log(\n    readBack === FAKE_CID\n      ? '✅ MATCH — the V1 write/read pattern works. lib/sns.js uses the ' +\n        'mainnet-equivalent functions of the same names, so this validates ' +\n        'the approach, though lib/sns.js itself should still get one live ' +\n        'mainnet run before fully trusting it.'\n      : '❌ MISMATCH — do not trust lib/sns.js yet. Check the devnet.bindings list above for the correct write function.'\n  );\n}\n\nmain().catch((err) => {\n  console.error('\\n❌ Devnet test failed:', err);\n  process.exit(1);\n});\n"}