# Build a Multichain Dapp Tutorial - MetaMask Connect

> Step-by-step tutorial to build a React dapp that connects to Ethereum, Linea, Base, and Solana using MetaMask Connect Multichain SDK.

# Create a multichain dapp

In this tutorial, you'll build a React dapp that connects to four networks (Ethereum, Linea, Base, and Solana) using MetaMask Connect Multichain.
Your dapp will handle wallet sign-in and sign-out, read balances across all four chains, sign messages, and send transactions on all four chains. You'll learn how to do the following:

- Set up a multichain session with a single connection prompt.
- Read account balances across EVM networks and Solana.
- Sign messages in both ecosystems.
- Send transactions on EVM and Solana.

## Key concepts

This tutorial uses [scopes](../concepts/scopes.md), [account IDs](../concepts/accounts.md), and [sessions](../concepts/sessions.md) to identify chains and
accounts across ecosystems. If you're unfamiliar with these concepts, review them before continuing.

This tutorial uses the following scopes:

| Chain            | Scope (CAIP-2)                            |
| ---------------- | ----------------------------------------- |
| Ethereum Mainnet | `eip155:1`                                |
| Linea Mainnet    | `eip155:59144`                            |
| Base Mainnet     | `eip155:8453`                             |
| Solana Mainnet   | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` |

:::note
Bitcoin and Tron support is coming soon.
The Multichain API is designed to be ecosystem-agnostic, so new chains can be added without changing
your integration pattern.
:::

## Prerequisites

- [Node.js](https://nodejs.org/) version 19 or later installed.
- A package manager such as [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), [Yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/installation), or [bun](https://bun.sh/).
- [MetaMask](https://metamask.io/download) installed in your browser.
- An [Infura API key](/developer-tools/dashboard/get-started/create-api) from the [Infura dashboard](https://app.infura.io).

## Steps

### 1. Scaffold the project

Create a new React + TypeScript project with Vite:

```bash
npm create vite@latest multichain-dapp -- --template react-ts
cd multichain-dapp
```

Install the MetaMask Connect multichain client and Solana Kit:

```bash npm2yarn
npm install @metamask/connect-multichain @solana/kit
```

`@solana/kit` is needed to query Solana balances directly, since
[`invokeMethod`](../reference/api.md#wallet_invokemethod) doesn't currently support `getBalance` for
Solana.

### 2. Initialize the multichain client

Create a file `src/multichain.ts`, and initialize the client using [`createMultichainClient`](../reference/methods.md#createmultichainclient):

```typescript title="src/multichain.ts"

export const SCOPES = {
  ETHEREUM: 'eip155:1',
  LINEA: 'eip155:59144',
  BASE: 'eip155:8453',
  SOLANA: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
} as const

let client: Awaited<ReturnType<typeof createMultichainClient>> | null = null

export async function getClient() {
  if (!client) {
    client = await createMultichainClient({
      dapp: {
        name: 'Multichain Tutorial Dapp',
        url: window.location.href,
      },
      api: {
        supportedNetworks: {
          [SCOPES.ETHEREUM]: 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.LINEA]: 'https://linea-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.BASE]: 'https://base-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.SOLANA]: 'https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
        },
      },
    })
  }
  return client
}
```

Configure MetaMask Connect Multichain with the following options:

- `dapp`: Your dapp's identity.
  MetaMask shows the `name` and `url` during the connection prompt so users know who is requesting
  access.
- `api.supportedNetworks`: A map of [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md)
  scope IDs to RPC endpoint URLs.
  Each entry tells the client which chains your dapp supports and where to send RPC requests.

### 3. Connect (sign-in)

Call [`connect`](../reference/methods.md#connect) with the scopes you want.
The user sees a single approval prompt for all four chains:

```typescript

const client = await getClient()

await client.connect([SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE, SCOPES.SOLANA], [])
```

The second argument is an optional array of [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md)
account preferences.
Pass an empty array to let the user choose their own accounts.

After connecting, retrieve the session to see which accounts the user authorized:

```typescript
const session = await client.provider.getSession()

const ethAccounts = session.sessionScopes[SCOPES.ETHEREUM]?.accounts || []
const lineaAccounts = session.sessionScopes[SCOPES.LINEA]?.accounts || []
const baseAccounts = session.sessionScopes[SCOPES.BASE]?.accounts || []
const solAccounts = session.sessionScopes[SCOPES.SOLANA]?.accounts || []
```

Each account is a CAIP-10 string like `eip155:1:0xabc123...` or `solana:5eykt...:83ast...`.
To extract the raw address, split on `:` and take everything after the second colon:

```typescript
function extractAddress(caip10Account: string): string {
  return caip10Account.split(':').slice(2).join(':')
}

