1use 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 let allowance_data = format!(
24 "0xdd62ed3e{:0>64}{:0>64}", 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 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 let approve_data = format!(
74 "0x095ea7b3{:0>64}{}",
75 spender_address.trim_start_matches("0x"),
76 MAX_APPROVAL_AMOUNT
77 );
78
79 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 };
113
114 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) } else {
141 1_000_000_000 };
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 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 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 tracing::info!("Approval transaction: {:?}", res);
176
177 Ok(res)
178}