use alloy_eips::eip7702::Authorization;
use alloy_network::{Network, TransactionBuilder, TransactionBuilder7702};
use alloy_primitives::{Address, B256, U256};
use alloy_provider::{PendingTransactionBuilder, Provider, WalletProvider};
use alloy_signer::Signer;
use crate::deployer::{self, DEFAULT_ADDRESS, DeployError};
const INSTALL_GAS_LIMIT_FALLBACK: u64 = 100_000;
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
#[error(
"self-install requires authority == provider's default signer (authority {authority}, provider signer {provider_signer})"
)]
AuthorityMismatch {
authority: Address,
provider_signer: Address,
},
#[error(
"`TridentAccount` is not deployed at {target}; deploy it first or call the installer without `without_deploy_if_missing`"
)]
TridentAccountNotDeployed { target: Address },
#[error(transparent)]
Deploy(#[from] DeployError),
#[error(transparent)]
Signer(#[from] alloy_signer::Error),
#[error("rpc: {0}")]
Rpc(Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Debug, Clone, Copy)]
pub struct SelfInstaller {
target: Option<Address>,
deploy_if_missing: bool,
}
impl Default for SelfInstaller {
fn default() -> Self {
Self::new()
}
}
impl SelfInstaller {
pub const fn new() -> Self {
Self {
target: None,
deploy_if_missing: true,
}
}
pub const fn with_target(mut self, target: Address) -> Self {
self.target = Some(target);
self
}
pub const fn without_deploy_if_missing(mut self) -> Self {
self.deploy_if_missing = false;
self
}
fn ensure_matching_authority<P, N, S>(provider: &P, authority: &S) -> Result<(), InstallError>
where
P: Provider<N> + WalletProvider<N>,
N: Network,
S: Signer + Send + Sync,
{
let authority_addr = authority.address();
let provider_signer = provider.default_signer_address();
if authority_addr != provider_signer {
return Err(InstallError::AuthorityMismatch {
authority: authority_addr,
provider_signer,
});
}
Ok(())
}
pub async fn prepare_install_tx<P, N, S>(
self,
provider: &P,
authority: &S,
) -> Result<N::TransactionRequest, InstallError>
where
P: Provider<N> + WalletProvider<N>,
N: Network,
<N as Network>::TransactionRequest: TransactionBuilder7702,
S: Signer + Send + Sync,
{
Self::ensure_matching_authority(provider, authority)?;
self.prepare_install_tx_unchecked(provider, authority).await
}
pub async fn prepare_install_tx_unchecked<P, N, S>(
self,
provider: &P,
authority: &S,
) -> Result<N::TransactionRequest, InstallError>
where
P: Provider<N>,
N: Network,
<N as Network>::TransactionRequest: TransactionBuilder7702,
S: Signer + Send + Sync,
{
let target = self.target.unwrap_or(DEFAULT_ADDRESS);
let using_default_target = target == DEFAULT_ADDRESS;
let code = provider
.get_code_at(target)
.await
.map_err(|e| InstallError::Rpc(Box::new(e)))?;
if code.is_empty() {
if self.deploy_if_missing && using_default_target {
deployer::deploy_deterministic(provider, B256::ZERO).await?;
} else {
return Err(InstallError::TridentAccountNotDeployed { target });
}
}
let chain_id = provider
.get_chain_id()
.await
.map_err(|e| InstallError::Rpc(Box::new(e)))?;
let current_nonce = provider
.get_transaction_count(authority.address())
.await
.map_err(|e| InstallError::Rpc(Box::new(e)))?;
let auth = Authorization {
chain_id: U256::from(chain_id),
address: target,
nonce: current_nonce + 1,
};
let sig = authority.sign_hash(&auth.signature_hash()).await?;
let signed_auth = auth.into_signed(sig);
Ok(<N::TransactionRequest as Default>::default()
.with_to(authority.address())
.with_nonce(current_nonce)
.with_authorization_list(vec![signed_auth]))
}
pub async fn install<P, N, S>(
self,
provider: P,
authority: &S,
) -> Result<PendingTransactionBuilder<N>, InstallError>
where
P: Provider<N> + WalletProvider<N>,
N: Network,
<N as Network>::TransactionRequest: TransactionBuilder7702 + Clone,
S: Signer + Send + Sync,
{
Self::ensure_matching_authority(&provider, authority)?;
self.install_unchecked(provider, authority).await
}
pub async fn install_unchecked<P, N, S>(
self,
provider: P,
authority: &S,
) -> Result<PendingTransactionBuilder<N>, InstallError>
where
P: Provider<N>,
N: Network,
<N as Network>::TransactionRequest: TransactionBuilder7702 + Clone,
S: Signer + Send + Sync,
{
let tx = self
.prepare_install_tx_unchecked(&provider, authority)
.await?;
match provider.send_transaction(tx.clone()).await {
Ok(pending) => Ok(pending),
Err(err) if should_retry_install_with_explicit_gas(&err) => provider
.send_transaction(tx.with_gas_limit(INSTALL_GAS_LIMIT_FALLBACK))
.await
.map_err(|e| InstallError::Rpc(Box::new(e))),
Err(err) => Err(InstallError::Rpc(Box::new(err))),
}
}
}
fn install_tx_with_gas_retry_message(message: &str) -> bool {
let message = message.to_ascii_lowercase();
message.contains("intrinsic gas too low") || message.contains("gas uint64 overflow")
}
fn should_retry_install_with_explicit_gas(err: &(dyn std::error::Error + 'static)) -> bool {
let mut current = Some(err);
while let Some(err) = current {
if install_tx_with_gas_retry_message(&err.to_string()) {
return true;
}
current = err.source();
}
false
}
#[cfg(test)]
mod tests {
use alloy_eips::eip7702::constants::EIP7702_DELEGATION_DESIGNATOR;
use alloy_network::{AnyNetwork, EthereumWallet};
use alloy_primitives::Bytes;
use alloy_provider::ProviderBuilder;
use alloy_signer_local::PrivateKeySigner;
use super::*;
use crate::{abi::TridentAccount, test_util::TestSetup};
#[test]
fn install_gas_retry_message_matches_arbitrum_failures() {
assert!(install_tx_with_gas_retry_message(
"server returned an error response: error code -32000: gas uint64 overflow"
));
assert!(install_tx_with_gas_retry_message(
"Rpc(ErrorResp(ErrorPayload { code: -32000, message: \"intrinsic gas too low\", data: None }))"
));
assert!(!install_tx_with_gas_retry_message("nonce too low"));
}
#[tokio::test]
async fn install_rejects_authority_mismatched_with_provider_signer() {
let setup = TestSetup::new().await;
let provider = setup.provider();
let other_signer = PrivateKeySigner::random();
assert_ne!(setup.signer.address(), other_signer.address());
let err = SelfInstaller::new()
.install(provider, &other_signer)
.await
.expect_err("should reject mismatched authority");
match err {
InstallError::AuthorityMismatch {
authority,
provider_signer: ps,
} => {
assert_eq!(authority, other_signer.address());
assert_eq!(ps, setup.signer.address());
}
other => panic!("expected AuthorityMismatch, got {other:?}"),
}
}
#[tokio::test]
async fn install_auto_deploys_and_delegates_to_default_address() {
let setup = TestSetup::new().await;
let provider = setup.provider();
let authority = setup.signer.address();
SelfInstaller::new()
.install(provider.clone(), &setup.signer)
.await
.expect("install")
.get_receipt()
.await
.expect("install confirmed");
let target = DEFAULT_ADDRESS;
let deployed_code = provider
.get_code_at(target)
.await
.expect("get_code_at target");
assert!(
!deployed_code.is_empty(),
"`TridentAccount` should be deployed at DEFAULT_ADDRESS"
);
let code = provider
.get_code_at(authority)
.await
.expect("get_code_at authority");
assert_eq!(code.len(), 23, "delegation designator is 23 bytes");
assert_eq!(&code[..3], &EIP7702_DELEGATION_DESIGNATOR);
assert_eq!(&code[3..], target.as_slice());
}
#[tokio::test]
async fn execute_reverts_with_not_self_when_called_by_another_sender() {
let setup = TestSetup::new().await;
let authority = setup.signer.address();
SelfInstaller::new()
.install(setup.provider(), &setup.signer)
.await
.expect("install")
.get_receipt()
.await
.expect("install confirmed");
let signer_b: PrivateKeySigner = setup.anvil.keys()[1].clone().into();
assert_ne!(signer_b.address(), authority);
let provider_b = ProviderBuilder::new()
.network::<AnyNetwork>()
.with_simple_nonce_management()
.wallet(EthereumWallet::from(signer_b))
.connect_http(setup.anvil.endpoint_url());
let err = TridentAccount::new(authority, provider_b)
.execute(Address::ZERO, Bytes::new())
.call()
.await
.expect_err("expected revert: B is not the account");
assert!(
matches!(
err.as_decoded_interface_error::<TridentAccount::TridentAccountErrors>(),
Some(TridentAccount::TridentAccountErrors::NotSelf(_))
),
"expected NotSelf, got {err:?}"
);
}
#[tokio::test]
async fn install_errors_when_auto_deploy_disabled_and_target_missing() {
let setup = TestSetup::new().await;
let provider = setup.provider();
let err = SelfInstaller::new()
.without_deploy_if_missing()
.install(provider, &setup.signer)
.await
.expect_err("should reject missing `TridentAccount`");
match err {
InstallError::TridentAccountNotDeployed { target } => {
assert_eq!(target, DEFAULT_ADDRESS);
}
other => panic!("expected TridentAccountNotDeployed, got {other:?}"),
}
}
}