const ethAddress = extractAddress(ethAccounts[0])
// "0xabc123..."
```

### 4. Disconnect (sign-out)

Call [`disconnect`](../reference/methods.md#disconnect) to end the session and clear all authorizations:

```typescript
await client.disconnect()
```

This revokes the active session
(`disconnect` wraps [`wallet_revokeSession`](../reference/api.md#wallet_revokesession)).
The user will need to approve a new connection prompt to use your dapp again.

### 5. Fetch balances

#### EVM balances

Use [`invokeMethod`](../reference/methods.md#invokemethod) with [`eth_getBalance`](../../evm/reference/json-rpc-api/eth_getBalance.mdx) to query the
balance on any EVM chain in the session.
The result is a hex-encoded wei value:

```typescript
async function getEvmBalance(scope: string, accounts: string[]): Promise<string> {
  if (accounts.length === 0) return '0'

  const address = extractAddress(accounts[0])
  const balanceHex = await client.invokeMethod({
    scope,
    request: {
      method: 'eth_getBalance',
      params: [address, 'latest'],
    },
  })

  const wei = BigInt(balanceHex as string)
  const eth = Number(wei) / 1e18
  return eth.toFixed(6)
}

const ethBalance = await getEvmBalance(SCOPES.ETHEREUM, ethAccounts)
const lineaBalance = await getEvmBalance(SCOPES.LINEA, lineaAccounts)
const baseBalance = await getEvmBalance(SCOPES.BASE, baseAccounts)
```

The same function works for all three EVM chains; only the `scope` changes.

#### Solana balance

:::note
`invokeMethod` doesn't currently support `getBalance` for Solana.
If this is something you'd like to see,
[raise a feature request](https://builder.metamask.io/c/feature-request/10) on the MetaMask
Builder Hub.
:::

Use `@solana/kit` to query the Solana RPC directly:

```typescript

async function getSolBalance(accounts: string[]): Promise<string> {
  if (accounts.length === 0) return '0'

  const solAddress = extractAddress(accounts[0])
  const rpc = createSolanaRpc('https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY')
  const { value } = await rpc.getBalance(address(solAddress)).send()

  const sol = Number(value) / 1e9
  return sol.toFixed(6)
}

const solBalance = await getSolBalance(solAccounts)
```

### 6. Sign a message

#### EVM (`personal_sign`)

To sign a message on an EVM chain, hex-encode the message and use [`invokeMethod`](../reference/methods.md#invokemethod) with [`personal_sign`](../../evm/reference/json-rpc-api/personal_sign.mdx):

```typescript
function toHex(str: string): string {
  return (
    '0x' + Array.from(new TextEncoder().encode(str), b => b.toString(16).padStart(2, '0')).join('')
  )
}

const evmAddress = extractAddress(ethAccounts[0])
const signature = await client.invokeMethod({
  scope: SCOPES.ETHEREUM,
  request: {
    method: 'personal_sign',
    params: [toHex('Hello from my multichain dapp!'), evmAddress],
  },
})
console.log('EVM signature:', signature)
```

#### Solana (`solana_signMessage`)

Use [`invokeMethod`](../reference/methods.md#invokemethod) with `solana_signMessage` to sign a message on Solana:

```typescript
const solAddress = extractAddress(solAccounts[0])
const signature = await client.invokeMethod({
  scope: SCOPES.SOLANA,
  request: {
    method: 'solana_signMessage',
    params: {
      message: btoa('Hello from my multichain dapp!'),
      pubkey: solAddress,
    },
  },
})
console.log('SOL signature:', signature)
```

### 7. Send a transaction

#### EVM transaction

Use [`invokeMethod`](../reference/methods.md#invokemethod) with [`eth_sendTransaction`](../../evm/reference/json-rpc-api/eth_sendTransaction.mdx) to send a transaction on any EVM scope:

```typescript
const fromAddress = extractAddress(ethAccounts[0])

