> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sei.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ERC-721 Interaction

> Reading and writing ERC-721 NFTs on Sei with viem and ethers

export const AddSeiButton = ({network = 'mainnet', label = 'Add Sei to MetaMask'}) => {
  const SEI_MAINNET_CHAIN_PARAMS = {
    chainId: '0x531',
    chainName: 'Sei Network',
    rpcUrls: ['https://evm-rpc.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://seiscan.io']
  };
  const SEI_TESTNET_CHAIN_PARAMS = {
    chainId: '0x530',
    chainName: 'Sei Testnet',
    rpcUrls: ['https://evm-rpc-testnet.sei-apis.com'],
    nativeCurrency: {
      name: 'Sei',
      symbol: 'SEI',
      decimals: 18
    },
    blockExplorerUrls: ['https://testnet.seiscan.io']
  };
  const chainParams = network === 'testnet' ? SEI_TESTNET_CHAIN_PARAMS : SEI_MAINNET_CHAIN_PARAMS;
  const [status, setStatus] = useState(null);
  const [isHovered, setIsHovered] = useState(false);
  const [isBusy, setIsBusy] = useState(false);
  const addOrSwitchSeiNetwork = async params => {
    if (typeof window === 'undefined' || !window.ethereum) {
      throw new Error('MetaMask is not installed');
    }
    const ethereum = window.ethereum;
    try {
      await ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{
          chainId: params.chainId
        }]
      });
    } catch (switchError) {
      if (switchError && switchError.code === 4902) {
        await ethereum.request({
          method: 'wallet_addEthereumChain',
          params: [params]
        });
      } else {
        throw switchError;
      }
    }
  };
  const onClick = async e => {
    e.preventDefault();
    setIsBusy(true);
    setStatus(null);
    try {
      await addOrSwitchSeiNetwork(chainParams);
      setStatus({
        type: 'success',
        message: `${chainParams.chainName} added or switched.`
      });
    } catch (err) {
      const message = err && err.message ? err.message : 'Failed to add or switch network.';
      setStatus({
        type: 'error',
        message
      });
    } finally {
      setIsBusy(false);
    }
  };
  return <span className="inline-flex flex-col items-start gap-1">
      <button type="button" onClick={onClick} disabled={isBusy} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className="inline-flex items-center gap-1 px-3 py-1.5 text-white transition-colors min-w-[160px]" style={{
    backgroundColor: isHovered ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    opacity: isBusy ? 0.7 : 1,
    cursor: isBusy ? 'default' : 'pointer'
  }}>
        {isBusy ? 'Adding…' : label}
      </button>
      {status && <span className={status.type === 'error' ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'} style={{
    fontSize: '11px'
  }}>
          {status.message}
        </span>}
    </span>;
};

export const SandboxEmbed = props => {
  const {src, kind = 'codesandbox', title, description, height, label} = props || ({});
  const KINDS = {
    codesandbox: {
      name: 'CodeSandbox',
      host: 'codesandbox.io',
      defaultHeight: 500
    },
    remix: {
      name: 'Remix IDE',
      host: 'remix.ethereum.org',
      defaultHeight: 620
    },
    stackblitz: {
      name: 'StackBlitz',
      host: 'stackblitz.com',
      defaultHeight: 500
    }
  };
  const meta = KINDS[kind] || KINDS.codesandbox;
  const parsedHeight = Number(height);
  const frameHeight = Number.isFinite(parsedHeight) && parsedHeight > 0 ? parsedHeight : meta.defaultHeight;
  const allowAttr = 'clipboard-read; clipboard-write';
  const [loaded, setLoaded] = useState(false);
  const [btnHover, setBtnHover] = useState(false);
  const HAIRLINE = 'rgba(128, 128, 128, 0.25)';
  const surfaceStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.08)'
  };
  const monoStyle = {
    fontFamily: 'var(--sei-font-mono)'
  };
  const cardClass = 'not-prose w-full rounded-lg border overflow-hidden my-4';
  const headerClass = 'flex items-center justify-between gap-3 px-4 py-2.5 border-b';
  const buttonStyle = {
    backgroundColor: btnHover ? 'var(--sei-maroon-200)' : 'var(--sei-maroon-100)',
    color: '#ffffff',
    fontFamily: 'var(--sei-font-mono)',
    textTransform: 'uppercase',
    letterSpacing: '0.04em',
    fontSize: '10px',
    cursor: 'pointer'
  };
  const PlayIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
			<path d="M8 5v14l11-7z" />
		</svg>;
  const ExternalIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
			<path d="M14 5h5v5" />
			<path d="M19 5l-9 9" />
			<path d="M19 14v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h5" />
		</svg>;
  return <div className={cardClass} style={{
    borderColor: HAIRLINE
  }}>
			<div className={headerClass} style={{
    ...surfaceStyle,
    borderBottomColor: HAIRLINE
  }}>
				<div className="flex flex-col min-w-0">
					<span className="text-sm font-medium text-neutral-900 dark:text-white truncate" style={monoStyle}>
						{title || meta.name}
					</span>
					<span className="text-xs text-neutral-500 dark:text-neutral-500">{meta.name}</span>
				</div>
				<div className="flex items-center gap-3 shrink-0">
					{src ? <a href={src} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors">
							Open <ExternalIcon />
						</a> : null}
					{!loaded && src ? <button type="button" onClick={() => setLoaded(true)} onMouseEnter={() => setBtnHover(true)} onMouseLeave={() => setBtnHover(false)} className="inline-flex items-center gap-1.5 px-3 py-1.5 transition-colors" style={buttonStyle}>
							<PlayIcon />
							{label || 'Load editor'}
						</button> : null}
				</div>
			</div>

			{description ? <div className="px-4 pt-3 pb-1 text-sm text-neutral-600 dark:text-neutral-400">{description}</div> : null}

			{!src ? <div className="px-4 py-6 text-sm text-red-600 dark:text-red-400" style={monoStyle}>
					SandboxEmbed: missing required `src`.
				</div> : loaded ? <iframe src={src} title={title || meta.name} className="w-full block border-0" style={{
    height: frameHeight + 'px',
    backgroundColor: 'rgba(128, 128, 128, 0.05)'
  }} allow={allowAttr} loading="lazy" allowFullScreen /> : <button type="button" onClick={() => setLoaded(true)} className="w-full flex flex-col items-center justify-center gap-2 text-neutral-500 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 transition-colors" style={{
    height: frameHeight + 'px',
    cursor: 'pointer',
    ...surfaceStyle
  }}>
					<PlayIcon />
					<span className="text-sm" style={monoStyle}>Click to load {meta.name}</span>
					<span className="text-xs">Loads {meta.host} in an embedded editor</span>
				</button>}
		</div>;
};

