Skip to main content

Connect to multichain using React Native

Get started with MetaMask Connect Multichain in your React Native or Expo dapp. Connect to EVM and Solana networks simultaneously through a single session.

Steps

1. Create a new project

Create a new React Native or Expo project:

npx react-native@latest init MyMultichainProject

2. Install dependencies

Install MetaMask Connect Multichain and required polyfill packages:

npm install @metamask/connect-multichain react-native-get-random-values buffer @react-native-async-storage/async-storage readable-stream

For Solana transaction building (optional):

npm install @solana/web3.js

3. Create polyfills

Create polyfills.ts (at the project root or in src/) with all required global shims. This file must be imported before any SDK code:

polyfills.ts
import { Buffer } from 'buffer';

global.Buffer = Buffer;

let windowObj: any;
if (typeof global !== 'undefined' && global.window) {
windowObj = global.window;
} else if (typeof window !== 'undefined') {
windowObj = window;
} else {
windowObj = {};
}

if (!windowObj.location) {
windowObj.location = {
hostname: 'mydapp.com',
href: 'https://mydapp.com',
};
}
if (typeof windowObj.addEventListener !== 'function') {
windowObj.addEventListener = () => {};
}
if (typeof windowObj.removeEventListener !== 'function') {
windowObj.removeEventListener = () => {};
}
if (typeof windowObj.dispatchEvent !== 'function') {
windowObj.dispatchEvent = () => true;
}

if (typeof global !== 'undefined') {
global.window = windowObj;
}

if (typeof global.Event === 'undefined') {
class EventPolyfill {
type: string;
bubbles: boolean;
cancelable: boolean;
defaultPrevented = false;
constructor(type: string, options?: EventInit) {
this.type = type;
this.bubbles = options?.bubbles ?? false;
this.cancelable = options?.cancelable ?? false;
}
preventDefault() { this.defaultPrevented = true; }
stopPropagation() {}
stopImmediatePropagation() {}
}
global.Event = EventPolyfill as any;
windowObj.Event = EventPolyfill as any;
}

if (typeof global.CustomEvent === 'undefined') {
const EventClass = global.Event || class { type: string; constructor(type: string) { this.type = type; } };
class CustomEventPolyfill extends (EventClass as any) {
detail: any;
constructor(type: string, options?: CustomEventInit) {
super(type, options);
this.detail = options?.detail ?? null;
}
}
global.CustomEvent = CustomEventPolyfill as any;
windowObj.CustomEvent = CustomEventPolyfill as any;
}

Create the empty module stub used by the Metro config:

src/empty-module.js
module.exports = {};
tip

For detailed troubleshooting of polyfill issues, see React Native Metro polyfill issues.

4. Configure Metro

Metro cannot resolve Node.js built-in modules. Map them to React Native-compatible shims or the empty module stub:

metro.config.js
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config")
const path = require("path")

const emptyModule = path.resolve(__dirname, "src/empty-module.js")

const config = {
resolver: {
extraNodeModules: {
stream: require.resolve("readable-stream"),
crypto: emptyModule,
http: emptyModule,
https: emptyModule,
net: emptyModule,
tls: emptyModule,
zlib: emptyModule,
os: emptyModule,
dns: emptyModule,
assert: emptyModule,
url: emptyModule,
path: emptyModule,
fs: emptyModule,
},
},
}

module.exports = mergeConfig(getDefaultConfig(__dirname), config)

5. Set up the entry file

The import order is critical. react-native-get-random-values must be the very first import, followed by the polyfills file, before any other code:

index.js or App.tsx (Bare RN) / app/_layout.tsx (Expo Router)
import 'react-native-get-random-values'
import './polyfills'
caution

If you import anything from @metamask/connect-multichain before react-native-get-random-values, you will get crypto.getRandomValues is not a function.

6. Use MetaMask Connect Multichain

Initialize the multichain client and use it to connect to both EVM and Solana networks in a single session. mobile.preferredOpenLink is required -- it tells MetaMask Connect how to open deeplinks to the MetaMask Mobile app:

import React, { useEffect, useRef, useState, useCallback } from 'react'
import { View, Text, TouchableOpacity, StyleSheet, Alert, Linking, ScrollView } from 'react-native'
import {
createMultichainClient,
getInfuraRpcUrls,
} from '@metamask/connect-multichain'

const ETH_MAINNET = 'eip155:1'
const POLYGON = 'eip155:137'
const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'

let clientPromise = null

function getClient() {
if (!clientPromise) {
clientPromise = createMultichainClient({
dapp: {
name: 'My Multichain RN DApp',
url: 'https://mydapp.com',
},
api: {
supportedNetworks: getInfuraRpcUrls({
infuraApiKey: 'YOUR_INFURA_API_KEY',
}),
},
mobile: {
preferredOpenLink: (deeplink) => Linking.openURL(deeplink),
},
})
}
return clientPromise
}