const txHash = await client.invokeMethod({
  scope: SCOPES.ETHEREUM,
  request: {
    method: 'eth_sendTransaction',
    params: [
      {
        from: fromAddress,
        to: '0x0000000000000000000000000000000000000000',
        value: '0x0',
      },
    ],
  },
})
console.log('EVM tx hash:', txHash)
```

To send on a different chain, change the `scope`; for example, `SCOPES.LINEA` or `SCOPES.BASE`.
The same address format and RPC method works across all EVM chains.

#### Solana transaction

Use [`invokeMethod`](../reference/methods.md#invokemethod) with `solana_signAndSendTransaction` to send a Solana base64-encoded transaction:

```typescript
const result = await client.invokeMethod({
  scope: SCOPES.SOLANA,
  request: {
    method: 'solana_signAndSendTransaction',
    params: {
      transaction: '<base64-encoded-transaction>',
    },
  },
})
console.log('SOL tx signature:', result)
```

Building a Solana transaction requires assembling instructions, setting a fee payer, and fetching a
recent block hash using `@solana/kit`.
See [Send transactions](../guides/send-transactions.md) for a complete example.

## Full example

The following is the complete source for the two main files.
After scaffolding the project (Step 1), replace the contents of these files, run `npm run dev`, and
open the dapp in your browser.

```typescript title="src/multichain.ts"

export const SCOPES = {
  ETHEREUM: 'eip155:1',
  LINEA: 'eip155:59144',
  BASE: 'eip155:8453',
  SOLANA: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
} as const

export const SCOPE_LABELS: Record<string, string> = {
  [SCOPES.ETHEREUM]: 'Ethereum',
  [SCOPES.LINEA]: 'Linea',
  [SCOPES.BASE]: 'Base',
  [SCOPES.SOLANA]: 'Solana',
}

let client: Awaited<ReturnType<typeof createMultichainClient>> | null = null

export async function getClient() {
  if (!client) {
    client = await createMultichainClient({
      dapp: {
        name: 'Multichain Tutorial Dapp',
        url: window.location.href,
      },
      api: {
        supportedNetworks: {
          [SCOPES.ETHEREUM]: 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.LINEA]: 'https://linea-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.BASE]: 'https://base-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.SOLANA]: 'https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
        },
      },
    })
  }
  return client
}

export function extractAddress(caip10Account: string): string {
  return caip10Account.split(':').slice(2).join(':')
}

export function toHex(str: string): string {
  return (
    '0x' + Array.from(new TextEncoder().encode(str), b => b.toString(16).padStart(2, '0')).join('')
  )
}
```

```tsx title="src/App.tsx"

type ChainAccounts = Record<string, string[]>

