npm package: @monadns/sdk
Description: Monad Name Service SDK - resolve and register .mon names on Monad
Author: Monad Name Service
Latest version: 2.3.3

# @monadns/sdk

Resolve and register **.mon** names on the Monad network. The official SDK for the [Monad Name Service](https://monadscan.com/address/0x98866c55adbc73ec6c272bb3604ddbdee3f282a8#readContract).

## Install

```bash
npm install @monadns/sdk viem
```

## Quick Start

```typescript
import { resolveName, lookupAddress } from '@monadns/sdk';

// Forward: name → address
const address = await resolveName('alice.mon');
// '0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47'

// Reverse: address → name
const name = await lookupAddress('0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47');
// 'alice.mon'
```

## React Hooks

All hooks are compatible with Next.js App Router, Remix, and any React 17+ framework. The `'use client'` directive is included in the build.

```bash
npm install @monadns/sdk viem react
```

### Display .mon names

```tsx
import { useMNSDisplay } from '@monadns/sdk/react';

function UserBadge({ address }: { address: string }) {
  const { displayName, monName, loading } = useMNSDisplay(address);

  return (
    <span className={monName ? 'text-purple-400 font-bold' : 'text-gray-400'}>
      {loading ? '...' : displayName}
    </span>
  );
}
```

### Resolve input (name or address)

```tsx
import { useMNSResolve } from '@monadns/sdk/react';

function RecipientInput() {
  const { resolve, address, name, loading, error } = useMNSResolve();

  return (
    <div>
      <input
        placeholder="Address or .mon name"
        onChange={(e) => resolve(e.target.value)}
      />
      {loading && <span>Resolving...</span>}
      {error && <span className="text-red-500">{error}</span>}
      {address && <span className="text-green-500">→ {address}</span>}
    </div>
  );
}
```

### Get avatar

```tsx
import { useMNSAvatar } from '@monadns/sdk/react';

function Avatar({ name }: { name: string }) {
  const { url, loading } = useMNSAvatar(name);
  if (loading)
    return <div className="animate-pulse w-10 h-10 rounded-full bg-gray-700" />;
  return url ? <img src={url} className="w-10 h-10 rounded-full" /> : null;
}
```

### Get text records

```tsx
import { useMNSText } from '@monadns/sdk/react';

function Twitter({ name }: { name: string }) {
  const { value: handle } = useMNSText(name, 'com.twitter');
  return handle ? <a href={`https://x.com/${handle}`}>@{handle}</a> : null;
}
```

### Check ownership and expiry

```tsx
import { useMNSOwner, useMNSExpiry } from '@monadns/sdk/react';

function NameInfo({ name }: { name: string }) {
  const { owner, loading: ownerLoading } = useMNSOwner(name);
  const {
    daysUntilExpiry,
    isExpired,
    loading: expiryLoading,
  } = useMNSExpiry(name);

  if (ownerLoading || expiryLoading) return <div>Loading...</div>;

  return (
    <div>
      <p>Owner: {owner}</p>
      {isExpired ? (
        <p className="text-red-500">This name has expired!</p>
      ) : (
        <p>Expires in {daysUntilExpiry} days</p>
      )}
    </div>
  );
}
```

## Registration (with wagmi)

The SDK returns transaction parameters that work directly with wagmi's `useWriteContract`:

```bash
npm install @monadns/sdk viem wagmi @tanstack/react-query
```

### Full Registration Page

```tsx
import { useState } from 'react';
import {
  useAccount,
  useWriteContract,
  useWaitForTransactionReceipt,
} from 'wagmi';
import { formatEther } from 'viem';
import { useRegistrationInfo, useMNSRegister } from '@monadns/sdk/react';
import { clearCache } from '@monadns/sdk';

