| name | setup-solana-react-native-app |
| description | Set up a React Native app with @metamask/connect-solana using polyfills and multichain invokeMethod for Solana operations. Use when building Solana support in React Native where wallet-adapter is not available. |
Setup Solana React Native App with MetaMask
When to use
Use this skill when:
- Integrating MetaMask with Solana in a React Native application
- Setting up the required polyfills and metro shims for
@metamask/connect-solana
- Building Solana sign and send flows in React Native using
invokeMethod
- You need Solana support in React Native where
@solana/wallet-adapter-react is not available
Workflow
Step 1: Install dependencies
npm install @metamask/connect-solana @metamask/connect-multichain @solana/web3.js react-native-get-random-values buffer readable-stream @react-native-async-storage/async-storage
@metamask/connect-multichain is a regular dependency of @metamask/connect-solana and is installed transitively — but this skill imports createMultichainClient directly (to configure mobile.preferredOpenLink, which createSolanaClient does not forward), so declare it explicitly to keep strict package managers (pnpm) happy. The SDK warns at runtime if duplicate or mismatched copies are resolved.
Step 2: Create the polyfills file
Create polyfills.ts at the root of your project.
import { Buffer } from 'buffer';
global.Buffer = Buffer;
const eventListeners = new Map<string, Set<EventListener>>();
if (typeof global.window === 'undefined') {
(global as any).window = {
location: {
hostname: 'my-rn-app',
href: 'https://my-rn-app.local',
},
navigator: { product: 'ReactNative' },
addEventListener: (event: string, listener: EventListener) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)?.add(listener);
},
removeEventListener: (event: string, listener: EventListener) => {
eventListeners.get(event)?.delete(listener);
},
dispatchEvent: (_event: Event) => true,
};
}
Step 3: Import polyfills FIRST in the entry file
react-native-get-random-values must be the very first import in the entry file — it cannot be inside polyfills.ts because Metro may have already touched crypto by the time that file runs.
import 'react-native-get-random-values';
import './polyfills';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
Step 4: Configure metro shims
Add extraNodeModules to metro.config.js so the bundler can resolve Node.js built-in modules:
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);
Step 5: Create the Solana client
createSolanaClient does not forward mobile options to the underlying multichain core. In React Native, you must call createMultichainClient first with mobile.preferredOpenLink so the singleton core is configured for deeplinks, then call createSolanaClient which reuses that same core.
import { createMultichainClient } from '@metamask/connect-multichain';
import { createSolanaClient } from '@metamask/connect-solana';
import { Linking } from 'react-native';
await createMultichainClient({
dapp: {
name: 'My Solana RN App',
url: 'https://myapp.com',
},
api: {
supportedNetworks: {},
},
mobile: {
preferredOpenLink: (deeplink: string) => Linking.openURL(deeplink),
},
});
const solanaClient = await createSolanaClient({
dapp: {
name: 'My Solana RN App',
url: 'https://myapp.com',
},
api: {
supportedNetworks: {
mainnet: 'https://api.mainnet-beta.solana.com',
},
},
});
Step 6: Use multichain invokeMethod for Solana operations
There is no @solana/wallet-adapter-react in React Native. Instead, use the core multichain client and invokeMethod to call Solana RPC methods on specific CAIP scopes.
Connect
await solanaClient.core.connect(
['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
[],
);
Listen for wallet_sessionChanged to get accounts after connection:
solanaClient.core.on('wallet_sessionChanged', (session) => {
const caipAccounts =
session?.sessionScopes?.['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']?.accounts ?? [];
const solanaAccounts = caipAccounts.map((a) => a.split(':')[2]);
console.log('Solana accounts:', solanaAccounts);
});
Sign a message
const message = new TextEncoder().encode('Hello from React Native!');
const messageBase64 = Buffer.from(message).toString('base64');
const result = await solanaClient.core.invokeMethod({
scope: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
request: {
method: 'signMessage',
params: {
account: { address: solanaAccounts[0] },
message: messageBase64,
},
},
});
console.log('Signature:', result.signature);
Sign and send a transaction
import {
Connection,
Transaction,
SystemProgram,
PublicKey,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
const connection = new Connection('https://api.mainnet-beta.solana.com');
const { blockhash } = await connection.getLatestBlockhash();
const senderPubkey = new PublicKey(solanaAccounts[0]);
const tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: senderPubkey,
toPubkey: new PublicKey('11111111111111111111111111111112'),
lamports: 0.001 * LAMPORTS_PER_SOL,
}),
);
tx.recentBlockhash = blockhash;
tx.feePayer = senderPubkey;
const serializedTx = tx.serialize({ requireAllSignatures: false });
const txBase64 = Buffer.from(serializedTx).toString('base64');
const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
const sendResult = await solanaClient.core.invokeMethod({
scope: SOLANA_MAINNET,
request: {
method: 'signAndSendTransaction',
params: {
account: { address: solanaAccounts[0] },
transaction: txBase64,
scope: SOLANA_MAINNET,
},
},
});
console.log('Transaction signature:', sendResult.signature);
Disconnect
await solanaClient.disconnect();
Step 7: Full React Native component
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert, Linking } from 'react-native';
import { createMultichainClient } from '@metamask/connect-multichain';
import { createSolanaClient, SolanaClient } from '@metamask/connect-solana';
import {
Connection,
Transaction,
SystemProgram,
PublicKey,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
const MAINNET_SCOPE = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
const RPC_URL = 'https://api.mainnet-beta.solana.com';
export default function SolanaScreen() {
const [client, setClient] = useState<SolanaClient | null>(null);
const [accounts, setAccounts] = useState<string[]>([]);
useEffect(() => {
(async () => {
await createMultichainClient({
dapp: { name: 'My RN App', url: 'https://myapp.com' },
api: { supportedNetworks: {} },
mobile: { preferredOpenLink: (dl) => Linking.openURL(dl) },
});
const c = await createSolanaClient({
dapp: { name: 'My RN App', url: 'https://myapp.com' },
api: { supportedNetworks: { mainnet: RPC_URL } },
});
setClient(c);
c.core.on('wallet_sessionChanged', (session) => {
const caipAccounts = session?.sessionScopes?.[MAINNET_SCOPE]?.accounts ?? [];
setAccounts(caipAccounts.map((a) => a.split(':')[2]));
});
})();
}, []);
const handleConnect = async () => {
if (!client) return;
try {
await client.core.connect([MAINNET_SCOPE], []);
} catch (err: any) {
Alert.alert('Connection failed', err.message);
}
};
const handleSignMessage = async () => {
if (!client || accounts.length === 0) return;
try {
const message = Buffer.from('Hello from React Native!').toString('base64');
const result = await client.core.invokeMethod({
scope: MAINNET_SCOPE,
request: { method: 'signMessage', params: { account: { address: accounts[0] }, message } },
});
Alert.alert('Signed', JSON.stringify(result.signature).slice(0, 40) + '...');
} catch (err: any) {
Alert.alert('Sign failed', err.message);
}
};
const handleSendTransaction = async () => {
if (!client || accounts.length === 0) return;
try {
const connection = new Connection(RPC_URL);
const { blockhash } = await connection.getLatestBlockhash();
const sender = new PublicKey(accounts[0]);
const tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: sender,
toPubkey: new PublicKey('11111111111111111111111111111112'),
lamports: 0.001 * LAMPORTS_PER_SOL,
}),
);
tx.recentBlockhash = blockhash;
tx.feePayer = sender;
const txBase64 = Buffer.from(
tx.serialize({ requireAllSignatures: false }),
).toString('base64');
const result = await client.core.invokeMethod({
scope: MAINNET_SCOPE,
request: {
method: 'signAndSendTransaction',
params: { account: { address: accounts[0] }, transaction: txBase64, scope: MAINNET_SCOPE },
},
});
Alert.alert('Sent', result.signature);
} catch (err: any) {
Alert.alert('Transaction failed', err.message);
}
};
const handleDisconnect = async () => {
if (!client) return;
await client.disconnect();
setAccounts([]);
};
if (accounts.length === 0) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Connect MetaMask (Solana)" onPress={handleConnect} />
</View>
);
}
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }}>
<Text>Address: {accounts[0]}</Text>
<Button title="Sign Message" onPress={handleSignMessage} />
<Button title="Send 0.001 SOL" onPress={handleSendTransaction} />
<Button title="Disconnect" onPress={handleDisconnect} />
</View>
);
}
Important Notes
- No wallet-adapter in React Native —
@solana/wallet-adapter-react does not support React Native. Use solanaClient.core.invokeMethod with CAIP-scoped Solana RPC methods instead.
- Polyfill import order is critical —
react-native-get-random-values must be the very first import in the entry file, not inside polyfills.ts. Buffer is self-polyfilled by @metamask/connect-multichain but set it early in polyfills.ts as a safety net for peer deps. Event/CustomEvent are NOT needed for standalone connect-solana (the SDK uses eventemitter3); only add them if using wagmi.
- Metro shims are required — the SDK and its dependencies reference Node.js built-ins (
stream, crypto, http, etc.) that Metro cannot resolve without explicit shims in metro.config.js.
mobile.preferredOpenLink is required — createSolanaClient does not forward mobile options. Call createMultichainClient first with mobile: { preferredOpenLink: (dl) => Linking.openURL(dl) } to configure the singleton core for deeplinks, then call createSolanaClient which reuses it.
- Solana networks — mainnet, devnet, and testnet scopes are all modeled by the SDK; non-mainnet availability depends on the connected MetaMask build/version, so handle connection errors rather than assuming a cluster is present.
skipAutoRegister option — pass skipAutoRegister: true to createSolanaClient to prevent automatic wallet-standard registration.
disconnect() only revokes Solana scopes — EVM sessions remain active if present.
- Call
createSolanaClient once — each call returns a new Solana client wrapper, but they share the singleton multichain core (whose options merge across calls). Create it once (e.g., in a useEffect or before app registration), don't recreate per render.