Skip to content

Commit bd31b19

Browse files
rplusqclaude
andcommitted
feat(wallets): add Turnkey signer support
Add Turnkey as a remote signer option alongside AWS KMS and GCP KMS. Key changes: - Added alloy-signer-turnkey dependency to foundry-wallets - Implemented Turnkey wallet CLI args and initialization logic - Added turnkey feature flags to cast and forge - Updated Dockerfile and Makefile to include turnkey in release builds Fixed forge wallet feature flags: - Moved foundry-wallets from dev-dependencies to dependencies in forge/Cargo.toml - This fixes an issue where aws-kms, gcp-kms, and turnkey features were defined but non-functional when using cargo run with --features flags - The features worked in release builds (via Makefile) but not in local dev - This aligns forge with cast, which already had foundry-wallets as a regular dependency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2d3f5e2 commit bd31b19

File tree

11 files changed

+189
-6
lines changed

11 files changed

+189
-6
lines changed

Cargo.lock

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ alloy-signer-gcp = { version = "1.0.42", default-features = false }
256256
alloy-signer-ledger = { version = "1.0.42", default-features = false }
257257
alloy-signer-local = { version = "1.0.42", default-features = false }
258258
alloy-signer-trezor = { version = "1.0.42", default-features = false }
259+
alloy-signer-turnkey = { version = "1.0.42", default-features = false }
259260
alloy-transport = { version = "1.0.42", default-features = false }
260261
alloy-transport-http = { version = "1.0.42", default-features = false }
261262
alloy-transport-ipc = { version = "1.0.42", default-features = false }

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ COPY . .
1919
RUN git update-index --force-write-index
2020

2121
RUN --mount=type=cache,target=/root/.cargo/registry --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/opt/foundry/target \
22-
source $HOME/.profile && cargo build --release --features anvil/js-tracer,cast/aws-kms,cast/gcp-kms,forge/aws-kms,forge/gcp-kms \
22+
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 \
2323
&& mkdir out \
2424
&& mv target/release/forge out/forge \
2525
&& mv target/release/cast out/cast \

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ CARGO_TARGET_DIR ?= target
1515
# List of features to use when building. Can be overridden via the environment.
1616
# No jemalloc on Windows
1717
ifeq ($(OS),Windows_NT)
18-
FEATURES ?= aws-kms gcp-kms cli asm-keccak
18+
FEATURES ?= aws-kms gcp-kms turnkey cli asm-keccak
1919
else
20-
FEATURES ?= jemalloc aws-kms gcp-kms cli asm-keccak
20+
FEATURES ?= jemalloc aws-kms gcp-kms turnkey cli asm-keccak
2121
endif
2222

2323
##@ Help
@@ -47,15 +47,15 @@ build-%:
4747
.PHONY: docker-build-push
4848
docker-build-push: docker-build-prepare ## Build and push a cross-arch Docker image tagged with DOCKER_IMAGE_NAME.
4949
# Build x86_64-unknown-linux-gnu.
50-
cargo build --target x86_64-unknown-linux-gnu --features "jemalloc aws-kms gcp-kms cli asm-keccak js-tracer" --profile "$(PROFILE)"
50+
cargo build --target x86_64-unknown-linux-gnu --features "jemalloc aws-kms gcp-kms turnkey cli asm-keccak js-tracer" --profile "$(PROFILE)"
5151
mkdir -p $(BIN_DIR)/amd64
5252
for bin in anvil cast chisel forge; do \
5353
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/$$bin $(BIN_DIR)/amd64/; \
5454
done
5555

5656
# Build aarch64-unknown-linux-gnu.
5757
rustup target add aarch64-unknown-linux-gnu
58-
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)"
58+
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)"
5959
mkdir -p $(BIN_DIR)/arm64
6060
for bin in anvil cast chisel forge; do \
6161
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/$$bin $(BIN_DIR)/arm64/; \

crates/cast/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,5 @@ mimalloc = ["foundry-cli/mimalloc"]
101101
tracy-allocator = ["foundry-cli/tracy-allocator"]
102102
aws-kms = ["foundry-wallets/aws-kms"]
103103
gcp-kms = ["foundry-wallets/gcp-kms"]
104+
turnkey = ["foundry-wallets/turnkey"]
104105
isolate-by-default = ["foundry-config/isolate-by-default"]

crates/forge/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ forge-script.workspace = true
5050
forge-sol-macro-gen.workspace = true
5151
foundry-cli.workspace = true
5252
foundry-debugger.workspace = true
53+
foundry-wallets.workspace = true
5354

5455
alloy-chains.workspace = true
5556
alloy-dyn-abi.workspace = true
@@ -101,7 +102,6 @@ alloy-hardforks.workspace = true
101102
anvil.workspace = true
102103
forge-script-sequence.workspace = true
103104
foundry-test-utils.workspace = true
104-
foundry-wallets.workspace = true
105105
futures.workspace = true
106106
reqwest = { workspace = true, features = ["json"] }
107107

@@ -122,4 +122,5 @@ mimalloc = ["foundry-cli/mimalloc"]
122122
tracy-allocator = ["foundry-cli/tracy-allocator"]
123123
aws-kms = ["foundry-wallets/aws-kms"]
124124
gcp-kms = ["foundry-wallets/gcp-kms"]
125+
turnkey = ["foundry-wallets/turnkey"]
125126
isolate-by-default = ["foundry-config/isolate-by-default"]

crates/wallets/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ aws-config = { version = "1", default-features = true, optional = true }
3232
# gcp-kms
3333
alloy-signer-gcp = { workspace = true, features = ["eip712"], optional = true }
3434

35+
# turnkey
36+
alloy-signer-turnkey = { workspace = true, features = ["eip712"], optional = true }
37+
3538
async-trait.workspace = true
3639
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
3740
derive_builder = "0.20"
@@ -48,3 +51,4 @@ tokio = { workspace = true, features = ["macros"] }
4851
[features]
4952
aws-kms = ["dep:alloy-signer-aws", "dep:aws-config"]
5053
gcp-kms = ["dep:alloy-signer-gcp"]
54+
turnkey = ["dep:alloy-signer-turnkey"]

crates/wallets/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use alloy_signer_aws::AwsSignerError;
1010
#[cfg(feature = "gcp-kms")]
1111
use alloy_signer_gcp::GcpSignerError;
1212

13+
#[cfg(feature = "turnkey")]
14+
use alloy_signer_turnkey::TurnkeySignerError;
15+
1316
#[derive(Debug, thiserror::Error)]
1417
pub enum PrivateKeyError {
1518
#[error("Failed to create wallet from private key. Private key is invalid hex: {0}")]
@@ -37,6 +40,9 @@ pub enum WalletSignerError {
3740
#[cfg(feature = "gcp-kms")]
3841
Gcp(#[from] Box<GcpSignerError>),
3942
#[error(transparent)]
43+
#[cfg(feature = "turnkey")]
44+
Turnkey(#[from] TurnkeySignerError),
45+
#[error(transparent)]
4046
Io(#[from] std::io::Error),
4147
#[error(transparent)]
4248
InvalidHex(#[from] FromHexError),
@@ -54,4 +60,8 @@ impl WalletSignerError {
5460
pub fn gcp_unsupported() -> Self {
5561
Self::UnsupportedSigner("Google Cloud KMS")
5662
}
63+
64+
pub fn turnkey_unsupported() -> Self {
65+
Self::UnsupportedSigner("Turnkey")
66+
}
5767
}

crates/wallets/src/multi_wallet.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ macro_rules! create_hw_wallets {
8686
/// 5. Private Keys (cleartext in CLI)
8787
/// 6. Private Keys (interactively via secure prompt)
8888
/// 7. AWS KMS
89+
/// 8. Turnkey
8990
#[derive(Builder, Clone, Debug, Default, Serialize, Parser)]
9091
#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
9192
pub struct MultiWalletOpts {
@@ -221,6 +222,15 @@ pub struct MultiWalletOpts {
221222
/// See: <https://cloud.google.com/kms/docs>
222223
#[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
223224
pub gcp: bool,
225+
226+
/// Use Turnkey.
227+
///
228+
/// Ensure the following environment variables are set: TURNKEY_API_PRIVATE_KEY,
229+
/// TURNKEY_ORGANIZATION_ID, TURNKEY_ADDRESS.
230+
///
231+
/// See: <https://docs.turnkey.com/getting-started/quickstart>
232+
#[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))]
233+
pub turnkey: bool,
224234
}
225235

226236
impl MultiWalletOpts {
@@ -241,6 +251,9 @@ impl MultiWalletOpts {
241251
if let Some(gcp_signer) = self.gcp_signers().await? {
242252
signers.extend(gcp_signer);
243253
}
254+
if let Some(turnkey_signers) = self.turnkey_signers()? {
255+
signers.extend(turnkey_signers);
256+
}
244257
if let Some((pending_keystores, unlocked)) = self.keystores()? {
245258
pending.extend(pending_keystores);
246259
signers.extend(unlocked);
@@ -449,6 +462,20 @@ impl MultiWalletOpts {
449462

450463
Ok(None)
451464
}
465+
466+
pub fn turnkey_signers(&self) -> Result<Option<Vec<WalletSigner>>> {
467+
#[cfg(feature = "turnkey")]
468+
if self.turnkey {
469+
let api_private_key = std::env::var("TURNKEY_API_PRIVATE_KEY")?;
470+
let organization_id = std::env::var("TURNKEY_ORGANIZATION_ID")?;
471+
let address = std::env::var("TURNKEY_ADDRESS")?.parse()?;
472+
473+
let signer = WalletSigner::from_turnkey(api_private_key, organization_id, address)?;
474+
return Ok(Some(vec![signer]));
475+
}
476+
477+
Ok(None)
478+
}
452479
}
453480

454481
#[cfg(test)]

crates/wallets/src/wallet.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use serde::Serialize;
1111
/// 4. Keystore (via file path)
1212
/// 5. AWS KMS
1313
/// 6. Google Cloud KMS
14+
/// 7. Turnkey
1415
#[derive(Clone, Debug, Default, Serialize, Parser)]
1516
#[command(next_help_heading = "Wallet options", about = None, long_about = None)]
1617
pub struct WalletOpts {
@@ -91,6 +92,15 @@ pub struct WalletOpts {
9192
/// See: <https://cloud.google.com/kms/docs>
9293
#[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "gcp-kms"))]
9394
pub gcp: bool,
95+
96+
/// Use Turnkey.
97+
///
98+
/// Ensure the following environment variables are set: TURNKEY_API_PRIVATE_KEY,
99+
/// TURNKEY_ORGANIZATION_ID, TURNKEY_ADDRESS.
100+
///
101+
/// See: <https://docs.turnkey.com/getting-started/quickstart>
102+
#[arg(long, help_heading = "Wallet options - remote", hide = !cfg!(feature = "turnkey"))]
103+
pub turnkey: bool,
94104
}
95105

96106
impl WalletOpts {
@@ -120,6 +130,14 @@ impl WalletOpts {
120130
.parse()
121131
.map_err(|_| eyre::eyre!("GCP_KEY_VERSION could not be parsed into u64"))?;
122132
WalletSigner::from_gcp(project_id, location, keyring, key_name, key_version).await?
133+
} else if self.turnkey {
134+
let api_private_key = get_env("TURNKEY_API_PRIVATE_KEY")?;
135+
let organization_id = get_env("TURNKEY_ORGANIZATION_ID")?;
136+
let address_str = get_env("TURNKEY_ADDRESS")?;
137+
let address = address_str.parse().map_err(|_| {
138+
eyre::eyre!("TURNKEY_ADDRESS could not be parsed as an Ethereum address")
139+
})?;
140+
WalletSigner::from_turnkey(api_private_key, organization_id, address)?
123141
} else if let Some(raw_wallet) = self.raw.signer()? {
124142
raw_wallet
125143
} else if let Some(path) = utils::maybe_get_keystore_path(
@@ -152,6 +170,7 @@ flag to set your key via:
152170
--mnemonic-path
153171
--aws
154172
--gcp
173+
--turnkey
155174
--trezor
156175
--ledger
157176
@@ -222,6 +241,7 @@ mod tests {
222241
trezor: false,
223242
aws: false,
224243
gcp: false,
244+
turnkey: false,
225245
};
226246
match wallet.signer().await {
227247
Ok(_) => {

0 commit comments

Comments
 (0)