Skip to content

7702 execution session keys #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"aa-types",
"aa-core",
"core",
"eip7702-core",
"executors",
"server",
"thirdweb-core",
Expand Down
31 changes: 26 additions & 5 deletions core/src/execution_options/eip7702.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
use alloy::primitives::Address;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::defs::AddressDef;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(title = "EIP-7702 Execution Options")]
#[serde(rename_all = "camelCase", untagged)]
Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the removal of JsonSchema derive is intentional.

The JsonSchema derive was removed from the enum while being retained on the struct variants. This might affect API documentation generation if schemas are used for OpenAPI specs.


🏁 Script executed:

#!/bin/bash
# Description: Check if JsonSchema removal is consistent across execution options

# Check if other execution option enums also removed JsonSchema
rg -A 3 "enum.*ExecutionOptions" --type rust

# Check for any schema-related issues or TODOs
rg -i "schema|openapi" -C 3 "ExecutionOptions" --type rust

Length of output: 890


🏁 Script executed:

#!/bin/bash
set -e
# List all files in the execution_options directory
find core/src/execution_options -maxdepth 1 -type f

# Check for any remaining JsonSchema derives in this directory
rg "#\[derive.*JsonSchema" -n core/src/execution_options

# List all derive macros to see which traits are used
rg "#\[derive" -n core/src/execution_options

# Show the first 20 lines of mod.rs to inspect its derive attributes
sed -n '1,20p' core/src/execution_options/mod.rs

Length of output: 3451


Add missing JsonSchema derive to EIP-7702 execution options

The Eip7702ExecutionOptions enum and its two variant structs currently derive Debug, Clone, Serialize, Deserialize, and utoipa::ToSchema, but omit JsonSchema. All other ExecutionOptions types in this module include the JsonSchema derive to feed Schemars into your OpenAPI docs. To keep schema generation consistent, please update the derives as follows:

• In core/src/execution_options/eip7702.rs:

- #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
+ #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]

• Apply the same change to the two structs at lines 17 and 26 so they also include JsonSchema.

This will restore Schemars support for the EIP-7702 options and align with the rest of the execution-options API.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(title = "EIP-7702 Execution Options")]
#[serde(rename_all = "camelCase", untagged)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]
#[schema(title = "EIP-7702 Execution Options")]
#[serde(rename_all = "camelCase", untagged)]
🤖 Prompt for AI Agents
In core/src/execution_options/eip7702.rs around lines 6 to 8, the
Eip7702ExecutionOptions enum is missing the JsonSchema derive attribute. Add
JsonSchema to the derive list alongside Debug, Clone, Serialize, Deserialize,
and utoipa::ToSchema. Also, locate the two variant structs at lines 17 and 26
and add the JsonSchema derive to them as well to ensure consistent schema
generation across all execution option types.

pub enum Eip7702ExecutionOptions {
/// Execute the transaction as the owner of the account
Owner(Eip7702OwnerExecution),
/// Execute a transaction on a different delegated account (`account_address`), which has granted a session key to the `session_key_address`
/// `session_key_address` is the signer for this transaction
SessionKey(Eip7702SessionKeyExecution),
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(title = "EIP-7702 Owner Execution")]
#[serde(rename_all = "camelCase")]
pub struct Eip7702ExecutionOptions {
/// The EOA address that will sign the EIP-7702 transaction
#[schemars(with = "AddressDef")]
pub struct Eip7702OwnerExecution {
#[schema(value_type = AddressDef)]
/// The delegated EOA address
pub from: Address,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(title = "EIP-7702 Session Key Execution")]
#[serde(rename_all = "camelCase")]
pub struct Eip7702SessionKeyExecution {
#[schema(value_type = AddressDef)]
/// The session key address is your server wallet, which has been granted a session key to the `account_address`
pub session_key_address: Address,
#[schema(value_type = AddressDef)]
/// The account address is the address of a delegated account you want to execute the transaction on. This account has granted a session key to the `session_key_address`
pub account_address: Address,
}
2 changes: 1 addition & 1 deletion core/src/execution_options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn default_idempotency_key() -> String {
}

/// All supported specific execution options are contained here
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "type")]
#[schema(title = "Execution Option Variants")]
pub enum SpecificExecutionOptions {
Expand Down
2 changes: 1 addition & 1 deletion core/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl AccountSigner for EoaSigner {
} => {
let iaw_result = self
.iaw_client
.sign_transaction(auth_token, thirdweb_auth, &transaction)
.sign_transaction(auth_token, thirdweb_auth, transaction)
.await
.map_err(|e| {
tracing::error!("Error signing transaction with EOA (IAW): {:?}", e);
Expand Down
14 changes: 14 additions & 0 deletions eip7702-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "engine-eip7702-core"
version = "0.1.0"
edition = "2024"

[dependencies]
alloy = { workspace = true, features = ["serde"] }
tokio = "1.44.2"
engine-core = { path = "../core" }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1.41"
rand = "0.9"
thiserror = "2.0"
Comment on lines +1 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

edition = "2024" and tokio = "1.44.2" are not yet available

  • edition = "2024" is unstable on the current stable toolchain; compilation will fail for all users not on nightly with the 2024-edition preview enabled.
  • The latest published tokio crate is 1.38.x (as of July 2025). Version 1.44.2 does not exist and will cause cargo resolution errors.
-edition = "2024"
+edition = "2021"

-tokio = "1.44.2"
+# keep in sync with workspace root
+tokio = "1.38"

Please bump once the official 2024 edition and the referenced Tokio release ship.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[package]
name = "engine-eip7702-core"
version = "0.1.0"
edition = "2024"
[dependencies]
alloy = { workspace = true, features = ["serde"] }
tokio = "1.44.2"
engine-core = { path = "../core" }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1.41"
rand = "0.9"
thiserror = "2.0"
[package]
name = "engine-eip7702-core"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy = { workspace = true, features = ["serde"] }
# keep in sync with workspace root
tokio = "1.38"
engine-core = { path = "../core" }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1.41"
rand = "0.9"
thiserror = "2.0"
🤖 Prompt for AI Agents
In eip7702-core/Cargo.toml lines 1 to 14, the edition is set to "2024" which is
not stable and the tokio version "1.44.2" does not exist. Change the edition to
the latest stable one, such as "2021", and set the tokio dependency to the
latest published version, for example "1.38". Update these once the official
2024 edition and the correct Tokio version are released.

11 changes: 11 additions & 0 deletions eip7702-core/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use alloy::primitives::{Address, address};

/// The minimal account implementation address used for EIP-7702 delegation
pub const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS: Address =
address!("0xD6999651Fc0964B9c6B444307a0ab20534a66560");

/// EIP-7702 delegation prefix bytes
pub const EIP_7702_DELEGATION_PREFIX: [u8; 3] = [0xef, 0x01, 0x00];

/// EIP-7702 delegation code length (prefix + address)
pub const EIP_7702_DELEGATION_CODE_LENGTH: usize = 23;
132 changes: 132 additions & 0 deletions eip7702-core/src/delegated_account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use alloy::{
primitives::{Address, FixedBytes},
providers::Provider,
};
use engine_core::{
chain::Chain,
credentials::SigningCredential,
error::{AlloyRpcErrorToEngineError, EngineError},
signer::{AccountSigner, EoaSigner, EoaSigningOptions},
};
use rand::Rng;

use crate::constants::{
EIP_7702_DELEGATION_CODE_LENGTH, EIP_7702_DELEGATION_PREFIX,
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
};

/// Represents an EOA address that can have EIP-7702 delegation, associated with a specific chain
#[derive(Clone, Debug)]
pub struct DelegatedAccount<C: Chain> {
/// The EOA address that may have delegation
pub eoa_address: Address,
/// The chain this account operates on
pub chain: C,
}

impl<C: Chain> DelegatedAccount<C> {
/// Create a new delegated account from an EOA address and chain
pub fn new(eoa_address: Address, chain: C) -> Self {
Self { eoa_address, chain }
}

/// Check if the EOA has EIP-7702 delegation to the minimal account implementation
pub async fn is_minimal_account(&self) -> Result<bool, EngineError> {
// Get the bytecode at the EOA address using eth_getCode
let code = self
.chain
.provider()
.get_code_at(self.eoa_address)
.await
.map_err(|e| e.to_engine_error(self.chain()))?;

tracing::debug!(
eoa_address = ?self.eoa_address,
code_length = code.len(),
code_hex = ?alloy::hex::encode(&code),
"Checking EIP-7702 delegation"
);

// Check if code exists and starts with EIP-7702 delegation prefix "0xef0100"
if code.len() < EIP_7702_DELEGATION_CODE_LENGTH
|| !code.starts_with(&EIP_7702_DELEGATION_PREFIX)
{
tracing::debug!(
eoa_address = ?self.eoa_address,
has_delegation = false,
reason = "Code too short or doesn't start with EIP-7702 prefix",
"EIP-7702 delegation check result"
);
return Ok(false);
}

// Extract the target address from bytes 3-23 (20 bytes for address)
// EIP-7702 format: 0xef0100 + 20 bytes address
let target_bytes = &code[3..23];
let target_address = Address::from_slice(target_bytes);

// Compare with the minimal account implementation address
let is_delegated = target_address == MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS;

tracing::debug!(
eoa_address = ?self.eoa_address,
target_address = ?target_address,
minimal_account_address = ?MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
has_delegation = is_delegated,
"EIP-7702 delegation check result"
);

Ok(is_delegated)
}

/// Get the EOA address
pub fn address(&self) -> Address {
self.eoa_address
}

/// Get the current nonce for the EOA
pub async fn get_nonce(&self) -> Result<u64, EngineError> {
self.chain
.provider()
.get_transaction_count(self.eoa_address)
.await
.map_err(|e| e.to_engine_error(self.chain()))
}

/// Get a reference to the chain
pub fn chain(&self) -> &C {
&self.chain
}

/// Sign authorization for EIP-7702 delegation (automatically fetches nonce)
pub async fn sign_authorization(
&self,
eoa_signer: &EoaSigner,
credentials: &SigningCredential,
) -> Result<alloy::eips::eip7702::SignedAuthorization, EngineError> {
let nonce = self.get_nonce().await?;

let signing_options = EoaSigningOptions {
from: self.eoa_address,
chain_id: Some(self.chain.chain_id()),
};

eoa_signer
.sign_authorization(
signing_options,
self.chain.chain_id(),
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
nonce,
credentials,
)
.await
}

/// Generate a random UID for wrapped calls
pub fn generate_random_uid() -> FixedBytes<32> {
let mut rng = rand::rng();
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
FixedBytes::from(bytes)
}
}
3 changes: 3 additions & 0 deletions eip7702-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod constants;
pub mod delegated_account;
pub mod transaction;
Loading
Loading