diff --git a/README.md b/README.md index d5639993..99b88ea6 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,11 @@ Examples can be found in the [examples folder](./examples): 10. [Deploy an OpenZeppelin account with Ledger](./examples/deploy_account_with_ledger.rs) -11. [Parsing a JSON-RPC request on the server side](./examples/parse_jsonrpc_request.rs) +11. [Transfer ERC20 tokens with Ledger](./examples/transfer_with_ledger.rs) -12. [Inspecting a erased provider-specific error type](./examples/downcast_provider_error.rs) +12. [Parsing a JSON-RPC request on the server side](./examples/parse_jsonrpc_request.rs) + +13. [Inspecting a erased provider-specific error type](./examples/downcast_provider_error.rs) ## License diff --git a/examples/declare_cairo0_contract.rs b/examples/declare_cairo0_contract.rs index bb7dadfc..1bf94b0d 100644 --- a/examples/declare_cairo0_contract.rs +++ b/examples/declare_cairo0_contract.rs @@ -45,5 +45,6 @@ async fn main() { .await .unwrap(); - dbg!(result); + println!("Transaction hash: {:#064x}", result.transaction_hash); + println!("Class hash: {:#064x}", result.class_hash); } diff --git a/examples/declare_cairo1_contract.rs b/examples/declare_cairo1_contract.rs index f6f2fb3d..86ab0fdd 100644 --- a/examples/declare_cairo1_contract.rs +++ b/examples/declare_cairo1_contract.rs @@ -53,5 +53,6 @@ async fn main() { .await .unwrap(); - dbg!(result); + println!("Transaction hash: {:#064x}", result.transaction_hash); + println!("Class hash: {:#064x}", result.class_hash); } diff --git a/examples/deploy_account_with_ledger.rs b/examples/deploy_account_with_ledger.rs index 0f25ad2b..ef4ba9ae 100644 --- a/examples/deploy_account_with_ledger.rs +++ b/examples/deploy_account_with_ledger.rs @@ -51,6 +51,7 @@ async fn main() { match result { Ok(tx) => { println!("Transaction hash: {:#064x}", tx.transaction_hash); + println!("Account: {:#064x}", tx.contract_address); } Err(err) => { eprintln!("Error: {err}"); diff --git a/examples/deploy_argent_account.rs b/examples/deploy_argent_account.rs index 8eff33cc..acd559c7 100644 --- a/examples/deploy_argent_account.rs +++ b/examples/deploy_argent_account.rs @@ -47,6 +47,7 @@ async fn main() { match result { Ok(tx) => { println!("Transaction hash: {:#064x}", tx.transaction_hash); + println!("Account: {:#064x}", tx.contract_address); } Err(err) => { eprintln!("Error: {err}"); diff --git a/examples/mint_tokens.rs b/examples/mint_tokens.rs index 33e86ea2..64fc7ada 100644 --- a/examples/mint_tokens.rs +++ b/examples/mint_tokens.rs @@ -51,5 +51,5 @@ async fn main() { .await .unwrap(); - dbg!(result); + println!("Transaction hash: {:#064x}", result.transaction_hash); } diff --git a/examples/transfer_with_ledger.rs b/examples/transfer_with_ledger.rs new file mode 100644 index 00000000..223da492 --- /dev/null +++ b/examples/transfer_with_ledger.rs @@ -0,0 +1,57 @@ +use starknet::{ + accounts::{Account, Call, ExecutionEncoding, SingleOwnerAccount}, + core::{ + chain_id, + types::{BlockId, BlockTag, Felt}, + utils::get_selector_from_name, + }, + macros::felt, + providers::{ + jsonrpc::{HttpTransport, JsonRpcClient}, + Url, + }, + signers::LedgerSigner, +}; + +#[tokio::main] +async fn main() { + let provider = JsonRpcClient::new(HttpTransport::new( + Url::parse("https://starknet-sepolia.public.blastapi.io/rpc/v0_7").unwrap(), + )); + + let signer = LedgerSigner::new( + "m/2645'/1195502025'/1470455285'/0'/0'/0" + .try_into() + .expect("unable to parse path"), + ) + .await + .expect("failed to initialize Starknet Ledger app"); + let address = Felt::from_hex("YOUR_ACCOUNT_CONTRACT_ADDRESS_IN_HEX_HERE").unwrap(); + let eth_token_address = + Felt::from_hex("0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7") + .unwrap(); + + let mut account = SingleOwnerAccount::new( + provider, + signer, + address, + chain_id::SEPOLIA, + ExecutionEncoding::New, + ); + + // `SingleOwnerAccount` defaults to checking nonce and estimating fees against the latest + // block. Optionally change the target block to pending with the following line: + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + let result = account + .execute_v1(vec![Call { + to: eth_token_address, + selector: get_selector_from_name("transfer").unwrap(), + calldata: vec![felt!("0x1234"), felt!("100"), Felt::ZERO], + }]) + .send() + .await + .unwrap(); + + println!("Transaction hash: {:#064x}", result.transaction_hash); +} diff --git a/starknet-accounts/src/account/declaration.rs b/starknet-accounts/src/account/declaration.rs index 5162320f..360dc924 100644 --- a/starknet-accounts/src/account/declaration.rs +++ b/starknet-accounts/src/account/declaration.rs @@ -12,6 +12,7 @@ use starknet_core::{ BroadcastedDeclareTransactionV2, BroadcastedDeclareTransactionV3, BroadcastedTransaction, DataAvailabilityMode, DeclareTransactionResult, FeeEstimate, Felt, FlattenedSierraClass, ResourceBounds, ResourceBoundsMapping, SimulatedTransaction, SimulationFlag, + SimulationFlagForEstimateFee, }, }; use starknet_crypto::PoseidonHasher; @@ -195,6 +196,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.account.is_signer_interactive(); + let prepared = PreparedDeclarationV2 { account: self.account, inner: RawDeclarationV2 { @@ -204,13 +207,19 @@ where max_fee: Felt::ZERO, }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; self.account .provider() .estimate_fee_single( BroadcastedTransaction::Declare(BroadcastedDeclareTransaction::V2(declare)), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.account.block_id(), ) .await @@ -223,6 +232,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.account.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedDeclarationV2 { account: self.account, inner: RawDeclarationV2 { @@ -232,7 +251,7 @@ where max_fee: self.max_fee.unwrap_or_default(), }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; let mut flags = vec![]; @@ -470,6 +489,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.account.is_signer_interactive(); + let prepared = PreparedDeclarationV3 { account: self.account, inner: RawDeclarationV3 { @@ -480,13 +501,19 @@ where gas_price: 0, }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; self.account .provider() .estimate_fee_single( BroadcastedTransaction::Declare(BroadcastedDeclareTransaction::V3(declare)), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.account.block_id(), ) .await @@ -499,6 +526,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.account.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedDeclarationV3 { account: self.account, inner: RawDeclarationV3 { @@ -509,7 +546,7 @@ where gas_price: self.gas_price.unwrap_or_default(), }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; let mut flags = vec![]; @@ -673,6 +710,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.account.is_signer_interactive(); + let prepared = PreparedLegacyDeclaration { account: self.account, inner: RawLegacyDeclaration { @@ -681,13 +720,19 @@ where max_fee: Felt::ZERO, }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; self.account .provider() .estimate_fee_single( BroadcastedTransaction::Declare(BroadcastedDeclareTransaction::V1(declare)), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.account.block_id(), ) .await @@ -700,6 +745,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.account.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedLegacyDeclaration { account: self.account, inner: RawLegacyDeclaration { @@ -708,7 +763,7 @@ where max_fee: self.max_fee.unwrap_or_default(), }, }; - let declare = prepared.get_declare_request(true).await?; + let declare = prepared.get_declare_request(true, skip_signature).await?; let mut flags = vec![]; @@ -895,7 +950,7 @@ where A: ConnectedAccount, { pub async fn send(&self) -> Result> { - let tx_request = self.get_declare_request(false).await?; + let tx_request = self.get_declare_request(false, false).await?; self.account .provider() .add_declare_transaction(BroadcastedDeclareTransaction::V2(tx_request)) @@ -903,19 +958,21 @@ where .map_err(AccountError::Provider) } - pub async fn get_declare_request( + async fn get_declare_request( &self, query_only: bool, + skip_signature: bool, ) -> Result> { - let signature = self - .account - .sign_declaration_v2(&self.inner, query_only) - .await - .map_err(AccountError::Signing)?; - Ok(BroadcastedDeclareTransactionV2 { max_fee: self.inner.max_fee, - signature, + signature: if skip_signature { + vec![] + } else { + self.account + .sign_declaration_v2(&self.inner, query_only) + .await + .map_err(AccountError::Signing)? + }, nonce: self.inner.nonce, contract_class: self.inner.contract_class.clone(), compiled_class_hash: self.inner.compiled_class_hash, @@ -942,7 +999,7 @@ where A: ConnectedAccount, { pub async fn send(&self) -> Result> { - let tx_request = self.get_declare_request(false).await?; + let tx_request = self.get_declare_request(false, false).await?; self.account .provider() .add_declare_transaction(BroadcastedDeclareTransaction::V3(tx_request)) @@ -950,20 +1007,22 @@ where .map_err(AccountError::Provider) } - pub async fn get_declare_request( + async fn get_declare_request( &self, query_only: bool, + skip_signature: bool, ) -> Result> { - let signature = self - .account - .sign_declaration_v3(&self.inner, query_only) - .await - .map_err(AccountError::Signing)?; - Ok(BroadcastedDeclareTransactionV3 { sender_address: self.account.address(), compiled_class_hash: self.inner.compiled_class_hash, - signature, + signature: if skip_signature { + vec![] + } else { + self.account + .sign_declaration_v3(&self.inner, query_only) + .await + .map_err(AccountError::Signing)? + }, nonce: self.inner.nonce, contract_class: self.inner.contract_class.clone(), resource_bounds: ResourceBoundsMapping { @@ -1008,7 +1067,7 @@ where A: ConnectedAccount, { pub async fn send(&self) -> Result> { - let tx_request = self.get_declare_request(false).await?; + let tx_request = self.get_declare_request(false, false).await?; self.account .provider() .add_declare_transaction(BroadcastedDeclareTransaction::V1(tx_request)) @@ -1016,21 +1075,23 @@ where .map_err(AccountError::Provider) } - pub async fn get_declare_request( + async fn get_declare_request( &self, query_only: bool, + skip_signature: bool, ) -> Result> { - let signature = self - .account - .sign_legacy_declaration(&self.inner, query_only) - .await - .map_err(AccountError::Signing)?; - let compressed_class = self.inner.contract_class.compress().unwrap(); Ok(BroadcastedDeclareTransactionV1 { max_fee: self.inner.max_fee, - signature, + signature: if skip_signature { + vec![] + } else { + self.account + .sign_legacy_declaration(&self.inner, query_only) + .await + .map_err(AccountError::Signing)? + }, nonce: self.inner.nonce, contract_class: Arc::new(compressed_class), sender_address: self.account.address(), diff --git a/starknet-accounts/src/account/execution.rs b/starknet-accounts/src/account/execution.rs index cef482fc..99757da0 100644 --- a/starknet-accounts/src/account/execution.rs +++ b/starknet-accounts/src/account/execution.rs @@ -10,7 +10,7 @@ use starknet_core::{ BroadcastedInvokeTransaction, BroadcastedInvokeTransactionV1, BroadcastedInvokeTransactionV3, BroadcastedTransaction, DataAvailabilityMode, FeeEstimate, Felt, InvokeTransactionResult, ResourceBounds, ResourceBoundsMapping, SimulatedTransaction, - SimulationFlag, + SimulationFlag, SimulationFlagForEstimateFee, }, }; use starknet_crypto::PoseidonHasher; @@ -245,6 +245,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.account.is_signer_interactive(); + let prepared = PreparedExecutionV1 { account: self.account, inner: RawExecutionV1 { @@ -254,7 +256,7 @@ where }, }; let invoke = prepared - .get_invoke_request(true) + .get_invoke_request(true, skip_signature) .await .map_err(AccountError::Signing)?; @@ -262,7 +264,13 @@ where .provider() .estimate_fee_single( BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V1(invoke)), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.account.block_id(), ) .await @@ -275,6 +283,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.account.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedExecutionV1 { account: self.account, inner: RawExecutionV1 { @@ -284,7 +302,7 @@ where }, }; let invoke = prepared - .get_invoke_request(true) + .get_invoke_request(true, skip_signature) .await .map_err(AccountError::Signing)?; @@ -450,6 +468,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.account.is_signer_interactive(); + let prepared = PreparedExecutionV3 { account: self.account, inner: RawExecutionV3 { @@ -460,7 +480,7 @@ where }, }; let invoke = prepared - .get_invoke_request(true) + .get_invoke_request(true, skip_signature) .await .map_err(AccountError::Signing)?; @@ -468,7 +488,13 @@ where .provider() .estimate_fee_single( BroadcastedTransaction::Invoke(BroadcastedInvokeTransaction::V3(invoke)), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.account.block_id(), ) .await @@ -481,6 +507,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.account.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedExecutionV3 { account: self.account, inner: RawExecutionV3 { @@ -491,7 +527,7 @@ where }, }; let invoke = prepared - .get_invoke_request(true) + .get_invoke_request(true, skip_signature) .await .map_err(AccountError::Signing)?; @@ -682,7 +718,7 @@ where { pub async fn send(&self) -> Result> { let tx_request = self - .get_invoke_request(false) + .get_invoke_request(false, false) .await .map_err(AccountError::Signing)?; self.account @@ -695,18 +731,20 @@ where // The `simulate` function is temporarily removed until it's supported in [Provider] // TODO: add `simulate` back once transaction simulation in supported - pub async fn get_invoke_request( + async fn get_invoke_request( &self, query_only: bool, + skip_signature: bool, ) -> Result { - let signature = self - .account - .sign_execution_v1(&self.inner, query_only) - .await?; - Ok(BroadcastedInvokeTransactionV1 { max_fee: self.inner.max_fee, - signature, + signature: if skip_signature { + vec![] + } else { + self.account + .sign_execution_v1(&self.inner, query_only) + .await? + }, nonce: self.inner.nonce, sender_address: self.account.address(), calldata: self.account.encode_calls(&self.inner.calls), @@ -721,7 +759,7 @@ where { pub async fn send(&self) -> Result> { let tx_request = self - .get_invoke_request(false) + .get_invoke_request(false, false) .await .map_err(AccountError::Signing)?; self.account @@ -734,19 +772,21 @@ where // The `simulate` function is temporarily removed until it's supported in [Provider] // TODO: add `simulate` back once transaction simulation in supported - pub async fn get_invoke_request( + async fn get_invoke_request( &self, query_only: bool, + skip_signature: bool, ) -> Result { - let signature = self - .account - .sign_execution_v3(&self.inner, query_only) - .await?; - Ok(BroadcastedInvokeTransactionV3 { sender_address: self.account.address(), calldata: self.account.encode_calls(&self.inner.calls), - signature, + signature: if skip_signature { + vec![] + } else { + self.account + .sign_execution_v3(&self.inner, query_only) + .await? + }, nonce: self.inner.nonce, resource_bounds: ResourceBoundsMapping { l1_gas: ResourceBounds { diff --git a/starknet-accounts/src/account/mod.rs b/starknet-accounts/src/account/mod.rs index 30e39730..f2e6d62e 100644 --- a/starknet-accounts/src/account/mod.rs +++ b/starknet-accounts/src/account/mod.rs @@ -55,6 +55,14 @@ pub trait Account: ExecutionEncoder + Sized { query_only: bool, ) -> Result, Self::SignError>; + /// Whether the underlying signer implementation is interactive, such as a hardware wallet. + /// Implementations should return `true` if the signing operation is very expensive, even if not + /// strictly "interactive" as in requiring human input. + /// + /// This affects how an account makes decision on whether to request a real signature for + /// estimation/simulation purposes. + fn is_signer_interactive(&self) -> bool; + fn execute_v1(&self, calls: Vec) -> ExecutionV1 { ExecutionV1::new(calls, self) } @@ -354,6 +362,10 @@ where .sign_legacy_declaration(legacy_declaration, query_only) .await } + + fn is_signer_interactive(&self) -> bool { + (*self).is_signer_interactive() + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -417,6 +429,10 @@ where .sign_legacy_declaration(legacy_declaration, query_only) .await } + + fn is_signer_interactive(&self) -> bool { + self.as_ref().is_signer_interactive() + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -480,6 +496,10 @@ where .sign_legacy_declaration(legacy_declaration, query_only) .await } + + fn is_signer_interactive(&self) -> bool { + self.as_ref().is_signer_interactive() + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/starknet-accounts/src/factory/argent.rs b/starknet-accounts/src/factory/argent.rs index bed65a97..5bfa0476 100644 --- a/starknet-accounts/src/factory/argent.rs +++ b/starknet-accounts/src/factory/argent.rs @@ -73,6 +73,10 @@ where &self.provider } + fn is_signer_interactive(&self) -> bool { + self.signer.is_interactive() + } + fn block_id(&self) -> BlockId { self.block_id } diff --git a/starknet-accounts/src/factory/mod.rs b/starknet-accounts/src/factory/mod.rs index aef251a4..1dc3b773 100644 --- a/starknet-accounts/src/factory/mod.rs +++ b/starknet-accounts/src/factory/mod.rs @@ -8,7 +8,7 @@ use starknet_core::{ BroadcastedDeployAccountTransactionV1, BroadcastedDeployAccountTransactionV3, BroadcastedTransaction, DataAvailabilityMode, DeployAccountTransactionResult, FeeEstimate, Felt, NonZeroFelt, ResourceBounds, ResourceBoundsMapping, SimulatedTransaction, - SimulationFlag, StarknetError, + SimulationFlag, SimulationFlagForEstimateFee, StarknetError, }, }; use starknet_crypto::PoseidonHasher; @@ -73,6 +73,14 @@ pub trait AccountFactory: Sized { fn provider(&self) -> &Self::Provider; + /// Whether the underlying signer implementation is interactive, such as a hardware wallet. + /// Implementations should return `true` if the signing operation is very expensive, even if not + /// strictly "interactive" as in requiring human input. + /// + /// This affects how an account factory makes decision on whether to request a real signature + /// for estimation/simulation purposes. + fn is_signer_interactive(&self) -> bool; + /// Block ID to use when estimating fees. fn block_id(&self) -> BlockId { BlockId::Tag(BlockTag::Latest) @@ -413,6 +421,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.factory.is_signer_interactive(); + let prepared = PreparedAccountDeploymentV1 { factory: self.factory, inner: RawAccountDeploymentV1 { @@ -422,7 +432,7 @@ where }, }; let deploy = prepared - .get_deploy_request(true) + .get_deploy_request(true, skip_signature) .await .map_err(AccountFactoryError::Signing)?; @@ -432,7 +442,13 @@ where BroadcastedTransaction::DeployAccount(BroadcastedDeployAccountTransaction::V1( deploy, )), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.factory.block_id(), ) .await @@ -445,6 +461,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.factory.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedAccountDeploymentV1 { factory: self.factory, inner: RawAccountDeploymentV1 { @@ -454,7 +480,7 @@ where }, }; let deploy = prepared - .get_deploy_request(true) + .get_deploy_request(true, skip_signature) .await .map_err(AccountFactoryError::Signing)?; @@ -645,6 +671,8 @@ where &self, nonce: Felt, ) -> Result> { + let skip_signature = self.factory.is_signer_interactive(); + let prepared = PreparedAccountDeploymentV3 { factory: self.factory, inner: RawAccountDeploymentV3 { @@ -655,7 +683,7 @@ where }, }; let deploy = prepared - .get_deploy_request(true) + .get_deploy_request(true, skip_signature) .await .map_err(AccountFactoryError::Signing)?; @@ -665,7 +693,13 @@ where BroadcastedTransaction::DeployAccount(BroadcastedDeployAccountTransaction::V3( deploy, )), - [], + if skip_signature { + // Validation would fail since real signature was not requested + vec![SimulationFlagForEstimateFee::SkipValidate] + } else { + // With the correct signature in place, run validation for accurate results + vec![] + }, self.factory.block_id(), ) .await @@ -678,6 +712,16 @@ where skip_validate: bool, skip_fee_charge: bool, ) -> Result> { + let skip_signature = if self.factory.is_signer_interactive() { + // If signer is interactive, we would try to minimize signing requests. However, if the + // caller has decided to not skip validation, it's best we still request a real + // signature, as otherwise the simulation would most likely fail. + skip_validate + } else { + // Signing with non-interactive signers is cheap so always request signatures. + false + }; + let prepared = PreparedAccountDeploymentV3 { factory: self.factory, inner: RawAccountDeploymentV3 { @@ -688,7 +732,7 @@ where }, }; let deploy = prepared - .get_deploy_request(true) + .get_deploy_request(true, skip_signature) .await .map_err(AccountFactoryError::Signing)?; @@ -802,7 +846,7 @@ where &self, ) -> Result> { let tx_request = self - .get_deploy_request(false) + .get_deploy_request(false, false) .await .map_err(AccountFactoryError::Signing)?; self.factory @@ -815,15 +859,17 @@ where async fn get_deploy_request( &self, query_only: bool, + skip_signature: bool, ) -> Result { - let signature = self - .factory - .sign_deployment_v1(&self.inner, query_only) - .await?; - Ok(BroadcastedDeployAccountTransactionV1 { max_fee: self.inner.max_fee, - signature, + signature: if skip_signature { + vec![] + } else { + self.factory + .sign_deployment_v1(&self.inner, query_only) + .await? + }, nonce: self.inner.nonce, contract_address_salt: self.inner.salt, constructor_calldata: self.factory.calldata(), @@ -911,7 +957,7 @@ where &self, ) -> Result> { let tx_request = self - .get_deploy_request(false) + .get_deploy_request(false, false) .await .map_err(AccountFactoryError::Signing)?; self.factory @@ -924,14 +970,16 @@ where async fn get_deploy_request( &self, query_only: bool, + skip_signature: bool, ) -> Result { - let signature = self - .factory - .sign_deployment_v3(&self.inner, query_only) - .await?; - Ok(BroadcastedDeployAccountTransactionV3 { - signature, + signature: if skip_signature { + vec![] + } else { + self.factory + .sign_deployment_v3(&self.inner, query_only) + .await? + }, nonce: self.inner.nonce, contract_address_salt: self.inner.salt, constructor_calldata: self.factory.calldata(), diff --git a/starknet-accounts/src/factory/open_zeppelin.rs b/starknet-accounts/src/factory/open_zeppelin.rs index 72882e45..3b23b16e 100644 --- a/starknet-accounts/src/factory/open_zeppelin.rs +++ b/starknet-accounts/src/factory/open_zeppelin.rs @@ -70,6 +70,10 @@ where &self.provider } + fn is_signer_interactive(&self) -> bool { + self.signer.is_interactive() + } + fn block_id(&self) -> BlockId { self.block_id } diff --git a/starknet-accounts/src/single_owner.rs b/starknet-accounts/src/single_owner.rs index 9aa7a5b7..0677a8d8 100644 --- a/starknet-accounts/src/single_owner.rs +++ b/starknet-accounts/src/single_owner.rs @@ -170,6 +170,10 @@ where Ok(vec![signature.r, signature.s]) } + + fn is_signer_interactive(&self) -> bool { + self.signer.is_interactive() + } } impl ExecutionEncoder for SingleOwnerAccount diff --git a/starknet-signers/src/ledger.rs b/starknet-signers/src/ledger.rs index 140d787c..8071a78f 100644 --- a/starknet-signers/src/ledger.rs +++ b/starknet-signers/src/ledger.rs @@ -163,6 +163,10 @@ impl Signer for LedgerSigner { Ok(signature) } + + fn is_interactive(&self) -> bool { + true + } } impl From for LedgerError { diff --git a/starknet-signers/src/local_wallet.rs b/starknet-signers/src/local_wallet.rs index 23c00b28..2c898da0 100644 --- a/starknet-signers/src/local_wallet.rs +++ b/starknet-signers/src/local_wallet.rs @@ -36,6 +36,10 @@ impl Signer for LocalWallet { async fn sign_hash(&self, hash: &Felt) -> Result { Ok(self.private_key.sign(hash)?) } + + fn is_interactive(&self) -> bool { + false + } } impl From for LocalWallet { diff --git a/starknet-signers/src/signer.rs b/starknet-signers/src/signer.rs index 0f586f82..429efede 100644 --- a/starknet-signers/src/signer.rs +++ b/starknet-signers/src/signer.rs @@ -15,4 +15,14 @@ pub trait Signer { async fn get_public_key(&self) -> Result; async fn sign_hash(&self, hash: &Felt) -> Result; + + /// Whether the underlying signer implementation is interactive, such as a hardware wallet. + /// Implementations should return `true` if the signing operation is very expensive, even if not + /// strictly "interactive" as in requiring human input. + /// + /// This mainly affects the transaction simulation strategy used by higher-level types. With + /// non-interactive signers, it's fine to sign multiple times for getting the most accurate + /// estimation/simulation possible; but with interactive signers, they would accept less + /// accurate results to minimize signing requests. + fn is_interactive(&self) -> bool; }