function RegisterPage() {
  const [label, setLabel] = useState('');
  const { address } = useAccount();
  const {
    info,
    loading: checking,
    error: checkError,
  } = useRegistrationInfo(label, address);
  const { prepare, loading: preparing, error } = useMNSRegister();
  const { writeContract, data: hash } = useWriteContract();
  const { isLoading: confirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });

  const handleRegister = async () => {
    const tx = await prepare(label, address!);
    if (tx) writeContract(tx);
  };

  // Clear cache after successful registration so lookups reflect new state
  if (isSuccess) clearCache();

  return (
    <div className="max-w-md mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">Register a .mon name</h1>

      <input
        className="w-full p-3 border rounded-lg mb-4"
        placeholder="Search for a name..."
        value={label}
        onChange={(e) => setLabel(e.target.value)}
      />

      {checking && <p className="text-gray-400">Checking availability...</p>}
      {checkError && <p className="text-red-500">❌ {checkError}</p>}

      {info && !checking && !checkError && (
        <div className="space-y-2">
          <p>{info.available ? '✅ Available!' : '❌ Already taken'}</p>

          {info.available && (
            <>
              <p className="text-lg">
                {info.isFreebie
                  ? '🎉 FREE for 100 years!'
                  : `${formatEther(info.price)} MON / year`}
              </p>

              <button
                className="w-full bg-purple-600 text-white p-3 rounded-lg disabled:opacity-50"
                onClick={handleRegister}
                disabled={preparing || confirming}
              >
                {confirming
                  ? 'Confirming...'
                  : preparing
                    ? 'Preparing...'
                    : `Register ${label}.mon`}
              </button>
            </>
          )}
        </div>
      )}

      {error && <p className="text-red-500 mt-2">❌ {error}</p>}
      {isSuccess && (
        <p className="text-green-500 mt-4">🎉 {label}.mon is yours!</p>
      )}
    </div>
  );
}
```

### Profile Editor (Avatar, Twitter, Bio)

```tsx
import { useState } from 'react';
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { useMNSText, useMNSTextRecords } from '@monadns/sdk/react';

function ProfileEditor({ name }: { name: string }) {
  const { value: currentAvatar } = useMNSText(name, 'avatar');
  const { value: currentTwitter } = useMNSText(name, 'com.twitter');
  const { value: currentBio } = useMNSText(name, 'description');

  const { setAvatar, setTwitter, setDescription } = useMNSTextRecords();
  const { writeContract, data: hash } = useWriteContract();
  const { isLoading: saving, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });

  const [avatar, setAvatarUrl] = useState('');
  const [twitter, setTwitterHandle] = useState('');
  const [bio, setBio] = useState('');

  return (
    <div className="max-w-md mx-auto p-6 space-y-4">
      <h2 className="text-xl font-bold">Edit {name} Profile</h2>

      {currentAvatar && (
        <img src={currentAvatar} className="w-20 h-20 rounded-full" />
      )}

      <div>
        <label className="block text-sm font-medium mb-1">Avatar URL</label>
        <div className="flex gap-2">
          <input
            className="flex-1 p-2 border rounded"
            placeholder={currentAvatar || 'https://example.com/avatar.png'}
            value={avatar}
            onChange={(e) => setAvatarUrl(e.target.value)}
          />
          <button
            className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
            onClick={() => writeContract(setAvatar(name, avatar))}
            disabled={saving || !avatar}
          >
            Save
          </button>
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Twitter</label>
        <div className="flex gap-2">
          <input
            className="flex-1 p-2 border rounded"
            placeholder={currentTwitter || '@handle'}
            value={twitter}
            onChange={(e) => setTwitterHandle(e.target.value)}
          />
          <button
            className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
            onClick={() => writeContract(setTwitter(name, twitter))}
            disabled={saving || !twitter}
          >
            Save
          </button>
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Bio</label>
        <div className="flex gap-2">
          <input
            className="flex-1 p-2 border rounded"
            placeholder={currentBio || 'Tell the world about yourself'}
            value={bio}
            onChange={(e) => setBio(e.target.value)}
          />
          <button
            className="px-4 py-2 bg-purple-600 text-white rounded disabled:opacity-50"
            onClick={() => writeContract(setDescription(name, bio))}
            disabled={saving || !bio}
          >
            Save
          </button>
        </div>
      </div>

      {saving && <p className="text-gray-400">⏳ Saving to blockchain...</p>}
      {isSuccess && <p className="text-green-500">✅ Profile updated!</p>}
    </div>
  );
}
```

## Name Validation

The SDK automatically validates names and provides clear error messages:

```typescript
import { validateMonName } from '@monadns/sdk';

const validation = validateMonName('alice');
if (!validation.valid) {
  console.error(validation.error);
}

