use crate::utils::from_env::FromEnv;
use alloy::{
consensus::SignableTransaction,
network::{Ethereum, EthereumWallet, IntoWallet},
primitives::{Address, ChainId, B256},
signers::{
aws::{AwsSigner, AwsSignerError},
local::{LocalSignerError, PrivateKeySigner},
Signature,
},
};
use aws_config::{load_defaults, BehaviorVersion};
use aws_sdk_kms::Client;
use std::borrow::Cow;
#[derive(FromEnv, Debug, Clone)]
#[from_env(crate)]
pub struct LocalOrAwsConfig {
#[from_env(var = "SIGNER_KEY", desc = "AWS KMS key ID or local private key")]
key_info: Cow<'static, str>,
#[from_env(var = "SIGNER_CHAIN_ID", desc = "Chain ID for AWS signer", optional)]
chain_id: Option<u64>,
}
impl LocalOrAwsConfig {
pub async fn connect_remote(&self) -> Result<LocalOrAws, SignerError> {
let signer = LocalOrAws::aws_signer(&self.key_info, self.chain_id).await?;
Ok(LocalOrAws::Aws(signer))
}
pub fn connect_local(&self) -> Result<LocalOrAws, SignerError> {
Ok(LocalOrAws::Local(LocalOrAws::wallet(&self.key_info)?))
}
pub async fn connect(&self) -> Result<LocalOrAws, SignerError> {
if let Ok(local) = self.connect_local() {
Ok(local)
} else {
self.connect_remote().await
}
}
}
#[derive(Debug, Clone)]
pub enum LocalOrAws {
Local(PrivateKeySigner),
Aws(AwsSigner),
}
#[derive(Debug, thiserror::Error)]
pub enum SignerError {
#[error("failed to connect AWS signer: {0}")]
AwsSigner(#[from] Box<AwsSignerError>),
#[error("failed to load private key: {0}")]
Wallet(#[from] LocalSignerError),
#[error("failed to parse hex: {0}")]
Hex(#[from] alloy::hex::FromHexError),
}
impl From<AwsSignerError> for SignerError {
fn from(err: AwsSignerError) -> Self {
SignerError::AwsSigner(Box::new(err))
}
}
impl LocalOrAws {
pub async fn load(key: &str, chain_id: Option<u64>) -> Result<Self, SignerError> {
if let Ok(wallet) = LocalOrAws::wallet(key) {
Ok(LocalOrAws::Local(wallet))
} else {
let signer = LocalOrAws::aws_signer(key, chain_id).await?;
Ok(LocalOrAws::Aws(signer))
}
}
fn wallet(private_key: &str) -> Result<PrivateKeySigner, SignerError> {
let bytes = alloy::hex::decode(private_key.strip_prefix("0x").unwrap_or(private_key))?;
Ok(PrivateKeySigner::from_slice(&bytes).unwrap())
}
async fn aws_signer(key_id: &str, chain_id: Option<u64>) -> Result<AwsSigner, SignerError> {
let config = load_defaults(BehaviorVersion::latest()).await;
let client = Client::new(&config);
AwsSigner::new(client, key_id.to_string(), chain_id)
.await
.map_err(Into::into)
}
}
#[async_trait::async_trait]
impl alloy::network::TxSigner<Signature> for LocalOrAws {
fn address(&self) -> Address {
match self {
LocalOrAws::Local(signer) => signer.address(),
LocalOrAws::Aws(signer) => signer.address(),
}
}
async fn sign_transaction(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> alloy::signers::Result<Signature> {
match self {
LocalOrAws::Local(signer) => signer.sign_transaction(tx).await,
LocalOrAws::Aws(signer) => signer.sign_transaction(tx).await,
}
}
}
#[async_trait::async_trait]
impl alloy::signers::Signer<Signature> for LocalOrAws {
async fn sign_hash(&self, hash: &B256) -> alloy::signers::Result<Signature> {
match self {
LocalOrAws::Local(signer) => signer.sign_hash(hash).await,
LocalOrAws::Aws(signer) => signer.sign_hash(hash).await,
}
}
fn address(&self) -> Address {
match self {
LocalOrAws::Local(signer) => signer.address(),
LocalOrAws::Aws(signer) => signer.address(),
}
}
fn chain_id(&self) -> Option<ChainId> {
match self {
LocalOrAws::Local(signer) => signer.chain_id(),
LocalOrAws::Aws(signer) => signer.chain_id(),
}
}
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
match self {
LocalOrAws::Local(signer) => signer.set_chain_id(chain_id),
LocalOrAws::Aws(signer) => signer.set_chain_id(chain_id),
}
}
}
impl IntoWallet<Ethereum> for LocalOrAws {
type NetworkWallet = EthereumWallet;
fn into_wallet(self) -> Self::NetworkWallet {
EthereumWallet::from(self)
}
}