Skip to content
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
316ff81
start drafting
zerosnacks Oct 23, 2025
95e9b8c
establish very basic flow
zerosnacks Oct 23, 2025
99d7365
nits
zerosnacks Oct 23, 2025
a4f3604
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 24, 2025
3e19baa
clean up api
zerosnacks Oct 24, 2025
e075005
create basic test suite
zerosnacks Oct 24, 2025
6a448e4
make timeout configurable, improve types, extend tests
zerosnacks Oct 24, 2025
372f07a
fix test
zerosnacks Oct 24, 2025
f103b76
clean up
zerosnacks Oct 24, 2025
a91b76c
add transaction accept test
zerosnacks Oct 24, 2025
231da13
clean up tests
zerosnacks Oct 24, 2025
4ec1602
add basic setup test
zerosnacks Oct 24, 2025
6ec39b2
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 27, 2025
6985f88
solidify api
zerosnacks Oct 27, 2025
c9757e2
simplify and harden api
zerosnacks Oct 27, 2025
d078fcb
tweaks
zerosnacks Oct 27, 2025
6b11c40
move tests
zerosnacks Oct 27, 2025
3f67cd0
expand test suite
zerosnacks Oct 27, 2025
5ef8040
clean up
zerosnacks Oct 27, 2025
7a35d55
nit
zerosnacks Oct 27, 2025
90ab4ef
add basic session token
zerosnacks Oct 27, 2025
68f3bdd
apply session token, fix tests by adding session token to reqwest
zerosnacks Oct 27, 2025
847050e
fix test
zerosnacks Oct 27, 2025
1395160
fix reqwest openssl issue, use workspace version
zerosnacks Oct 27, 2025
6fa6645
harden CSP, inject session token into JS, define 0 cache policy
zerosnacks Oct 27, 2025
5d01508
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 28, 2025
0b8b9f6
remove flawed session token for now, to implement actual /session rou…
zerosnacks Oct 29, 2025
7567262
Merge branch 'zerosnacks/browser-wallet' of github.com:foundry-rs/fou…
zerosnacks Oct 29, 2025
66077fd
port interface
zerosnacks Oct 29, 2025
f195964
replace with v0.0.0 release of foundry-browser-wallet
zerosnacks Oct 29, 2025
1e2ba23
host interface
zerosnacks Oct 29, 2025
97330b7
ignore build files from typos
zerosnacks Oct 29, 2025
a3de774
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 30, 2025
8d500b9
update
zerosnacks Oct 30, 2025
ed04e4c
fix CSP to allow for RPC calls
zerosnacks Oct 30, 2025
3dca7dd
update wallet v0.0.0
zerosnacks Oct 30, 2025
4aba1e3
nits
zerosnacks Oct 30, 2025
2ac184a
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 30, 2025
9b4e449
add development mode to relax certain security restrictions such as o…
zerosnacks Oct 31, 2025
ca1ab1f
update browser wallet v0.0.0, includes session token support
zerosnacks Oct 31, 2025
7d255cc
add notice
zerosnacks Oct 31, 2025
d313ad2
add signing flow
zerosnacks Oct 31, 2025
5879b32
restructure sign message
zerosnacks Oct 31, 2025
5f2e9f8
serialize camelcase
zerosnacks Oct 31, 2025
163368d
bump version
zerosnacks Oct 31, 2025
e05807c
Merge branch 'master' into zerosnacks/browser-wallet
zerosnacks Oct 31, 2025
f19313b
fix merge conflict
zerosnacks Nov 3, 2025
253c290
prefer fields
zerosnacks Nov 3, 2025
ef21e1e
prefer non-blocking async api
zerosnacks Nov 3, 2025
f8319db
fix merge conflicts
zerosnacks Nov 10, 2025
64c7f72
remove unintended bump of proptest
zerosnacks Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 172 additions & 58 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ alloy-transport = { version = "1.0.42", default-features = false }
alloy-transport-http = { version = "1.0.42", default-features = false }
alloy-transport-ipc = { version = "1.0.42", default-features = false }
alloy-transport-ws = { version = "1.0.42", default-features = false }
alloy-hardforks = { version = "0.4.0", default-features = false }
alloy-op-hardforks = { version = "0.4.0", default-features = false }
alloy-hardforks = { version = "0.4.3", default-features = false }
alloy-op-hardforks = { version = "0.4.3", default-features = false }

