SkillHub

ens-manager

v1.2.0

Register ENS names, create subdomains, and publish IPFS sites without manual contract calls

Sourced from ClawHub, Authored by Zeugh

Installation

Please help me install the skill `ens-manager` from SkillHub official store. npx skills add Zeugh-eth/ens-manager

ENS Manager

Complete ENS name management: register new .eth names, create subdomains, and publish IPFS content to decentralized gateways.

Quick Start

Register New ENS Name

# Check availability and price (dry run)
node scripts/register-ens-name.js mynewname --dry-run

# Register for 1 year
node scripts/register-ens-name.js mynewname 
  --years 1 
  --keystore /path/to/keystore.enc 
  --password "your-password"

What happens: 1. Phase 1: Makes commitment on-chain (anti-frontrunning) 2. Phase 2: Waits 60 seconds (required minimum) 3. Phase 3: Registers name with payment

Cost: ~$5-20 USD/year (varies by name length and demand)

Check ENS Name Status

node scripts/check-ens-name.js yourname.eth

Shows ownership, wrapped status, resolver, and content hash.

Create Subdomain with IPFS

node scripts/create-subdomain-ipfs.js yourname.eth subdomain QmIPFS123... 
  --keystore /path/to/keystore.enc 
  --password "your-password"

Creates subdomain.yourname.eth and sets its IPFS content hash in one command.


Complete Workflows

1. Register New ENS Name (Full Process)

Scenario: You want to own mynewname.eth

Step 1: Check Availability

node scripts/register-ens-name.js mynewname --dry-run

Output:

🦞 ENS Name Registration
========================
📛 Name: mynewname.eth
⏱️  Duration: 1 year

🔍 Checking availability...
✅ Name is available!

💰 Calculating price...
   Registration cost: 0.008 ETH
   (for 1 year)

✅ Dry run complete.

Step 2: Register

node scripts/register-ens-name.js mynewname 
  --years 1 
  --keystore ~/.openclaw/workspace/wallet-keystore.enc 
  --password "keystore-password"

What happens (three phases):

Phase 1: Commitment (TX 1)

📝 Phase 1: Making commitment...
   Commitment: 0xabc123...
   Secret: 0xdef456...
   TX: 0x789...
   Waiting for confirmation...

The commitment hash prevents frontrunning (someone stealing your name by seeing your TX and submitting theirs first with higher gas).

Phase 2: Wait 60 Seconds

⏳ Phase 2: Waiting 60 seconds (anti-frontrunning protection)...
   60 seconds remaining...
   59 seconds remaining...
   ...
   ✅ Wait complete!

This mandatory wait ensures your commitment is on-chain before registration.

Phase 3: Registration (TX 2 with payment)

📝 Phase 3: Registering name...
   Sending 0.008 ETH...
   TX: 0xghi789...

🎉 Registration complete!

📛 Your ENS name: mynewname.eth
🔍 View on ENS: https://app.ens.domains/mynewname.eth
🔗 Registry TX: https://etherscan.io/tx/0xghi789...

Next steps:
  1. Set your address: ens.domains → Records → ETH Address
  2. Create subdomains: node create-subdomain-ipfs.js
  3. Set reverse record: ens.domains → My Account → Primary Name

Total time: ~2-3 minutes (60s wait + TX confirmations)
Total cost: Registration price + ~$2-4 gas (at 10 gwei)


2. Publishing a Website to ENS

Scenario: You have a static website and want to publish it at meetup.yourname.eth.limo

Prerequisites

  • You own yourname.eth (registered via Step 1 or app.ens.domains)
  • Website files ready
  • IPFS access (local node or Infura/Pinata)

Steps

1. Add website to IPFS:

ipfs add -r ./website
# Output: added QmABC123... website

2. Create ENS subdomain and set content hash:

node scripts/create-subdomain-ipfs.js yourname.eth meetup QmABC123... 
  --keystore ~/.openclaw/workspace/wallet-keystore.enc 
  --password "keystore-password"

3. Access your site:

Open https://meetup.yourname.eth.limo in any browser!

Cost: ~$0.05 (at 0.1 gwei base fee)


3. Updating IPFS Content

Scenario: You updated your website and have a new IPFS CID

Option A: Use viem directly

const { createWalletClient, http, namehash } = require('viem');
const { mainnet } = require('viem/chains');
const contentHash = require('content-hash');

const node = namehash('subdomain.yourname.eth');
const encodedHash = '0x' + contentHash.encode('ipfs-ns', newCid);

await walletClient.writeContract({
  address: PUBLIC_RESOLVER,
  abi: [{
    name: 'setContenthash',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'node', type: 'bytes32' },
      { name: 'hash', type: 'bytes' }
    ],
    outputs: []
  }],
  functionName: 'setContenthash',
  args: [node, encodedHash]
});

Option B: Use ENS App

  1. Go to https://app.ens.domains/subdomain.yourname.eth
  2. Click "Records"
  3. Edit "Content Hash"
  4. Paste new IPFS CID
  5. Save (sign TX)

4. Checking What's Published

