Component
Cast
Describe the feature you would like
When cast send --browser (or any --browser flow) is invoked against a chain that doesn't match the connected browser wallet's current chain, the signer errors out with Transaction chainId does not match connected wallet chain ID and the user has to manually switch their wallet's network and rerun the command.
The non-browser --unlocked path already handles this — it calls wallet_switchEthereumChain when config.chain differs from the RPC's reported chain (introduced in #5077). That logic is currently gated to unlocked && browser.is_none() and skipped entirely on the browser path.
Where the gap lives
crates/cast/src/cmd/send.rs (foundry-rs/foundry, master HEAD):
// Case 1: --unlocked — switches chain
if unlocked && browser.is_none() {
if let Some(config_chain) = config.chain {
let current_chain_id = provider.get_chain_id().await?;
let config_chain_id = config_chain.id();
if config_chain_id != current_chain_id {
sh_warn!("Switching to chain {}", config_chain)?;
provider.raw_request::<_, ()>(
"wallet_switchEthereumChain".into(),
[serde_json::json!({ "chainId": format!("0x{:x}", config_chain_id) })],
).await?;
}
}
// ...
}
// Case 2: --browser — does NOT switch chain
} else if let Some(browser) = browser {
let chain = builder.chain(); // only used for is_tempo() gas buffer, never for switching
// ...
let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
}
crates/wallets/src/wallet_browser/signer.rs (foundry-rs/foundry-core, main HEAD) detects the mismatch but only fails:
if let Some(chain_id) = tx_request.chain_id()
&& chain_id != self.chain_id
{
return Err(alloy_signer::Error::other(
"Transaction `chainId` does not match connected wallet chain ID",
));
}
Motivation
The browser wallet flow is the recommended signing path for agentic workflows where private keys must not touch the terminal. In our case (a Sablier vesting-withdrawal skill that batches withdrawMultiple per Lockup contract), the agent already knows the target chain — it resolves the chain from the indexer and passes --rpc-url and --chain accordingly. The wallet state is the only thing not under the agent's control, so a chain mismatch surfaces as a runtime error mid-batch, requiring a manual extension click and a retry.
The browser wallet bridge already has the connected chain_id (POSTed to /api/connection during connect — see foundry-rs/foundry-browser-wallet/src/App.tsx and the Connection { address, chain_id } struct in foundry-core). And EIP-3326 (wallet_switchEthereumChain) is universally supported by MetaMask, Rabby, Frame, Coinbase Wallet, etc. The pieces are all there; they're just not wired together on the --browser path.
Symmetrically, cast wallet address --browser does not surface chainId either, so callers can't even pre-flight-check before launching the bridge.
Proposed solution
Mirror Case 1's logic in Case 2 of crates/cast/src/cmd/send.rs — before browser.send_transaction_via_browser(tx_request), if builder.chain() differs from browser.chain_id(), request a switch via the bridge. The bridge would forward the wallet_switchEthereumChain RPC to the connected EIP-1193 provider, exactly as a dapp would.
A first-pass sketch:
} else if let Some(browser) = browser {
let chain = builder.chain();
if chain.id() != browser.chain_id() {
sh_warn!("Switching browser wallet to chain {}", chain)?;
browser.switch_chain(chain.id()).await?;
}
// ...existing code
}
This requires a small new method on BrowserSigner that pushes a wallet_switchEthereumChain request through the same queue that handles eth_sendTransaction, plus a bit of UI in foundry-browser-wallet to surface the prompt.
Happy to put up the PR if the maintainers are open to it — flagging the issue first so we can agree on the UX (e.g., should it be opt-in via a flag, or always-on with a warn-and-switch like the --unlocked path).
Additional context
Component
Cast
Describe the feature you would like
When
cast send --browser(or any--browserflow) is invoked against a chain that doesn't match the connected browser wallet's current chain, the signer errors out withTransactionchainIddoes not match connected wallet chain IDand the user has to manually switch their wallet's network and rerun the command.The non-browser
--unlockedpath already handles this — it callswallet_switchEthereumChainwhenconfig.chaindiffers from the RPC's reported chain (introduced in #5077). That logic is currently gated tounlocked && browser.is_none()and skipped entirely on the browser path.Where the gap lives
crates/cast/src/cmd/send.rs(foundry-rs/foundry, master HEAD):crates/wallets/src/wallet_browser/signer.rs(foundry-rs/foundry-core, main HEAD) detects the mismatch but only fails:Motivation
The browser wallet flow is the recommended signing path for agentic workflows where private keys must not touch the terminal. In our case (a Sablier vesting-withdrawal skill that batches
withdrawMultipleper Lockup contract), the agent already knows the target chain — it resolves the chain from the indexer and passes--rpc-urland--chainaccordingly. The wallet state is the only thing not under the agent's control, so a chain mismatch surfaces as a runtime error mid-batch, requiring a manual extension click and a retry.The browser wallet bridge already has the connected
chain_id(POSTed to/api/connectionduring connect — seefoundry-rs/foundry-browser-wallet/src/App.tsxand theConnection { address, chain_id }struct in foundry-core). And EIP-3326 (wallet_switchEthereumChain) is universally supported by MetaMask, Rabby, Frame, Coinbase Wallet, etc. The pieces are all there; they're just not wired together on the--browserpath.Symmetrically,
cast wallet address --browserdoes not surface chainId either, so callers can't even pre-flight-check before launching the bridge.Proposed solution
Mirror Case 1's logic in Case 2 of
crates/cast/src/cmd/send.rs— beforebrowser.send_transaction_via_browser(tx_request), ifbuilder.chain()differs frombrowser.chain_id(), request a switch via the bridge. The bridge would forward thewallet_switchEthereumChainRPC to the connected EIP-1193 provider, exactly as a dapp would.A first-pass sketch:
This requires a small new method on
BrowserSignerthat pushes awallet_switchEthereumChainrequest through the same queue that handleseth_sendTransaction, plus a bit of UI infoundry-browser-walletto surface the prompt.Happy to put up the PR if the maintainers are open to it — flagging the issue first so we can agree on the UX (e.g., should it be opt-in via a flag, or always-on with a warn-and-switch like the
--unlockedpath).Additional context
cast 1.5.1-stable(Build 2025-12-22)--unlockedchain-switch logic, merged 2023-06-01)cast send --browserfails on Tempo with "No chain was provided" #13881 (a separatecast send --browserchain-handling bug on Tempo)foundry-rs/foundry-browser-wallet) currently doesn't surface any switch-chain UI — issues Add random fuzzing of inputs #16, hevm-style cheatcodes #17, benchmarks against other clients #19, package toaptandbrew#20 there are unrelated