## alloy-core
alloy-dyn-abi = "1.4.1"
Expand All @@ -288,10 +288,10 @@ op-alloy-flz = "0.13.1"
## revm
revm = { version = "30.2.0", default-features = false }
revm-inspectors = { version = "0.31.2", features = ["serde"] }
op-revm = { version = "11.1.2", default-features = false }
op-revm = { version = "11.3.0", default-features = false }
## alloy-evm
alloy-evm = "0.22.3"
alloy-op-evm = "0.22.3"
alloy-evm = "0.22.6"
alloy-op-evm = "0.22.6"

## cli
anstream = "0.6"
Expand Down Expand Up @@ -344,7 +344,7 @@ mesc = "0.3"
memchr = "2.7"
num-format = "0.4"
parking_lot = "0.12"
proptest = "1.8.0"
proptest = "1.9.0"
rand = "0.9"
rand_08 = { package = "rand", version = "0.8" }
rand_chacha = "0.9.0"
Expand Down
38 changes: 30 additions & 8 deletions crates/cast/src/cmd/send.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::{
Cast,
tx::{self, CastTxBuilder},
};
use std::{path::PathBuf, str::FromStr, time::Duration};

use alloy_ens::NameOrAddress;
use alloy_network::{AnyNetwork, EthereumWallet};
use alloy_provider::{Provider, ProviderBuilder};
Expand All @@ -15,7 +13,12 @@ use foundry_cli::{
utils,
utils::LoadConfig,
};
use std::{path::PathBuf, str::FromStr, time::Duration};
use foundry_wallets::WalletSigner;

use crate::{
Cast,
tx::{self, CastTxBuilder},
};