# ERC-721 Interaction

Standard ERC-721 contracts work on Sei without modification. This page covers reading token ownership, transferring NFTs, and managing approvals.

## Try it: deploy an ERC-721

Compile and deploy a real ERC-721 to Sei testnet from the browser — the Remix sandbox preloads an OpenZeppelin-based `DemoNFT` with a public `mint()`. Then point the patterns below at your deployed address.

First, add Sei testnet to your wallet:

<AddSeiButton network="testnet" label="Add Sei testnet" />

In Remix, compile under **Solidity Compiler**, then **Deploy & Run** with **Environment** set to **Injected Provider — MetaMask**.

<SandboxEmbed kind="remix" src="https://remix.ethereum.org/?#activate=solidity,fileManager&code=Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yNDsKCmltcG9ydCAiQG9wZW56ZXBwZWxpbi9jb250cmFjdHMvdG9rZW4vRVJDNzIxL0VSQzcyMS5zb2wiOwoKY29udHJhY3QgRGVtb05GVCBpcyBFUkM3MjEgewogICAgdWludDI1NiBwdWJsaWMgbmV4dElkOwogICAgY29uc3RydWN0b3IoKSBFUkM3MjEoIkRlbW8gTkZUIiwgIkRORlQiKSB7fQogICAgZnVuY3Rpb24gbWludCgpIGV4dGVybmFsIHsgX3NhZmVNaW50KG1zZy5zZW5kZXIsIG5leHRJZCsrKTsgfQp9" title="DemoNFT (ERC-721) · deploy to Sei testnet" description="Remix IDE with an OpenZeppelin ERC-721 preloaded. Compile in-browser and deploy to Sei testnet." />

## Setup

<CodeGroup>
  ```ts viem theme={"dark"}
  import { createPublicClient, createWalletClient, http, parseAbi } from 'viem';
  import { privateKeyToAccount } from 'viem/accounts';
  import { sei } from 'viem/chains';

  const client = createPublicClient({ chain: sei, transport: http() });
  const account = privateKeyToAccount('0xYourPrivateKey');
  const walletClient = createWalletClient({ account, chain: sei, transport: http() });

  const ERC721_ABI = parseAbi([
    'function name() view returns (string)',
    'function symbol() view returns (string)',
    'function totalSupply() view returns (uint256)',
    'function balanceOf(address owner) view returns (uint256)',
    'function ownerOf(uint256 tokenId) view returns (address)',
    'function tokenURI(uint256 tokenId) view returns (string)',
    'function getApproved(uint256 tokenId) view returns (address)',
    'function isApprovedForAll(address owner, address operator) view returns (bool)',
    'function approve(address to, uint256 tokenId)',
    'function setApprovalForAll(address operator, bool approved)',
    'function transferFrom(address from, address to, uint256 tokenId)',
    'function safeTransferFrom(address from, address to, uint256 tokenId)',
    'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
    'event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)',
  ]);

  const NFT = '0xNFTContractAddress';
  ```

  ```ts ethers theme={"dark"}
  import { ethers } from 'ethers';

  const provider = new ethers.JsonRpcProvider('https://evm-rpc.sei-apis.com');
  const wallet = new ethers.Wallet('0xYourPrivateKey', provider);

  const ERC721_ABI = [
    'function name() view returns (string)',
    'function symbol() view returns (string)',
    'function totalSupply() view returns (uint256)',
    'function balanceOf(address owner) view returns (uint256)',
    'function ownerOf(uint256 tokenId) view returns (address)',
    'function tokenURI(uint256 tokenId) view returns (string)',
    'function getApproved(uint256 tokenId) view returns (address)',
    'function isApprovedForAll(address owner, address operator) view returns (bool)',
    'function approve(address to, uint256 tokenId)',
    'function setApprovalForAll(address operator, bool approved)',
    'function transferFrom(address from, address to, uint256 tokenId)',
    'function safeTransferFrom(address from, address to, uint256 tokenId)',
    'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)',
    'event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)',
  ];

  const NFT = '0xNFTContractAddress';
  const readContract = new ethers.Contract(NFT, ERC721_ABI, provider);
  const writeContract = new ethers.Contract(NFT, ERC721_ABI, wallet);
  ```