export default function App() {
  const [connected, setConnected] = useState(false)
  const [accounts, setAccounts] = useState<ChainAccounts>({})
  const [balances, setBalances] = useState<Record<string, string>>({})
  const [log, setLog] = useState<string[]>([])

  const addLog = (entry: string) =>
    setLog(prev => [`[${new Date().toLocaleTimeString()}] ${entry}`, ...prev])

  // --- Connect / Disconnect ---

  const handleConnect = async () => {
    try {
      const client = await getClient()
      await client.connect([SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE, SCOPES.SOLANA], [])
      const session = await client.provider.getSession()
      const accts: ChainAccounts = {}
      for (const scope of Object.values(SCOPES)) {
        accts[scope] = session.sessionScopes[scope]?.accounts || []
      }
      setAccounts(accts)
      setConnected(true)
      addLog('Connected to all chains.')
    } catch (err: unknown) {
      addLog(`Connection failed: ${(err as Error).message}`)
    }
  }

  const handleDisconnect = async () => {
    try {
      const client = await getClient()
      await client.disconnect()
      setConnected(false)
      setAccounts({})
      setBalances({})
      addLog('Disconnected.')
    } catch (err: unknown) {
      addLog(`Disconnect failed: ${(err as Error).message}`)
    }
  }

  // --- Balances ---

  const fetchBalances = async () => {
    const client = await getClient()
    const result: Record<string, string> = {}

    for (const scope of [SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE] as const) {
      const accts = accounts[scope] || []
      if (accts.length > 0) {
        try {
          const addr = extractAddress(accts[0])
          const hex = (await client.invokeMethod({
            scope,
            request: { method: 'eth_getBalance', params: [addr, 'latest'] },
          })) as string
          result[scope] = (Number(BigInt(hex)) / 1e18).toFixed(6)
        } catch (err: unknown) {
          result[scope] = `Error: ${(err as Error).message}`
        }
      }
    }

    const solAccts = accounts[SCOPES.SOLANA] || []
    if (solAccts.length > 0) {
      try {
        const solAddr = extractAddress(solAccts[0])
        const rpc = createSolanaRpc('https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY')
        const { value } = await rpc.getBalance(address(solAddr)).send()
        result[SCOPES.SOLANA] = (Number(value) / 1e9).toFixed(6)
      } catch (err: unknown) {
        result[SCOPES.SOLANA] = `Error: ${(err as Error).message}`
      }
    }

    setBalances(result)
    addLog('Balances fetched.')
  }

  // --- Sign message ---

  const signEvmMessage = async () => {
    try {
      const client = await getClient()
      const addr = extractAddress(accounts[SCOPES.ETHEREUM]?.[0] || '')
      const sig = await client.invokeMethod({
        scope: SCOPES.ETHEREUM,
        request: {
          method: 'personal_sign',
          params: [toHex('Hello from my multichain dapp!'), addr],
        },
      })
      addLog(`EVM signature: ${sig}`)
    } catch (err: unknown) {
      addLog(`EVM sign failed: ${(err as Error).message}`)
    }
  }

  const signSolMessage = async () => {
    try {
      const client = await getClient()
      const solAddress = extractAddress(accounts[SCOPES.SOLANA]?.[0] || '')
      const sig = await client.invokeMethod({
        scope: SCOPES.SOLANA,
        request: {
          method: 'solana_signMessage',
          params: {
            message: btoa('Hello from my multichain dapp!'),
            pubkey: solAddress,
          },
        },
      })
      addLog(`SOL signature: ${JSON.stringify(sig)}`)
    } catch (err: unknown) {
      addLog(`SOL sign failed: ${(err as Error).message}`)
    }
  }

  // --- Send transaction ---

  const sendEvmTransaction = async () => {
    try {
      const client = await getClient()
      const addr = extractAddress(accounts[SCOPES.ETHEREUM]?.[0] || '')
      const txHash = await client.invokeMethod({
        scope: SCOPES.ETHEREUM,
        request: {
          method: 'eth_sendTransaction',
          params: [{ from: addr, to: addr, value: '0x0' }],
        },
      })
      addLog(`EVM tx hash: ${txHash}`)
    } catch (err: unknown) {
      addLog(`EVM tx failed: ${(err as Error).message}`)
    }
  }

  // --- Render ---

  return (
    
      Multichain Dapp

      {!connected ? (
        <button onClick={handleConnect}>Connect Wallet</button>
      ) : (
        <>
          <button onClick={handleDisconnect}>Disconnect</button>
          Accounts
          {Object.entries(accounts).map(([scope, accts]) => (
            
              {SCOPE_LABELS[scope] || scope}:{' '}
              <code>{accts.length > 0 ? extractAddress(accts[0]) : 'none'}</code>
            
          ))}
          Balances
          <button onClick={fetchBalances}>Fetch Balances</button>
          {Object.entries(balances).map(([scope, bal]) => (
            
              {SCOPE_LABELS[scope] || scope}: {bal}
            
          ))}
          Sign Message
          <button onClick={signEvmMessage}>Sign (EVM)</button>{' '}
          <button onClick={signSolMessage}>Sign (Solana)</button>
          Send Transaction
          <button onClick={sendEvmTransaction}>Send 0 ETH to Self</button>
        </>
      )}

      Log
      <pre
        style={{
          background: '#1a1a1a',
          color: '#e0e0e0',
          padding: 16,
          borderRadius: 8,
          maxHeight: 300,
          overflow: 'auto',
          fontSize: 13,
        }}>
        {log.length > 0 ? log.join('\n') : 'No activity yet.'}
      </pre>
    
  )
}
```

## Best practices

- **Handle user rejection.**
  Users can decline the connection prompt or any signing request.
  Always wrap SDK calls in `try/catch` and show a meaningful message when the user cancels.

- **Request only the scopes you need.**
  Don't request access to chains your dapp doesn't use.
  A shorter list of scopes makes the approval prompt clearer and builds trust.

- **Use CAIP-2 constants.**
  Define scope IDs in one place (as shown in `src/multichain.ts`) instead of scattering string
  literals across your codebase.

- **Leverage session persistence.**
  Sessions survive page reloads and new tabs.
  Check for an existing session on startup with `getSession` before prompting the user to connect
  again.

- **Show chain context clearly.**
  When displaying accounts, balances, or transaction prompts, always label which chain the action
  applies to.
  Users managing multiple chains need clear visual cues to avoid sending assets on the wrong
  network.

- **Degrade gracefully.**
  If the user declines access to some scopes, your dapp should still work for the chains they did
  approve.
  Check `session.sessionScopes` for each scope before calling `invokeMethod`.

## Next steps

- See the [Multichain SDK methods](../reference/methods.md) for all available methods and events.
- See the [Multichain API reference](../reference/api.md) for all available methods and events.
- See [Sign transactions](../guides/sign-transactions.md) for more signing patterns
  including Solana signing construction.
- See [Send transactions](../guides/send-transactions.md) for more transaction patterns
  including Solana transfer construction.
