> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ethers v6 Quickstart

> Using ethers v6 with Sei in a Node.js script or browser context

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 [frameSrc, setFrameSrc] = useState(null);
  const [btnHover, setBtnHover] = useState(false);
  const [isDark, setIsDark] = useState(true);
  useLayoutEffect(() => {
    const el = document.documentElement;
    const update = () => setIsDark(el.classList.contains('dark'));
    update();
    const obs = new MutationObserver(update);
    obs.observe(el, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => obs.disconnect();
  }, []);
  const themedSrc = (() => {
    if (!src) return src;
    try {
      const url = new URL(src);
      url.searchParams.set('theme', isDark ? 'dark' : 'light');
      return url.toString();
    } catch {
      return src;
    }
  })();
  const loadEditor = () => {
    if (themedSrc) setFrameSrc(themedSrc);
  };
  const loaded = frameSrc !== null;
  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 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="not-prose w-full rounded-lg border overflow-hidden my-4" style={{
    borderColor: HAIRLINE
  }}>
			<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b" 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-600 dark:text-neutral-400">{meta.name}</span>
				</div>
				<div className="flex items-center gap-3 shrink-0">
					{themedSrc ? <a href={themedSrc} 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 && themedSrc ? <button type="button" onClick={loadEditor} 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}

			{!themedSrc ? <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={frameSrc} 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={loadEditor} className="w-full flex flex-col items-center justify-center gap-2 text-neutral-600 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>;
};

export const RunSnippet = props => {
  const {method = 'eth_blockNumber', params = [], network = 'testnet', endpoint, label, title, description, decode = 'auto'} = props || ({});
  const ENDPOINTS = {
    testnet: 'https://evm-rpc-testnet.sei-apis.com',
    mainnet: 'https://evm-rpc.sei-apis.com'
  };
  const rpcUrl = endpoint || ENDPOINTS[network] || ENDPOINTS.testnet;
  const networkLabel = network === 'mainnet' ? 'Sei Mainnet' : network === 'testnet' ? 'Sei Testnet' : network;
  const requestBody = {
    jsonrpc: '2.0',
    id: 1,
    method,
    params
  };
  const requestJson = JSON.stringify(requestBody, null, 2);
  const [phase, setPhase] = useState('idle');
  const [result, setResult] = useState(null);
  const [errorMsg, setErrorMsg] = useState(null);
  const [elapsed, setElapsed] = useState(null);
  const [copied, setCopied] = useState(false);
  const [btnHover, setBtnHover] = useState(false);
  const groupThousands = s => s.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  const hexToDecimal = value => {
    if (typeof value !== 'string' || !(/^0x[0-9a-fA-F]+$/).test(value)) return null;
    if (value.length > 66) return null;
    try {
      return groupThousands(BigInt(value).toString(10));
    } catch (e) {
      return null;
    }
  };
  const run = async () => {
    setPhase('loading');
    setErrorMsg(null);
    setResult(null);
    setElapsed(null);
    const startedAt = typeof performance !== 'undefined' ? performance.now() : null;
    try {
      const response = await fetch(rpcUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });
      const data = await response.json();
      if (startedAt != null && typeof performance !== 'undefined') {
        setElapsed(Math.round(performance.now() - startedAt));
      }
      if (data && data.error) {
        setErrorMsg(data.error.message || 'RPC returned an error');
        setPhase('error');
        return;
      }
      setResult(data ? data.result : undefined);
      setPhase('success');
    } catch (err) {
      setErrorMsg(err && err.message ? err.message : 'Request failed');
      setPhase('error');
    }
  };
  const resultString = result === undefined ? 'undefined' : JSON.stringify(result, null, 2);
  const decoded = decode !== 'off' && typeof result === 'string' ? hexToDecimal(result) : null;
  const copyResult = () => {
    const flashCopied = () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    };
    if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(resultString).then(flashCopied, () => {});
      return;
    }
    if (typeof document !== 'undefined') {
      try {
        const ta = document.createElement('textarea');
        ta.value = resultString;
        ta.style.position = 'fixed';
        ta.style.opacity = '0';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        document.body.removeChild(ta);
        flashCopied();
      } catch (e) {}
    }
  };
  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 codeStyle = {
    backgroundColor: 'rgba(128, 128, 128, 0.05)',
    fontFamily: 'var(--sei-font-mono)'
  };
  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',
    opacity: phase === 'loading' ? 0.7 : 1,
    cursor: phase === 'loading' ? 'default' : 'pointer'
  };
  return <div className="not-prose w-full rounded-lg border overflow-hidden my-4" style={{
    borderColor: HAIRLINE
  }}>
			<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-b" 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 || method}
					</span>
					<span className="text-xs text-neutral-600 dark:text-neutral-400">{networkLabel}</span>
				</div>
				<button type="button" onClick={run} disabled={phase === 'loading'} onMouseEnter={() => setBtnHover(true)} onMouseLeave={() => setBtnHover(false)} className="inline-flex items-center gap-1.5 px-3 py-1.5 shrink-0 transition-colors" style={buttonStyle}>
					{phase === 'loading' ? <svg className="animate-spin h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
								<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
								<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
							</svg> : <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
								<path d="M8 5v14l11-7z" />
							</svg>}
					{phase === 'loading' ? 'Running…' : label || 'Run'}
				</button>
			</div>

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

			<div className="px-4 pt-3 pb-1">
				<span className="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">Request</span>
			</div>
			<pre className="m-0 px-4 py-3 text-sm overflow-x-auto text-neutral-700 dark:text-neutral-300" style={codeStyle}>
				{requestJson}
			</pre>

			{phase === 'success' ? <div className="border-t" style={{
    borderTopColor: HAIRLINE
  }}>
					<div className="flex items-center justify-between px-4 pt-3 pb-1">
						<span className="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">Response{elapsed != null ? ` · ${elapsed} ms` : ''}</span>
						<button type="button" onClick={copyResult} className="text-xs text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors">
							{copied ? 'Copied' : 'Copy'}
						</button>
					</div>
					<pre className="m-0 px-4 py-3 text-sm overflow-x-auto text-neutral-700 dark:text-neutral-300" style={codeStyle}>
						{resultString}
					</pre>
					{decoded ? <div className="px-4 pb-3 text-xs text-neutral-600 dark:text-neutral-400" style={monoStyle}>
							= {decoded} (decimal)
						</div> : null}
				</div> : null}

			{phase === 'error' ? <div className="border-t" style={{
    borderTopColor: HAIRLINE
  }}>
					<div className="px-4 pt-3 pb-1">
						<span className="text-xs uppercase tracking-wide text-neutral-600 dark:text-neutral-400">Error</span>
					</div>
					<pre className="m-0 px-4 py-3 text-sm overflow-x-auto text-red-600 dark:text-red-400" style={codeStyle}>
						{errorMsg}
					</pre>
				</div> : null}
		</div>;
};