node scripts/check-ens-name.js meetup.yourname.eth

Shows current IPFS CID and eth.limo URL.


Contract Addresses (Ethereum Mainnet)

All operations use these addresses:

// Core ENS contracts
const ENS_REGISTRY = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e';

// Registration (.eth names)
const ETH_REGISTRAR_CONTROLLER = '0x253553366Da8546fC250F225fe3d25d0C782303b';

// Wrapped names (modern, with fuses)
const NAME_WRAPPER = '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401';

// Default resolver (most common)
const PUBLIC_RESOLVER = '0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63';

Where they're used:

  • ENS_REGISTRY: Base registry for all names (ownership, resolvers)
  • ETH_REGISTRAR_CONTROLLER: Register/renew .eth names (3-phase process)
  • NAME_WRAPPER: Create subdomains for wrapped names
  • PUBLIC_RESOLVER: Resolve names to addresses/content hashes

Three-Phase Registration Process

Why Three Phases?

Problem: If you submit a registration transaction, miners or bots can see it in the mempool and frontrun you (submit their own TX with higher gas to steal the name).

Solution: Commitment scheme prevents this:

  1. Commit: Submit a hash of (name + secret). No one knows what name you want.
  2. Wait: 60 seconds minimum. Your commitment is now on-chain and timestamped.
  3. Register: Reveal the name + secret. Since your commitment was earlier, you win.

Phase 1: Commitment

What happens:

const commitment = keccak256(
  encodePacked(
    ['string', 'address', 'uint256', 'bytes32', 'address', 'bytes[]', 'bool', 'uint16'],
    [
      name,              // 'mynewname' (without .eth)
      owner,             // Your wallet address
      duration,          // 31536000 (1 year in seconds)
      secret,            // Random bytes32 (generated)
      resolver,          // PUBLIC_RESOLVER
      [],                // data (empty for basic registration)
      true,              // reverseRecord (set primary name)
      0                  // ownerControlledFuses (0 = none)
    ]
  )
);

// Submit to ETHRegistrarController
await controller.commit(commitment);

Gas cost: ~45,000 gas (~$1-2 at 10 gwei)

Phase 2: Wait

Mandatory 60-second wait ensures: - Your commitment is confirmed on-chain - Timestamp is set - You can't be frontrun

Technical reason: The contract checks that block.timestamp >= commitmentTimestamp + 60 seconds when you register.

Phase 3: Registration

What happens:

await controller.register(
  name,              // 'mynewname'
  owner,             // Your address
  duration,          // 31536000 (1 year)
  secret,            // Same secret from commitment
  resolver,          // PUBLIC_RESOLVER
  [],                // data
  true,              // reverseRecord
  0,                 // fuses
  { value: price }   // Payment in ETH
);

Gas cost: ~150,000 gas (~$3-5 at 10 gwei) + registration price

Total registration: ~$4-7 gas + registration price (~$5-20/year)


Wrapped vs Unwrapped Names

Check if wrapped:

node scripts/check-ens-name.js yourname.eth

Look for "🎁 Wrapped (NameWrapper)" or "📦 Unwrapped (Registry)".

Key differences:

Feature Wrapped Unwrapped
Subdomain creation NameWrapper.setSubnodeRecord Registry.setSubnodeOwner + Registry.setResolver
Label parameter String ("meetup") Bytes32 hash (keccak256)
Owner location NameWrapper ERC-1155 Registry mapping
Advanced features Fuses, expiry None

The create-subdomain-ipfs.js script handles both automatically.


Current vs Typical Gas Costs

Gas costs vary based on network congestion. The skill shows real-time costs during dry-run.

Registration (2 TXs)

Current conditions (0.22 gwei - very low): - Commitment: 45,000 gas = $0.02 - Registration: 265,000 gas = $0.15 - Total gas: $0.17

Typical conditions (10 gwei - normal): - Commitment: 45,000 gas = $1.13 - Registration: 265,000 gas = $6.63 - Total gas: $7.75

ENS Name Pricing Tiers (Annual Rental)

Name Length Cost/Year Example
3 characters ~$773 abc.eth
4 characters ~$193 cool.eth
5+ characters ~$6 hello.eth, example.eth

Complete Registration Examples

5-letter name (most common): - Current: Name ($6) + Gas ($0.17) = $6.17 total - Typical: Name ($6) + Gas ($7.75) = $13.75 total

4-letter name: - Current: Name ($193) + Gas ($0.17) = $193.17 total - Typical: Name ($193) + Gas ($7.75) = $200.75 total

Recommendation: Use --dry-run to check current costs before registering.

See COSTS.md for detailed cost breakdowns and optimization tips.


Troubleshooting

Registration: "Name not available"

The name is already registered. Try:

node scripts/check-ens-name.js <name>.eth

To see who owns it and when it expires.

Registration: "insufficient funds"

You need ETH for: 1. Registration price (~$5-20 for most names) 2. Gas fees (~$4-7)

Check balance:

# In script or cast
const balance = await publicClient.getBalance({ address });
console.log('Balance:', formatEther(balance), 'ETH');