// Invalid examples that are rejected:
validateMonName('0x123'); // ❌ "Name cannot start with "0x" (reserved for addresses)"
validateMonName('hello--world'); // ❌ "Name cannot contain consecutive hyphens"
validateMonName('-alice'); // ❌ "Name can only contain lowercase letters..."
validateMonName('a'.repeat(64)); // ❌ "Name must be 63 characters or less"
validateMonName(''); // ❌ "Name cannot be empty"

// Valid examples:
validateMonName('alice'); // ✅
validateMonName('a'); // ✅ 1-character names allowed
validateMonName('ab'); // ✅ 2-character names allowed
validateMonName('web3-name'); // ✅ Hyphens allowed (not at start/end)
```

## Avatar Best Practices

### Recommended Workflow (Enforced by SDK)

```tsx
import {
  validateAvatarFull,
  useMNSTextRecords,
  MAX_AVATAR_BYTES,
} from '@monadns/sdk/react';
import { useWriteContract } from 'wagmi';

function AvatarUploader({ name }: { name: string }) {
  const [error, setError] = useState('');
  const { setAvatarValidated } = useMNSTextRecords();
  const { writeContract } = useWriteContract();

  const handleUpload = async (url: string) => {
    try {
      setError('');

      // SDK validates size automatically (throws if > 50KB)
      const tx = await setAvatarValidated(name, url);
      writeContract(tx);
    } catch (e: any) {
      setError(e.message);
      // "Avatar file size (125.3KB) exceeds 50KB limit. Please optimize..."
    }
  };

  return (
    <div>
      <input onChange={(e) => handleUpload(e.target.value)} />
      {error && <p className="text-red-500">{error}</p>}
      <p className="text-sm text-gray-500">
        Max size: {(MAX_AVATAR_BYTES / 1024).toFixed(0)}KB (WebP/SVG
        recommended)
      </p>
    </div>
  );
}
```

### Size Limits (Automatically Enforced)

- **Data URIs**: Strict 50KB limit (enforced immediately)
- **Remote URLs** (IPFS/HTTP): Validated when using `setAvatarValidated()`
- **NFT avatars**: No size check (resolved at display time)

### Supported Avatar Formats

```typescript
// ✅ HTTPS URLs
setAvatar('alice.mon', 'https://example.com/avatar.png');

// ✅ IPFS with protocol
setAvatar('alice.mon', 'ipfs://QmX...');

// ✅ Raw IPFS CID (auto-converted)
setAvatar('alice.mon', 'QmX...'); // → ipfs://QmX...

// ✅ Arweave
setAvatar('alice.mon', 'ar://abc123...');

// ✅ NFT avatar
setAvatar('alice.mon', 'eip155:1/erc721:0x.../123');

// ✅ Data URI (with size check)
setAvatar('alice.mon', 'data:image/svg+xml;base64,...');
```

### Manual Validation (Advanced)

```typescript
import { validateAvatarUri, validateAvatarFull } from '@monadns/sdk';

// Quick format check (synchronous)
const validation = validateAvatarUri(avatarUrl);
if (!validation.valid) {
  console.error(validation.error);
}

// Full validation including size check (async)
try {
  await validateAvatarFull(avatarUrl);
  // Safe to use
} catch (error) {
  console.error(error.message);
  // "Avatar file size (65.3KB) exceeds 50KB limit..."
}

// Check remote file size
import { getRemoteAvatarSize, MAX_AVATAR_BYTES } from '@monadns/sdk';

const size = await getRemoteAvatarSize('https://example.com/avatar.png');
if (size && size > MAX_AVATAR_BYTES) {
  alert(`Avatar too large: ${(size / 1024).toFixed(1)}KB. Max is 50KB.`);
}
```

### Fallback Avatars

```typescript
import { getAvatarUrl, DEFAULT_AVATAR_PLACEHOLDER } from '@monadns/sdk';

// Use fallback if avatar not set or fails to resolve
const avatar = await getAvatarUrl('alice.mon', undefined, {
  fallback: DEFAULT_AVATAR_PLACEHOLDER,
});
```

### Override Validation (Not Recommended)

If you need to bypass validation (not recommended for production):

```typescript
// Use basic setAvatar() - only validates data URIs
const tx = setAvatar(name, largeAvatarUrl); // Logs warning
```

## Advanced Usage

### Check Name Ownership & Expiry

```typescript
import { getOwner, getExpiry, getResolver } from '@monadns/sdk';

