Skip to main content

evm_approvals/
lib.rs

1//! For EVM transactions, approvals are required and for delegated actions,
2//! those have to be handled server-side
3use anyhow::Result;
4
5pub mod chain_id;
6pub mod error;
7
8pub use chain_id::*;
9pub use error::*;
10
11pub const MAX_APPROVAL_AMOUNT: &str =
12    "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
13
14pub async fn get_allowance(
15    token_address: &str,
16    owner_address: &str,
17    spender_address: &str,
18    chain_id: &str,
19) -> Result<u128, ApprovalsError> {
20    let rpc_url = chain_id_to_ethereum_rpc_url(chain_id)?;
21
22    // Construct the allowance function call data
23    let allowance_data = format!(
24        "0xdd62ed3e{:0>64}{:0>64}", // allowance(address,address) function selector
25        owner_address.trim_start_matches("0x"),
26        spender_address.trim_start_matches("0x")
27    );
28
29    let rpc_request = serde_json::json!({
30        "jsonrpc": "2.0",
31        "method": "eth_call",
32        "params": [{
33            "to": token_address,
34            "data": allowance_data
35        }, "latest"],
36        "id": 1
37    });
38
39    let client = reqwest::Client::new();
40    let res = client
41        .post(rpc_url)
42        .json(&rpc_request)
43        .send()
44        .await
45        .map_err(ApprovalsError::FailedToGetAllowance)?;
46
47    let response: serde_json::Value = res
48        .json()
49        .await
50        .map_err(ApprovalsError::FailedToGetAllowance)?;
51
52    // Parse the response
53    let allowance = if let Some(result) = response.get("result") {
54        let allowance_hex = result.as_str().unwrap_or("0x0");
55        u128::from_str_radix(allowance_hex.trim_start_matches("0x"), 16).unwrap_or(0)
56    } else {
57        0
58    };
59
60    Ok(allowance)
61}
62
63pub async fn estimate_gas_params(
64    token_address: &str,
65    spender_address: &str,
66    from_address: &str,
67    chain_id: &str,
68) -> Result<(u64, u64), ApprovalsError> {
69    let rpc_url = chain_id_to_ethereum_rpc_url(chain_id)?;
70    let client = reqwest::Client::new();
71
72    // Construct approval data for gas estimation
73    let approve_data = format!(
74        "0x095ea7b3{:0>64}{}",
75        spender_address.trim_start_matches("0x"),
76        MAX_APPROVAL_AMOUNT
77    );
78
79    // Estimate gas limit
80    let gas_estimate_request = serde_json::json!({
81        "jsonrpc": "2.0",
82        "method": "eth_estimateGas",
83        "params": [{
84            "from": from_address,
85            "to": token_address,
86            "data": approve_data,
87            "value": "0x0"
88        }, "latest"],
89        "id": 1
90    });
91
92    let res = client
93        .post(&rpc_url)
94        .json(&gas_estimate_request)
95        .send()
96        .await
97        .map_err(|e| ApprovalsError::FailedToEstimateGas(e.to_string()))?;
98
99    let response: serde_json::Value = res
100        .json()
101        .await
102        .map_err(|e| ApprovalsError::FailedToEstimateGas(e.to_string()))?;
103
104    let gas_limit = if let Some(result) = response.get("result") {
105        u64::from_str_radix(
106            result.as_str().unwrap_or("0x0").trim_start_matches("0x"),
107            16,
108        )
109        .unwrap_or(21000)
110    } else {
111        21000 // fallback gas limit
112    };
113
114    // Get current gas price
115    let gas_price_request = serde_json::json!({
116        "jsonrpc": "2.0",
117        "method": "eth_gasPrice",
118        "params": [],
119        "id": 1
120    });
121
122    let res = client
123        .post(&rpc_url)
124        .json(&gas_price_request)
125        .send()
126        .await
127        .map_err(|e| ApprovalsError::FailedToEstimateGas(e.to_string()))?;
128
129    let response: serde_json::Value = res
130        .json()
131        .await
132        .map_err(|e| ApprovalsError::FailedToEstimateGas(e.to_string()))?;
133
134    let gas_price = if let Some(result) = response.get("result") {
135        u64::from_str_radix(
136            result.as_str().unwrap_or("0x0").trim_start_matches("0x"),
137            16,
138        )
139        .unwrap_or(1_000_000_000) // 1 gwei fallback
140    } else {
141        1_000_000_000 // 1 gwei fallback
142    };
143
144    Ok((gas_limit, gas_price))
145}
146
147pub async fn create_approval_transaction(
148    token_address: &str,
149    spender_address: &str,
150    from_address: &str,
151    chain_id: &str,
152) -> Result<serde_json::Value, ApprovalsError> {
153    // Get gas parameters
154    let (gas_limit, gas_price) =
155        estimate_gas_params(token_address, spender_address, from_address, chain_id).await?;
156
157    let approve_data = format!(
158        "0x095ea7b3{:0>64}{}",
159        spender_address.trim_start_matches("0x"),
160        MAX_APPROVAL_AMOUNT
161    );
162
163    // Construct the JSON-RPC transaction format
164    let res = serde_json::json!({
165        "from": from_address,
166        "to": token_address,
167        "data": approve_data,
168        "chainId": format!("0x{:x}", chain_id.parse::<u64>().map_err(|e| ApprovalsError::InvalidChainId(e.to_string()))?),
169        "gasLimit": format!("0x{:x}", gas_limit),
170        "gasPrice": format!("0x{:x}", gas_price),
171        "value": "0x0"
172    });
173
174    // TODO debug instead of info
175    tracing::info!("Approval transaction: {:?}", res);
176
177    Ok(res)
178}