Sign Solana Transactions via Multichain Client
When to use
Use this skill when:
- Signing or sending Solana transactions through
invokeMethodon a multichain client - Building Solana transactions with
@solana/web3.jsand encoding them for the multichain API - Signing Solana messages through the multichain client
- Selecting the correct Solana CAIP-2 scope (mainnet, devnet)
- Disconnecting only Solana scopes while keeping EVM sessions active
Workflow
Step 1: Connect with Solana scopes
import { createMultichainClient, getInfuraRpcUrls } from '@metamask/connect-multichain';
const client = await createMultichainClient({
dapp: { name: 'My DApp', url: window.location.href },
api: {
supportedNetworks: {
...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', caipChainIds: ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'] }),
},
},
});
await client.connect(
[
'eip155:1',
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // mainnet
],
[],
); // resolves with no value — read session via client.provider.getSession()
Solana CAIP-2 scope identifiers:
| Network | CAIP-2 Scope |
|---|---|
| Mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
| Testnet | solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z |
All three Solana scopes are 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.
Step 2: Understand Solana RPC routing
All Solana methods route through the wallet. Unlike EVM where read calls go to an RPC node, every Solana invokeMethod call is handled by MetaMask. There is no RPC node fallback for Solana.
Step 3: Sign a message (signMessage)
Method names have no solana_ prefix. The message must be base64 encoded, and the signing account is passed as account: { address }.
const message = btoa('Hello from Solana via MetaMask!');
const result = await client.invokeMethod({
scope: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
request: {
method: 'signMessage',
params: {
account: { address: 'YourSolanaAddressBase58' },
message,
},
},
});
// result: { signature: <base58 string>, signedMessage: <base64>, signatureType: 'ed25519' }
console.log('Signature:', result.signature);
Step 4: Build a Solana transaction with @solana/web3.js
Build the transaction using @solana/web3.js, serialize it, then base64-encode for invokeMethod.
import {
Connection,
PublicKey,
SystemProgram,
Transaction,
clusterApiUrl,
} from '@solana/web3.js';
const connection = new Connection(clusterApiUrl('mainnet-beta'));
const fromPubkey = new PublicKey('YourSolanaPublicKey');
const toPubkey = new PublicKey('RecipientSolanaPublicKey');
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports: 1_000_000, // 0.001 SOL
}),
);
transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
transaction.feePayer = fromPubkey;
const serialized = transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
const base64Transaction = Buffer.from(serialized).toString('base64');
Step 5: Sign a transaction (signTransaction)
Returns the signed transaction without broadcasting it.
const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
const signResult = await client.invokeMethod({
scope: SOLANA_MAINNET,
request: {
method: 'signTransaction',
params: {
account: { address: 'YourSolanaAddressBase58' },
transaction: base64Transaction,
scope: SOLANA_MAINNET,
},
},
});
// The result field is `signedTransaction` (base64), not `transaction`
console.log('Signed transaction:', signResult.signedTransaction);
You can then broadcast the signed transaction yourself:
const signedBuffer = Buffer.from(signResult.signedTransaction, 'base64');
const txId = await connection.sendRawTransaction(signedBuffer);
console.log('Transaction ID:', txId);
Step 6: Sign and send a transaction (signAndSendTransaction)
Signs and broadcasts the transaction in one step.
const sendResult = await client.invokeMethod({
scope: SOLANA_MAINNET,
request: {
method: 'signAndSendTransaction',
params: {
account: { address: 'YourSolanaAddressBase58' },
transaction: base64Transaction,
scope: SOLANA_MAINNET,
},
},
});
// sendResult: { signature: <base58 transaction signature> }
console.log('Transaction signature:', sendResult.signature);
Step 7: Devnet transactions
Connect with the devnet scope and point @solana/web3.js at the devnet cluster:
const SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
await client.connect([SOLANA_DEVNET], []);
const connection = new Connection(clusterApiUrl('devnet'));
// Build transaction with devnet connection...
const base64Tx = buildAndSerializeTransaction(connection);
const result = await client.invokeMethod({
scope: SOLANA_DEVNET,
request: {
method: 'signAndSendTransaction',
params: {
account: { address: 'YourSolanaAddressBase58' },
transaction: base64Tx,
scope: SOLANA_DEVNET,
},
},
});
Step 8: Selective disconnect
Disconnect only Solana scopes while keeping EVM sessions active:
// Disconnect only Solana mainnet — EVM scopes remain connected
await client.disconnect(['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp']);
// Disconnect all scopes (full session teardown)
await client.disconnect();
Step 9: Error handling
invokeMethod errors are wrapped in RPCInvokeMethodErr — its own code is always 53, and the wallet's original EIP-1193 / JSON-RPC code (e.g. 4001 user rejection) is on rpcCode:
import { RPCInvokeMethodErr } from '@metamask/connect-multichain';
try {
await client.invokeMethod({
scope: SOLANA_MAINNET,
request: {
method: 'signAndSendTransaction',
params: {
account: { address: 'YourSolanaAddressBase58' },
transaction: base64Tx,
scope: SOLANA_MAINNET,
},
},
});
} catch (err) {
if (err instanceof RPCInvokeMethodErr && err.rpcCode === 4001) {
// User rejected the transaction in MetaMask — not an app error
} else {
console.error('Solana transaction error:', err);
}
}
Important Notes
- All Solana methods go to the wallet. There is no RPC node routing for Solana — every
invokeMethodcall with a Solana scope prompts MetaMask. - Base64 encoding required. Transactions and messages must be base64-encoded strings, not raw buffers or hex.
- Use
@solana/web3.jsto build transactions. ConstructTransactionobjects, setrecentBlockhashandfeePayer, serialize withrequireAllSignatures: false, then base64-encode. - CAIP-2 genesis hash IDs. Mainnet is
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp. Devnet issolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1. These are not cluster URLs — they are genesis hash identifiers. - 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.
- Selective disconnect preserves other scopes. Passing specific Solana scopes to
disconnect()only revokes those scopes. EVM scopes remain active. - Connected scopes required.
invokeMethodfails if the Solana scope was not included in the originalconnect()call. - Method names have no
solana_prefix. The MetaMask Multichain API methods aresignMessage,signTransaction, andsignAndSendTransaction, each takingaccount: { address }in params. (solana_*-prefixed names are WalletConnect's schema, not MetaMask's.) signTransactionvssignAndSendTransaction: UsesignTransactionwhen you need to inspect or modify the signed output before broadcasting (result field:signedTransaction, base64). UsesignAndSendTransactionfor the common case where you want a single atomic operation (result field:signature, base58).