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    rpc::types::TransactionReceipt,
8    signers::local::PrivateKeySigner,
9};
10
11use crate::erc20::IERC20;
12use crate::erc4626::IERC4626;
13use crate::error::{ContractError, Result};
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    /// Approve a spender to use tokens if needed.
90    /// Returns the transaction receipt if approval was needed, None otherwise.
91    pub async fn approve_if_needed(
92        &self,
93        token: Address,
94        spender: Address,
95        amount: U256,
96    ) -> Result<Option<TransactionReceipt>> {
97        let current_allowance = self
98            .get_allowance(token, self.signer_address, spender)
99            .await?;
100
101        if current_allowance >= amount {
102            return Ok(None);
103        }
104
105        let contract = IERC20::new(token, &self.provider);
106        let tx = contract.approve(spender, amount);
107
108        let pending = tx.send().await.map_err(|e| {
109            ContractError::TransactionFailed(format!("Failed to send approval: {}", e))
110        })?;
111
112        let receipt = pending.get_receipt().await.map_err(|e| {
113            ContractError::TransactionFailed(format!("Failed to get approval receipt: {}", e))
114        })?;
115
116        Ok(Some(receipt))
117    }
118
119    /// Deposit assets into a vault.
120    /// Returns the transaction receipt.
121    pub async fn deposit(
122        &self,
123        vault: Address,
124        amount: U256,
125        receiver: Address,
126    ) -> Result<TransactionReceipt> {
127        let contract = IERC4626::new(vault, &self.provider);
128        let tx = contract.deposit(amount, receiver);
129
130        let pending = tx.send().await.map_err(|e| {
131            ContractError::TransactionFailed(format!("Failed to send deposit: {}", e))
132        })?;
133
134        let receipt = pending.get_receipt().await.map_err(|e| {
135            ContractError::TransactionFailed(format!("Failed to get deposit receipt: {}", e))
136        })?;
137
138        Ok(receipt)
139    }
140
141    /// Withdraw assets from a vault.
142    /// Returns the transaction receipt.
143    pub async fn withdraw(
144        &self,
145        vault: Address,
146        amount: U256,
147        receiver: Address,
148        owner: Address,
149    ) -> Result<TransactionReceipt> {
150        let contract = IERC4626::new(vault, &self.provider);
151        let tx = contract.withdraw(amount, receiver, owner);
152
153        let pending = tx.send().await.map_err(|e| {
154            ContractError::TransactionFailed(format!("Failed to send withdraw: {}", e))
155        })?;
156
157        let receipt = pending.get_receipt().await.map_err(|e| {
158            ContractError::TransactionFailed(format!("Failed to get withdraw receipt: {}", e))
159        })?;
160
161        Ok(receipt)
162    }
163
164    /// Get the signer's address.
165    pub fn signer_address(&self) -> Address {
166        self.signer_address
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_invalid_private_key() {
176        let result = VaultV1TransactionClient::new("http://localhost:8545", "invalid_key");
177        assert!(matches!(result, Err(ContractError::InvalidPrivateKey)));
178    }
179
180    #[test]
181    fn test_invalid_rpc_url() {
182        // Valid private key (32 bytes hex)
183        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
184        let result = VaultV1TransactionClient::new("not a valid url", private_key);
185        assert!(matches!(result, Err(ContractError::RpcConnection(_))));
186    }
187
188    #[test]
189    fn test_valid_construction() {
190        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
191        let result = VaultV1TransactionClient::new("http://localhost:8545", private_key);
192        assert!(result.is_ok());
193    }
194}