Skip to content

feat(cast): auto-switch chain via wallet_switchEthereumChain when --browser is used #14641

@PaulRBerg

Description

@PaulRBerg

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions