use alloy_consensus::SignableTransaction;
use alloy_network::{TxSigner, TxSignerSync};
use alloy_primitives::{Address, B256, ChainId, Signature};
use alloy_signer::{Result, Signer, SignerSync};
use crate::{AuthorizationContext, subclients::WalletsClient};
#[derive(Clone, Debug)]
pub struct PrivyAlloyWallet {
wallet_id: String,
address: Address,
wallets_client: WalletsClient,
authorization_context: AuthorizationContext,
chain_id: Option<ChainId>,
}
impl PrivyAlloyWallet {
pub fn new(
wallet_id: String,
address: Address,
wallets_client: WalletsClient,
authorization_context: AuthorizationContext,
) -> Self {
Self {
wallet_id,
address,
wallets_client,
authorization_context,
chain_id: None,
}
}
pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn wallet_id(&self) -> &str {
&self.wallet_id
}
}
impl SignerSync for PrivyAlloyWallet {
fn sign_hash_sync(&self, hash: &B256) -> Result<Signature> {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
tokio::task::block_in_place(|| handle.block_on(self.sign_hash(hash)))
} else {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| alloy_signer::Error::other(format!("Failed to build runtime: {e}")))?
.block_on(self.sign_hash(hash))
}
}
fn chain_id_sync(&self) -> Option<ChainId> {
self.chain_id
}
}
impl TxSignerSync<Signature> for PrivyAlloyWallet {
fn address(&self) -> Address {
self.address
}
fn sign_transaction_sync(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
let sig_hash = tx.signature_hash();
self.sign_hash_sync(&sig_hash)
}
}
#[async_trait::async_trait]
impl Signer for PrivyAlloyWallet {
async fn sign_hash(&self, hash: &B256) -> Result<Signature> {
let hash_hex = format!("{hash:#x}");
let response = match self
.wallets_client
.ethereum()
.sign_secp256k1(
&self.wallet_id,
&hash_hex,
&self.authorization_context,
Some(&hash_hex), )
.await
{
Ok(resp) => resp,
Err(e) => {
let msg = match e {
crate::PrivySignedApiError::Api(
crate::generated::Error::UnexpectedResponse(resp),
) => {
let status = resp.status();
let body = resp
.text()
.await
.unwrap_or_else(|_| "<body read error>".into());
format!("Privy API unexpected response: {status} — {body}")
}
other => format!("Privy API error: {other}"),
};
return Err(alloy_signer::Error::other(msg));
}
};
let wallet_response = response.into_inner();
let sig_hex = match wallet_response {
crate::generated::types::WalletRpcResponse::EthereumSecp256k1SignRpcResponse(
sig_response,
) => sig_response.data.signature,
_ => {
return Err(alloy_signer::Error::other(
"Unexpected response type from Privy API",
));
}
};
sig_hex
.parse::<Signature>()
.map_err(|e| alloy_signer::Error::other(format!("Failed to parse signature: {e}")))
}
fn address(&self) -> Address {
self.address
}
fn chain_id(&self) -> Option<ChainId> {
self.chain_id
}
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
self.chain_id = chain_id;
}
}
#[async_trait::async_trait]
impl TxSigner<Signature> for PrivyAlloyWallet {
fn address(&self) -> Address {
self.address
}
async fn sign_transaction(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
let sig_hash = tx.signature_hash();
self.sign_hash(&sig_hash).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::address;
fn create_test_wallet() -> PrivyAlloyWallet {
let wallet_id = "test_wallet_123".to_string();
let address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
let client = crate::PrivyClient::new("test_app_id".to_string(), "test_secret".to_string())
.expect("Failed to create test client");
let wallets_client = client.wallets();
let auth_context = AuthorizationContext::new();
PrivyAlloyWallet::new(wallet_id, address, wallets_client, auth_context)
}
#[test]
fn test_wallet_creation() {
let wallet = create_test_wallet();
assert_eq!(wallet.wallet_id(), "test_wallet_123");
assert_eq!(
wallet.address,
address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
);
assert_eq!(wallet.chain_id, None);
}
#[test]
fn test_with_chain_id() {
let wallet = create_test_wallet();
assert_eq!(wallet.chain_id, None);
let wallet_with_chain = wallet.with_chain_id(1);
assert_eq!(wallet_with_chain.chain_id, Some(1));
let wallet_mainnet = create_test_wallet().with_chain_id(1);
assert_eq!(wallet_mainnet.chain_id, Some(1));
let wallet_sepolia = create_test_wallet().with_chain_id(11155111);
assert_eq!(wallet_sepolia.chain_id, Some(11155111));
}
#[test]
fn test_signer_chain_id() {
let wallet = create_test_wallet();
assert_eq!(Signer::chain_id(&wallet), None);
let wallet_with_chain = wallet.with_chain_id(42161); assert_eq!(Signer::chain_id(&wallet_with_chain), Some(42161));
}
#[test]
fn test_set_chain_id() {
let mut wallet = create_test_wallet();
assert_eq!(wallet.chain_id, None);
wallet.set_chain_id(Some(10)); assert_eq!(wallet.chain_id, Some(10));
wallet.set_chain_id(None);
assert_eq!(wallet.chain_id, None);
}
#[test]
fn test_address_from_signer_trait() {
let wallet = create_test_wallet();
let expected = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
assert_eq!(Signer::address(&wallet), expected);
}
#[test]
fn test_address_from_tx_signer_sync_trait() {
let wallet = create_test_wallet();
let expected = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
assert_eq!(TxSignerSync::address(&wallet), expected);
}
#[test]
fn test_wallet_id_getter() {
let wallet = create_test_wallet();
assert_eq!(wallet.wallet_id(), "test_wallet_123");
}
}