Skip to content

Commit 70a863b

Browse files
authored
feat: add LocalOrAws (#23)
1 parent b9f06c0 commit 70a863b

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

Cargo.toml

+7-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ chrono = "0.4.40"
3737

3838
# Other
3939
thiserror = "2.0.11"
40-
alloy = { version = "0.12.6", optional = true, default-features = false, features = ["std"] }
40+
alloy = { version = "0.12.6", optional = true, default-features = false, features = ["std", "signer-aws", "signer-local", "consensus", "network"] }
4141
serde = { version = "1", features = ["derive"] }
42+
async-trait = { version = "0.1.80", optional = true }
43+
44+
# AWS
45+
aws-config = { version = "1.1.7", optional = true }
46+
aws-sdk-kms = { version = "1.15.0", optional = true }
4247

4348
[dev-dependencies]
4449
ajj = "0.3.1"
@@ -49,5 +54,5 @@ tokio = { version = "1.43.0", features = ["macros"] }
4954

5055
[features]
5156
default = ["alloy"]
52-
alloy = ["dep:alloy"]
57+
alloy = ["dep:alloy", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"]
5358
perms = []

src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ pub mod utils {
3636
/// Slot calculator for determining the current slot and timepoint within a
3737
/// slot.
3838
pub mod calc;
39+
40+
#[cfg(feature = "alloy")]
41+
/// Signer using a local private key or AWS KMS key.
42+
pub mod signer;
3943
}
4044

4145
/// Re-exports of common dependencies.

src/utils/signer.rs

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use crate::utils::from_env::FromEnv;
2+
use alloy::{
3+
consensus::SignableTransaction,
4+
primitives::{Address, ChainId, B256},
5+
signers::{
6+
aws::{AwsSigner, AwsSignerError},
7+
local::{LocalSignerError, PrivateKeySigner},
8+
Signature,
9+
},
10+
};
11+
use aws_config::{load_defaults, BehaviorVersion};
12+
use aws_sdk_kms::Client;
13+
use std::borrow::Cow;
14+
15+
/// Configuration for a LocalOrAws signer.
16+
///
17+
/// Usage:
18+
/// ```
19+
/// # async fn test() -> Result<(), Box<dyn std::error::Error>> {
20+
/// use init4_bin_base::utils::{signer::LocalOrAwsConfig, from_env::FromEnv};
21+
/// let signer = LocalOrAwsConfig::from_env()?.connect().await?;
22+
/// # Ok(())
23+
/// # }
24+
/// ```
25+
#[derive(FromEnv, Debug, Clone)]
26+
#[from_env(crate)]
27+
pub struct LocalOrAwsConfig {
28+
/// The private key or AWS signer key ID.
29+
#[from_env(var = "SIGNER_KEY", desc = "AWS KMS key ID or local private key")]
30+
key_info: Cow<'static, str>,
31+
/// Chain ID for the AWS signer.
32+
#[from_env(var = "SIGNER_CHAIN_ID", desc = "Chain ID for AWS signer", optional)]
33+
chain_id: Option<u64>,
34+
}
35+
36+
impl LocalOrAwsConfig {
37+
/// Connect signer, but only if remote
38+
pub async fn connect_remote(&self) -> Result<LocalOrAws, SignerError> {
39+
let signer = LocalOrAws::aws_signer(&self.key_info, self.chain_id).await?;
40+
Ok(LocalOrAws::Aws(signer))
41+
}
42+
43+
/// Connect signer, but only if local
44+
pub fn connect_local(&self) -> Result<LocalOrAws, SignerError> {
45+
Ok(LocalOrAws::Local(LocalOrAws::wallet(&self.key_info)?))
46+
}
47+
48+
/// Connect signer, either local or remote
49+
pub async fn connect(&self) -> Result<LocalOrAws, SignerError> {
50+
if let Ok(local) = self.connect_local() {
51+
Ok(local)
52+
} else {
53+
self.connect_remote().await
54+
}
55+
}
56+
}
57+
58+
/// Abstraction over local signer or
59+
#[derive(Debug, Clone)]
60+
pub enum LocalOrAws {
61+
/// Local signer
62+
Local(PrivateKeySigner),
63+
/// AWS signer
64+
Aws(AwsSigner),
65+
}
66+
67+
/// Error during signing
68+
#[derive(Debug, thiserror::Error)]
69+
pub enum SignerError {
70+
/// Error during [`AwsSigner`] instantiation
71+
#[error("failed to connect AWS signer: {0}")]
72+
AwsSigner(#[from] AwsSignerError),
73+
/// Error loading the private key
74+
#[error("failed to load private key: {0}")]
75+
Wallet(#[from] LocalSignerError),
76+
/// Error parsing hex
77+
#[error("failed to parse hex: {0}")]
78+
Hex(#[from] alloy::hex::FromHexError),
79+
}
80+
81+
impl LocalOrAws {
82+
/// Load a privkey or AWS signer from environment variables.
83+
pub async fn load(key: &str, chain_id: Option<u64>) -> Result<Self, SignerError> {
84+
if let Ok(wallet) = LocalOrAws::wallet(key) {
85+
Ok(LocalOrAws::Local(wallet))
86+
} else {
87+
let signer = LocalOrAws::aws_signer(key, chain_id).await?;
88+
Ok(LocalOrAws::Aws(signer))
89+
}
90+
}
91+
92+
/// Load the wallet from environment variables.
93+
///
94+
/// # Panics
95+
///
96+
/// Panics if the env var contents is not a valid secp256k1 private key.
97+
fn wallet(private_key: &str) -> Result<PrivateKeySigner, SignerError> {
98+
let bytes = alloy::hex::decode(private_key.strip_prefix("0x").unwrap_or(private_key))?;
99+
Ok(PrivateKeySigner::from_slice(&bytes).unwrap())
100+
}
101+
102+
/// Load the AWS signer from environment variables./s
103+
async fn aws_signer(key_id: &str, chain_id: Option<u64>) -> Result<AwsSigner, SignerError> {
104+
let config = load_defaults(BehaviorVersion::latest()).await;
105+
let client = Client::new(&config);
106+
AwsSigner::new(client, key_id.to_string(), chain_id)
107+
.await
108+
.map_err(Into::into)
109+
}
110+
}
111+
112+
#[async_trait::async_trait]
113+
impl alloy::network::TxSigner<Signature> for LocalOrAws {
114+
fn address(&self) -> Address {
115+
match self {
116+
LocalOrAws::Local(signer) => signer.address(),
117+
LocalOrAws::Aws(signer) => signer.address(),
118+
}
119+
}
120+
121+
async fn sign_transaction(
122+
&self,
123+
tx: &mut dyn SignableTransaction<Signature>,
124+
) -> alloy::signers::Result<Signature> {
125+
match self {
126+
LocalOrAws::Local(signer) => signer.sign_transaction(tx).await,
127+
LocalOrAws::Aws(signer) => signer.sign_transaction(tx).await,
128+
}
129+
}
130+
}
131+
132+
#[async_trait::async_trait]
133+
impl alloy::signers::Signer<Signature> for LocalOrAws {
134+
/// Signs the given hash.
135+
async fn sign_hash(&self, hash: &B256) -> alloy::signers::Result<Signature> {
136+
match self {
137+
LocalOrAws::Local(signer) => signer.sign_hash(hash).await,
138+
LocalOrAws::Aws(signer) => signer.sign_hash(hash).await,
139+
}
140+
}
141+
142+
/// Returns the signer's Ethereum Address.
143+
fn address(&self) -> Address {
144+
match self {
145+
LocalOrAws::Local(signer) => signer.address(),
146+
LocalOrAws::Aws(signer) => signer.address(),
147+
}
148+
}
149+
150+
/// Returns the signer's chain ID.
151+
fn chain_id(&self) -> Option<ChainId> {
152+
match self {
153+
LocalOrAws::Local(signer) => signer.chain_id(),
154+
LocalOrAws::Aws(signer) => signer.chain_id(),
155+
}
156+
}
157+
158+
/// Sets the signer's chain ID.
159+
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
160+
match self {
161+
LocalOrAws::Local(signer) => signer.set_chain_id(chain_id),
162+
LocalOrAws::Aws(signer) => signer.set_chain_id(chain_id),
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)