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            .wallet(wallet)
37            .connect_http(url);
38
39        Ok(Self {
40            provider,
41            signer_address,
42        })
43    }
44
45    /// Get the underlying asset address of a vault.
46    pub async fn get_asset(&self, vault: Address) -> Result<Address> {
47        let contract = IERC4626::new(vault, &self.provider);
48        let result = contract
49            .asset()
50            .call()
51            .await
52            .map_err(|e| ContractError::TransactionFailed(format!("Failed to get asset: {}", e)))?;
53        Ok(result)
54    }
55
56    /// Get the decimals of a token.
57    pub async fn get_decimals(&self, token: Address) -> Result<u8> {
58        let contract = IERC20::new(token, &self.provider);
59        let result = contract.decimals().call().await.map_err(|e| {
60            ContractError::TransactionFailed(format!("Failed to get decimals: {}", e))
61        })?;
62        Ok(result)
63    }
64
65    /// Get the balance of a token for an address.
66    pub async fn get_balance(&self, token: Address, owner: Address) -> Result<U256> {
67        let contract = IERC20::new(token, &self.provider);
68        let result = contract.balanceOf(owner).call().await.map_err(|e| {
69            ContractError::TransactionFailed(format!("Failed to get balance: {}", e))
70        })?;
71        Ok(result)
72    }
73
74    /// Get the allowance of a token for a spender.
75    pub async fn get_allowance(
76        &self,
77        token: Address,
78        owner: Address,
79        spender: Address,
80    ) -> Result<U256> {
81        let contract = IERC20::new(token, &self.provider);
82        let result = contract.allowance(owner, spender).call().await.map_err(|e| {
83            ContractError::TransactionFailed(format!("Failed to get allowance: {}", e))
84        })?;
85        Ok(result)
86    }
87
88    /// Create a prepared approval transaction.
89    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
90    pub fn approve(
91        &self,
92        token: Address,
93        spender: Address,
94        amount: U256,
95    ) -> PreparedCall<'_, IERC20::approveCall> {
96        let call = IERC20::approveCall { spender, amount };
97        PreparedCall::new(token, call, U256::ZERO, &self.provider)
98    }
99
100    /// Approve a spender to use tokens if needed.
101    /// Returns a `PreparedCall` if approval is needed, None otherwise.
102    pub async fn approve_if_needed(
103        &self,
104        token: Address,
105        spender: Address,
106        amount: U256,
107    ) -> Result<Option<PreparedCall<'_, IERC20::approveCall>>> {
108        let current_allowance = self
109            .get_allowance(token, self.signer_address, spender)
110            .await?;
111
112        if current_allowance >= amount {
113            return Ok(None);
114        }
115
116        Ok(Some(self.approve(token, spender, amount)))
117    }
118
119    /// Create a prepared deposit transaction.
120    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
121    pub fn deposit(
122        &self,
123        vault: Address,
124        amount: U256,
125        receiver: Address,
126    ) -> PreparedCall<'_, IERC4626::depositCall> {
127        let call = IERC4626::depositCall { assets: amount, receiver };
128        PreparedCall::new(vault, call, U256::ZERO, &self.provider)
129    }
130
131    /// Create a prepared withdraw transaction.
132    /// Returns a `PreparedCall` that can be sent or used with `MulticallBuilder`.
133    pub fn withdraw(
134        &self,
135        vault: Address,
136        amount: U256,
137        receiver: Address,
138        owner: Address,
139    ) -> PreparedCall<'_, IERC4626::withdrawCall> {
140        let call = IERC4626::withdrawCall { assets: amount, receiver, owner };
141        PreparedCall::new(vault, call, U256::ZERO, &self.provider)
142    }
143
144    /// Get the signer's address.
145    pub fn signer_address(&self) -> Address {
146        self.signer_address
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_invalid_private_key() {
156        let result = VaultV1TransactionClient::new("http://localhost:8545", "invalid_key");
157        assert!(matches!(result, Err(ContractError::InvalidPrivateKey)));
158    }
159
160    #[test]
161    fn test_invalid_rpc_url() {
162        // Valid private key (32 bytes hex)
163        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
164        let result = VaultV1TransactionClient::new("not a valid url", private_key);
165        assert!(matches!(result, Err(ContractError::RpcConnection(_))));
166    }
167
168    #[test]
169    fn test_valid_construction() {
170        let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
171        let result = VaultV1TransactionClient::new("http://localhost:8545", private_key);
172        assert!(result.is_ok());
173    }
174}