Skill: planetai87/warren-deploy-mainnet (version 1.0.6)

--- name: warren-deploy description: Deploy websites and files permanently on MegaETH mainnet using SSTORE2. Agents use their own wallet and pay gas. metadata: {"openclaw":{"emoji":"⛓️","homepage":"https://thewarren.app","source":"https://github.com/planetai87/warren-tools","requires":{"anyBins":["node"],"env":["PRIVATE_KEY"]},"primaryEnv":"PRIVATE_KEY"}} user-invocable: true --- # Warren - On-Chain Website Deployment Deploy websites and files permanently on MegaETH mainnet. **Network**: MegaETH Mainnet (Chain ID: 4326) **RPC**: `https://mainnet.megaeth.com/rpc` **Explorer**: https://megaeth.blockscout.com ## Setup (One Time) ```bash cd {baseDir} bash setup.sh ``` ## Contract Addresses (Mainnet) | Contract | Address | |----------|---------| | Genesis Key NFT (0xRabbitNeo) | [REDACTED_SECRET] | | 0xRabbit.agent Key NFT | [REDACTED_SECRET] | | MasterNFT Registry | [REDACTED_SECRET] | ## Prerequisites ### 1. Wallet + MegaETH ETH You need a wallet with real ETH on MegaETH mainnet for gas fees. - Bridge ETH from Ethereum via the official MegaETH bridge. - Approximate cost: ~0.001 ETH per site deploy. Set your private key: ```bash export PRIVATE_KEY=0xYourPrivateKey ``` ### 2. Genesis Access Requirement The deploy script checks access in this order: 1. Human Genesis Key (0xRabbitNeo) ownership 2. 0xRabbit.agent Key ownership 3. If missing, auto-mint 0xRabbit.agent Key (free) Default `RABBIT_AGENT_ADDRESS`: [REDACTED_SECRET] (override via env). If you override or unset it, mint a human key manually at: - https://thewarren.app/mint ## Environment Variables | Variable | Required | Default | Purpose | |----------|----------|---------|---------| | `PRIVATE_KEY` | **Yes** | — | Wallet private key for signing transactions | | `RPC_URL` | No | `https://mainnet.megaeth.com/rpc` | MegaETH RPC endpoint | | `CHAIN_ID` | No | `4326` | MegaETH mainnet chain ID | | `GENESIS_KEY_ADDRESS` | No | `0x0d7B...5a88` | Genesis Key NFT contract | | `RABBIT_AGENT_ADDRESS` | No | `0x3f0C...61f2` | 0xRabbit.agent NFT contract | | `MASTER_NFT_ADDRESS` | No | `0xf299...eFC` | MasterNFT registry contract | | `CHUNK_SIZE` | No | `15000` | Bytes per chunk (15KB) | | `GROUP_SIZE` | No | `500` | Max addresses per tree node | ## Deploy ### Deploy HTML string ```bash cd {baseDir} PRIVATE_KEY=0x... node deploy.js \ --html "<html><body><h1>Hello Warren!</h1></body></html>" \ --name "My First Site" ``` ### Deploy HTML file ```bash PRIVATE_KEY=0x... node deploy.js \ --file ./my-site.html \ --name "My Website" ``` ### Deploy via stdin ```bash echo "<h1>Hello</h1>" | PRIVATE_KEY=0x... node deploy.js --name "Piped" ``` ### CLI Options ``` --private-key <key> Wallet private key (or PRIVATE_KEY env) --html <string> HTML content to deploy --file <path> Path to file to deploy --name <name> Site name (default: "Untitled") --type <type> file|image|video|audio|script (default: "file") ``` ### Output ```json { "tokenId": 102, "rootChunk": "0x019E5E...", "depth": 0, "url": "https://thewarren.app/v/site=102" } ``` ## Example Workflows ### Quick deploy loop ```bash cd {baseDir} for i in $(seq 1 5); do HTML="<html><body><h1>Site #$i</h1><p>$(date)</p></body></html>" PRIVATE_KEY=0x... node deploy.js --html "$HTML" --name "Site $i" sleep 2 done ``` ### Deploy a file ```bash cd {baseDir} PRIVATE_KEY=0x... node deploy.js --file ./my-site.html --name "Large Site" ``` ## View Sites ``` https://thewarren.app/v/site={TOKEN_ID} ``` ## Troubleshooting **"No ETH balance"** - Bridge ETH to MegaETH mainnet and retry. **"No Genesis Key found and RABBIT_AGENT_ADDRESS is not configured"** - Set [REDACTED_SECRET], or mint human Genesis Key at `https://thewarren.app/mint`. **"RPC rate limit"** - The script retries automatically. Add `sleep 5` between repeated deployments. **Site does not load immediately** - Wait 10-30 seconds and retry the viewer URL. ## Notes - Mainnet content is permanent and immutable. - Max 500KB per deployment. - Default chunk size is 15KB (`CHUNK_SIZE=15000`). - You pay gas from your own wallet. ## Security & Privacy - **No data exfiltration**: Content is sent only as blockchain transactions to the configured RPC endpoint. No intermediary servers. - **PRIVATE_KEY handling**: Used solely to sign transactions. Never logged, stored on disk, or transmitted to third parties. - **Network endpoints**: Only the configured `RPC_URL` (default: `mainnet.megaeth.com/rpc`). No other outbound connections. - **File access**: Reads only the single file specified by `--file`. No directory scanning or glob expansion. - **No telemetry**: No analytics, tracking, or usage reporting.

