1use alloy_primitives::{Address, U256};
4use alloy_sol_types::{SolCall, sol};
5use serde_json::json;
6
7use crate::chain::environment::{POLYESTER_TESTNET_ENVIRONMENT, PolyesterChainEnvironment};
8use crate::chain::rpc::JsonRpcClient;
9use crate::errors::{Error, Result};
10
11sol! {
12 function feeFactory() external view returns (address);
13 function decimals() external view returns (uint8);
14 function getFee(uint16 chainId, address zToken) external view returns (uint256);
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ZipperFeeQuote {
20 pub fee: U256,
21 pub z_token_decimals: u8,
22 pub fee_factory: String,
23 pub zipper_endpoint: String,
24}
25
26pub async fn quote_zipper_fee(
31 chain_id: u16,
32 z_token: &str,
33 zipper_endpoint: &str,
34 environment: Option<&PolyesterChainEnvironment>,
35 rpc: Option<&JsonRpcClient>,
36) -> Result<ZipperFeeQuote> {
37 if chain_id == 0 {
38 return Err(Error::validation("chain_id must be a uint16 > 0"));
39 }
40 let token = normalize_address(z_token, "z_token")?;
41 let endpoint = normalize_address(zipper_endpoint, "zipper_endpoint")?;
42
43 let env = environment.unwrap_or(&POLYESTER_TESTNET_ENVIRONMENT);
44 let owned_client;
45 let client = match rpc {
46 Some(c) => c,
47 None => {
48 owned_client = JsonRpcClient::new(env.rpc_url, std::time::Duration::from_secs(60));
49 &owned_client
50 }
51 };
52
53 let ff_raw = eth_call(client, &endpoint, feeFactoryCall {}.abi_encode()).await?;
54 let fee_factory = address_from_eth_call_result(&ff_raw)?;
55
56 let decimals_raw = eth_call(client, &token, decimalsCall {}.abi_encode()).await?;
57 let decimals = u256_from_eth_call_result(&decimals_raw)?;
58 let z_token_decimals = u8::try_from(decimals)
59 .map_err(|_| Error::validation(format!("decimals out of range: {decimals}")))?;
60
61 let token_addr: Address = token
62 .parse()
63 .map_err(|_| Error::validation("z_token is not a valid hex address"))?;
64 let fee_raw = eth_call(
65 client,
66 &fee_factory,
67 getFeeCall {
68 chainId: chain_id,
69 zToken: token_addr,
70 }
71 .abi_encode(),
72 )
73 .await?;
74 let fee = u256_from_eth_call_result(&fee_raw)?;
75
76 Ok(ZipperFeeQuote {
77 fee,
78 z_token_decimals,
79 fee_factory,
80 zipper_endpoint: endpoint,
81 })
82}
83
84async fn eth_call(client: &JsonRpcClient, to: &str, data: Vec<u8>) -> Result<String> {
85 let result = client
86 .request(
87 "eth_call",
88 json!([
89 {
90 "to": to,
91 "data": format!("0x{}", hex::encode(data)),
92 },
93 "latest"
94 ]),
95 )
96 .await?;
97 result
98 .as_str()
99 .map(str::to_owned)
100 .ok_or_else(|| Error::transport("eth_call result is not a hex string"))
101}
102
103fn address_from_eth_call_result(raw: &str) -> Result<String> {
104 let hex_body = raw.trim().trim_start_matches("0x").to_ascii_lowercase();
105 if hex_body.len() < 40 {
106 return Err(Error::transport(format!(
107 "eth_call address result too short: {raw}"
108 )));
109 }
110 Ok(format!("0x{}", &hex_body[hex_body.len() - 40..]))
111}
112
113fn u256_from_eth_call_result(raw: &str) -> Result<U256> {
114 let hex_body = raw.trim().trim_start_matches("0x");
115 U256::from_str_radix(hex_body, 16)
116 .map_err(|_| Error::transport(format!("eth_call u256 decode failed: {raw}")))
117}
118
119fn normalize_address(value: &str, field: &str) -> Result<String> {
120 let addr = value.trim();
121 if !addr.starts_with("0x") || addr.len() != 42 {
122 return Err(Error::validation(format!(
123 "{field} must be a 20-byte 0x-prefixed address"
124 )));
125 }
126 if hex::decode(&addr[2..]).is_err() {
127 return Err(Error::validation(format!(
128 "{field} is not a valid hex address"
129 )));
130 }
131 Ok(addr.to_ascii_lowercase())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn fee_factory_selector() {
140 assert_eq!(
141 feeFactoryCall::SELECTOR.as_slice(),
142 &alloy_primitives::keccak256(b"feeFactory()")[..4]
143 );
144 }
145
146 #[test]
147 fn get_fee_encode_round_trip() {
148 let token: Address = "0x5555555555555555555555555555555555555555"
149 .parse()
150 .unwrap();
151 let data = getFeeCall {
152 chainId: 56,
153 zToken: token,
154 }
155 .abi_encode();
156 assert_eq!(&data[..4], getFeeCall::SELECTOR.as_slice());
157 let decoded = getFeeCall::abi_decode(&data).unwrap();
158 assert_eq!(decoded.chainId, 56);
159 assert_eq!(decoded.zToken, token);
160 }
161
162 #[test]
163 fn address_from_padded_eth_call() {
164 let raw = "0x000000000000000000000000ae6b981be9b73421eb1ba5372d1a4a937d63fffb";
165 assert_eq!(
166 address_from_eth_call_result(raw).unwrap(),
167 "0xae6b981be9b73421eb1ba5372d1a4a937d63fffb"
168 );
169 }
170}