Registration: "Commitment not found" or "Too soon"

If you see this during Phase 3: - Too soon: Wait the full 60 seconds - Commitment not found: Your Phase 1 TX didn't confirm yet. Wait longer or check TX status.

Transaction reverts

Common causes: 1. Not the owner - You must own the parent name to create subdomains 2. Name is wrapped - Use NameWrapper methods, not Registry 3. Gas limit too low - Increase gas parameter 4. Commitment too old - If you wait >24 hours between Phase 1 and 3, start over

Content not appearing on eth.limo

  1. Wait a few minutes (DNS propagation)
  2. Check content hash is set: node scripts/check-ens-name.js yourname.eth
  3. Verify IPFS CID is accessible: https://ipfs.io/ipfs/YOUR_CID
  4. Check resolver is PUBLIC_RESOLVER (most gateways only read from it)

Manual Operations (Advanced)

Unwrapped Name: Create Subdomain

const { keccak256, encodePacked } = require('viem');

// 1. Create subdomain
const parentNode = namehash('yourname.eth');
const labelHash = keccak256(encodePacked(['string'], ['subdomain']));

await walletClient.writeContract({
  address: ENS_REGISTRY,
  abi: [{
    name: 'setSubnodeOwner',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'node', type: 'bytes32' },
      { name: 'label', type: 'bytes32' },
      { name: 'owner', type: 'address' }
    ],
    outputs: [{ name: '', type: 'bytes32' }]
  }],
  functionName: 'setSubnodeOwner',
  args: [parentNode, labelHash, ownerAddress]
});

// 2. Set resolver
const subnode = namehash('subdomain.yourname.eth');

await walletClient.writeContract({
  address: ENS_REGISTRY,
  abi: [{
    name: 'setResolver',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'node', type: 'bytes32' },
      { name: 'resolver', type: 'address' }
    ],
    outputs: []
  }],
  functionName: 'setResolver',
  args: [subnode, PUBLIC_RESOLVER]
});

Wrapped Name: Create Subdomain

await walletClient.writeContract({
  address: NAME_WRAPPER,
  abi: [{
    name: 'setSubnodeRecord',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'parentNode', type: 'bytes32' },
      { name: 'label', type: 'string' },
      { name: 'owner', type: 'address' },
      { name: 'resolver', type: 'address' },
      { name: 'ttl', type: 'uint64' },
      { name: 'fuses', type: 'uint32' },
      { name: 'expiry', type: 'uint64' }
    ],
    outputs: [{ name: '', type: 'bytes32' }]
  }],
  functionName: 'setSubnodeRecord',
  args: [
    namehash('yourname.eth'),  // parent node
    'subdomain',                // label (string!)
    ownerAddress,               // owner
    PUBLIC_RESOLVER,            // resolver
    0n,                         // ttl (0 = default)
    0,                          // fuses (0 = none)
    0n                          // expiry (0 = inherit from parent)
  ]
});

IPFS Content Hash

Encoding CID

const contentHash = require('content-hash');
const ipfsCid = 'QmABC123...';
const encoded = '0x' + contentHash.encode('ipfs-ns', ipfsCid);

Setting Content Hash

await walletClient.writeContract({
  address: PUBLIC_RESOLVER,
  abi: [{
    name: 'setContenthash',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'node', type: 'bytes32' },
      { name: 'hash', type: 'bytes' }
    ],
    outputs: []
  }],
  functionName: 'setContenthash',
  args: [namehash('subdomain.yourname.eth'), encodedHash]
});

Reading Content Hash

const contenthash = await publicClient.readContract({
  address: resolverAddress,
  abi: [{
    name: 'contenthash',
    type: 'function',
    stateMutability: 'view',
    inputs: [{ name: 'node', type: 'bytes32' }],
    outputs: [{ name: '', type: 'bytes' }]
  }],
  functionName: 'contenthash',
  args: [namehash('subdomain.yourname.eth')]
});

if (contenthash && contenthash !== '0x') {
  const decoded = contentHash.decode(contenthash);
  console.log('IPFS CID:', decoded);
}

Gateway Access

After setting a content hash, the subdomain becomes accessible via:

  • eth.limo: https://subdomain.yourname.eth.limo
  • eth.link: https://subdomain.yourname.eth.link
  • IPFS directly: https://ipfs.io/ipfs/QmABC123...
  • dweb.link: https://QmABC123....ipfs.dweb.link/

Note: eth.limo/eth.link may take a few minutes to update after setting a new content hash.


Resources

  • ENS Documentation: https://docs.ens.domains
  • eth.limo: https://eth.limo
  • ENS App (UI): https://app.ens.domains
  • NameWrapper Guide: https://docs.ens.domains/wrapper/overview
  • Content Hash Spec: See references/ens-basics.md
  • Registrar Controller: https://etherscan.io/address/0x253553366Da8546fC250F225fe3d25d0C782303b

For detailed ENS concepts, wrapped vs unwrapped differences, and content hash encoding, read references/ens-basics.md.