path: setup.sh
content: #!/bin/bash
set -euo pipefail
# Warren Deploy - Quick Setup
# Run: bash setup.sh

cd "$(dirname "$0")"
npm init -y > /dev/null 2>&1
npm install ethers
echo ""
echo "✅ Setup complete! Usage:"
echo "  PRIVATE_KEY=0x... node deploy.js --html '<h1>Hello</h1>' --name 'My Site'"
echo ""
echo "Prerequisites:"
echo "  1. Get MegaETH mainnet ETH via official bridge"
echo "  2. 0xRabbit.agent Key auto-mints (free)"


path: page_bytecode.js
content: /**
 * Page.sol compiled EVM bytecode (Solidity 0.8.26, Cancun EVM, viaIR)
 *
 * Source: foundry-app/src/Page.sol
 * Compiler: solc 0.8.26+commit.8a97fa7a
 * Verified on: https://megaeth.blockscout.com
 *
 * SSTORE2 Page contract - stores arbitrary data as immutable contract bytecode.
 * Contains no external calls, no selfdestruct, no delegatecall.
 *
 * To reproduce: cd foundry-app && forge build
 * Then: cat out/Page.sol/Page.json | jq -r .bytecode.object
 */
module.exports = '0x60a0604052346100e45761025c80380380610019816100fc565b9283398101906020818303126100e4578051906001600160401b0382116100e4570181601f820112156100e4578051906001600160401b0382116100e85761006a601f8301601f19166020016100fc565b92828452602083830101116100e457815f9260208093018386015e8301015280518060401b6bfe61000180600a3d393df3000161fffe8211830152600b8101601583015ff09182156100d7575260805260405161013a908161012282396080518181816044015260d50152f35b63301164255f526004601cfd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b038111838210176100e85760405256fe6080806040526004361015610012575f80fd5b5f3560e01c9081634822da1c146100c357506357de26a414610032575f80fd5b346100bf575f3660031901126100bf577f00000000000000000000000000000000000000000000000000000000000000006020608060405180935f19813b0164ffffffffff16905f6021830191601f8501903c808252016040810193846040528385528051938491826060850152018383015e5f828483010152603f1992601f8019910116810103010190f35b5f80fd5b346100bf575f3660031901126100bf577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f3fea2646970667358221220fe4f8a378a0b003d3e1438e45234630293b1a4f7cf94a9f28a4c2d2531789b9d64736f6c634300081a0033';


path: deploy.js
content: #!/usr/bin/env node