</CodeGroup>

## Reading Token Data

<CodeGroup>
  ```ts viem theme={"dark"}
  const tokenId = 1n;

  const [owner, uri, approved] = await Promise.all([
    client.readContract({ address: NFT, abi: ERC721_ABI, functionName: 'ownerOf', args: [tokenId] }),
    client.readContract({ address: NFT, abi: ERC721_ABI, functionName: 'tokenURI', args: [tokenId] }),
    client.readContract({ address: NFT, abi: ERC721_ABI, functionName: 'getApproved', args: [tokenId] }),
  ]);
  ```

  ```ts ethers theme={"dark"}
  const tokenId = 1n;

  const [owner, uri, approved] = await Promise.all([
    readContract.ownerOf(tokenId),
    readContract.tokenURI(tokenId),
    readContract.getApproved(tokenId),
  ]);
  ```
</CodeGroup>

## Checking Ownership Count

<CodeGroup>
  ```ts viem theme={"dark"}
  const balance = await client.readContract({
    address: NFT,
    abi: ERC721_ABI,
    functionName: 'balanceOf',
    args: ['0xOwnerAddress'],
  });
  ```

  ```ts ethers theme={"dark"}
  const balance = await readContract.balanceOf('0xOwnerAddress');
  ```
</CodeGroup>

## Transferring an NFT

<CodeGroup>
  ```ts viem theme={"dark"}
  const hash = await walletClient.writeContract({
    address: NFT,
    abi: ERC721_ABI,
    functionName: 'safeTransferFrom',
    args: [account.address, '0xRecipient', 1n],
  });

  const receipt = await client.waitForTransactionReceipt({ hash });
  ```

  ```ts ethers theme={"dark"}
  const tx = await writeContract.safeTransferFrom(wallet.address, '0xRecipient', 1n);
  const receipt = await tx.wait();
  ```
</CodeGroup>

## Approving a Single Token

<CodeGroup>
  ```ts viem theme={"dark"}
  const hash = await walletClient.writeContract({
    address: NFT,
    abi: ERC721_ABI,
    functionName: 'approve',
    args: ['0xOperatorAddress', 1n],
  });
  ```

  ```ts ethers theme={"dark"}
  const tx = await writeContract.approve('0xOperatorAddress', 1n);
  ```
</CodeGroup>

## Approving All Tokens (Operator)

<CodeGroup>
  ```ts viem theme={"dark"}
  // Grant operator approval
  const hash = await walletClient.writeContract({
    address: NFT,
    abi: ERC721_ABI,
    functionName: 'setApprovalForAll',
    args: ['0xOperatorAddress', true],
  });

  // Revoke
  const revokeHash = await walletClient.writeContract({
    address: NFT,
    abi: ERC721_ABI,
    functionName: 'setApprovalForAll',
    args: ['0xOperatorAddress', false],
  });
  ```

  ```ts ethers theme={"dark"}
  // Grant operator approval
  const tx = await writeContract.setApprovalForAll('0xOperatorAddress', true);

  // Revoke
  const revokeTx = await writeContract.setApprovalForAll('0xOperatorAddress', false);
  ```
</CodeGroup>

## Watching Transfer Events

<CodeGroup>
  ```ts viem theme={"dark"}
  const unwatch = client.watchContractEvent({
    address: NFT,
    abi: ERC721_ABI,
    eventName: 'Transfer',
    onLogs: (logs) => {
      logs.forEach(({ args }) => {
        console.log(`Token ${args.tokenId}: ${args.from} → ${args.to}`);
      });
    },
  });

  // Stop watching
  unwatch();
  ```

  ```ts ethers theme={"dark"}
  readContract.on('Transfer', (from, to, tokenId) => {
    console.log(`Token ${tokenId}: ${from} → ${to}`);
  });

  // Stop watching
  readContract.off('Transfer');
  ```
</CodeGroup>

## CosmWasm NFT Compatibility

CW721 tokens on Sei have ERC-721 pointer contracts that expose the standard ERC-721 interface. You can use all of the patterns above against a CW721 pointer address. See [Pointer Contracts](/evm/evm-parity/examples/pointer-contracts) for how to look up the pointer address.
