morpho_rs_contracts/
vault_v1.rs

1//! V1 Vault transaction client for executing deposits and withdrawals.
2
3use alloy::{
4    network::EthereumWallet,
5    primitives::{Address, U256},
6    providers::ProviderBuilder,
7    signers::local::PrivateKeySigner,
8};
9
10use crate::erc20::IERC20;
11use crate::erc4626::IERC4626;
12use crate::error::{ContractError, Result};
13use crate::prepared_call::PreparedCall;
14use crate::provider::HttpProvider;
15
16/// Client for executing transactions against V1 (MetaMorpho) vaults.
17pub struct VaultV1TransactionClient {
18    provider: HttpProvider,
19    signer_address: Address,
20}
21
22impl VaultV1TransactionClient {
23    /// Create a new V1 transaction client.
24    pub fn new(rpc_url: &str, private_key: &str) -> Result<Self> {
25        let signer: PrivateKeySigner = private_key
26            .parse()
27            .map_err(|_| ContractError::InvalidPrivateKey)?;
28        let signer_address = signer.address();
29        let wallet = EthereumWallet::from(signer);
30
31        let url: url::Url = rpc_url
32            .parse()
33            .map_err(|e| ContractError::RpcConnection(format!("{}", e)))?;
34
35        let provider = ProviderBuilder::new()
36            .with_recommended_fillers()
37            .wallet(wallet)
38            .on_http(url);
39
40        Ok(Self {
41            provider,
42            signer_address,
43        })
44    }
45
46    /// Get the underlying asset address of a vault.
47    pub async fn get_asset(&self, vault: Address) -> Result<Address> {
48        let contract = IERC4626::new(vault, &self.provider);
49        let result = contract
50            .asset()
51            .call()
52            .await
53            .map_err(|e| ContractError::TransactionFailed(format!("Failed to get asset: {}", e)))?;
54        Ok(result._0)
55    }
56
57    /// Get the decimals of a token.
58    pub async fn get_decimals(&self, token: Address) -> Result<u8> {
59        let contract = IERC20::new(token, &self.provider);
60        let result = contract.decimals().call().await.map_err(|e| {
61            ContractError::TransactionFailed(format!("Failed to get decimals: {}", e))
62        })?;
63        Ok(result._0)
64    }
65
66    /// Get the balance of a token for an address.
67    pub async fn get_balance(&self, token: Address, owner: Address) -> Result<U256> {
68        let contract = IERC20::new(token, &self.provider);
69        let result = contract.balanceOf(owner).call().await.map_err(|e| {
70            ContractError::TransactionFailed(format!("Failed to get balance: {}", e))
71        })?;
72        Ok(result._0)
73    }
74
75    /// Get the allowance of a token for a spender.
76    pub async fn get_allowance(
77        &self,
78        token: Address,
79        owner: Address,
80        spender: Address,
81    ) -> Result<U256> {
82        let contract = IERC20::new(token, &self.provider);
83        let result = contract.allowance(owner, spender).call().await.map_err(|e| {
84            ContractError::TransactionFailed(format!("Failed to get allowance: {}", e))
85        })?;
86        Ok(result._0)
87    }
88
89    /// Create a prepared approval transaction.
90    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
91    pub fn approve(
92        &self,
93        token: Address,
94        spender: Address,
95        amount: U256,
96    ) -> PreparedCall<'_, IERC20::approveCall> {
97        let call = IERC20::approveCall { spender, amount };
98        PreparedCall::new(token, call, U256::ZERO, &self.provider)
99    }
100
101    /// Approve a spender to use tokens if needed.
102    /// Returns a `PreparedCall` if approval is needed, None otherwise.
103    pub async fn approve_if_needed(
104        &self,
105        token: Address,
106        spender: Address,
107        amount: U256,
108    ) -> Result<Option<PreparedCall<'_, IERC20::approveCall>>> {
109        let current_allowance = self
110            .get_allowance(token, self.signer_address, spender)
111            .await?;
112
113        if current_allowance >= amount {
114            return Ok(None);
115        }
116
117        Ok(Some(self.approve(token, spender, amount)))
118    }
119
120    /// Create a prepared deposit transaction.
121    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
122    pub fn deposit(
123        &self,
124        vault: Address,
125        amount: U256,
126        receiver: Address,
127    ) -> PreparedCall<'_, IERC4626::depositCall> {
128        let call = IERC4626::depositCall { assets: amount, receiver };
129        PreparedCall::new(vault, call, U256::ZERO, &self.provider)
130    }
131
132    /// Create a prepared withdraw transaction.
133    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
134    pub fn withdraw(
135        &self,
136        vault: Address,
137        amount: U256,
138        receiver: Address,
139        owner: Address,
140    ) -> PreparedCall<'_, IERC4626::withdrawCall> {
141        let call = IERC4626::withdrawCall { assets: amount, receiver, owner };
142        PreparedCall::new(vault, call, U256::ZERO, &self.provider)
143    }
144
145    /// Get the signer's address.
146    pub fn signer_address(&self) -> Address {
147        self.signer_address
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_invalid_private_key() {
157        let result = VaultV1TransactionClient::new("http://localhost:8545", "invalid_key");
158        assert!(matches!(result, Err(ContractError::InvalidPrivateKey)));
159    }
160
161    #[test]
162    fn test_invalid_rpc_url() {
163        // Valid private key (32 bytes hex)
164        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
165        let result = VaultV1TransactionClient::new("not a valid url", private_key);
166        assert!(matches!(result, Err(ContractError::RpcConnection(_))));
167    }
168
169    #[test]
170    fn test_valid_construction() {
171        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
172        let result = VaultV1TransactionClient::new("http://localhost:8545", private_key);
173        assert!(result.is_ok());
174    }
175}