/**
 * Warren Direct Deploy - Self-contained on-chain deployment script
 *
 * Deploys HTML/files permanently to MegaETH blockchain via SSTORE2.
 * No external dependencies besides ethers.js.
 *
 * Setup:  npm init -y && npm install ethers
 * Usage:  PRIVATE_KEY=0x... node deploy.js --html "<h1>Hello</h1>" --name "My Site"
 *         PRIVATE_KEY=0x... node deploy.js --file ./site.html --name "My Site"
 */

const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');

// ============================================================================
// Configuration
// ============================================================================

const RPC_URL = process.env.RPC_URL || 'https://mainnet.megaeth.com/rpc';
const CHAIN_ID = parseInt(process.env.CHAIN_ID || '4326');
const GENESIS_KEY_ADDRESS = process.env.GENESIS_KEY_ADDRESS || [REDACTED_SECRET];
const RABBIT_AGENT_ADDRESS = process.env.RABBIT_AGENT_ADDRESS || [REDACTED_SECRET];
const MASTER_NFT_ADDRESS = process.env.MASTER_NFT_ADDRESS || [REDACTED_SECRET];
const CHUNK_SIZE = parseInt(process.env.CHUNK_SIZE || '15000'); // 15KB
const GROUP_SIZE = parseInt(process.env.GROUP_SIZE || '500');
const BATCH_SIZE = 5;
const RETRY_MAX = 3;
const RETRY_DELAY = 2000;

// ============================================================================
// MegaETH Multidimensional Gas Model (ported from Warren's megaeth-gas.ts)
// ============================================================================

const MEGAETH_GAS = {
  INTRINSIC_COMPUTE: 21_000n,
  INTRINSIC_STORAGE: 39_000n,
  CONTRACT_CREATION_COMPUTE: 32_000n,
  CONTRACT_CREATION_STORAGE_BASE: 32_000n,
  CODE_DEPOSIT_PER_BYTE: 10_000n,
  CALLDATA_ZERO_PER_BYTE: 40n,
  CALLDATA_NONZERO_PER_BYTE: 160n,
  EIP7623_FLOOR_ZERO_PER_BYTE: 100n,
  EIP7623_FLOOR_NONZERO_PER_BYTE: 400n,
  SSTORE2_COMPUTE_ESTIMATE: 700_000n,
  PAGE_BYTECODE_OVERHEAD: 100n,
};

const MAX_GAS_PER_TX = 200_000_000n;

function estimateChunkGasLimit(chunkSizeBytes) {
  const dataSize = BigInt(chunkSizeBytes) + MEGAETH_GAS.PAGE_BYTECODE_OVERHEAD;

  // Compute gas
  const computeGas = MEGAETH_GAS.INTRINSIC_COMPUTE
    + MEGAETH_GAS.CONTRACT_CREATION_COMPUTE
    + MEGAETH_GAS.SSTORE2_COMPUTE_ESTIMATE;

  // Storage gas
  const codeDeposit = dataSize * MEGAETH_GAS.CODE_DEPOSIT_PER_BYTE;
  const nonZeroBytes = BigInt(Math.floor(chunkSizeBytes * 0.8));
  const zeroBytes = BigInt(chunkSizeBytes) - nonZeroBytes;

  const calldataRegular = zeroBytes * MEGAETH_GAS.CALLDATA_ZERO_PER_BYTE
    + nonZeroBytes * MEGAETH_GAS.CALLDATA_NONZERO_PER_BYTE;
  const calldataFloor = zeroBytes * MEGAETH_GAS.EIP7623_FLOOR_ZERO_PER_BYTE
    + nonZeroBytes * MEGAETH_GAS.EIP7623_FLOOR_NONZERO_PER_BYTE;
  const calldata = calldataRegular > calldataFloor ? calldataRegular : calldataFloor;

  const storageGas = MEGAETH_GAS.INTRINSIC_STORAGE + codeDeposit + calldata;

  // Total with 50% buffer, capped at 200M
  const total = computeGas + storageGas;
  const withBuffer = (total * 150n) / 100n;
  // Minimum 10M gas floor — SSTORE2 actual compute cost is much higher than model estimate
  const MIN_GAS = 10_000_000n;
  const gasLimit = withBuffer < MIN_GAS ? MIN_GAS : withBuffer;
  return gasLimit < MAX_GAS_PER_TX ? gasLimit : MAX_GAS_PER_TX;
}

