1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use crate::prelude::*;
use crate::types::bsc_types::*;
use crate::environ::Context;
use isahc::prelude::*;
use url::Url;
use regex::Regex;
/// Contracts namespace containing related APIs about contracts
pub struct Contracts;
impl Contracts {
/// Check whether the content of text is in json format.
/// See
/// https://docs.soliditylang.org/en/v0.5.8/using-the-compiler.html#compiler-input-and-output-json-description
///
/// With that we need to
///
/// # Arguments
/// * `text` - text to check
fn is_content_in_json_format(text: &str) -> bool {
let lower_cased_text = text.to_lowercase();
let regex: Regex = Regex::new(r#".*"language":\s*"solidity".*"#).unwrap();
regex.is_match(&lower_cased_text)
}
/// Replace literal CRLF characters into ASCII code represent CR and LF
/// individually
fn replace_literal_crlf_with_char_codes(text: &str) -> String {
str::replace(&str::replace(text, "\\n", "\n"), "\\r", "\r")
}
/// Get contract ABI.
///
/// # Arguments
/// * `ctx` - Context
/// * `address` - contract address to get ABI
/// * `is_pretty_print` - whether or not to pretty print
pub fn get_abi(self, ctx: &Context, address: &str, is_pretty_print: bool) -> Result<String, BscError> {
let raw_url_str = format!("https://api.bscscan.com/api?module=contract&action=getabi&address={address}&apikey={api_key}", address=address, api_key=ctx.api_key);
let url = match Url::parse(&raw_url_str) {
Ok(res) => res,
Err(_) => return Err(BscError::ErrorInternalUrlParsing),
};
let request = match isahc::Request::get(url.as_str())
.version_negotiation(isahc::config::VersionNegotiation::http2())
.body(()) {
Ok(res) => res,
Err(e) => return Err(BscError::ErrorInternalGeneric(Some(format!("Error creating a HTTP request; err={}", e)))),
};
match isahc::send(request) {
Ok(mut res) => {
// early return for non-200 HTTP returned code
if res.status() != 200 {
return Err(BscError::ErrorApiResponse(format!("Error API response, with HTTP {code} returned", code=res.status().as_str())));
}
match res.json::<BSCContractABIResponse>() {
Ok(json) => {
if json.status == "1" {
// clean the text e.g. \
let mut cleaned_res = Contracts::replace_literal_crlf_with_char_codes(&json.result);
cleaned_res = str::replace(&cleaned_res, "\\", "");
if is_pretty_print {
// deserialize json string into object
// in order to pretty print it later
match serde_json::from_str::<Vec<BSCContractABIItem>>(&cleaned_res) {
Ok(json_obj) => {
match serde_json::to_string_pretty(&json_obj) {
Ok(pretty_json_string) => Ok(pretty_json_string),
Err(e) => return Err(BscError::ErrorInternalGeneric(Some(format!("create pretty JSON string from JSON object; err={}", e)))),
}
},
Err(e) => {
return Err(BscError::ErrorInternalGeneric(Some(format!("create JSON object from string; err={}", e))));
},
}
}
else {
Ok(cleaned_res)
}
}
else {
return Err(BscError::ErrorApiResponse(format!("message:{}", json.message)));
}
},
Err(e) => {
eprintln!("{:?}", e);
return Err(BscError::ErrorJsonParsing(None));
}
}
},
Err(e) => {
let err_msg = format!("{}", e);
return Err(BscError::ErrorSendingHttpRequest(Some(err_msg)));
}
}
}
/// Get verified contract's source code from the specified address.
///
/// # Arguments
/// * `ctx` - Context
/// * `address` - contract address to get verified source code
///
/// # Result
/// Return a tuple of `(Vec<BSCContractSourceCode>, bool)` for success case.
///
/// * If second part is `true`, then it means vector might have more than 1 items
/// to represent the number of files. But it will have 1 + N ; wheres N is
/// the number of files. The first item of vector will always be the raw
/// combined altogether of all files the same as `false` case of second part.
///
/// * If second part is `false`, then user just need to focus on the first item
/// of the vector as the source code is combined altogether in there.
/// This is due to the way API returned back. Even with mutiple source
/// files uploaded and verified but it will altogether combined into one
/// long string of code.
pub fn get_verified_source_code(self, ctx: &Context, address: &str) -> Result<(Vec<BSCContractSourceCode>, bool), BscError> {
let raw_url_str = format!("https://api.bscscan.com/api?module=contract&action=getsourcecode&address={address}&apikey={api_key}", address=address, api_key=ctx.api_key);
let url = match Url::parse(&raw_url_str) {
Ok(res) => res,
Err(_) => return Err(BscError::ErrorInternalUrlParsing),
};
let request = match isahc::Request::get(url.as_str())
.version_negotiation(isahc::config::VersionNegotiation::http2())
.body(()) {
Ok(res) => res,
Err(e) => return Err(BscError::ErrorInternalGeneric(Some(format!("Error creating a HTTP request; err={}", e)))),
};
match isahc::send(request) {
Ok(mut res) => {
// early return for non-200 HTTP returned code
if res.status() != 200 {
return Err(BscError::ErrorApiResponse(format!("Error API response, with HTTP {code} returned", code=res.status().as_str())));
}
match res.json::<BSCContractSourceCodeResponse>() {
Ok(json) => {
if json.status == "1" {
match json.result {
BSCContractSourceCodeResult::Success(mut contracts) => {
if contracts.is_empty() {
return Err(BscError::ErrorApiResponse(format!("source code is empty")));
}
// this one is not that necessary for ABI, but
// it happens for code. So ensure it's clean
// as well for ABI.
contracts[0].abi = Contracts::replace_literal_crlf_with_char_codes(&contracts[0].abi);
// Clean the text e.g. \ for its abi and code
// the same way as contract ABI API would do.
// With that, the output string is ready
// to be piped and viewed by text editor
// in which newlines will be taken into effect.
// NOTE: do this after CR/LF cleaning
contracts[0].abi = str::replace(&contracts[0].abi, "\\", "");
// there can be a chance that source code is in
// json format. It allows clear multiple source
// files that we can better utilize and return
// as such instead of combined it altogether.
//
// with this way, we will replace the items
// as held by contracts to what we will find
// from multiple files here
let mut additional_vec: Vec<BSCContractSourceCode> = Vec::new(); // create a new addtional vector just to avoid borrow issue
// within the loop
if Contracts::is_content_in_json_format(&contracts[0].source_code) {
let regex = Regex::new(r#""(.+)":\s*\{\s*.*?"content":\s*"(.+)""#).unwrap();
for cap in regex.captures_iter(&contracts[0].source_code) {
// NOTE: do this in order to not interfere
// with CR/LF cleaning
let mut cleaned_contract_code = Contracts::replace_literal_crlf_with_char_codes(&cap[2]);
cleaned_contract_code = str::replace(&cleaned_contract_code, "\\", "");
additional_vec.push(BSCContractSourceCode {
source_code: cleaned_contract_code,
abi: contracts[0].abi.clone(),
contract_name: cap[1].to_owned(),
compiler_version: contracts[0].compiler_version.clone(),
optimization_used: contracts[0].optimization_used.clone(),
runs: contracts[0].runs.clone(),
constructor_arguments: contracts[0].constructor_arguments.clone(),
evm_version: contracts[0].evm_version.clone(),
library: contracts[0].library.clone(),
license_type: contracts[0].license_type.clone(),
proxy: contracts[0].proxy.clone(),
implementation: contracts[0].implementation.clone(),
swarm_source: contracts[0].swarm_source.clone(),
});
}
contracts.append(&mut additional_vec);
return Ok((contracts, true));
}
// also check whether we made query for un-verified source code
// in which response back from server still
// has `status` field as "1". We need to check
// against `ABI` field for exact string of
// "Contract source code not verified".
//
// We treat this as an error as it doesn't make
// sense to return with no meta-detail information
// at all for non-verified source code even
// though its `status` is "1".
//
// NOTE: we only have interest towards the first
// item of `contracts` as this is the way
// API returns to us although there are
// multiple of source files uploaded.
if contracts.first().unwrap().abi == "Contract source code not verified" {
return Err(BscError::ErrorApiResponse(format!("made query to un-verified contract source code")));
}
else {
// NOTE: do this in order to not interfere
// with CR/LF cleaning
contracts[0].source_code = Contracts::replace_literal_crlf_with_char_codes(&contracts[0].source_code);
contracts[0].source_code = str::replace(&contracts[0].source_code, "\\", "");
return Ok((contracts, false));
}
},
BSCContractSourceCodeResult::Failed(result_msg) => {
return Err(BscError::ErrorApiResponse(format!("un-expected error for success case ({msg})", msg=result_msg)));
},
}
}
else {
// safely get text from "result" field
// this will ensure that the type of `json.result` is
// actually BSCContractSourceCodeResult which is
// the failed case.
let result_text = match json.result {
BSCContractSourceCodeResult::Failed(txt) => Some(txt),
_ => None,
};
match result_text {
Some(txt) => {
return Err(BscError::ErrorApiResponse(format!("message:{}, result:{}", json.message, txt)));
},
None => {
return Err(BscError::ErrorApiResponse(format!("message:{}", json.message)));
},
}
}
},
Err(e) => {
eprintln!("{:?}", e);
return Err(BscError::ErrorJsonParsing(None));
}
}
},
Err(e) => {
let err_msg = format!("{}", e);
return Err(BscError::ErrorSendingHttpRequest(Some(err_msg)));
}
}
}
}