diff --git a/Cargo.lock b/Cargo.lock index b6f44ac1c26a2..011380f2bd6dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -782,6 +782,22 @@ dependencies = [ "trezor-client", ] +[[package]] +name = "alloy-signer-turnkey" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3528e3fc1294e1706e44e44c855956681b26861758216ad63eb81221223b18" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "thiserror 2.0.17", + "tracing", + "turnkey_client", +] + [[package]] name = "alloy-sol-macro" version = "1.4.1" @@ -4079,6 +4095,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "forge" version = "1.4.2" @@ -4985,6 +5016,7 @@ dependencies = [ "alloy-signer-ledger", "alloy-signer-local", "alloy-signer-trezor", + "alloy-signer-turnkey", "alloy-sol-types", "async-trait", "aws-config", @@ -5542,6 +5574,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -6609,6 +6657,23 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -7024,12 +7089,50 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -7601,6 +7704,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + [[package]] name = "prost" version = "0.13.5" @@ -7621,6 +7734,19 @@ dependencies = [ "prost-derive 0.14.1", ] +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "prost-derive" version = "0.13.5" @@ -7647,6 +7773,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", +] + [[package]] name = "prost-types" version = "0.13.5" @@ -8050,11 +8185,13 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -8066,6 +8203,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -8533,7 +8671,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] @@ -8776,6 +8914,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.5.1" @@ -9987,6 +10138,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -10432,6 +10593,40 @@ dependencies = [ "utf-8", ] +[[package]] +name = "turnkey_api_key_stamper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ff3d11991690d61cc4c44fb12fa95656bf2a1480be8abbb44d05ee7b75177a" +dependencies = [ + "base64 0.22.1", + "hex", + "k256", + "p256", + "rand_core 0.6.4", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "turnkey_client" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9846185545f447fd6ae7b19cc04623b8be9f430e2230950565ad0400b6a62b6" +dependencies = [ + "mime", + "prost 0.12.6", + "prost-types 0.12.6", + "reqwest", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "turnkey_api_key_stamper", +] + [[package]] name = "typeid" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index 269c294f25b83..cfe64458a3955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -256,6 +256,7 @@ alloy-signer-gcp = { version = "1.0.41", default-features = false } alloy-signer-ledger = { version = "1.0.41", default-features = false } alloy-signer-local = { version = "1.0.41", default-features = false } alloy-signer-trezor = { version = "1.0.41", default-features = false } +alloy-signer-turnkey = { version = "1.0.41", default-features = false } alloy-transport = { version = "1.0.41", default-features = false } alloy-transport-http = { version = "1.0.41", default-features = false } alloy-transport-ipc = { version = "1.0.41", default-features = false } diff --git a/Dockerfile b/Dockerfile index 6d7fe761f3f80..536ba243c39d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ COPY . . RUN git update-index --force-write-index RUN --mount=type=cache,target=/root/.cargo/registry --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/opt/foundry/target \ - source $HOME/.profile && cargo build --release --features anvil/js-tracer,cast/aws-kms,cast/gcp-kms,forge/aws-kms,forge/gcp-kms \ + source $HOME/.profile && cargo build --release --features anvil/js-tracer,cast/aws-kms,cast/gcp-kms,cast/turnkey,forge/aws-kms,forge/gcp-kms,forge/turnkey \ && mkdir out \ && mv target/release/forge out/forge \ && mv target/release/cast out/cast \ diff --git a/Makefile b/Makefile index 5127d9551c392..1bd4ad4a28999 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,9 @@ CARGO_TARGET_DIR ?= target # List of features to use when building. Can be overridden via the environment. # No jemalloc on Windows ifeq ($(OS),Windows_NT) - FEATURES ?= aws-kms gcp-kms cli asm-keccak + FEATURES ?= aws-kms gcp-kms turnkey cli asm-keccak else - FEATURES ?= jemalloc aws-kms gcp-kms cli asm-keccak + FEATURES ?= jemalloc aws-kms gcp-kms turnkey cli asm-keccak endif ##@ Help @@ -47,7 +47,7 @@ build-%: .PHONY: docker-build-push docker-build-push: docker-build-prepare ## Build and push a cross-arch Docker image tagged with DOCKER_IMAGE_NAME. # Build x86_64-unknown-linux-gnu. - cargo build --target x86_64-unknown-linux-gnu --features "jemalloc aws-kms gcp-kms cli asm-keccak js-tracer" --profile "$(PROFILE)" + cargo build --target x86_64-unknown-linux-gnu --features "jemalloc aws-kms gcp-kms turnkey cli asm-keccak js-tracer" --profile "$(PROFILE)" mkdir -p $(BIN_DIR)/amd64 for bin in anvil cast chisel forge; do \ cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/$$bin $(BIN_DIR)/amd64/; \ @@ -55,7 +55,7 @@ docker-build-push: docker-build-prepare ## Build and push a cross-arch Docker im # Build aarch64-unknown-linux-gnu. rustup target add aarch64-unknown-linux-gnu - RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc" cargo build --target aarch64-unknown-linux-gnu --features "aws-kms gcp-kms cli asm-keccak js-tracer" --profile "$(PROFILE)" + RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc" cargo build --target aarch64-unknown-linux-gnu --features "aws-kms gcp-kms turnkey cli asm-keccak js-tracer" --profile "$(PROFILE)" mkdir -p $(BIN_DIR)/arm64 for bin in anvil cast chisel forge; do \ cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/$$bin $(BIN_DIR)/arm64/; \ diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index 8f187565701b0..b10e5f3a8df8d 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -101,4 +101,5 @@ mimalloc = ["foundry-cli/mimalloc"] tracy-allocator = ["foundry-cli/tracy-allocator"] aws-kms = ["foundry-wallets/aws-kms"] gcp-kms = ["foundry-wallets/gcp-kms"] +turnkey = ["foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 2b8143b334cee..c8b34b6b771bc 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -50,6 +50,7 @@ forge-script.workspace = true forge-sol-macro-gen.workspace = true foundry-cli.workspace = true foundry-debugger.workspace = true +foundry-wallets = { workspace = true, optional = true } alloy-chains.workspace = true alloy-dyn-abi.workspace = true @@ -101,7 +102,6 @@ alloy-hardforks.workspace = true anvil.workspace = true forge-script-sequence.workspace = true foundry-test-utils.workspace = true -foundry-wallets.workspace = true futures.workspace = true reqwest = { workspace = true, features = ["json"] } @@ -120,6 +120,7 @@ asm-keccak = ["alloy-primitives/asm-keccak"] jemalloc = ["foundry-cli/jemalloc"] mimalloc = ["foundry-cli/mimalloc"] tracy-allocator = ["foundry-cli/tracy-allocator"] -aws-kms = ["foundry-wallets/aws-kms"] -gcp-kms = ["foundry-wallets/gcp-kms"] +aws-kms = ["dep:foundry-wallets", "foundry-wallets/aws-kms"] +gcp-kms = ["dep:foundry-wallets", "foundry-wallets/gcp-kms"] +turnkey = ["dep:foundry-wallets", "foundry-wallets/turnkey"] isolate-by-default = ["foundry-config/isolate-by-default"] diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 23879df9156f6..d898032a78fd9 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -9,6 +9,10 @@ extern crate foundry_common; #[macro_use] extern crate tracing; +// Required for optional features (aws-kms, gcp-kms, turnkey) +#[cfg(any(feature = "aws-kms", feature = "gcp-kms", feature = "turnkey"))] +use foundry_wallets as _; + pub mod args; pub mod cmd; pub mod opts; diff --git a/crates/wallets/Cargo.toml b/crates/wallets/Cargo.toml index 8b83021df32ea..438e604a5fc21 100644 --- a/crates/wallets/Cargo.toml +++ b/crates/wallets/Cargo.toml @@ -32,6 +32,9 @@ aws-config = { version = "1", default-features = true, optional = true } # gcp-kms alloy-signer-gcp = { workspace = true, features = ["eip712"], optional = true } +# turnkey +alloy-signer-turnkey = { workspace = true, features = ["eip712"], optional = true } + async-trait.workspace = true clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } derive_builder = "0.20" @@ -48,3 +51,4 @@ tokio = { workspace = true, features = ["macros"] } [features] aws-kms = ["dep:alloy-signer-aws", "dep:aws-config"] gcp-kms = ["dep:alloy-signer-gcp"] +turnkey = ["dep:alloy-signer-turnkey"] diff --git a/crates/wallets/src/error.rs b/crates/wallets/src/error.rs index c77f266f481d5..db83651eb7cfd 100644 --- a/crates/wallets/src/error.rs +++ b/crates/wallets/src/error.rs @@ -10,6 +10,9 @@ use alloy_signer_aws::AwsSignerError; #[cfg(feature = "gcp-kms")] use alloy_signer_gcp::GcpSignerError; +#[cfg(feature = "turnkey")] +use alloy_signer_turnkey::TurnkeySignerError; + #[derive(Debug, thiserror::Error)] pub enum PrivateKeyError { #[error("Failed to create wallet from private key. Private key is invalid hex: {0}")] @@ -37,6 +40,9 @@ pub enum WalletSignerError { #[cfg(feature = "gcp-kms")] Gcp(#[from] Box), #[error(transparent)] + #[cfg(feature = "turnkey")] + Turnkey(#[from] TurnkeySignerError), + #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] InvalidHex(#[from] FromHexError), @@ -54,4 +60,8 @@ impl WalletSignerError { pub fn gcp_unsupported() -> Self { Self::UnsupportedSigner("Google Cloud KMS") } + + pub fn turnkey_unsupported() -> Self { + Self::UnsupportedSigner("Turnkey") + } } diff --git a/crates/wallets/src/multi_wallet.rs b/crates/wallets/src/multi_wallet.rs index 7b89e683e47f9..18a01aeb741c8 100644 --- a/crates/wallets/src/multi_wallet.rs +++ b/crates/wallets/src/multi_wallet.rs @@ -86,6 +86,7 @@ macro_rules! create_hw_wallets { /// 5. Private Keys (cleartext in CLI) /// 6. Private Keys (interactively via secure prompt) /// 7. AWS KMS +/// 8. Turnkey #[derive(Builder, Clone, Debug, Default, Serialize, Parser)] #[command(next_help_heading = "Wallet options", about = None, long_about = None)] pub struct MultiWalletOpts { @@ -221,6 +222,15 @@ pub struct MultiWalletOpts { /// See: #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))] pub gcp: bool, + + /// Use Turnkey. + /// + /// Ensure the following environment variables are set: TURNKEY_API_PRIVATE_KEY, + /// TURNKEY_ORGANIZATION_ID, TURNKEY_ADDRESS. + /// + /// See: + #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))] + pub turnkey: bool, } impl MultiWalletOpts { @@ -241,6 +251,9 @@ impl MultiWalletOpts { if let Some(gcp_signer) = self.gcp_signers().await? { signers.extend(gcp_signer); } + if let Some(turnkey_signers) = self.turnkey_signers()? { + signers.extend(turnkey_signers); + } if let Some((pending_keystores, unlocked)) = self.keystores()? { pending.extend(pending_keystores); signers.extend(unlocked); @@ -443,6 +456,20 @@ impl MultiWalletOpts { Ok(None) } + + pub fn turnkey_signers(&self) -> Result>> { + #[cfg(feature = "turnkey")] + if self.turnkey { + let api_private_key = std::env::var("TURNKEY_API_PRIVATE_KEY")?; + let organization_id = std::env::var("TURNKEY_ORGANIZATION_ID")?; + let address = std::env::var("TURNKEY_ADDRESS")?.parse()?; + + let signer = WalletSigner::from_turnkey(api_private_key, organization_id, address)?; + return Ok(Some(vec![signer])); + } + + Ok(None) + } } #[cfg(test)] @@ -501,6 +528,7 @@ mod tests { ("ledger", "--mnemonic-indexes", 1), ("trezor", "--mnemonic-indexes", 2), ("aws", "--mnemonic-indexes", 10), + ("turnkey", "--mnemonic-indexes", 11), ]; for test_case in wallet_options { @@ -515,6 +543,7 @@ mod tests { "ledger" => assert!(args.ledger), "trezor" => assert!(args.trezor), "aws" => assert!(args.aws), + "turnkey" => assert!(args.turnkey), _ => panic!("Should have matched one of the previous wallet options"), } diff --git a/crates/wallets/src/wallet.rs b/crates/wallets/src/wallet.rs index 3feb8f99202c2..0c39ef43f6051 100644 --- a/crates/wallets/src/wallet.rs +++ b/crates/wallets/src/wallet.rs @@ -11,6 +11,7 @@ use serde::Serialize; /// 4. Keystore (via file path) /// 5. AWS KMS /// 6. Google Cloud KMS +/// 7. Turnkey #[derive(Clone, Debug, Default, Serialize, Parser)] #[command(next_help_heading = "Wallet options", about = None, long_about = None)] pub struct WalletOpts { @@ -91,6 +92,15 @@ pub struct WalletOpts { /// See: #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))] pub gcp: bool, + + /// Use Turnkey. + /// + /// Ensure the following environment variables are set: TURNKEY_API_PRIVATE_KEY, + /// TURNKEY_ORGANIZATION_ID, TURNKEY_ADDRESS. + /// + /// See: + #[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))] + pub turnkey: bool, } impl WalletOpts { @@ -120,6 +130,14 @@ 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.turnkey { + let api_private_key = get_env("TURNKEY_API_PRIVATE_KEY")?; + let organization_id = get_env("TURNKEY_ORGANIZATION_ID")?; + let address_str = get_env("TURNKEY_ADDRESS")?; + let address = address_str.parse().map_err(|_| { + eyre::eyre!("TURNKEY_ADDRESS could not be parsed as an Ethereum address") + })?; + WalletSigner::from_turnkey(api_private_key, organization_id, address)? } else if let Some(raw_wallet) = self.raw.signer()? { raw_wallet } else if let Some(path) = utils::maybe_get_keystore_path( @@ -152,6 +170,7 @@ flag to set your key via: --mnemonic-path --aws --gcp +--turnkey --trezor --ledger @@ -222,6 +241,7 @@ mod tests { trezor: false, aws: false, gcp: false, + turnkey: false, }; match wallet.signer().await { Ok(_) => { diff --git a/crates/wallets/src/wallet_signer.rs b/crates/wallets/src/wallet_signer.rs index cc619041b1b4c..ef6c7aa759859 100644 --- a/crates/wallets/src/wallet_signer.rs +++ b/crates/wallets/src/wallet_signer.rs @@ -24,6 +24,9 @@ use alloy_signer_gcp::{ }, }; +#[cfg(feature = "turnkey")] +use alloy_signer_turnkey::TurnkeySigner; + pub type Result = std::result::Result; /// Wrapper enum around different signers. @@ -41,6 +44,9 @@ pub enum WalletSigner { /// Wrapper around Google Cloud KMS signer. #[cfg(feature = "gcp-kms")] Gcp(GcpSigner), + /// Wrapper around Turnkey signer. + #[cfg(feature = "turnkey")] + Turnkey(TurnkeySigner), } impl WalletSigner { @@ -120,6 +126,30 @@ impl WalletSigner { } } + pub fn from_turnkey( + api_private_key: String, + organization_id: String, + address: Address, + ) -> Result { + #[cfg(feature = "turnkey")] + { + Ok(Self::Turnkey(TurnkeySigner::from_api_key( + &api_private_key, + organization_id, + address, + None, + )?)) + } + + #[cfg(not(feature = "turnkey"))] + { + let _ = api_private_key; + let _ = organization_id; + let _ = address; + Err(WalletSignerError::UnsupportedSigner("Turnkey")) + } + } + pub fn from_private_key(private_key: &B256) -> Result { Ok(Self::Local(PrivateKeySigner::from_bytes(private_key)?)) } @@ -183,6 +213,10 @@ impl WalletSigner { Self::Gcp(gcp) => { senders.insert(alloy_signer::Signer::address(gcp)); } + #[cfg(feature = "turnkey")] + Self::Turnkey(turnkey) => { + senders.insert(alloy_signer::Signer::address(turnkey)); + } } Ok(senders.into_iter().collect()) } @@ -219,6 +253,8 @@ macro_rules! delegate { Self::Aws($inner) => $e, #[cfg(feature = "gcp-kms")] Self::Gcp($inner) => $e, + #[cfg(feature = "turnkey")] + Self::Turnkey($inner) => $e, } }; }