// Check who owns a name
const owner = await getOwner('alice.mon');
console.log('Owner:', owner);

// Check when it expires
const expiry = await getExpiry('alice.mon');
const expiryDate = new Date(expiry * 1000);
const daysLeft = Math.floor(
  (expiry * 1000 - Date.now()) / (1000 * 60 * 60 * 24),
);
console.log(`Expires: ${expiryDate.toLocaleDateString()} (${daysLeft} days)`);

// Get resolver contract
const resolver = await getResolver('alice.mon');
console.log('Resolver:', resolver);
```

### Check Availability Before Registration

```typescript
import { getAvailable } from '@monadns/sdk';

const available = await getAvailable('alice.mon');
if (available) {
  console.log('alice.mon is available for registration!');
} else {
  console.log('alice.mon is already taken');
}
```

## API Reference

### Core Functions (`@monadns/sdk`)

| Function                              | Description                                   |
| ------------------------------------- | --------------------------------------------- |
| `resolveName(name)`                   | Forward resolve: `alice.mon` → `0x...`        |
| `lookupAddress(address)`              | Reverse resolve: `0x...` → `alice.mon`        |
| `getDisplayName(address)`             | Returns `.mon` name or truncated address      |
| `resolveInput(input)`                 | Accepts name or address, returns address      |
| `getTextRecord(name, key)`            | Get text record (avatar, url, etc.)           |
| `getAvatarUrl(name, config, options)` | Get resolved avatar URL (handles IPFS, NFTs)  |
| `getAvailable(name)`                  | Check if a name is available for registration |
| `getOwner(name)`                      | Get the owner address of a name               |
| `getResolver(name)`                   | Get the resolver contract address             |
| `getExpiry(name)`                     | Get expiry timestamp (Unix seconds)           |
| `clearCache()`                        | Clear the resolution cache                    |
| `getRegistrationInfo(label, address)` | Get price, availability, freebie status       |
| `getRegisterTx(label, address)`       | Get tx params for registration                |
| `getSetTextTx(name, key, value)`      | Get tx params for setting a text record       |
| `getSetAddrTx(name, address)`         | Get tx params for updating the address record |
| `validateMonName(label)`              | Validate .mon name format and rules           |
| `validateAvatarUri(uri)`              | Validate avatar format and data URI size      |
| `validateAvatarFull(uri)`             | Async validation including remote file size   |
| `getRemoteAvatarSize(url)`            | Get file size of remote URL (helper for UI)   |

### React Hooks (`@monadns/sdk/react`)

| Hook                                  | Returns                                                              | Description                   |
| ------------------------------------- | -------------------------------------------------------------------- | ----------------------------- |
| `useMNSName(address)`                 | `{ name, loading, error }`                                           | Reverse resolution            |
| `useMNSAddress(name)`                 | `{ address, loading, error }`                                        | Forward resolution            |
| `useMNSDisplay(address)`              | `{ displayName, monName, loading }`                                  | Display-ready name            |
| `useMNSResolve()`                     | `{ resolve, address, name, loading, error }`                         | On-demand resolver for inputs |
| `useMNSText(name, key)`               | `{ value, loading, error }`                                          | Read text records             |
| `useMNSAvatar(name)`                  | `{ url, loading, error }`                                            | Resolved avatar URL           |
| `useMNSOwner(name)`                   | `{ owner, loading, error }`                                          | Get name owner                |
| `useMNSResolver(name)`                | `{ resolver, loading, error }`                                       | Get resolver address          |
| `useMNSExpiry(name)`                  | `{ expiry, expiryDate, daysUntilExpiry, isExpired, loading, error }` | Get expiration info           |
| `useRegistrationInfo(label, address)` | `{ info, loading, error }`                                           | Pre-registration data         |
| `useMNSRegister()`                    | `{ prepare, tx, loading, error }`                                    | Prepare registration tx       |
| `useMNSTextRecords()`                 | `{ setAvatar, setAvatarValidated, setTwitter, ... }`                 | Prepare text record txs       |
| `useMNSAddr()`                        | `{ setAddr }`                                                        | Prepare address update tx     |

### Constants

| Constant                     | Value                                        |
| ---------------------------- | -------------------------------------------- |
| `MAX_AVATAR_BYTES`           | `51200` (50KB)                               |
| `DEFAULT_AVATAR_PLACEHOLDER` | Transparent 1x1 SVG data URI                 |
| `MNS_REGISTRY`               | `0x13f963486e741c8d3fcdc0a34a910920339a19ce` |
| `MNS_PUBLIC_RESOLVER`        | `0xa2eb94c88e55d944aced2066c5cec9b759801f97` |
| `MNS_CONTROLLER`             | `0x98866c55adbc73ec6c272bb3604ddbdee3f282a8` |
| `MNS_BASE_REGISTRAR`         | `0x104a49db9318c284d462841b6940bdb46624ca55` |

### Custom RPC

All functions accept an optional config object:

```typescript
const name = await lookupAddress('0x...', {
  rpcUrl: 'https://your-custom-rpc.com',
});
```

Or pass an existing viem client:

```typescript
import { createPublicClient, http } from 'viem';
import { monad } from 'viem/chains';