# ethers v6 quickstart

This example shows how to use ethers v6 with Sei. The patterns apply whether you write a Node.js script, a CLI tool, or a browser dApp.

## Install

```bash theme={"dark"}
npm install ethers
```

These snippets are TypeScript, and they use top-level `await`. To run one without a separate build step, save it as `script.mts`. Then run it with [`tsx`](https://www.npmjs.com/package/tsx) (Node 18+):

```bash theme={"dark"}
npx tsx script.mts
```

The `.mts` extension tells Node.js to treat the file as an ES module. If you use a plain `.ts` file in a new npm project, `tsx` fails with `Top-level await is currently not supported with the "cjs" output format`.

## Read-only provider

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

const provider = new ethers.JsonRpcProvider('https://evm-rpc.sei-apis.com');
```

## Reading chain data

This is the first milestone. It does not need a private key.

```ts theme={"dark"}
// Any valid Sei EVM address — swap in your own
const address = '0x0000000000000000000000000000000000000000';

const blockNumber = await provider.getBlockNumber(); // number
console.log('Block number:', blockNumber);

const balance = await provider.getBalance(address); // bigint, wei
console.log('Balance (SEI):', ethers.formatEther(balance));

const nonce = await provider.getTransactionCount(address); // number
console.log('Nonce:', nonce);
```

**You are done when you see:**

```text theme={"dark"}
Block number: 148203117
Balance (SEI): 12.5
Nonce: 7
```

The exact numbers are illustrative. Your block number and balance will differ. `getBlockNumber()` and `getTransactionCount()` return a `number`, while `getBalance()` returns a `bigint` in wei. To format the balance, use `ethers.formatEther()`.

## Try it live

Run these read-only calls against Sei Mainnet. You do not need to install anything or use a private key. They are plain JSON-RPC calls, the same reads that the script above performs.

<RunSnippet method="eth_blockNumber" network="mainnet" title="eth_blockNumber" />

<RunSnippet method="eth_getBalance" params={["0x0000000000000000000000000000000000000000", "latest"]} network="mainnet" title="eth_getBalance" description="Balance in wei of the example address at the latest block. You can replace the example address with any address." />

## Edit and run

Edit and re-run the full ethers code from this page live in the browser. The sandbox uses a real ethers v6 `JsonRpcProvider` that reads **Sei Testnet**. Its block number and balances therefore do not match the Sei Mainnet reads above:

<SandboxEmbed kind="codesandbox" src="https://codesandbox.io/embed/qs82lw?view=split&hidenavigation=1&theme=dark" title="ethers · read Sei Testnet" description="A real ethers v6 provider that reads Sei Testnet. Edit index.js and the preview re-runs." />

## Browser provider

In a browser context, connect to the user's injected wallet:

```ts theme={"dark"}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
```

## Wallet from private key

Use this pattern in scripts and backend services:

```ts theme={"dark"}
const wallet = new ethers.Wallet('0xYourPrivateKey', provider);
```

## Sending a transaction

This next step requires a funded account. Get testnet SEI from the [faucet](/learn/faucet).

```ts theme={"dark"}
const tx = await wallet.sendTransaction({
  to: '0xRecipient',
  value: ethers.parseEther('1'),
});

const receipt = await tx.wait();
// receipt is final immediately — Sei has instant finality
```

## Reading a contract

```ts theme={"dark"}
const abi = ['function balanceOf(address owner) view returns (uint256)'];
const contract = new ethers.Contract('0xTokenAddress', abi, provider);

const balance = await contract.balanceOf('0xYourAddress');
```

## Writing to a contract

For write operations, pass a signer (a wallet or a browser signer) to the contract constructor:

```ts theme={"dark"}
const contract = new ethers.Contract('0xTokenAddress', abi, wallet);

const tx = await contract.transfer('0xRecipient', 1_000_000n);
const receipt = await tx.wait();
```

## Estimating gas

```ts theme={"dark"}
const gas = await provider.estimateGas({
  from: wallet.address,
  to: '0xContractAddress',
  data: '0xCalldata',
});
```

## Listening for events

Listen for new events over the WebSocket endpoint. Listeners that poll the public HTTP endpoint can miss events or report them twice. The contract needs the event in its ABI:

```ts theme={"dark"}
const wsProvider = new ethers.WebSocketProvider('wss://evm-ws.sei-apis.com');
const token = new ethers.Contract(
  '0xTokenAddress',
  ['event Transfer(address indexed from, address indexed to, uint256 value)'],
  wsProvider
);

token.on('Transfer', (from, to, value) => {
  console.log('Transfer:', { from, to, value });
});

// To stop: await token.off('Transfer'); await wsProvider.destroy();
```

For past events and more examples, see [Listen to events](/evm/cookbook/listen-to-events).

## Next steps

* [Python quickstart (web3.py)](/evm/python-quickstart): the same first steps in Python
* [ERC-20 interaction](/evm/evm-parity/examples/erc20): full token read and write examples
* [ERC-721 interaction](/evm/evm-parity/examples/erc721): NFT ownership, transfers, and approvals
* [Pointer contracts](/evm/evm-parity/examples/pointer-contracts): interact with CosmWasm tokens through ERC interfaces
* [WebSocket connections](/evm/evm-parity/websocket): real-time block and event subscriptions


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.