goplus_rs/
lib.rs

1use std::time::UNIX_EPOCH;
2use reqwest::Client;
3use serde_json::{json, Value};
4use thiserror::Error;
5
6mod api_structs;
7use api_structs::*;
8
9const BASE_URL: &str = "https://api.gopluslabs.io/api/v1";
10
11
12#[derive(Error, Debug)]
13pub enum GpError {
14    #[error("Status {0} - {1}")]
15    RequestError(u32, String),
16    #[error("Parsing failed - {0}")]
17    ParseError(String),
18}
19
20impl From<reqwest::Error> for GpError {
21    fn from(value: reqwest::Error) -> Self {
22        match value.to_string().contains("missing field") {
23            true => Self::ParseError(value.to_string()),
24            false => Self::RequestError(value.status().unwrap().as_u16().into(), value.to_string())
25        }
26        
27    }
28}
29
30/// API Driver - handles all interaction with GoPlus endpoints
31#[derive(Default)]
32pub struct Session {
33    inner: Client,
34    access_token: Option<String>,
35}
36
37/// Used in V2Approval Call
38pub enum V2ApprovalERC {
39    ERC20,
40    ERC721,
41    ERC1155
42}
43
44impl Session {
45    pub fn new() -> Self {
46        // If app_key env var set
47        let app_key = std::env::var("GP_PUBLIC");
48        let secret_key = std::env::var("GP_SECRET");
49
50        if app_key.is_err() || secret_key.is_err(){
51            // No access token
52            tracing::warn!("Set enviornment variables to get access code");
53            tracing::warn!("  `export GP_PUBLIC = $APP_PUBLIC_KEY$`");
54            tracing::warn!("  `export GP_PUBLIC = $APP_PRIVATE_KEY$`");
55            Self {
56                inner: Client::new(),
57                access_token: None,
58            }
59        } 
60        else {
61            // UNCERTAIN IF WORKS, CAN'T TEST W/OUT KEYS
62            use sha1::{Sha1, Digest};
63            let mut hasher = Sha1::new();
64            let time: u64 = std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
65            let hash_str = format!("{}{}{}", app_key.unwrap(), time, secret_key.unwrap());
66            hasher.update(hash_str);
67            let f = hasher.finalize();
68            let str_hash = format!("{:x}", f);
69            
70            Self {
71                inner: Client::new(),
72                access_token: Some(str_hash),
73            }
74        }
75
76        
77    }
78
79    /// Retrieves a list of supported blockchain chains from the API.
80    ///
81    /// 
82    /// # Example Usage
83    /// ```ignore
84    /// let session = Session::new();
85    /// let response = session.supported_chains().await?;
86    /// let chains: Vec<Chain> = response.result;
87    /// ```
88    /// Tablular form of return data available [here](https://docs.gopluslabs.io/reference/response-details-9)
89    pub async fn supported_chains(&self) -> Result<SupportedChainsResponse, GpError> {
90        let url = format!("{BASE_URL}/supported_chains");
91        let res = self
92            .inner
93            .get(url)
94            .header("access_token", self.access_token.clone().unwrap_or("None".to_string()))
95            .send()
96            .await?
97            .error_for_status()?
98            .json::<SupportedChainsResponse>()
99            .await?;
100
101        Ok(res)
102    }
103
104    /// Fetches token risk data based on the blockchain chain ID and address.
105    ///
106    /// # Parameters
107    /// - `chain_id`: The blockchain chain ID.
108    /// - `addr`: The address to check.
109    ///
110    /// # Example Usage
111    /// ```ignore
112    /// let session = Session::new();
113    /// let response = session.token_risk("56", "0xEa51801b8F5B88543DdaD3D1727400c15b209D8f").await?;
114    /// let risk_data: Hashmap<String, TokenData> = response.result;
115    /// ```
116    /// Response fields in depth [here](https://docs.gopluslabs.io/reference/response-details)
117    pub async fn token_risk(&self, chain_id: &str, addr: &str) -> Result<TokenResponse, anyhow::Error> {
118        let url = format!(
119            "{}/token_security/{}", BASE_URL, chain_id
120        );
121
122        Ok(self.inner.get(url)
123            .header("access_token", self.access_token.clone().unwrap_or("None".to_string()))
124            .query(&[("contract_addresses", addr)])
125            .send()
126            .await?
127            .error_for_status()?
128            .json::<TokenResponse>()
129            .await?)
130    }
131
132    /// Retrieves risk information about an address, optionally filtered by chain ID.
133    ///
134    /// If only the address is provided without specifying the chain ID, the `contract_address` 
135    /// field in the response may be omitted. This occurs because the same address can represent 
136    /// a contract on one blockchain but not on another. Determination of `contract_address` involves
137    /// querying a third-party blockchain browser interface, which may delay the response. 
138    /// The `contract_address` field may initially be empty due to this delay. A subsequent request 
139    /// after about 5 seconds typically returns complete data, including the `contract_address`.
140    ///
141    /// # Parameters
142    /// - `addr`: The address to analyze.
143    /// - `chain_id`: Optional blockchain chain ID for filtering.
144    ///
145    /// # Example Usage
146    /// ```ignore
147    /// let session = Session::new();
148    /// let response = session.address_risk("0xEa51801b8F5B88543DdaD3D1727400c15b209D8f", Some("56")).await;
149    /// let risk_data: AccountRisk = response.result;
150    /// ```
151    /// Response fields in depth [here](https://docs.gopluslabs.io/reference/response-details-1)
152    pub async fn address_risk(&self, addr: &str, chain_id: Option<&str>) -> Result<AccountRiskResponse, GpError> {
153        let url = format!("{}/address_security/{}", BASE_URL, addr);
154
155        Ok(self.inner.get(url)
156            .header("access_token", self.access_token.clone().unwrap_or("None".to_string()))
157            .query(&[("chain_id", chain_id.unwrap_or("None"))])
158            .send()
159            .await?
160            .error_for_status()?
161            .json::<AccountRiskResponse>()
162            .await?)
163    }
164
165    pub async fn approval_security_v1(&self, chain_id: &str, contract_addr: &str) -> Result<V1ApprovalResponse, GpError> {
166        let url = format!("{}/approval_security/{}", BASE_URL, chain_id);
167        Ok(self.inner.get(url)
168            .header("access_token", self.access_token.clone().unwrap_or("None".to_string()))
169            .query(&[("contract_addresses", contract_addr)])
170            .send()
171            .await?
172            .error_for_status()?
173            .json::<V1ApprovalResponse>()
174            .await?)
175    }
176
177    
178    pub async fn approval_security_v2(&self, erc: V2ApprovalERC, chain_id: &str, address: &str) -> Result<V2ApprovalResponse, GpError> {
179        let base_url = "https://api.gopluslabs.io/api/v2";
180        let url = match erc {
181            V2ApprovalERC::ERC20 => format!("{}/token_approval_security/{}", base_url, chain_id),
182            V2ApprovalERC::ERC721 => format!("{}/nft721_approval_security/{}", base_url, chain_id),
183            V2ApprovalERC::ERC1155 => format!("{}/nft1155_approval_security/{}", base_url, chain_id),
184        };
185        
186        Ok(self.inner.get(url)
187            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
188            .query(&[("addresses", address)])
189            .send()
190            .await?
191            .error_for_status()?
192            .json::<V2ApprovalResponse>()
193            .await?)
194
195        
196    }
197
198    /// Decodes ABI input data for interacting with smart contracts.
199    ///
200    /// # Parameters
201    /// - `chain_id`: Blockchain chain ID.
202    /// - `data`: ABI data to decode.
203    /// - `contract_addr`: Optional contract address.
204    /// - `signer`: Optional signer.
205    /// - `txn_type`: Optional transaction type.
206    ///
207    /// # Example Usage
208    /// ```ignore
209    /// let session = Session::new();
210    /// let response = session.abi_decode(
211    ///     "56", 
212    ///     "0xa9059cbb00000000000000000000000055d398326f99059ff775485246999027b319795500000000000000000000000000000000000000000000000acc749097d9d00000", 
213    ///     Some("0x55d398326f99059ff775485246999027b3197955"),
214    ///     // None,
215    ///     None, 
216    ///     None
217    /// ).await?;
218    /// ```
219    /// Parameters and response fields in depth [here](https://docs.gopluslabs.io/reference/response-details-4)
220    pub async fn abi_decode(&self, 
221        chain_id: &str, 
222        data: &str,
223        contract_addr: Option<&str>,
224        signer: Option<&str>,
225        txn_type: Option<&str>
226    ) -> Result<AbiDecodeResponse, anyhow::Error> {
227        
228        let url = format!("{}/abi/input_decode", BASE_URL);
229
230        let params = json!({
231            "chain_id": chain_id,
232            "data": data,
233            "contract_address": contract_addr,
234            "signer": signer,
235            "transaction_type": txn_type
236        });
237
238        Ok(self.inner.post(url)
239            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
240            .json(&params)
241            .send()
242            .await?
243            .json::<AbiDecodeResponse>()
244            .await?)
245    }
246
247    /// Evaluates NFT risk for a specific contract address on a blockchain.
248    ///
249    /// # Parameters
250    /// - `chain_id`: Blockchain chain ID.
251    /// - `contract_addr`: Contract address.
252    /// - `token_id`: Optional token ID.
253    ///
254    /// # Example Usage
255    /// ```ignore
256    /// let session = Session::new();
257    /// let response = session.nft_risk("1", "0x...", Some("123")).await?;
258    /// let nft_risk: NftRisk = response.result;
259    /// ```
260    /// 
261    /// Response fields explained in depth [here](https://docs.gopluslabs.io/reference/response-details-5)
262    pub async fn nft_risk(&self, chain_id: &str, contract_addr: &str, token_id: Option<&str>) -> Result<NftRiskResponse, GpError> {
263        let url = format!("{}/nft_security/{}",BASE_URL, chain_id);
264
265        Ok(self.inner.get(url)
266            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
267            .query(&[("contract_addresses", contract_addr), ("token_id", token_id.unwrap_or("None"))])
268            .send()
269            .await?
270            .json::<NftRiskResponse>()
271            .await?)
272    }
273
274    // TODO: No successfully found url
275    pub async fn dapp_risk_by_url(&self, dapp_url: &str) -> Result<Value, anyhow::Error> {
276        tracing::warn!("The only response I've been able to get is 'DAPP NOT FOUND'");
277        let url = format!("{}/dapp_security", BASE_URL);
278        
279        Ok(self.inner.get(url)
280            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
281            .query(&[("url", dapp_url)])
282            .send()
283            .await?
284            .error_for_status()?
285            .json::<Value>()
286            .await?)
287    }
288
289    /// Analyzes phishing risks for a given site URL.
290    ///
291    /// # Parameters
292    /// - `site_url`: URL of the site to check.
293    ///
294    /// # Example Usage
295    /// ```ignore
296    /// let session = Session::new();
297    /// let response = session.phishing_site_risk("go-ethdenver.com").await?;
298    /// ```
299    /// Response fields in depth [here](https://docs.gopluslabs.io/reference/phishingsiteusingget)
300    pub async fn phishing_site_risk(&self, site_url: &str) -> Result<PhishingSiteResponse, GpError> {
301        let url = format!("{}/phishing_site", BASE_URL);
302
303        Ok(self.inner.get(url)
304            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
305            .query(&[("url", site_url)])
306            .send()
307            .await?
308            .error_for_status()?
309            .json::<PhishingSiteResponse>()
310            .await?)
311    }
312    
313    /// Assesses the risk of a rug pull for a contract on a specific blockchain.
314    ///
315    /// # Parameters
316    /// - `chain_id`: Blockchain chain ID.
317    /// - `contract_addr`: Contract address.
318    ///
319    /// # Example Usage
320    /// ```ignore
321    /// let session = Session::new();
322    /// let response = session.rug_pull_risk("1", "0x6B175474E89094C44Da98b954EedeAC495271d0F").await?;
323    /// ```
324    /// Response fields in depth [here](https://docs.gopluslabs.io/reference/response-details-7)
325    pub async fn rug_pull_risk(&self, chain_id: &str, contract_addr: &str) -> Result<RugPullRiskResponse, GpError> {
326        let url = format!("{}/rugpull_detecting/{}", BASE_URL, chain_id);
327
328        Ok(self.inner.get(url)
329            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
330            .query(&[("contract_addresses", contract_addr)])
331            .send()
332            .await?
333            .error_for_status()?
334            .json::<RugPullRiskResponse>()
335            .await?)
336    }
337
338    #[deprecated = "Token retrieved on initialization when keys are env variables. 
339    Can be used if you compute signature (method in new()/docs)."]
340    /// Obtains an access token using SHA-1 signature method.
341    ///
342    /// # Sign Method
343    /// Concatenate `app_key`, `time`, and `app_secret` in turn, and apply SHA-1 hashing.
344    /// 
345    /// `time` should be +- 1000s around the current timestamp
346    /// 
347    /// # Example
348    /// ```ignore
349    /// let app_key = "mBOMg20QW11BbtyH4Zh0";
350    /// let time = 1647847498;
351    /// let app_secret = "V6aRfxlPJwN3ViJSIFSCdxPvneajuJsh";
352    /// let sign = "sha1(mBOMg20QW11BbtyH4Zh01647847498V6aRfxlPJwN3ViJSIFSCdxPvneajuJsh)"; // This results in `7293d385b9225b3c3f232b76ba97255d0e21063e`
353    /// ```
354    ///
355    /// # Parameters
356    /// - `app_key`: Application key provided by the service.
357    /// - `signature`: Computed SHA-1 hash as a string.
358    /// - `time`: Current time as a UNIX timestamp.
359    ///
360    /// # Example Usage
361    /// ```ignore
362    /// let mut instance = Session::new(None);
363    /// instance.get_access_token("mBOMg20QW11BbtyH4Zh0", "7293d385b9225b3c3f232b76ba97255d0e21063e", 1647847498).await?;
364    /// ```
365    pub async fn get_access_token(&mut self, app_key: &str, signature: &str, time: u64) -> Result<(), GpError> {
366        let url = format!("{}/token", BASE_URL);
367
368        let params = json!({
369            "app_key": app_key,
370            "sign": signature,
371            "time": time,
372        });
373
374        let access_code_res = self.inner.get(url)
375            .header("access_token", self.access_token.as_ref().unwrap_or(&"None".to_string()))
376            .json(&params)
377            .send()
378            .await?
379            .error_for_status()?
380            .json::<AccessCodeResponse>()
381            .await?;
382
383        if access_code_res.code == 1 {
384            tracing::trace!("New access token expires in {} minutes", (access_code_res.result.as_ref().unwrap().expires_in)/60);
385            self.access_token = Some(access_code_res.result.unwrap().access_token);
386            Ok(())
387            
388        } else {
389            tracing::error!("Error getting access token\nCode: {}", access_code_res.code);
390            Err(GpError::RequestError(access_code_res.code, access_code_res.message))
391        }
392        
393        
394
395    }
396    
397}
398
399
400
401
402pub fn interpret_gp_status_code(code: u32) -> &'static str {
403    match code {
404        1 => "Complete data prepared",
405        2 => "Partial data obtained. The complete data can be requested again in about 15 seconds.",
406        2004 => "Contract address format error!",
407        2018 => "ChainID not supported",
408        2020 => "Non-contract address",
409        2021 => "No info for this contract",
410        2022 => "Non-supported chainId",
411        2026 => "dApp not found",
412        2027 => "ABI not found",
413        2028 => "The ABI not support parsing",
414        4010 => "App_key not exist",
415        4011 => "Signature expiration (the same request parameters cannot be requested more than once)",
416        4012 => "Wrong Signature",
417        4023 => "Access token not found",
418        4029 => "Request limit reached",
419        5000 => "System error",
420        5006 => "Param error!",
421        _ => "Unknown status code",
422    }
423}