// ============================================================================
// ABIs & Bytecode
// ============================================================================

// Page.sol compiled bytecode — see page_bytecode.js for source & verification info
const PAGE_BYTECODE = require('./page_bytecode.js');

const PAGE_ABI = [{"type":"constructor","inputs":[{"name":"_content","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"}];

const MASTER_NFT_ABI = [
  {"type":"function","name":"mint","inputs":[{"name":"to","type":"address"},{"name":"rootChunk","type":"address"},{"name":"depth","type":"uint8"},{"name":"totalSize","type":"uint256"},{"name":"siteType","type":"uint8"}],"outputs":[{"name":"tokenId","type":"uint256"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"sites","inputs":[{"name":"","type":"uint256"}],"outputs":[{"name":"rootChunk","type":"address"},{"name":"depth","type":"uint8"},{"name":"totalSize","type":"uint256"},{"name":"siteType","type":"uint8"},{"name":"creator","type":"address"},{"name":"version","type":"uint256"},{"name":"createdAt","type":"uint256"},{"name":"updatedAt","type":"uint256"}],"stateMutability":"view"}
];

const GENESIS_KEY_ABI = [
  'function mint() external payable',
  'function balanceOf(address) view returns (uint256)',
];
const RABBIT_AGENT_ABI = [
  'function mint() external',
  'function balanceOf(address) view returns (uint256)',
];

const SITE_TYPES = { file: 0, image: 3, video: 4, audio: 5, script: 7 };

// ============================================================================
// Helpers
// ============================================================================

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

function isRetryable(e) {
  const msg = (e.message || '').toLowerCase();
  return msg.includes('rate') || msg.includes('429') || msg.includes('timeout') || msg.includes('nonce') || e.code === -32022;
}

async function withRetry(fn, label) {
  for (let i = 1; i <= RETRY_MAX; i++) {
    try { return await fn(); } catch (e) {
      if (isRetryable(e) && i < RETRY_MAX) {
        console.log(`  ⚠ ${label} attempt ${i} failed, retrying...`);
        await sleep(RETRY_DELAY * i);
      } else throw e;
    }
  }
}

// ============================================================================
// Genesis Key NFT
// ============================================================================

async function ensureGenesisKey(wallet) {
  const humanNft = new ethers.Contract(GENESIS_KEY_ADDRESS, GENESIS_KEY_ABI, wallet);
  if (Number(await humanNft.balanceOf(wallet.address)) > 0) {
    console.log('🔑 Genesis Key (Human): ✅');
    return;
  }

  if (!RABBIT_AGENT_ADDRESS || RABBIT_AGENT_ADDRESS === ethers.ZeroAddress) {
    throw new Error('No Genesis Key found and RABBIT_AGENT_ADDRESS is not configured.');
  }

  const botNft = new ethers.Contract(RABBIT_AGENT_ADDRESS, RABBIT_AGENT_ABI, wallet);
  if (Number(await botNft.balanceOf(wallet.address)) > 0) {
    console.log('🤖 0xRabbit.agent Key: ✅');
    return;
  }

  console.log('🤖 Minting 0xRabbit.agent Key (free)...');
  const tx = await botNft.mint({ gasLimit: 500000n });
  await tx.wait();
  console.log('  ✅ 0xRabbit.agent Key minted!');
}

// ============================================================================
// Page Deployment
// ============================================================================

async function deployPage(wallet, provider, content, nonce, label) {
  return withRetry(async () => {
    const factory = new ethers.ContractFactory(PAGE_ABI, PAGE_BYTECODE, wallet);
    const deployTx = await factory.getDeployTransaction(content);
    const contentSize = Buffer.isBuffer(content) ? content.length : Buffer.from(content).length;
    deployTx.gasLimit = estimateChunkGasLimit(contentSize);
    if (nonce !== undefined) deployTx.nonce = nonce;
    const tx = await wallet.sendTransaction(deployTx);
    const receipt = await tx.wait();
    const code = await provider.getCode(receipt.contractAddress);
    if (!code || code.length <= 2) throw new Error(`${label}: verification failed`);
    return receipt;
  }, label);
}

// ============================================================================
// Fractal Tree
// ============================================================================

function chunkBuffer(buf, size) {
  const chunks = [];
  for (let i = 0; i < buf.length; i += size) chunks.push(buf.subarray(i, Math.min(i + size, buf.length)));
  return chunks;
}

async function deployChunks(wallet, provider, chunks) {
  const addrs = [];
  console.log(`\n📦 Phase 1: Deploying ${chunks.length} chunk(s)...`);
  for (let bs = 0; bs < chunks.length; bs += BATCH_SIZE) {
    const be = Math.min(bs + BATCH_SIZE, chunks.length);
    const batch = chunks.slice(bs, be);
    const baseNonce = await provider.getTransactionCount(wallet.address, 'latest');
    const results = await Promise.all(batch.map(async (chunk, idx) => {
      await sleep(idx * 50);
      const receipt = await deployPage(wallet, provider, chunk, baseNonce + idx, `Chunk ${bs + idx + 1}/${chunks.length}`);
      return { receipt, idx: bs + idx };
    }));
    for (const { receipt, idx } of results.sort((a, b) => a.idx - b.idx)) {
      addrs.push(receipt.contractAddress);
      console.log(`  ✅ Chunk ${idx + 1}/${chunks.length}: ${receipt.contractAddress}`);
    }
    if (be < chunks.length) await sleep(300);
  }
  return addrs;
}

async function buildTree(wallet, provider, addresses) {
  let level = [...addresses];
  let depth = 0;
  while (level.length > 1) {
    depth++;
    const parent = [];
    const groups = Math.ceil(level.length / GROUP_SIZE);
    console.log(`\n🌲 Phase 2: Building tree depth ${depth} (${groups} node(s))...`);
    const baseNonce = await provider.getTransactionCount(wallet.address, 'latest');
    for (let i = 0; i < level.length; i += GROUP_SIZE) {
      const group = level.slice(i, i + GROUP_SIZE);
      const concat = Buffer.concat(group.map(a => Buffer.from(ethers.getBytes(a))));
      const ni = Math.floor(i / GROUP_SIZE);
      await sleep(ni * 50);
      const receipt = await deployPage(wallet, provider, concat, baseNonce + ni, `Node D${depth}-${ni + 1}/${groups}`);
      parent.push(receipt.contractAddress);
      console.log(`  ✅ Node D${depth}-${ni + 1}: ${receipt.contractAddress}`);
    }
    level = parent;
  }
  return { rootChunk: level[0], depth };
}

// ============================================================================
// MasterNFT Mint
// ============================================================================

async function mintSite(wallet, rootChunk, depth, totalSize, siteType) {
  console.log('\n🎨 Phase 3: Minting site NFT...');
  const master = new ethers.Contract(MASTER_NFT_ADDRESS, MASTER_NFT_ABI, wallet);
  const tx = await master.mint(wallet.address, rootChunk, depth, totalSize, siteType, { gasLimit: 100_000_000n });
  const receipt = await tx.wait();
  const topic = ethers.id('Transfer(address,address,uint256)');
  const log = receipt.logs.find(l => l.topics?.[0] === topic);
  const tokenId = log?.topics?.[3] ? Number(BigInt(log.topics[3])) : null;
  console.log(`  ✅ Token ID: ${tokenId}, Gas: ${receipt.gasUsed}`);
  return tokenId;
}

// ============================================================================
// Main
// ============================================================================

async function deploy(privateKey, content, options = {}) {
  const { name = 'Untitled', siteType = 'file' } = options;
  const provider = new ethers.JsonRpcProvider(RPC_URL, CHAIN_ID);
  const wallet = new ethers.Wallet(privateKey, provider);

  console.log('='.repeat(60));
  console.log('Warren Direct Deploy');
  console.log('='.repeat(60));
  console.log(`Address: ${wallet.address}`);
  console.log(`Network: MegaETH Mainnet (Chain ${CHAIN_ID})`);
  console.log(`Content: ${content.length} bytes | Name: ${name}`);

  const balance = await provider.getBalance(wallet.address);
  console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
  if (balance === 0n) throw new Error('No ETH balance. You need real MegaETH ETH for mainnet deployment.');

  await ensureGenesisKey(wallet);

  const chunks = chunkBuffer(content, CHUNK_SIZE);
  const addrs = await deployChunks(wallet, provider, chunks);

  let rootChunk, depth;
  if (addrs.length === 1) { rootChunk = addrs[0]; depth = 0; }
  else { ({ rootChunk, depth } = await buildTree(wallet, provider, addrs)); }

  const tokenId = await mintSite(wallet, rootChunk, depth, content.length, SITE_TYPES[siteType] ?? 0);
  const url = `https://thewarren.app/v/site=${tokenId}`;

  console.log('\n' + '='.repeat(60));
  console.log('🎉 Deployment Complete!');
  console.log('='.repeat(60));
  console.log(`Token ID: ${tokenId}`);
  console.log(`URL: ${url}`);

  return { tokenId, rootChunk, depth, url };
}

// ============================================================================
// CLI
// ============================================================================

async function main() {
  const args = process.argv.slice(2);
  const getArg = (n) => { const i = args.indexOf(`--${n}`); return i >= 0 ? args[i + 1] : undefined; };

  if (args.includes('--help') || args.includes('-h')) {
    console.log(`
Warren Direct Deploy - Deploy HTML/files to MegaETH blockchain

Usage:
  PRIVATE_KEY=0x... node deploy.js --html "<h1>Hello</h1>" [options]
  PRIVATE_KEY=0x... node deploy.js --file ./site.html [options]

Options:
  --private-key <key>   Wallet private key (or set PRIVATE_KEY env)
  --html <string>       HTML content to deploy
  --file <path>         Path to file to deploy
  --name <name>         Site name (default: "Untitled")
  --type <type>         file|image|video|audio|script (default: "file")

Prerequisites:
  1. npm install ethers (run 'bash setup.sh' for quick setup)
  2. You need MegaETH mainnet ETH for gas
  3. 0xRabbit.agent Key auto-mints (free)
`);
    process.exit(0);
  }

  const privateKey = getArg('private-key') || process.env.PRIVATE_KEY;
  if (!privateKey) { console.error('Error: set PRIVATE_KEY env or use --private-key'); process.exit(1); }

  const filePath = getArg('file');
  const htmlString = getArg('html');
  let content;

  if (filePath) {
    const resolved = path.resolve(filePath);
    if (!fs.existsSync(resolved)) { console.error(`Error: file not found: ${resolved}`); process.exit(1); }
    if (!fs.statSync(resolved).isFile()) { console.error(`Error: not a regular file: ${resolved}`); process.exit(1); }
    content = fs.readFileSync(resolved);
  }
  else if (htmlString) content = Buffer.from(htmlString, 'utf8');
  else {
    const chunks = [];
    process.stdin.resume();
    for await (const c of process.stdin) chunks.push(c);
    content = Buffer.concat(chunks);
    if (!content.length) { console.error('Error: provide --file, --html, or pipe stdin'); process.exit(1); }
  }

  if (content.length > 500 * 1024) { console.error(`Error: ${content.length} bytes exceeds 500KB limit`); process.exit(1); }

  try {
    const result = await deploy(privateKey, content, { name: getArg('name') || 'Untitled', siteType: getArg('type') || 'file' });
    console.log('\n--- JSON ---');
    console.log(JSON.stringify(result, null, 2));
  } catch (e) { console.error(`\n❌ Failed: ${e.message}`); process.exit(1); }
}

main();


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)