/// CLI arguments for `cast send`.
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -158,7 +161,7 @@ impl SendTxArgs {
// Default to sending via eth_sendTransaction if the --unlocked flag is passed.
// This should be the only way this RPC method is used as it requires a local node
// or remote RPC with unlocked accounts.
if unlocked {
if unlocked && !eth.wallet.browser {
// only check current chain id if it was specified in the config
if let Some(config_chain) = config.chain {
let current_chain_id = provider.get_chain_id().await?;
Expand Down Expand Up @@ -192,14 +195,33 @@ impl SendTxArgs {

tx::validate_from_address(eth.wallet.from, from)?;

let (tx, _) = builder.build(&signer).await?;
// Browser wallets work differently as they sign and send the transaction in one step.
if eth.wallet.browser
&& let WalletSigner::Browser(ref browser_signer) = signer
{
let (tx_request, _) = builder.build(from).await?;
let tx_hash = browser_signer.send_transaction_via_browser(tx_request.inner).await?;

if cast_async {
sh_println!("{tx_hash:#x}")?;
} else {
let receipt = Cast::new(&provider)
.receipt(format!("{tx_hash:#x}"), None, confirmations, Some(timeout), false)
.await?;
sh_println!("{receipt}")?;
}

return Ok(());
}

let (tx_request, _) = builder.build(&signer).await?;

let wallet = EthereumWallet::from(signer);
let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
.wallet(wallet)
.connect_provider(&provider);

cast_send(provider, tx, cast_async, confirmations, timeout).await
cast_send(provider, tx_request, cast_async, confirmations, timeout).await
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/cast/src/cmd/wallet/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::env;

use foundry_common::{fs, sh_err, sh_println};
use foundry_config::Config;
use foundry_wallets::multi_wallet::MultiWalletOptsBuilder;
use foundry_wallets::wallet_multi::MultiWalletOptsBuilder;

/// CLI arguments for `cast wallet list`.
#[derive(Clone, Debug, Parser)]
Expand Down
2 changes: 1 addition & 1 deletion crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use foundry_evm_core::{
use foundry_evm_traces::{
TracingInspector, TracingInspectorConfig, identifier::SignaturesIdentifier,
};
use foundry_wallets::multi_wallet::MultiWallet;
use foundry_wallets::wallet_multi::MultiWallet;
use itertools::Itertools;
use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner};
use rand::Rng;
Expand Down
2 changes: 1 addition & 1 deletion crates/cheatcodes/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use alloy_rpc_types::Authorization;
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::SolValue;
use foundry_wallets::{WalletSigner, multi_wallet::MultiWallet};
use foundry_wallets::{WalletSigner, wallet_multi::MultiWallet};
use parking_lot::Mutex;
use revm::{
bytecode::Bytecode,
Expand Down
1 change: 0 additions & 1 deletion crates/lint/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

[package]
name = "forge-lint"

Expand Down
12 changes: 12 additions & 0 deletions crates/wallets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ alloy-consensus.workspace = true
alloy-sol-types.workspace = true
alloy-dyn-abi.workspace = true

# browser wallet
alloy-rpc-types.workspace = true
axum.workspace = true
foundry-common.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["macros"] }
uuid.workspace = true
webbrowser = "1.0.6"

# aws-kms
alloy-signer-aws = { workspace = true, features = ["eip712"], optional = true }
aws-config = { version = "1", default-features = true, optional = true }
Expand All @@ -39,11 +48,14 @@ eyre.workspace = true
rpassword = "7"
serde.workspace = true
thiserror.workspace = true
tower.workspace = true
tower-http = { workspace = true, features = ["cors", "set-header"] }
tracing.workspace = true
eth-keystore = "0.5.0"

[dev-dependencies]
tokio = { workspace = true, features = ["macros"] }
reqwest = { workspace = true, features = ["json"] }

[features]
aws-kms = ["dep:alloy-signer-aws", "dep:aws-config"]
Expand Down
8 changes: 8 additions & 0 deletions crates/wallets/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use alloy_signer_aws::AwsSignerError;
#[cfg(feature = "gcp-kms")]
use alloy_signer_gcp::GcpSignerError;

use crate::wallet_browser::error::BrowserWalletError;

#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyError {
#[error("Failed to create wallet from private key. Private key is invalid hex: {0}")]
Expand Down Expand Up @@ -37,6 +39,8 @@ pub enum WalletSignerError {
#[cfg(feature = "gcp-kms")]
Gcp(#[from] Box<GcpSignerError>),
#[error(transparent)]
Browser(#[from] BrowserWalletError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
InvalidHex(#[from] FromHexError),
Expand All @@ -54,4 +58,8 @@ impl WalletSignerError {
pub fn gcp_unsupported() -> Self {
Self::UnsupportedSigner("Google Cloud KMS")
}

pub fn browser_unsupported() -> Self {
Self::UnsupportedSigner("Browser Wallet")
}
}
20 changes: 12 additions & 8 deletions crates/wallets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[macro_use]
extern crate foundry_common;

#[macro_use]
extern crate tracing;

pub mod error;
pub mod multi_wallet;
pub mod raw_wallet;
pub mod opts;
pub mod signer;
pub mod utils;
pub mod wallet;
pub mod wallet_signer;
pub mod wallet_browser;
pub mod wallet_multi;
pub mod wallet_raw;

pub use multi_wallet::MultiWalletOpts;
pub use raw_wallet::RawWalletOpts;
pub use wallet::WalletOpts;
pub use wallet_signer::{PendingSigner, WalletSigner};
pub use opts::WalletOpts;
pub use signer::{PendingSigner, WalletSigner};
pub use wallet_multi::MultiWalletOpts;
pub use wallet_raw::RawWalletOpts;

#[cfg(feature = "aws-kms")]
use aws_config as _;
49 changes: 46 additions & 3 deletions crates/wallets/src/wallet.rs → crates/wallets/src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{raw_wallet::RawWalletOpts, utils, wallet_signer::WalletSigner};
use crate::{signer::WalletSigner, utils, wallet_raw::RawWalletOpts};
use alloy_primitives::Address;
use clap::Parser;
use eyre::Result;
Expand All @@ -9,8 +9,9 @@ use serde::Serialize;
/// 2. Ledger
/// 3. Trezor
/// 4. Keystore (via file path)
/// 5. AWS KMS
/// 6. Google Cloud KMS
/// 5. Browser wallet
/// 6. AWS KMS
/// 7. Google Cloud KMS
#[derive(Clone, Debug, Default, Serialize, Parser)]
#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
pub struct WalletOpts {
Expand Down Expand Up @@ -91,6 +92,36 @@ pub struct WalletOpts {
/// See: <https://cloud.google.com/kms/docs>
#[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
pub gcp: bool,

/// Use a browser wallet.
#[arg(long, help_heading = "Wallet options - browser")]
pub browser: bool,

/// Port for the browser wallet server.
#[arg(
long,
help_heading = "Wallet options - browser",
value_name = "PORT",
default_value = "9545",
requires = "browser"
)]
pub browser_port: u16,

/// Whether to open the browser for wallet connection.
#[arg(
long,
help_heading = "Wallet options - browser",
default_value_t = false,
requires = "browser"
)]
pub browser_disable_open: bool,

/// Enable development mode for the browser wallet.
/// This relaxes certain security features for local development.
///
/// **WARNING**: This should only be used in a development environment.
#[arg(long, help_heading = "Wallet options - browser", hide = true)]
pub browser_development: bool,
}

impl WalletOpts {
Expand Down Expand Up @@ -120,6 +151,13 @@ impl WalletOpts {
.parse()
.map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
} else if self.browser {
WalletSigner::from_browser(
self.browser_port,
!self.browser_disable_open,
self.browser_development,
)
.await?
} else if let Some(raw_wallet) = self.raw.signer()? {
raw_wallet
} else if let Some(path) = utils::maybe_get_keystore_path(
Expand Down Expand Up @@ -154,6 +192,7 @@ flag to set your key via:
--gcp
--trezor
--ledger
--browser

Alternatively, when using the `cast send` or `cast mktx` commands with a local node
or RPC that has unlocked accounts, the --unlocked or --ethsign flags can be used,
Expand Down Expand Up @@ -222,6 +261,10 @@ mod tests {
trezor: false,
aws: false,
gcp: false,
browser: false,
browser_port: 9545,
browser_development: false,
browser_disable_open: false,
};
match wallet.signer().await {
Ok(_) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::WalletSignerError;
use crate::{error::WalletSignerError, wallet_browser::signer::BrowserSigner};
use alloy_consensus::SignableTransaction;
use alloy_dyn_abi::TypedData;
use alloy_network::TxSigner;
Expand All @@ -9,7 +9,7 @@ use alloy_signer_local::{MnemonicBuilder, PrivateKeySigner, coins_bip39::English
use alloy_signer_trezor::{HDPath as TrezorHDPath, TrezorSigner};
use alloy_sol_types::{Eip712Domain, SolStruct};
use async_trait::async_trait;
use std::{collections::HashSet, path::PathBuf};
use std::{collections::HashSet, path::PathBuf, time::Duration};
use tracing::warn;

#[cfg(feature = "aws-kms")]
Expand All @@ -35,6 +35,8 @@ pub enum WalletSigner {
Ledger(LedgerSigner),
/// Wrapper around Trezor signer.
Trezor(TrezorSigner),
/// Wrapper around browser wallet.
Browser(BrowserSigner),
/// Wrapper around AWS KMS signer.
#[cfg(feature = "aws-kms")]
Aws(AwsSigner),
Expand All @@ -54,6 +56,18 @@ impl WalletSigner {
Ok(Self::Trezor(trezor))
}

pub async fn from_browser(
port: u16,
open_browser: bool,
browser_development: bool,
) -> Result<Self> {
let browser_signer =
BrowserSigner::new(port, open_browser, Duration::from_secs(300), browser_development)
.await
.map_err(|e| WalletSignerError::Browser(e.into()))?;
Ok(Self::Browser(browser_signer))
}

pub async fn from_aws(key_id: String) -> Result<Self> {
#[cfg(feature = "aws-kms")]
{
Expand Down Expand Up @@ -175,6 +189,9 @@ impl WalletSigner {
}
}
}
Self::Browser(browser) => {
senders.insert(alloy_signer::Signer::address(browser));
}
#[cfg(feature = "aws-kms")]
Self::Aws(aws) => {
senders.insert(alloy_signer::Signer::address(aws));
Expand Down Expand Up @@ -215,6 +232,7 @@ macro_rules! delegate {
Self::Local($inner) => $e,
Self::Ledger($inner) => $e,
Self::Trezor($inner) => $e,
Self::Browser($inner) => $e,
#[cfg(feature = "aws-kms")]
Self::Aws($inner) => $e,
#[cfg(feature = "gcp-kms")]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions crates/wallets/src/wallet_browser/app/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Foundry</title>
<script type="module" crossorigin src="/main.js"></script>
<link rel="stylesheet" crossorigin href="/styles.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions crates/wallets/src/wallet_browser/app/assets/main.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/wallets/src/wallet_browser/app/assets/styles.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading