diff --git a/Cargo.lock b/Cargo.lock index 3dcca16..8167189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,9 @@ dependencies = [ "alloy-eips 0.15.8", "alloy-serde 0.15.8", "alloy-signer 0.15.8", - "alloy-signer-aws", - "alloy-signer-gcp", - "alloy-signer-ledger", + "alloy-signer-aws 0.15.8", + "alloy-signer-gcp 0.15.8", + "alloy-signer-ledger 0.15.8", ] [[package]] @@ -109,6 +109,9 @@ dependencies = [ "alloy-rpc-types", "alloy-serde 1.0.8", "alloy-signer 1.0.8", + "alloy-signer-aws 1.0.9", + "alloy-signer-gcp 1.0.9", + "alloy-signer-ledger 1.0.9", "alloy-signer-local", "alloy-transport", "alloy-transport-http", @@ -692,7 +695,9 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afebd60fa84d9ce793326941509d8f26ce7b383f2aabd7a42ba215c1b92ea96b" dependencies = [ + "alloy-dyn-abi", "alloy-primitives", + "alloy-sol-types", "async-trait", "auto_impl", "either", @@ -719,6 +724,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "alloy-signer-aws" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be3d371299b62eac5aa459fa58e8d1c761aabdc637573ae258ab744457fcc88" +dependencies = [ + "alloy-consensus 1.0.8", + "alloy-network 1.0.8", + "alloy-primitives", + "alloy-signer 1.0.8", + "async-trait", + "aws-sdk-kms", + "k256", + "spki", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "alloy-signer-gcp" version = "0.15.8" @@ -737,6 +760,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "alloy-signer-gcp" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df298e47bbb7d0a8e06b603046b91062c11ba70d22f8a6c9bab1c1468bd856d0" +dependencies = [ + "alloy-consensus 1.0.8", + "alloy-network 1.0.8", + "alloy-primitives", + "alloy-signer 1.0.8", + "async-trait", + "gcloud-sdk", + "k256", + "spki", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "alloy-signer-ledger" version = "0.15.8" @@ -757,6 +798,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "alloy-signer-ledger" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0e049299cc7e131a438a904f89a493bcea45cd92bbed3e50116a28bc27987c" +dependencies = [ + "alloy-consensus 1.0.8", + "alloy-dyn-abi", + "alloy-network 1.0.8", + "alloy-primitives", + "alloy-signer 1.0.8", + "alloy-sol-types", + "async-trait", + "coins-ledger", + "futures-util", + "semver 1.0.26", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "alloy-signer-local" version = "1.0.8" @@ -2364,6 +2425,7 @@ name = "engine-aa-core" version = "0.1.0" dependencies = [ "alloy 1.0.9", + "engine-aa-types", "engine-core", "serde", "tokio", @@ -2372,12 +2434,25 @@ dependencies = [ "vault-types", ] +[[package]] +name = "engine-aa-types" +version = "0.1.0" +dependencies = [ + "alloy 1.0.9", + "schemars 0.8.22", + "serde", + "serde_json", + "thiserror 2.0.12", + "utoipa", +] + [[package]] name = "engine-core" version = "0.1.0" dependencies = [ "alloy 1.0.9", "async-nats", + "engine-aa-types", "schemars 0.8.22", "serde", "serde_json", @@ -2400,6 +2475,7 @@ dependencies = [ "alloy 1.0.9", "chrono", "engine-aa-core", + "engine-aa-types", "engine-core", "hex", "hmac", @@ -5361,6 +5437,7 @@ name = "thirdweb-core" version = "0.1.0" dependencies = [ "alloy 1.0.9", + "engine-aa-types", "moka", "reqwest", "schemars 0.8.22", diff --git a/Cargo.toml b/Cargo.toml index 3506f4c..32fe580 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["aa-core", "core", "executors", "server", "thirdweb-core", "twmq"] +members = ["aa-types", "aa-core", "core", "executors", "server", "thirdweb-core", "twmq"] resolver = "2" diff --git a/aa-core/Cargo.toml b/aa-core/Cargo.toml index 229fc18..8644cc7 100644 --- a/aa-core/Cargo.toml +++ b/aa-core/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] alloy = { version = "1.0.8", features = ["serde"] } tokio = "1.44.2" +engine-aa-types = { path = "../aa-types" } engine-core = { path = "../core" } vault-types = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "main" } vault-sdk = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "main" } diff --git a/aa-core/src/userop/builder.rs b/aa-core/src/userop/builder.rs index 13c88d8..29c0d7b 100644 --- a/aa-core/src/userop/builder.rs +++ b/aa-core/src/userop/builder.rs @@ -6,12 +6,13 @@ use alloy::{ providers::Provider, rpc::types::{PackedUserOperation, UserOperation}, }; +use engine_aa_types::VersionedUserOp; use engine_core::{ chain::Chain, credentials::SigningCredential, error::{AlloyRpcErrorToEngineError, EngineError}, execution_options::aa::{EntrypointAndFactoryDetails, EntrypointVersion}, - userop::{UserOpSigner, UserOpSignerParams, UserOpVersion}, + userop::{UserOpSigner, UserOpSignerParams}, }; pub struct UserOpBuilderConfig<'a, C: Chain> { @@ -40,7 +41,7 @@ impl<'a, C: Chain> UserOpBuilder<'a, C> { Self { config } } - pub async fn build(self) -> Result { + pub async fn build(self) -> Result { let mut userop = match self.config.entrypoint_and_factory.version { EntrypointVersion::V0_6 => UserOpBuilderV0_6::new(&self.config).build().await?, EntrypointVersion::V0_7 => UserOpBuilderV0_7::new(&self.config).build().await?, @@ -61,10 +62,10 @@ impl<'a, C: Chain> UserOpBuilder<'a, C> { .await?; match &mut userop { - UserOpVersion::V0_6(userop) => { + VersionedUserOp::V0_6(userop) => { userop.signature = signature; } - UserOpVersion::V0_7(userop) => { + VersionedUserOp::V0_7(userop) => { userop.signature = signature; } } @@ -114,7 +115,7 @@ impl<'a, C: Chain> UserOpBuilderV0_6<'a, C> { } } - async fn build(mut self) -> Result { + async fn build(mut self) -> Result { let prices = self .chain .provider() @@ -153,7 +154,7 @@ impl<'a, C: Chain> UserOpBuilderV0_6<'a, C> { .chain .bundler_client() .estimate_user_op_gas( - &UserOpVersion::V0_6(self.userop.clone()), + &VersionedUserOp::V0_6(self.userop.clone()), self.entrypoint, None, ) @@ -172,7 +173,7 @@ impl<'a, C: Chain> UserOpBuilderV0_6<'a, C> { self.userop.verification_gas_limit = verification_gas_limit; self.userop.pre_verification_gas = pre_verification_gas; - Ok(UserOpVersion::V0_6(self.userop)) + Ok(VersionedUserOp::V0_6(self.userop)) } } @@ -219,7 +220,7 @@ impl<'a, C: Chain> UserOpBuilderV0_7<'a, C> { } } - async fn build(mut self) -> Result { + async fn build(mut self) -> Result { // Get gas prices, same as v0.6 let prices = self .chain @@ -272,7 +273,7 @@ impl<'a, C: Chain> UserOpBuilderV0_7<'a, C> { .chain .bundler_client() .estimate_user_op_gas( - &UserOpVersion::V0_7(self.userop.clone()), + &VersionedUserOp::V0_7(self.userop.clone()), self.entrypoint, None, ) @@ -300,6 +301,6 @@ impl<'a, C: Chain> UserOpBuilderV0_7<'a, C> { self.userop.paymaster_verification_gas_limit = Some(paymaster_verification_gas_limit); self.userop.paymaster_post_op_gas_limit = Some(paymaster_post_op_gas_limit); - Ok(UserOpVersion::V0_7(self.userop)) + Ok(VersionedUserOp::V0_7(self.userop)) } } diff --git a/aa-types/Cargo.toml b/aa-types/Cargo.toml new file mode 100644 index 0000000..cc33f28 --- /dev/null +++ b/aa-types/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "engine-aa-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +alloy = { version = "1.0.8", features = ["serde"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +thiserror = "2.0.12" +schemars = "0.8.22" +utoipa = "5.4.0" \ No newline at end of file diff --git a/aa-types/src/lib.rs b/aa-types/src/lib.rs new file mode 100644 index 0000000..18cb98e --- /dev/null +++ b/aa-types/src/lib.rs @@ -0,0 +1,3 @@ +pub mod userop; + +pub use userop::*; \ No newline at end of file diff --git a/aa-types/src/userop.rs b/aa-types/src/userop.rs new file mode 100644 index 0000000..e164082 --- /dev/null +++ b/aa-types/src/userop.rs @@ -0,0 +1,157 @@ +use alloy::{ + core::sol_types::SolValue, + primitives::{keccak256, Address, ChainId, Bytes, U256, B256}, + rpc::types::{PackedUserOperation, UserOperation}, +}; +use serde::{Deserialize, Serialize}; + +/// UserOp version enum +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum VersionedUserOp { + V0_6(UserOperation), + V0_7(PackedUserOperation), +} + +/// Error type for UserOp operations +#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UserOpError { + #[error("Unexpected error: {0}")] + UnexpectedError(String), +} + +/// Compute UserOperation v0.6 hash +pub fn compute_user_op_v06_hash( + op: &UserOperation, + entrypoint: Address, + chain_id: ChainId, +) -> Result { + // Hash the byte fields first + let init_code_hash = keccak256(&op.init_code); + let call_data_hash = keccak256(&op.call_data); + let paymaster_and_data_hash = keccak256(&op.paymaster_and_data); + + // Create the inner tuple (WITHOUT signature!) + let inner_tuple = ( + op.sender, + op.nonce, + init_code_hash, + call_data_hash, + op.call_gas_limit, + op.verification_gas_limit, + op.pre_verification_gas, + op.max_fee_per_gas, + op.max_priority_fee_per_gas, + paymaster_and_data_hash, + ); + + // ABI encode and hash the inner tuple + let inner_encoded = inner_tuple.abi_encode(); + let inner_hash = keccak256(&inner_encoded); + + // Create the outer tuple + let outer_tuple = (inner_hash, entrypoint, U256::from(chain_id)); + + // ABI encode and hash the outer tuple + let outer_encoded = outer_tuple.abi_encode(); + let final_hash = keccak256(&outer_encoded); + Ok(final_hash) +} + +/// Compute UserOperation v0.7 hash +pub fn compute_user_op_v07_hash( + op: &PackedUserOperation, + entrypoint: Address, + chain_id: ChainId, +) -> Result { + // Construct initCode from factory and factoryData + let init_code: Bytes = if let Some(factory) = op.factory { + if factory != Address::ZERO { + [&factory[..], &op.factory_data.clone().unwrap_or_default()[..]].concat().into() + } else { + op.factory_data.clone().unwrap_or_default() + } + } else { + Bytes::default() + }; + + // Construct accountGasLimits + let vgl_u128: u128 = op.verification_gas_limit.try_into().map_err(|_| { + UserOpError::UnexpectedError("verification_gas_limit too large".to_string()) + })?; + let cgl_u128: u128 = op.call_gas_limit.try_into().map_err(|_| { + UserOpError::UnexpectedError("call_gas_limit too large".to_string()) + })?; + + let mut account_gas_limits_bytes = [0u8; 32]; + account_gas_limits_bytes[0..16].copy_from_slice(&vgl_u128.to_be_bytes()); + account_gas_limits_bytes[16..32].copy_from_slice(&cgl_u128.to_be_bytes()); + let account_gas_limits = B256::from(account_gas_limits_bytes); + + // Construct gasFees + let mpfpg_u128: u128 = op.max_priority_fee_per_gas.try_into().map_err(|_| { + UserOpError::UnexpectedError("max_priority_fee_per_gas too large".to_string()) + })?; + let mfpg_u128: u128 = op.max_fee_per_gas.try_into().map_err(|_| { + UserOpError::UnexpectedError("max_fee_per_gas too large".to_string()) + })?; + + let mut gas_fees_bytes = [0u8; 32]; + gas_fees_bytes[0..16].copy_from_slice(&mpfpg_u128.to_be_bytes()); + gas_fees_bytes[16..32].copy_from_slice(&mfpg_u128.to_be_bytes()); + let gas_fees = B256::from(gas_fees_bytes); + + // Construct paymasterAndData + let paymaster_and_data: Bytes = if let Some(paymaster) = op.paymaster { + if paymaster != Address::ZERO { + let pm_vgl_u128: u128 = op.paymaster_verification_gas_limit.unwrap_or_default().try_into().map_err(|_| { + UserOpError::UnexpectedError("paymaster_verification_gas_limit too large".to_string()) + })?; + let pm_pogl_u128: u128 = op.paymaster_post_op_gas_limit.unwrap_or_default().try_into().map_err(|_| { + UserOpError::UnexpectedError("paymaster_post_op_gas_limit too large".to_string()) + })?; + [ + &paymaster[..], + &pm_vgl_u128.to_be_bytes()[..], + &pm_pogl_u128.to_be_bytes()[..], + &op.paymaster_data.clone().unwrap_or_default()[..], + ] + .concat() + .into() + } else { + op.paymaster_data.clone().unwrap_or_default() + } + } else { + Bytes::default() + }; + + // Hash the byte fields + let init_code_hash = keccak256(&init_code); + let call_data_hash = keccak256(&op.call_data); + let paymaster_and_data_hash = keccak256(&paymaster_and_data); + + // Create the inner tuple + let inner_tuple = ( + op.sender, + op.nonce, + init_code_hash, + call_data_hash, + account_gas_limits, + op.pre_verification_gas, + gas_fees, + paymaster_and_data_hash, + ); + + // ABI encode and hash the inner tuple + let inner_encoded = inner_tuple.abi_encode(); + let inner_hash = keccak256(&inner_encoded); + + // Create the outer tuple + let outer_tuple = (inner_hash, entrypoint, U256::from(chain_id)); + + // ABI encode and hash the outer tuple + let outer_encoded = outer_tuple.abi_encode(); + let final_hash = keccak256(&outer_encoded); + Ok(final_hash) +} \ No newline at end of file diff --git a/core/Cargo.toml b/core/Cargo.toml index 614d3e2..88b9ab2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] alloy = { version = "1.0.8", features = ["serde", "json-rpc"] } +engine-aa-types = { path = "../aa-types" } schemars = "0.8.22" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/core/src/credentials.rs b/core/src/credentials.rs index 3b390c1..ff311fd 100644 --- a/core/src/credentials.rs +++ b/core/src/credentials.rs @@ -1,7 +1,13 @@ use serde::{Deserialize, Serialize}; +use thirdweb_core::auth::ThirdwebAuth; +use thirdweb_core::iaw::AuthToken; use vault_types::enclave::auth::Auth; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SigningCredential { Vault(Auth), + Iaw { + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth + }, } diff --git a/core/src/error.rs b/core/src/error.rs index 8fae8a1..d3779e9 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -8,6 +8,7 @@ use alloy::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thirdweb_core::error::ThirdwebError; + use thiserror::Error; use twmq::error::TwmqError; @@ -206,6 +207,14 @@ pub enum EngineError { #[serde(rename_all = "camelCase")] VaultError { message: String }, + #[schema(title = "Engine IAW Service Error")] + #[error("Error interaction with IAW service: {error}")] + #[serde(rename_all = "camelCase")] + IawError { + #[from] + error: thirdweb_core::iaw::IAWError, + }, + #[schema(title = "RPC Configuration Error")] #[error("Bad RPC configuration: {message}")] RpcConfigError { message: String }, @@ -456,3 +465,4 @@ impl From for EngineError { } } } + diff --git a/core/src/rpc_clients/bundler.rs b/core/src/rpc_clients/bundler.rs index 0ee27b5..ecb27d7 100644 --- a/core/src/rpc_clients/bundler.rs +++ b/core/src/rpc_clients/bundler.rs @@ -6,7 +6,7 @@ use alloy::transports::{IntoBoxTransport, TransportResult}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::userop::UserOpVersion; +use crate::userop::VersionedUserOp; // Gas buffer added for managed account factories (matches TypeScript) pub const MANAGED_ACCOUNT_GAS_BUFFER: U256 = U256::from_limbs([21_000, 0, 0, 0]); @@ -81,7 +81,7 @@ impl BundlerClient { /// Estimate the gas for a user operation pub async fn estimate_user_op_gas( &self, - user_op: &UserOpVersion, + user_op: &VersionedUserOp, entrypoint: Address, state_overrides: Option>>, ) -> TransportResult { @@ -104,7 +104,7 @@ impl BundlerClient { pub async fn send_user_op( &self, - user_op: &UserOpVersion, + user_op: &VersionedUserOp, entrypoint: Address, ) -> TransportResult { let result: Bytes = self diff --git a/core/src/signer.rs b/core/src/signer.rs index 29f6b8b..4aeb816 100644 --- a/core/src/signer.rs +++ b/core/src/signer.rs @@ -5,6 +5,7 @@ use alloy::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, PickFirst, serde_as}; +use thirdweb_core::iaw::IAWClient; use vault_sdk::VaultClient; use vault_types::enclave::encrypted::eoa::MessageFormat; @@ -164,12 +165,13 @@ pub trait AccountSigner { #[derive(Clone)] pub struct EoaSigner { pub vault_client: VaultClient, + pub iaw_client: IAWClient, } impl EoaSigner { /// Create a new EOA signer - pub fn new(vault_client: VaultClient) -> Self { - Self { vault_client } + pub fn new(vault_client: VaultClient, iaw_client: IAWClient) -> Self { + Self { vault_client, iaw_client } } } @@ -196,12 +198,37 @@ impl AccountSigner for EoaSigner { ) .await .map_err(|e| { - tracing::error!("Error signing message with EOA: {:?}", e); + tracing::error!("Error signing message with EOA (Vault): {:?}", e); e })?; Ok(vault_result.signature) } + SigningCredential::Iaw { auth_token, thirdweb_auth } => { + // Convert MessageFormat to IAW MessageFormat + let iaw_format = match format { + MessageFormat::Text => thirdweb_core::iaw::MessageFormat::Text, + MessageFormat::Hex => thirdweb_core::iaw::MessageFormat::Hex, + }; + + let iaw_result = self + .iaw_client + .sign_message( + auth_token, + thirdweb_auth, + message.to_string(), + options.from, + options.chain_id, + Some(iaw_format), + ) + .await + .map_err(|e| { + tracing::error!("Error signing message with EOA (IAW): {:?}", e); + EngineError::from(e) + })?; + + Ok(iaw_result.signature) + } } } @@ -218,12 +245,29 @@ impl AccountSigner for EoaSigner { .sign_typed_data(auth_method.clone(), typed_data.clone(), options.from) .await .map_err(|e| { - tracing::error!("Error signing typed data with EOA: {:?}", e); + tracing::error!("Error signing typed data with EOA (Vault): {:?}", e); e })?; Ok(vault_result.signature) } + SigningCredential::Iaw { auth_token, thirdweb_auth } => { + let iaw_result = self + .iaw_client + .sign_typed_data( + auth_token.clone(), + thirdweb_auth.clone(), + typed_data.clone(), + options.from, + ) + .await + .map_err(|e| { + tracing::error!("Error signing typed data with EOA (IAW): {:?}", e); + EngineError::from(e) + })?; + + Ok(iaw_result.signature) + } } } } diff --git a/core/src/userop.rs b/core/src/userop.rs index 8a13e8c..cbdefcb 100644 --- a/core/src/userop.rs +++ b/core/src/userop.rs @@ -1,9 +1,8 @@ use alloy::{ hex::FromHex, primitives::{Address, Bytes, ChainId}, - rpc::types::{PackedUserOperation, UserOperation}, }; -use serde::{Deserialize, Serialize}; +use thirdweb_core::iaw::IAWClient; use vault_sdk::VaultClient; use vault_types::{ enclave::encrypted::eoa::StructuredMessageInput, @@ -12,69 +11,62 @@ use vault_types::{ use crate::{credentials::SigningCredential, error::EngineError}; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum UserOpVersion { - V0_6(UserOperation), - V0_7(PackedUserOperation), -} +// Re-export for convenience +pub use engine_aa_types::VersionedUserOp; #[derive(Clone)] pub struct UserOpSigner { pub vault_client: VaultClient, + pub iaw_client: IAWClient, } pub struct UserOpSignerParams { pub credentials: SigningCredential, pub entrypoint: Address, - pub userop: UserOpVersion, + pub userop: VersionedUserOp, pub signer_address: Address, pub chain_id: ChainId, } -impl UserOpVersion { - fn to_vault_input(&self, entrypoint: Address) -> StructuredMessageInput { - match self { - UserOpVersion::V0_6(userop) => { - StructuredMessageInput::UserOperationV06Input(UserOperationV06Input { - call_data: userop.call_data.clone(), - init_code: userop.init_code.clone(), - nonce: userop.nonce, - pre_verification_gas: userop.pre_verification_gas, - max_fee_per_gas: userop.max_fee_per_gas, - verification_gas_limit: userop.verification_gas_limit, - sender: userop.sender, - paymaster_and_data: userop.paymaster_and_data.clone(), - signature: userop.signature.clone(), - call_gas_limit: userop.call_gas_limit, - max_priority_fee_per_gas: userop.max_priority_fee_per_gas, - entrypoint, - }) - } - UserOpVersion::V0_7(userop) => { - StructuredMessageInput::UserOperationV07Input(UserOperationV07Input { - call_data: userop.call_data.clone(), - nonce: userop.nonce, - pre_verification_gas: userop.pre_verification_gas, - max_fee_per_gas: userop.max_fee_per_gas, - verification_gas_limit: userop.verification_gas_limit, - sender: userop.sender, - paymaster_data: userop.paymaster_data.clone().unwrap_or_default(), - factory: userop.factory.unwrap_or_default(), - factory_data: userop.factory_data.clone().unwrap_or_default(), - paymaster_post_op_gas_limit: userop - .paymaster_post_op_gas_limit - .unwrap_or_default(), - paymaster_verification_gas_limit: userop - .paymaster_verification_gas_limit - .unwrap_or_default(), - signature: userop.signature.clone(), - call_gas_limit: userop.call_gas_limit, - max_priority_fee_per_gas: userop.max_priority_fee_per_gas, - paymaster: userop.paymaster.unwrap_or_default(), - entrypoint, - }) - } +fn userop_to_vault_input(userop: &VersionedUserOp, entrypoint: Address) -> StructuredMessageInput { + match userop { + VersionedUserOp::V0_6(userop) => { + StructuredMessageInput::UserOperationV06Input(UserOperationV06Input { + call_data: userop.call_data.clone(), + init_code: userop.init_code.clone(), + nonce: userop.nonce, + pre_verification_gas: userop.pre_verification_gas, + max_fee_per_gas: userop.max_fee_per_gas, + verification_gas_limit: userop.verification_gas_limit, + sender: userop.sender, + paymaster_and_data: userop.paymaster_and_data.clone(), + signature: userop.signature.clone(), + call_gas_limit: userop.call_gas_limit, + max_priority_fee_per_gas: userop.max_priority_fee_per_gas, + entrypoint, + }) + } + VersionedUserOp::V0_7(userop) => { + StructuredMessageInput::UserOperationV07Input(UserOperationV07Input { + call_data: userop.call_data.clone(), + nonce: userop.nonce, + pre_verification_gas: userop.pre_verification_gas, + max_fee_per_gas: userop.max_fee_per_gas, + verification_gas_limit: userop.verification_gas_limit, + sender: userop.sender, + paymaster_data: userop.paymaster_data.clone().unwrap_or_default(), + factory: userop.factory.unwrap_or_default(), + factory_data: userop.factory_data.clone().unwrap_or_default(), + paymaster_post_op_gas_limit: userop.paymaster_post_op_gas_limit.unwrap_or_default(), + paymaster_verification_gas_limit: userop + .paymaster_verification_gas_limit + .unwrap_or_default(), + signature: userop.signature.clone(), + call_gas_limit: userop.call_gas_limit, + max_priority_fee_per_gas: userop.max_priority_fee_per_gas, + paymaster: userop.paymaster.unwrap_or_default(), + entrypoint, + }) } } } @@ -88,7 +80,7 @@ impl UserOpSigner { .sign_structured_message( auth_method.clone(), params.signer_address, - params.userop.to_vault_input(params.entrypoint), + userop_to_vault_input(¶ms.userop, params.entrypoint), Some(params.chain_id), ) .await @@ -103,6 +95,31 @@ impl UserOpSigner { } })?) } + SigningCredential::Iaw { + auth_token, + thirdweb_auth, + } => { + let result = self + .iaw_client + .sign_userop( + auth_token.clone(), + thirdweb_auth.clone(), + params.userop, + params.entrypoint, + params.signer_address, + params.chain_id, + ) + .await + .map_err(|e| EngineError::ValidationError { + message: format!("Failed to sign userop: {}", e), + })?; + + Ok(Bytes::from_hex(&result.signature).map_err(|_| { + EngineError::ValidationError { + message: "Bad signature received from IAW".to_string(), + } + })?) + } } } } diff --git a/executors/Cargo.toml b/executors/Cargo.toml index 251e9fa..21efa2e 100644 --- a/executors/Cargo.toml +++ b/executors/Cargo.toml @@ -14,6 +14,7 @@ sha2 = "0.10.9" thiserror = "2.0.12" tracing = "0.1.41" twmq = { version = "0.1.0", path = "../twmq" } +engine-aa-types = { version = "0.1.0", path = "../aa-types" } engine-core = { version = "0.1.0", path = "../core" } engine-aa-core = { version = "0.1.0", path = "../aa-core" } rand = "0.9.1" diff --git a/executors/src/external_bundler/confirm.rs b/executors/src/external_bundler/confirm.rs index 65af653..a3dc929 100644 --- a/executors/src/external_bundler/confirm.rs +++ b/executors/src/external_bundler/confirm.rs @@ -202,6 +202,7 @@ where tracing::info!( transaction_id = %job_data.transaction_id, user_op_hash = ?job_data.user_op_hash, + transaction_hash = ?receipt.receipt.transaction_hash, success = %receipt.success, "User operation confirmed on-chain" ); diff --git a/executors/src/external_bundler/send.rs b/executors/src/external_bundler/send.rs index e9de879..80ca863 100644 --- a/executors/src/external_bundler/send.rs +++ b/executors/src/external_bundler/send.rs @@ -10,13 +10,14 @@ use engine_aa_core::{ deployment::{AcquireLockResult, DeploymentLock, DeploymentManager, DeploymentStatus}, }, }; +use engine_aa_types::VersionedUserOp; use engine_core::{ chain::{Chain, ChainService, RpcCredentials}, credentials::SigningCredential, error::{AlloyRpcErrorToEngineError, EngineError, RpcErrorKind}, - execution_options::{WebhookOptions, aa::Erc4337ExecutionOptions}, + execution_options::{aa::Erc4337ExecutionOptions, WebhookOptions}, transaction::InnerTransaction, - userop::{UserOpSigner, UserOpVersion}, + userop::UserOpSigner, }; use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; @@ -68,7 +69,7 @@ pub struct ExternalBundlerSendResult { pub account_address: Address, pub nonce: U256, pub user_op_hash: Bytes, - pub user_operation_sent: UserOpVersion, + pub user_operation_sent: VersionedUserOp, pub deployment_lock_acquired: bool, } @@ -112,7 +113,7 @@ pub enum ExternalBundlerSendError { account_address: Address, nonce_used: U256, had_deployment_lock: bool, - user_op: UserOpVersion, + user_op: VersionedUserOp, message: String, inner_error: Option, }, @@ -440,16 +441,20 @@ where needs_init_code, ); - // if is_bundler_error_retryable(&e) { - if job.job.attempts < 100 { - mapped_error.nack(Some(Duration::from_secs(10)), RequeuePosition::Last) + tracing::warn!( + error = serde_json::to_string(&mapped_error).unwrap(), + "error" + ); + + if is_bundler_error_retryable(&mapped_error.to_string()) { + if job.job.attempts < 100 { + mapped_error.nack(Some(Duration::from_secs(10)), RequeuePosition::Last) + } else { + mapped_error.fail() + } } else { mapped_error.fail() } - - // } else { - // mapped_error.fail() - // } })?; tracing::debug!(userop_hash = ?user_op_hash, "User operation sent to bundler"); @@ -591,7 +596,7 @@ fn map_bundler_error( bundler_error: impl AlloyRpcErrorToEngineError, smart_account: &DeterminedSmartAccount, nonce: U256, - signed_user_op: &UserOpVersion, + signed_user_op: &VersionedUserOp, chain: &impl Chain, had_lock: bool, ) -> ExternalBundlerSendError { @@ -679,3 +684,55 @@ fn is_non_retryable_rpc_code(code: i64) -> bool { _ => false, } } + +/// Determines if a bundler error should be retried based on its content +fn is_bundler_error_retryable(error_msg: &str) -> bool { + // Check for specific AA error codes that should not be retried + if error_msg.contains("AA10") || // sender already constructed + error_msg.contains("AA13") || // initCode failed or OOG + error_msg.contains("AA14") || // initCode must return sender + error_msg.contains("AA15") || // initCode must create sender + error_msg.contains("AA21") || // didn't pay prefund + error_msg.contains("AA22") || // expired or not due + error_msg.contains("AA23") || // reverted (or OOG) + error_msg.contains("AA24") || // signature error + error_msg.contains("AA25") || // invalid account nonce + error_msg.contains("AA31") || // paymaster deposit too low + error_msg.contains("AA32") || // paymaster stake too low + error_msg.contains("AA33") || // reverted (or OOG) + error_msg.contains("AA34") || // signature error + error_msg.contains("AA40") || // over verificationGasLimit + error_msg.contains("AA41") || // too little verificationGas + error_msg.contains("AA50") || // postOp reverted + error_msg.contains("AA51") // prefund below actualGasCost + { + return false; + } + + // Check for revert-related messages that indicate permanent failures + if error_msg.contains("execution reverted") || + error_msg.contains("UserOperation reverted") || + error_msg.contains("reverted during simulation") || + error_msg.contains("invalid signature") || + error_msg.contains("signature error") || + error_msg.contains("nonce too low") || + error_msg.contains("nonce too high") || + error_msg.contains("insufficient funds") + { + return false; + } + + // Check for HTTP status codes that shouldn't be retried (4xx client errors) + if error_msg.contains("status: 400") || + error_msg.contains("status: 401") || + error_msg.contains("status: 403") || + error_msg.contains("status: 404") || + error_msg.contains("status: 422") || + error_msg.contains("status: 429") // rate limit - could be retried but often permanent + { + return false; + } + + // Retry everything else (network issues, 5xx errors, timeouts, etc.) + true +} diff --git a/server/configuration/server_base.yaml b/server/configuration/server_base.yaml index b262d88..e236348 100644 --- a/server/configuration/server_base.yaml +++ b/server/configuration/server_base.yaml @@ -9,6 +9,7 @@ thirdweb: paymaster: bundler.thirdweb-dev.com vault: https://d2w4ge7u2axqfk.cloudfront.net abi_service: https://contract.thirdweb.com/abi/ + iaw_service: https://embedded-wallet.thirdweb-dev.com secret: read_from_env client_id: read_from_env diff --git a/server/configuration/server_production.yaml b/server/configuration/server_production.yaml index fb469c7..db9102c 100644 --- a/server/configuration/server_production.yaml +++ b/server/configuration/server_production.yaml @@ -9,6 +9,7 @@ thirdweb: paymaster: bundler.thirdweb.com vault: https://d145tet905juug.cloudfront.net # override in env abi_service: https://contract.thirdweb.com/abi/ # override in env + iaw_service: https://embedded-wallet.thirdweb.com # override in env secret: read_from_env client_id: read_from_env diff --git a/server/src/config.rs b/server/src/config.rs index 4f469cb..9aaec8e 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -51,6 +51,7 @@ pub struct ThirdwebUrls { pub vault: String, pub paymaster: String, pub abi_service: String, + pub iaw_service: String, } impl Default for ServerConfig { diff --git a/server/src/http/error.rs b/server/src/http/error.rs index f99ddab..a80f914 100644 --- a/server/src/http/error.rs +++ b/server/src/http/error.rs @@ -67,6 +67,17 @@ impl ApiEngineError { _ => StatusCode::INTERNAL_SERVER_ERROR, }, EngineError::VaultError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + EngineError::IawError { error } => match error { + thirdweb_core::iaw::IAWError::ApiError(_) => StatusCode::INTERNAL_SERVER_ERROR, + thirdweb_core::iaw::IAWError::SerializationError { .. } => StatusCode::BAD_REQUEST, + thirdweb_core::iaw::IAWError::NetworkError { .. } => StatusCode::BAD_REQUEST, + thirdweb_core::iaw::IAWError::AuthError(_) => StatusCode::UNAUTHORIZED, + thirdweb_core::iaw::IAWError::ThirdwebError(_) => StatusCode::INTERNAL_SERVER_ERROR, + thirdweb_core::iaw::IAWError::UnexpectedError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + thirdweb_core::iaw::IAWError::UserOpError(_) => StatusCode::BAD_REQUEST, + }, EngineError::BundlerError { .. } => StatusCode::BAD_REQUEST, EngineError::PaymasterError { .. } => StatusCode::BAD_REQUEST, EngineError::ValidationError { .. } => StatusCode::BAD_REQUEST, diff --git a/server/src/http/extractors.rs b/server/src/http/extractors.rs index 4a9a391..7c458d4 100644 --- a/server/src/http/extractors.rs +++ b/server/src/http/extractors.rs @@ -92,13 +92,62 @@ where type Rejection = ApiEngineError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + // Check for IAW credentials first (x-wallet-access-token) + // TODO: this will be deprecated in the future, we should use x-vault-access-token instead for all wallets + if let Some(wallet_token) = parts + .headers + .get("x-wallet-access-token") + .and_then(|v| v.to_str().ok()) + { + // Extract ThirdwebAuth for billing purposes + let thirdweb_auth = if let Some(secret_key) = parts + .headers + .get("x-thirdweb-secret-key") + .and_then(|v| v.to_str().ok()) + { + ThirdwebAuth::SecretKey(secret_key.to_string()) + } else { + // Try client ID and service key combination + let client_id = parts + .headers + .get("x-thirdweb-client-id") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "Missing x-thirdweb-client-id header when using IAW".to_string(), + }) + })?; + + let service_key = parts + .headers + .get("x-thirdweb-service-key") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + ApiEngineError(EngineError::ValidationError { + message: "Missing x-thirdweb-service-key header when using IAW".to_string(), + }) + })?; + + ThirdwebAuth::ClientIdServiceKey(thirdweb_core::auth::ThirdwebClientIdAndServiceKey { + client_id: client_id.to_string(), + service_key: service_key.to_string(), + }) + }; + + return Ok(SigningCredentialsExtractor(SigningCredential::Iaw { + auth_token: wallet_token.to_string(), + thirdweb_auth, + })); + } + + // Fall back to Vault credentials let vault_access_token = parts .headers .get("x-vault-access-token") .and_then(|v| v.to_str().ok()) .ok_or_else(|| { ApiEngineError(EngineError::ValidationError { - message: "Missing x-vault-access-token header".to_string(), + message: "Missing x-vault-access-token or x-wallet-token header".to_string(), }) })?; diff --git a/server/src/main.rs b/server/src/main.rs index b549646..26dda88 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use engine_core::{signer::EoaSigner, userop::UserOpSigner}; -use thirdweb_core::{abi::ThirdwebAbiServiceBuilder, auth::ThirdwebAuth}; +use thirdweb_core::{abi::ThirdwebAbiServiceBuilder, auth::ThirdwebAuth, iaw::IAWClient}; use thirdweb_engine::{ chains::ThirdwebChainService, config, @@ -38,10 +38,14 @@ async fn main() -> anyhow::Result<()> { rpc_base_url: config.thirdweb.urls.rpc, }); + let iaw_client = IAWClient::new(&config.thirdweb.urls.iaw_service)?; + tracing::info!("IAW client initialized"); + let signer = Arc::new(UserOpSigner { vault_client: vault_client.clone(), + iaw_client: iaw_client.clone(), }); - let eoa_signer = Arc::new(EoaSigner { vault_client }); + let eoa_signer = Arc::new(EoaSigner::new(vault_client, iaw_client)); let queue_manager = QueueManager::new(&config.redis, &config.queue, chains.clone(), signer.clone()).await?; diff --git a/thirdweb-core/Cargo.toml b/thirdweb-core/Cargo.toml index f47cd7b..d38fb0d 100644 --- a/thirdweb-core/Cargo.toml +++ b/thirdweb-core/Cargo.toml @@ -4,7 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] -alloy = { version = "1.0.9", features = ["json-abi"] } +alloy = { version = "1.0.9", features = [ + "json-abi", + "consensus", + "dyn-abi", + "eips", + "eip712", +] } +engine-aa-types = { path = "../aa-types" } moka = { version = "0.12.10", features = ["future"] } reqwest = "0.12.18" schemars = "0.8.22" diff --git a/thirdweb-core/src/iaw/mod.rs b/thirdweb-core/src/iaw/mod.rs new file mode 100644 index 0000000..bc02b3a --- /dev/null +++ b/thirdweb-core/src/iaw/mod.rs @@ -0,0 +1,509 @@ +use alloy::{ + consensus::{EthereumTypedTransaction, TxEip4844Variant}, + dyn_abi::TypedData, + eips::eip7702::{Authorization, SignedAuthorization}, + hex, + primitives::{Address, ChainId}, +}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::time::Duration; +use thiserror::Error; + +use crate::{auth::ThirdwebAuth, error::SerializableReqwestError}; +use engine_aa_types::{ + UserOpError, VersionedUserOp, compute_user_op_v06_hash, compute_user_op_v07_hash, +}; + +/// Authentication token for IAW operations +pub type AuthToken = String; + +/// Error types for IAW operations +#[derive( + Error, + Debug, + Clone, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + utoipa::ToSchema, +)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IAWError { + #[error("API error: {0}")] + ApiError(String), + #[error("Serialization error: {message}")] + SerializationError { message: String }, + #[error("Network error: {error}")] + NetworkError { + #[from] + error: SerializableReqwestError, + }, + #[error("Authentication error: {0}")] + AuthError(String), + #[error("Thirdweb error: {0}")] + ThirdwebError(#[from] crate::error::ThirdwebError), + #[error("Unexpected error: {0}")] + UnexpectedError(String), + #[error("UserOp error: {0}")] + UserOpError(#[from] UserOpError), +} + +impl From for IAWError { + fn from(err: serde_json::Error) -> Self { + IAWError::SerializationError { + message: err.to_string(), + } + } +} + +impl From for IAWError { + fn from(err: reqwest::Error) -> Self { + IAWError::NetworkError { + error: SerializableReqwestError::from(err), + } + } +} + +/// Message format for signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageFormat { + Text, + Hex, +} + +impl Default for MessageFormat { + fn default() -> Self { + MessageFormat::Text + } +} + +/// Response data for message signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignMessageData { + pub signature: String, +} + +/// Response data for typed data signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignTypedDataData { + pub signature: String, +} + +/// Response data for transaction signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignTransactionData { + pub signature: String, +} + +/// Response data for authorization signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignAuthorizationData { + pub signed_authorization: SignedAuthorization, +} + +/// Response data for userop signing operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignUserOpData { + pub signature: String, +} + +/// Client for interacting with the IAW (In-App Wallet) service +#[derive(Clone)] +pub struct IAWClient { + base_url: String, + http_client: reqwest::Client, +} + +impl IAWClient { + /// Create a new IAWClient with the given base URL + pub fn new(base_url: impl Into) -> Result { + let http_client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(10)) + .pool_idle_timeout(Duration::from_secs(90)) + .pool_max_idle_per_host(10) + .http2_keep_alive_interval(Duration::from_secs(20)) + .http2_keep_alive_timeout(Duration::from_secs(5)) + .http2_keep_alive_while_idle(true) + .build() + .map_err(IAWError::from)?; + + Ok(Self { + base_url: base_url.into(), + http_client, + }) + } + + /// Create a new IAWClient with a custom HTTP client + pub fn with_http_client(base_url: impl Into, http_client: reqwest::Client) -> Self { + Self { + base_url: base_url.into(), + http_client, + } + } + + /// Sign a message with an EOA + pub async fn sign_message( + &self, + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, + message: String, + _from: Address, + _chain_id: Option, + format: Option, + ) -> Result { + // Get ThirdwebAuth headers for billing/authentication + let mut headers = thirdweb_auth.to_header_map()?; + + // Add IAW service authentication + headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer embedded-wallet-token:{}", + auth_token + )) + .map_err(|_| IAWError::AuthError("Invalid auth token format".to_string()))?, + ); + + // Add content type + headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + + // Convert MessageFormat to isRaw boolean (Hex = true, Text = false) + let is_raw = match format.unwrap_or_default() { + MessageFormat::Hex => true, + MessageFormat::Text => false, + }; + + // Build the request payload + let payload = serde_json::json!({ + "messagePayload": { + "message": message, + "isRaw": is_raw, + } + }); + + // Make the request to IAW service + let url = format!("{}/api/v1/enclave-wallet/sign-message", self.base_url); + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + return Err(IAWError::ApiError(format!( + "Failed to sign message - {} {}", + response.status(), + response + .status() + .canonical_reason() + .unwrap_or("Unknown error") + ))); + } + + // Parse the response + let signed_response: serde_json::Value = response.json().await?; + + // Extract just the signature as requested + let signature = signed_response + .get("signature") + .and_then(|s| s.as_str()) + .ok_or_else(|| IAWError::ApiError("No signature in response".to_string()))?; + + Ok(SignMessageData { + signature: signature.to_string(), + }) + } + + /// Sign a typed data structure with an EOA + pub async fn sign_typed_data( + &self, + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, + typed_data: TypedData, + _from: Address, + ) -> Result { + // Get ThirdwebAuth headers for billing/authentication + let mut headers = thirdweb_auth.to_header_map()?; + + // Add IAW service authentication + headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer embedded-wallet-token:{}", + auth_token + )) + .map_err(|_| IAWError::AuthError("Invalid auth token format".to_string()))?, + ); + + // Add content type + headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + + // Build the request payload + let payload = serde_json::json!(typed_data); + + // Make the request to IAW service + let url = format!("{}/api/v1/enclave-wallet/sign-typed-data", self.base_url); + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + return Err(IAWError::ApiError(format!( + "Failed to sign typed data - {} {}", + response.status(), + response + .status() + .canonical_reason() + .unwrap_or("Unknown error") + ))); + } + + // Parse the response + let signed_response: serde_json::Value = response.json().await?; + + // Extract just the signature as requested + let signature = signed_response + .get("signature") + .and_then(|s| s.as_str()) + .ok_or_else(|| IAWError::ApiError("No signature in response".to_string()))?; + + Ok(SignTypedDataData { + signature: signature.to_string(), + }) + } + + /// Sign a transaction with an EOA + pub async fn sign_transaction( + &self, + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, + transaction: EthereumTypedTransaction, + ) -> Result { + // Get ThirdwebAuth headers for billing/authentication + let mut headers = thirdweb_auth.to_header_map()?; + + // Add IAW service authentication + headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer embedded-wallet-token:{}", + auth_token + )) + .map_err(|_| IAWError::AuthError("Invalid auth token format".to_string()))?, + ); + + // Add content type + headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + + // Build the request payload + let payload = serde_json::json!({ + "transactionPayload": transaction, + }); + + // Make the request to IAW service + let url = format!("{}/api/v1/enclave-wallet/sign-transaction", self.base_url); + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + return Err(IAWError::ApiError(format!( + "Failed to sign transaction - {} {}", + response.status(), + response + .status() + .canonical_reason() + .unwrap_or("Unknown error") + ))); + } + + // Parse the response + let signed_response: serde_json::Value = response.json().await?; + + // Extract just the signature as requested + let signature = signed_response + .get("signature") + .and_then(|s| s.as_str()) + .ok_or_else(|| IAWError::ApiError("No signature in response".to_string()))?; + + Ok(SignTransactionData { + signature: signature.to_string(), + }) + } + + /// Sign an authorization with an EOA + pub async fn sign_authorization( + &self, + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, + _from: Address, + authorization: Authorization, + ) -> Result { + // Get ThirdwebAuth headers for billing/authentication + let mut headers = thirdweb_auth.to_header_map()?; + + // Add IAW service authentication + headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer embedded-wallet-token:{}", + auth_token + )) + .map_err(|_| IAWError::AuthError("Invalid auth token format".to_string()))?, + ); + + // Add content type + headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + + // Build the request payload + let payload = serde_json::json!({ + "address": authorization.address, + "chainId": authorization.chain_id, + "nonce": authorization.nonce.to_string(), + }); + + // Make the request to IAW service + let url = format!("{}/api/v1/enclave-wallet/sign-authorization", self.base_url); + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + return Err(IAWError::ApiError(format!( + "Failed to sign authorization - {} {}", + response.status(), + response + .status() + .canonical_reason() + .unwrap_or("Unknown error") + ))); + } + + // Parse the response + let signed_response: serde_json::Value = response.json().await?; + + // Extract the signed authorization from the response + let signed_authorization: SignedAuthorization = serde_json::from_value( + signed_response + .get("signedAuthorization") + .ok_or_else(|| { + IAWError::ApiError("No signedAuthorization in response".to_string()) + })? + .clone(), + )?; + + Ok(SignAuthorizationData { + signed_authorization, + }) + } + + /// Sign a user operation with an EOA + pub async fn sign_userop( + &self, + auth_token: AuthToken, + thirdweb_auth: ThirdwebAuth, + userop: VersionedUserOp, + entrypoint: Address, + _from: Address, + chain_id: ChainId, + ) -> Result { + // Compute the userop hash based on version + let hash = match &userop { + VersionedUserOp::V0_6(op) => compute_user_op_v06_hash(op, entrypoint, chain_id)?, + VersionedUserOp::V0_7(op) => compute_user_op_v07_hash(op, entrypoint, chain_id)?, + }; + + let userop_hash = format!("0x{}", hex::encode(hash.as_slice())); + tracing::info!("Computed userop hash: {}", userop_hash); + // Get ThirdwebAuth headers for billing/authentication + let mut headers = thirdweb_auth.to_header_map()?; + + // Add IAW service authentication + headers.insert( + "Authorization", + reqwest::header::HeaderValue::from_str(&format!( + "Bearer embedded-wallet-token:{}", + auth_token + )) + .map_err(|_| IAWError::AuthError("Invalid auth token format".to_string()))?, + ); + + // Add content type + headers.insert( + "Content-Type", + reqwest::header::HeaderValue::from_static("application/json"), + ); + + // Build the request payload - sign as hex message + let payload = serde_json::json!({ + "messagePayload": { + "message": userop_hash, + "isRaw": true, + "chainId": chain_id, + "originalMessage": serde_json::to_string(&userop).map_err(|e| IAWError::SerializationError { message: e.to_string() })?, + } + }); + + // Make the request to IAW service with explicit timeout + let url = format!("{}/api/v1/enclave-wallet/sign-message", self.base_url); + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&payload) + .send() + .await?; + + if !response.status().is_success() { + return Err(IAWError::ApiError(format!( + "Failed to sign userop - {} {}", + response.status(), + response + .status() + .canonical_reason() + .unwrap_or("Unknown error") + ))); + } + + // Parse the response + let signed_response: serde_json::Value = response.json().await?; + + // Extract just the signature as requested + let signature = signed_response + .get("signature") + .and_then(|s| s.as_str()) + .ok_or_else(|| IAWError::ApiError("No signature in response".to_string()))?; + + Ok(SignUserOpData { + signature: signature.to_string(), + }) + } +} diff --git a/thirdweb-core/src/lib.rs b/thirdweb-core/src/lib.rs index 22cde6b..6d816f5 100644 --- a/thirdweb-core/src/lib.rs +++ b/thirdweb-core/src/lib.rs @@ -1,3 +1,4 @@ pub mod abi; pub mod auth; pub mod error; +pub mod iaw;