const client = createPublicClient({ chain: monad, transport: http() });
const name = await lookupAddress('0x...', { client });
```

## Contracts

| Contract          | Address                                      |
| ----------------- | -------------------------------------------- |
| Registry          | `0x13f963486e741c8d3fcdc0a34a910920339a19ce` |
| Public Resolver   | `0xa2eb94c88e55d944aced2066c5cec9b759801f97` |
| Controller        | `0x98866c55adbc73ec6c272bb3604ddbdee3f282a8` |
| Base Registrar    | `0x104a49db9318c284d462841b6940bdb46624ca55` |
| Reverse Registrar | `0x6fff58419ad964f2357590c3f1bae576ebe45f52` |

**Chain:** Monad Mainnet (Chain ID 143)

## Changelog

### v2.3.0

**New Features:**

- **Name validation**: Added `validateMonName()` function with comprehensive validation rules
- **1 & 2-character names**: Removed artificial 3-character minimum - all lengths now supported
- **0x prefix protection**: Names cannot start with "0x" to prevent confusion with addresses
- **Automatic validation**: `useRegistrationInfo()` and write functions now validate names automatically

**Improvements:**

- Better error messages for invalid names
- Validation happens client-side before RPC calls (faster feedback)
- Consistent validation across all entry points

### v2.2.0

**New Features:**

- **Avatar validation**: `validateAvatarUri()` and `validateAvatarFull()` for size checking
- **Avatar constants**: `MAX_AVATAR_BYTES` (50KB limit) and `DEFAULT_AVATAR_PLACEHOLDER`
- **Enhanced avatar resolution**: Support for raw IPFS CIDs and Arweave URIs
- **Validated avatar setter**: `setAvatarValidated()` hook with automatic size validation
- **Size helper**: `getRemoteAvatarSize()` for checking remote file sizes
- **Fallback support**: `getAvatarUrl()` now accepts fallback option

**Behavior Changes:**

- Data URI avatars are now strictly limited to 50KB (throws error if exceeded)
- Setting non-data-URI avatars without validation logs a warning
- `getAvatarUrl()` now handles raw IPFS CIDs (e.g., `QmX...`) automatically

### v2.1.0

**New Features:**

- `getOwner(name)` - Get the owner of a name
- `getResolver(name)` - Get the resolver contract
- `getExpiry(name)` - Get expiration timestamp
- `useMNSOwner(name)` - React hook for ownership info
- `useMNSResolver(name)` - React hook for resolver info
- `useMNSExpiry(name)` - React hook for expiry info with computed fields
- `MNS_BASE_REGISTRAR` constant now exported

**Breaking Changes:**

- `isRegistered()` → `getAvailable()` (inverted boolean logic)

## Compatibility

- **Frameworks:** React 17+, Next.js (App Router & Pages), Remix, Vite, CRA
- **Wallet libraries:** wagmi, RainbowKit, ConnectKit, Privy, or any viem-based client
- **Non-React:** All core functions work without React in any JS/TS environment
- **Server-side:** Core functions can run in Node.js, edge functions, or API routes

## License

[Unlicense](https://unlicense.org) – public domain, no attribution required.