export default function App() {
const clientRef = useRef(null)
const [session, setSession] = useState(null)
const [connecting, setConnecting] = useState(false)

useEffect(() => {
let mounted = true

async function init() {
const client = await getClient()
if (!mounted) return
clientRef.current = client

client.on('wallet_sessionChanged', (newSession) => {
if (mounted) setSession(newSession)
})
}

init()
return () => { mounted = false }
}, [])

const handleConnect = useCallback(async () => {
const client = clientRef.current
if (!client) return

setConnecting(true)
try {
await client.connect(
[ETH_MAINNET, POLYGON, SOLANA_MAINNET],
[],
)
const newSession = await client.getSession()
setSession(newSession)
} catch (err) {
if (err.code === 4001) {
Alert.alert('Rejected', 'Connection was rejected.')
return
}
Alert.alert('Error', err.message ?? 'Connection failed')
} finally {
setConnecting(false)
}
}, [])

const handleGetBalance = useCallback(async () => {
const client = clientRef.current
if (!client || !session) return

const ethAccounts = session.sessionScopes?.[ETH_MAINNET]?.accounts ?? []
if (ethAccounts.length === 0) return

try {
const address = ethAccounts[0].split(':').pop()
const balance = await client.invokeMethod({
scope: ETH_MAINNET,
request: {
method: 'eth_getBalance',
params: [address, 'latest'],
},
})
const ethBalance = (parseInt(balance, 16) / 1e18).toFixed(6)
Alert.alert('ETH Balance', `${ethBalance} ETH`)
} catch (err) {
Alert.alert('Error', err.message)
}
}, [session])

const handleSignSolana = useCallback(async () => {
const client = clientRef.current
if (!client || !session) return

const solAccounts = session.sessionScopes?.[SOLANA_MAINNET]?.accounts ?? []
if (solAccounts.length === 0) return

try {
const pubkey = solAccounts[0].split(':').pop()
const message = Buffer.from('Hello from Multichain RN!').toString('base64')
const result = await client.invokeMethod({
scope: SOLANA_MAINNET,
request: {
method: 'solana_signMessage',
params: { message, pubkey },
},
})
Alert.alert('Signed', result.signature.slice(0, 40) + '...')
} catch (err) {
Alert.alert('Sign failed', err.message)
}
}, [session])

const handleDisconnect = useCallback(async () => {
const client = clientRef.current
if (!client) return
await client.disconnect()
setSession(null)
}, [])

const scopes = Object.keys(session?.sessionScopes ?? {})
const isConnected = scopes.length > 0

return (
<ScrollView contentContainerStyle={styles.container}>
{!isConnected ? (
<TouchableOpacity
style={styles.button}
onPress={handleConnect}
disabled={connecting}
>
<Text style={styles.buttonText}>
{connecting ? 'Connecting...' : 'Connect (EVM + Solana)'}
</Text>
</TouchableOpacity>
) : (
<View>
<Text style={styles.heading}>Connected Scopes</Text>
{scopes.map((scope) => {
const accs = session.sessionScopes[scope]?.accounts ?? []
return (
<View key={scope} style={styles.scopeCard}>
<Text style={styles.scopeLabel}>{scope}</Text>
{accs.map((acc) => (
<Text key={acc} style={styles.label}>
{acc.split(':').pop()}
</Text>
))}
</View>
)
})}
<TouchableOpacity style={styles.button} onPress={handleGetBalance}>
<Text style={styles.buttonText}>Get ETH Balance</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={handleSignSolana}>
<Text style={styles.buttonText}>Sign Solana Message</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={handleDisconnect}>
<Text style={styles.buttonText}>Disconnect All</Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
)
}

const styles = StyleSheet.create({
container: { flexGrow: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
heading: { fontSize: 18, fontWeight: 'bold', marginBottom: 12 },
scopeCard: { backgroundColor: '#f5f5f5', padding: 12, borderRadius: 8, marginVertical: 6, width: '100%' },
scopeLabel: { fontSize: 14, fontWeight: '600', marginBottom: 4 },
button: { backgroundColor: '#037DD6', padding: 14, borderRadius: 8, marginVertical: 8, width: '100%' },
buttonText: { color: '#fff', fontSize: 16, textAlign: 'center' },
label: { fontSize: 12, color: '#555' },
})

7. iOS configuration

Add the metamask URL scheme to your Info.plist so the app can open MetaMask Mobile:

ios/MyMultichainProject/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>metamask</string>
</array>

8. Build and run

npx react-native run-android
npx react-native run-ios

Multichain client methods at a glance

MethodDescription
connect(scopes, caipAccountIds)Connects to MetaMask with multichain scopes
getSession()Returns the current session with approved accounts
invokeMethod({ scope, request })Calls an RPC method on a specific chain
disconnect()Disconnects all scopes and ends the session
disconnect(scopes)Disconnects specific scopes without ending the session
on(event, handler)Registers an event handler

Next steps