From f4c9290df3ead9d7a5fdd499e15a9e0842746f2d Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Tue, 2 Apr 2024 20:17:32 +0800 Subject: [PATCH 01/16] update --- crates/verify/src/lib.rs | 1 + crates/verify/src/oklink.rs | 278 ++++++++++++++++++++++++++++++++++ crates/verify/src/provider.rs | 12 ++ 3 files changed, 291 insertions(+) create mode 100644 crates/verify/src/oklink.rs diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index be451d83fbb2..c934965aad39 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -25,6 +25,7 @@ use provider::VerificationProvider; pub mod retry; mod sourcify; +mod oklink; pub use retry::RetryArgs; diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs new file mode 100644 index 000000000000..3d04a8652155 --- /dev/null +++ b/crates/verify/src/oklink.rs @@ -0,0 +1,278 @@ +use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; +use async_trait::async_trait; +use eyre::Result; +use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; +use foundry_common::{evm, fs, retry::Retry}; +use foundry_compilers::ConfigurableContractArtifact; +use futures::FutureExt; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +pub static OKLINK_URL: &str = "https://www.oklink.com/api/v5/explorer/contract/verify-source-code-plugin/"; + +/// The type that can verify a contract on `oklink` +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct OklinkVerificationProvider; + +#[async_trait] +impl VerificationProvider for OklinkVerificationProvider { + async fn preflight_check(&mut self, args: VerifyArgs) -> Result<()> { + let _ = self.prepare_request(&args)?; + Ok(()) + } + + async fn verify(&mut self, args: VerifyArgs) -> Result<()> { + let body = self.prepare_request(&args)?; + + trace!("submitting verification request {:?}", body); + + let client = reqwest::Client::new(); + + let retry: Retry = args.retry.into(); + let resp = retry + .run_async(|| { + async { + println!( + "\nSubmitting verification for [{}] {:?}.", + args.contract.name, + args.address.to_string() + ); + let response = client + .post(args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL)) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&body)?) + .send() + .await?; + + let status = response.status(); + if !status.is_success() { + let error: serde_json::Value = response.json().await?; + eyre::bail!( + "Oklink verification request for address ({}) failed with status code {status}\nDetails: {error:#}", + args.address, + ); + } + + let text = response.text().await?; + Ok(Some(serde_json::from_str::(&text)?)) + } + .boxed() + }) + .await?; + + self.process_oklink_response(resp.map(|r| r.result)) + } + + async fn check(&self, args: VerifyCheckArgs) -> Result<()> { + let retry: Retry = args.retry.into(); + let resp = retry + .run_async(|| { + async { + let url = Url::from_str( + args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), + )?; + let query = format!( + "check-by-addresses?addresses={}&chainIds={}", + args.id, + args.etherscan.chain.unwrap_or_default().id(), + ); + let url = url.join(&query)?; + let response = reqwest::get(url).await?; + if !response.status().is_success() { + eyre::bail!( + "Failed to request verification status with status code {}", + response.status() + ); + }; + + Ok(Some(response.json::>().await?)) + } + .boxed() + }) + .await?; + + self.process_oklink_response(resp) + } +} + +impl OklinkVerificationProvider { + /// Configures the API request to the oklink API using the given [`VerifyArgs`]. + fn prepare_request(&self, args: &VerifyArgs) -> Result { + let mut config = args.try_load_config_emit_warnings()?; + config.libraries.extend(args.libraries.clone()); + + let project = config.project()?; + + if !config.cache { + eyre::bail!("Cache is required for oklink verification.") + } + + let cache = project.read_cache_file()?; + let (path, entry) = get_cached_entry_by_name(&cache, &args.contract.name)?; + + + + // the metadata is included in the contract's artifact file + let artifact_path = entry + .find_artifact_path(&args.contract.name) + .ok_or_else(|| eyre::eyre!("No artifact found for contract {}", args.contract.name))?; + + let artifact: ConfigurableContractArtifact = fs::read_json_file(artifact_path)?; + let compiler_version; + let optimization_used; + let runs; + + let evm_version; + let license_type; + let library_name; + let libaray_address; + + if let Some(metadata) = artifact.metadata { + compiler_version = metadata.compiler.version.clone(); + let settings = metadata.settings; + evm_version = match settings.evm_version { + Some(version) => Some(version.as_str().to_string()), + None => None, + }; + optimization_used = match settings.optimizer.enabled { + Some(enabled) => match enabled { + true => "1".to_string(), + false => "0".to_string(), + }, + None => "0".to_string(), + }; + runs = match settings.optimizer.runs { + Some(runs) => Some(runs.to_string()), + None => None, + }; + license_type = match metadata.sources.inner.get(&args.contract.name) { + Some(metadata_source) => metadata_source.license.clone(), + None => None, + }; + library_name = match { + + } + + + + } else { + eyre::bail!( + r#"No metadata found in artifact `{}` for contract {}. +Oklink requires contract metadata for verification. +metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.toml`"#, + artifact_path.display(), + args.contract.name + ) + } + + let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); + let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); + + let mut source_code = fs::read_to_string(&contract_path)?; + + // for import in entry.imports { + // let import_entry = format!("{}", import.display()); + // files.insert(import_entry, fs::read_to_string(&import)?); + // } + + let req = OklinkVerifyRequest { + sourceCode: source_code, + contractaddress: args.address.to_string(), + codeformat: CodeFormat::SingleFile.as_str().to_string(), + contractname: args.contract.name.clone(), + compilerversion: compiler_version, + optimizationUsed: optimization_used, + runs: runs, + constructorArguments: args.constructor_args.clone(), + evmversion: evm_version, + licenseType: license_type, + libraryname: library_name, + libraryaddress: libaray_address, + + }; + + Ok(req) + } + + fn process_oklink_response( + &self, + response: Option>, + ) -> Result<()> { + let Some([response, ..]) = response.as_deref() else { return Ok(()) }; + match response.status.as_str() { + "perfect" => { + if let Some(ts) = &response.GUID { + println!("Contract source code already verified. Storage Timestamp: {ts}"); + } else { + println!("Contract successfully verified"); + } + } + "partial" => { + println!("The recompiled contract partially matches the deployed version"); + } + "false" => println!("Contract source code is not verified"), + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + } + Ok(()) + } +} +#[derive(Debug, Serialize)] +pub enum CodeFormat { + SingleFile, + JsonInput, + Vyper, +} +impl CodeFormat { + fn as_str(&self) -> &'static str { + match self { + CodeFormat::SingleFile => "solidity-single-file", + CodeFormat::JsonInput => "solidity-standard-json-input", + CodeFormat::Vyper => "Vyper", + } + } +} +#[derive(Debug, Serialize)] +pub struct OklinkVerifyRequest { + sourceCode: String, + contractaddress: String, + codeformat: String, + contractname: String, + compilerversion: String, + optimizationUsed:String, + runs: Option, + constructorArguments: Option, + evmversion:Option, + licenseType:Option, + libraryname:Option, + libraryaddress:Option, +} + +#[derive(Debug, Deserialize)] +pub struct OklinkVerificationResponse { + result: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct OklinkResponseElement { + status: String, + message: String, + #[serde(rename = "result")] + GUID: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_check_addresses_url() { + let url = Url::from_str("https://server-verify.hashscan.io").unwrap(); + let url = url.join("check-by-addresses?addresses=0x1234&chainIds=1").unwrap(); + assert_eq!( + url.as_str(), + "https://server-verify.hashscan.io/check-by-addresses?addresses=0x1234&chainIds=1" + ); + } +} diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index d5721018dce8..707604c307a0 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -1,5 +1,6 @@ use super::{ etherscan::EtherscanVerificationProvider, sourcify::SourcifyVerificationProvider, VerifyArgs, + oklink::OklinkVerificationProvider, VerifyCheckArgs, }; use async_trait::async_trait; @@ -33,6 +34,7 @@ impl FromStr for VerificationProviderType { "e" | "etherscan" => Ok(VerificationProviderType::Etherscan), "s" | "sourcify" => Ok(VerificationProviderType::Sourcify), "b" | "blockscout" => Ok(VerificationProviderType::Blockscout), + "o" | "oklink" => Ok(VerificationProviderType::Oklink), _ => Err(format!("Unknown provider: {s}")), } } @@ -50,6 +52,9 @@ impl fmt::Display for VerificationProviderType { VerificationProviderType::Blockscout => { write!(f, "blockscout")?; } + VerificationProviderType::Oklink => { + write!(f, "oklink")?; + } }; Ok(()) } @@ -61,6 +66,7 @@ pub enum VerificationProviderType { Etherscan, Sourcify, Blockscout, + Oklink, } impl VerificationProviderType { @@ -79,6 +85,12 @@ impl VerificationProviderType { VerificationProviderType::Blockscout => { Ok(Box::::default()) } + VerificationProviderType::Oklink => { + if key.as_ref().map_or(true, |key| key.is_empty()) { + eyre::bail!("OKLINK_API_KEY must be set") + } + Ok(Box::::default()) + } } } } From b171b939022522fc0a6ef65e8772af168ed453e7 Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Thu, 4 Apr 2024 16:10:09 +0800 Subject: [PATCH 02/16] update --- crates/verify/src/oklink.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 3d04a8652155..2c8aa6a74970 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -3,6 +3,9 @@ use async_trait::async_trait; use eyre::Result; use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; use foundry_common::{evm, fs, retry::Retry}; +use foundry_block_explorers::{ + Client, +}; use foundry_compilers::ConfigurableContractArtifact; use futures::FutureExt; use reqwest::Url; @@ -126,8 +129,8 @@ impl OklinkVerificationProvider { let evm_version; let license_type; - let library_name; - let libaray_address; + let mut library_name:Vec = vec![]; + let mut library_address: Vec = vec![]; if let Some(metadata) = artifact.metadata { compiler_version = metadata.compiler.version.clone(); @@ -147,16 +150,16 @@ impl OklinkVerificationProvider { Some(runs) => Some(runs.to_string()), None => None, }; - license_type = match metadata.sources.inner.get(&args.contract.name) { + let contract_path = args.contract.path.as_ref().unwrap(); + license_type = match metadata.sources.inner.get(contract_path) { Some(metadata_source) => metadata_source.license.clone(), None => None, }; - library_name = match { - + for (name, address) in settings.libraries.into_iter() { + library_name.push(name.clone()); + library_address.push(address.clone()); } - - } else { eyre::bail!( r#"No metadata found in artifact `{}` for contract {}. @@ -168,18 +171,13 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom } let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); - let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); - - let mut source_code = fs::read_to_string(&contract_path)?; + let source_code = fs::read_to_string(&contract_path)?; - // for import in entry.imports { - // let import_entry = format!("{}", import.display()); - // files.insert(import_entry, fs::read_to_string(&import)?); - // } let req = OklinkVerifyRequest { sourceCode: source_code, - contractaddress: args.address.to_string(), + contractaddress: args.address.clone().to_string(), + // currently only single file supported codeformat: CodeFormat::SingleFile.as_str().to_string(), contractname: args.contract.name.clone(), compilerversion: compiler_version, @@ -188,8 +186,8 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom constructorArguments: args.constructor_args.clone(), evmversion: evm_version, licenseType: license_type, - libraryname: library_name, - libraryaddress: libaray_address, + libraryname: Some(library_name.join(",")), + libraryaddress: Some(library_address.join(",")), }; @@ -218,6 +216,8 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom Ok(()) } } + + #[derive(Debug, Serialize)] pub enum CodeFormat { SingleFile, From a3904c8162b714f52dd859840e5ca1cfb216a965 Mon Sep 17 00:00:00 2001 From: comcat Date: Thu, 4 Apr 2024 18:59:29 +0800 Subject: [PATCH 03/16] update --- crates/verify/src/lib.rs | 2 +- crates/verify/src/oklink.rs | 81 ++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index c934965aad39..9b1f5071de3f 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -238,7 +238,7 @@ pub struct VerifyCheckArgs { /// The verification ID. /// /// For Etherscan - Submission GUID. - /// + /// For Oklink - Submission GUID /// For Sourcify - Contract Address. id: String, diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 2c8aa6a74970..82c6a73c2c36 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf, str::FromStr}; pub static OKLINK_URL: &str = "https://www.oklink.com/api/v5/explorer/contract/verify-source-code-plugin/"; - +pub static OKLINK_URL_CHECK: &str = "https://www.oklink.com/api/v5/explorer/eth/api?module=contract&action=checkverifystatus"; /// The type that can verify a contract on `oklink` #[derive(Clone, Debug, Default)] #[non_exhaustive] @@ -27,9 +27,10 @@ impl VerificationProvider for OklinkVerificationProvider { } async fn verify(&mut self, args: VerifyArgs) -> Result<()> { - let body = self.prepare_request(&args)?; + let (body, api_key) = self.prepare_request(&args)?; - trace!("submitting verification request {:?}", body); + debug!("submitting verification request {:?}", serde_json::to_string(&body)?); + debug!("api key {:?}", api_key); let client = reqwest::Client::new(); @@ -45,10 +46,11 @@ impl VerificationProvider for OklinkVerificationProvider { let response = client .post(args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL)) .header("Content-Type", "application/json") + .header("Ok-Access-Key", &api_key) .body(serde_json::to_string(&body)?) .send() .await?; - + debug!("response {:?}", response); let status = response.status(); if !status.is_success() { let error: serde_json::Value = response.json().await?; @@ -59,6 +61,7 @@ impl VerificationProvider for OklinkVerificationProvider { } let text = response.text().await?; + debug!("text {:?}", text); Ok(Some(serde_json::from_str::(&text)?)) } .boxed() @@ -74,12 +77,11 @@ impl VerificationProvider for OklinkVerificationProvider { .run_async(|| { async { let url = Url::from_str( - args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), + args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL_CHECK), )?; let query = format!( - "check-by-addresses?addresses={}&chainIds={}", - args.id, - args.etherscan.chain.unwrap_or_default().id(), + "&guid={}", + args.id ); let url = url.join(&query)?; let response = reqwest::get(url).await?; @@ -102,10 +104,13 @@ impl VerificationProvider for OklinkVerificationProvider { impl OklinkVerificationProvider { /// Configures the API request to the oklink API using the given [`VerifyArgs`]. - fn prepare_request(&self, args: &VerifyArgs) -> Result { + fn prepare_request(&self, args: &VerifyArgs) -> Result<(OklinkVerifyRequest, String)> { let mut config = args.try_load_config_emit_warnings()?; config.libraries.extend(args.libraries.clone()); - + let api_key = match args.etherscan.key.clone() { + None => eyre::bail!("API KEY is not set"), + Some(key) => key + }; let project = config.project()?; if !config.cache { @@ -150,8 +155,12 @@ impl OklinkVerificationProvider { Some(runs) => Some(runs.to_string()), None => None, }; - let contract_path = args.contract.path.as_ref().unwrap(); - license_type = match metadata.sources.inner.get(contract_path) { + println!("{:?}",args.contract.path); + let contract_path = args.contract.path.clone().map_or(path.clone(), PathBuf::from); + let contract_path = contract_path.to_string_lossy().to_string(); + println!("contract Path {:?}", contract_path); + // let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); + license_type = match metadata.sources.inner.get(&contract_path) { Some(metadata_source) => metadata_source.license.clone(), None => None, }; @@ -169,6 +178,14 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom args.contract.name ) } + let library_name = match library_name.len() { + 0 => None, + _ => Some(library_name.join(",")) + }; + let library_address = match library_address.len() { + 0 => None, + _ => Some(library_address.join(",")) + }; let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); let source_code = fs::read_to_string(&contract_path)?; @@ -186,12 +203,11 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom constructorArguments: args.constructor_args.clone(), evmversion: evm_version, licenseType: license_type, - libraryname: Some(library_name.join(",")), - libraryaddress: Some(library_address.join(",")), - + libraryname: library_name, + libraryaddress: library_address, }; - Ok(req) + Ok((req, api_key)) } fn process_oklink_response( @@ -200,24 +216,32 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom ) -> Result<()> { let Some([response, ..]) = response.as_deref() else { return Ok(()) }; match response.status.as_str() { - "perfect" => { - if let Some(ts) = &response.GUID { - println!("Contract source code already verified. Storage Timestamp: {ts}"); - } else { - println!("Contract successfully verified"); + "1" => match response.message.as_str() { + "OK" => { + if let Some(result) = &response.result { + println!("Contract source code already verified. the result is {result}"); + } else { + println!("Contract successfully verified"); + } } + "NOTOK" => { + if let Some(result) = &response.result { + println!("Contract source code verified fail. the result is {result}") + } else { + println!("Contract verified fail") + } + + } + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), } - "partial" => { - println!("The recompiled contract partially matches the deployed version"); - } - "false" => println!("Contract source code is not verified"), - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + _ => println!("POST fail") } + Ok(()) } } - +#[warn(dead_code)] #[derive(Debug, Serialize)] pub enum CodeFormat { SingleFile, @@ -258,8 +282,7 @@ pub struct OklinkVerificationResponse { pub struct OklinkResponseElement { status: String, message: String, - #[serde(rename = "result")] - GUID: Option, + result: Option, } #[cfg(test)] From 359cdb6bbe92c83a50ae02679e7599abf11b34cb Mon Sep 17 00:00:00 2001 From: comcat Date: Thu, 4 Apr 2024 19:02:09 +0800 Subject: [PATCH 04/16] udpate --- crates/verify/src/oklink.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 82c6a73c2c36..83c390eece39 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -108,7 +108,7 @@ impl OklinkVerificationProvider { let mut config = args.try_load_config_emit_warnings()?; config.libraries.extend(args.libraries.clone()); let api_key = match args.etherscan.key.clone() { - None => eyre::bail!("API KEY is not set"), + None => eyre::bail!("OKLINK API KEY is not set"), Some(key) => key }; let project = config.project()?; From bd1759b40a79102f3e0b079e64c178c95baa3424 Mon Sep 17 00:00:00 2001 From: comcat Date: Thu, 4 Apr 2024 22:48:16 +0800 Subject: [PATCH 05/16] license --- crates/verify/src/oklink.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 83c390eece39..2136aec54385 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -12,8 +12,7 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf, str::FromStr}; -pub static OKLINK_URL: &str = "https://www.oklink.com/api/v5/explorer/contract/verify-source-code-plugin/"; -pub static OKLINK_URL_CHECK: &str = "https://www.oklink.com/api/v5/explorer/eth/api?module=contract&action=checkverifystatus"; +pub static OKLINK_URL: &str = "https://www.oklink.com/api"; /// The type that can verify a contract on `oklink` #[derive(Clone, Debug, Default)] #[non_exhaustive] @@ -72,26 +71,37 @@ impl VerificationProvider for OklinkVerificationProvider { } async fn check(&self, args: VerifyCheckArgs) -> Result<()> { + let api_key = match args.etherscan.key.clone() { + None => eyre::bail!("OKLINK API KEY is not set"), + Some(key) => key + }; + debug!("api key {:?}", api_key); let retry: Retry = args.retry.into(); let resp = retry .run_async(|| { async { let url = Url::from_str( - args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL_CHECK), + args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), )?; let query = format!( - "&guid={}", + "?module=contract&action=getabi&address={}", args.id ); let url = url.join(&query)?; - let response = reqwest::get(url).await?; + let client = reqwest::Client::new(); + debug!("url {:?}", url); + let response = client.get(url).header("Ok-Access-Key", &api_key).send().await?; + debug!("response: {:?}", response); + if !response.status().is_success() { eyre::bail!( "Failed to request verification status with status code {}", response.status() ); }; - + + + debug!("response.json {:?}", response.json().await?); Ok(Some(response.json::>().await?)) } .boxed() @@ -157,7 +167,7 @@ impl OklinkVerificationProvider { }; println!("{:?}",args.contract.path); let contract_path = args.contract.path.clone().map_or(path.clone(), PathBuf::from); - let contract_path = contract_path.to_string_lossy().to_string(); + let contract_path = contract_path.strip_prefix(project.root())?.to_string_lossy().to_string(); println!("contract Path {:?}", contract_path); // let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); license_type = match metadata.sources.inner.get(&contract_path) { From 40b90aa08fd9738451a02c1f571ed1b30dbc1629 Mon Sep 17 00:00:00 2001 From: comcat Date: Thu, 4 Apr 2024 23:07:36 +0800 Subject: [PATCH 06/16] udpate --- crates/verify/src/oklink.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 2136aec54385..8361f986edd4 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -1,6 +1,6 @@ use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; use async_trait::async_trait; -use eyre::Result; +use eyre::{Ok, Result}; use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; use foundry_common::{evm, fs, retry::Retry}; use foundry_block_explorers::{ @@ -61,13 +61,13 @@ impl VerificationProvider for OklinkVerificationProvider { let text = response.text().await?; debug!("text {:?}", text); - Ok(Some(serde_json::from_str::(&text)?)) + Ok(Some(serde_json::from_str::(&text)?)) } .boxed() }) .await?; - self.process_oklink_response(resp.map(|r| r.result)) + self.process_oklink_response(resp) } async fn check(&self, args: VerifyCheckArgs) -> Result<()> { @@ -101,8 +101,10 @@ impl VerificationProvider for OklinkVerificationProvider { }; - debug!("response.json {:?}", response.json().await?); - Ok(Some(response.json::>().await?)) + let text = response.text().await?; + debug!("text {:?}", text); + Ok(Some(serde_json::from_str::(&text)?)) + } .boxed() }) @@ -222,9 +224,9 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom fn process_oklink_response( &self, - response: Option>, + response: Option, ) -> Result<()> { - let Some([response, ..]) = response.as_deref() else { return Ok(()) }; + let Some(response) = response else { return Ok(()) }; match response.status.as_str() { "1" => match response.message.as_str() { "OK" => { @@ -244,7 +246,17 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom } s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), } - _ => println!("POST fail") + "0" => match response.message.as_str() { + "NOTOK" => { + if let Some(result) = &response.result { + println!("Contract source code verified fail. the result is {result}") + } else { + println!("Contract verified fail") + } + } + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + } + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), } Ok(()) From b079c024a7ff490e0f506a289bfdedc59cf20a67 Mon Sep 17 00:00:00 2001 From: comcat Date: Thu, 4 Apr 2024 23:11:20 +0800 Subject: [PATCH 07/16] udpate --- crates/verify/src/oklink.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 8361f986edd4..82bb2ec0b433 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -99,8 +99,6 @@ impl VerificationProvider for OklinkVerificationProvider { response.status() ); }; - - let text = response.text().await?; debug!("text {:?}", text); Ok(Some(serde_json::from_str::(&text)?)) @@ -249,12 +247,12 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom "0" => match response.message.as_str() { "NOTOK" => { if let Some(result) = &response.result { - println!("Contract source code verified fail. the result is {result}") + println!("Contract source code verified fail. the result is '{result}'") } else { println!("Contract verified fail") } } - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + s => eyre::bail!("Unknown message from oklink. message: {s:?}"), } s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), } @@ -295,10 +293,10 @@ pub struct OklinkVerifyRequest { libraryaddress:Option, } -#[derive(Debug, Deserialize)] -pub struct OklinkVerificationResponse { - result: Vec, -} +// #[derive(Debug, Deserialize)] +// pub struct OklinkVerificationResponse { +// result: Vec, +// } #[derive(Debug, Deserialize)] pub struct OklinkResponseElement { From ba3696cfd8dd67ac462bbaf892ee7432951fb7c1 Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Sun, 7 Apr 2024 10:34:20 +0800 Subject: [PATCH 08/16] update --- crates/verify/src/oklink.rs | 951 +++++++++++++++++++++++-------- crates/verify/src/oklink.rs.back | 321 +++++++++++ 2 files changed, 1032 insertions(+), 240 deletions(-) create mode 100644 crates/verify/src/oklink.rs.back diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 82bb2ec0b433..71f1cfec85d6 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -1,321 +1,792 @@ use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; -use async_trait::async_trait; -use eyre::{Ok, Result}; -use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; -use foundry_common::{evm, fs, retry::Retry}; +use crate::{retry::RETRY_CHECK_ON_VERIFY}; +use alloy_json_abi::Function; +use ethers_providers::Middleware; +use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ + errors::EtherscanError, + utils::lookup_compiler_version, + verify::{CodeFormat, VerifyContract}, Client, }; -use foundry_compilers::ConfigurableContractArtifact; +use foundry_cli::utils::{self, get_cached_entry_by_name, read_constructor_args_file, LoadConfig}; +use foundry_common::{abi::encode_function_args, retry::Retry, types::ToEthers}; +use foundry_compilers::{ + artifacts::{BytecodeObject, CompactContract, StandardJsonCompilerInput}, + cache::CacheEntry, + info::ContractInfo, + Artifact, Project, Solc, +}; +use foundry_config::{Chain, Config, SolcReq}; +use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, hashbrown::HashSet}; use futures::FutureExt; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::PathBuf, str::FromStr}; +use once_cell::sync::Lazy; +use regex::Regex; +use semver::{BuildMetadata, Version}; +use serde::Serialize; +use std::{ + borrow::Cow, fmt::Debug, path::{Path, PathBuf} +}; + +pub static OKLINK_URL: &str = "https://www.oklink.com/"; +pub static RE_BUILD_COMMIT: Lazy = + Lazy::new(|| Regex::new(r"(?Pcommit\.[0-9,a-f]{8})").unwrap()); -pub static OKLINK_URL: &str = "https://www.oklink.com/api"; -/// The type that can verify a contract on `oklink` #[derive(Clone, Debug, Default)] #[non_exhaustive] -pub struct OklinkVerificationProvider; +pub struct OklinkVerificationProvider { + /// Memoized cached entry of the target contract + cached_entry: Option<(PathBuf, CacheEntry, CompactContract)>, +} + +#[derive(Clone, Debug, Serialize)] +struct Query<'a, T: Serialize> { + #[serde(skip_serializing_if = "Option::is_none")] + apikey: Option>, + module: Cow<'a, str>, + action: Cow<'a, str>, + #[serde(flatten)] + other: T, +} -#[async_trait] +#[async_trait::async_trait] impl VerificationProvider for OklinkVerificationProvider { async fn preflight_check(&mut self, args: VerifyArgs) -> Result<()> { - let _ = self.prepare_request(&args)?; + let _ = self.prepare_request(&args).await?; Ok(()) } async fn verify(&mut self, args: VerifyArgs) -> Result<()> { - let (body, api_key) = self.prepare_request(&args)?; + let (oklink, verify_args) = self.prepare_request(&args).await?; - debug!("submitting verification request {:?}", serde_json::to_string(&body)?); - debug!("api key {:?}", api_key); + // if !args.skip_is_verified_check && + // self.is_contract_verified(&oklink, &verify_args).await? + // { + // println!( + // "\nContract [{}] {:?} is already verified. Skipping verification.", + // verify_args.contract_name, + // verify_args.address.to_checksum(None) + // ); + // return Ok(()) + // } + debug!("client {:?}", oklink); + + trace!(target: "forge::verify", ?verify_args, "submitting verification request"); let client = reqwest::Client::new(); + let retry: Retry = args.retry.into(); let resp = retry - .run_async(|| { - async { - println!( - "\nSubmitting verification for [{}] {:?}.", - args.contract.name, - args.address.to_string() - ); - let response = client - .post(args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL)) - .header("Content-Type", "application/json") - .header("Ok-Access-Key", &api_key) - .body(serde_json::to_string(&body)?) - .send() - .await?; - debug!("response {:?}", response); - let status = response.status(); - if !status.is_success() { - let error: serde_json::Value = response.json().await?; - eyre::bail!( - "Oklink verification request for address ({}) failed with status code {status}\nDetails: {error:#}", - args.address, - ); - } + .run_async(|| async { + println!( + "\nSubmitting verification for [{}] {}.", + verify_args.contract_name, verify_args.address + ); + let body = create_query("24b1a1b1-701f-4653-9d88-47d4f5c12512","contract", "verifysourcecode", &verify_args); + debug!("body {:?}", body); + let resp = client + .post(args.verifier.verifier_url.clone().unwrap()) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Ok-Access-Key", &args.etherscan.key().unwrap()) + .header("x-apiKey", &args.etherscan.key().unwrap()) + .form(&body) + .send() + .await? + .text() + .await?; + + // let resp = oklink + // .submit_contract_verification(&verify_args) + // .await + // .wrap_err_with(|| { + // // valid json + // let args = serde_json::to_string(&verify_args).unwrap(); + // error!(target: "forge::verify", ?args, "Failed to submit verification"); + // format!("Failed to submit contract verification, payload:\n{args}") + // })?; - let text = response.text().await?; - debug!("text {:?}", text); - Ok(Some(serde_json::from_str::(&text)?)) - } - .boxed() + debug!(target: "forge::verify", ?resp, "Received verification response"); + + // if resp.status == "0" { + // if resp.result == "Contract source code already verified" + // // specific for blockscout response + // || resp.result == "Smart-contract already verified." + // { + // return Ok(None) + // } + + // if resp.result.starts_with("Unable to locate ContractCode at") { + // warn!("{}", resp.result); + // return Err(eyre!("Oklink could not detect the deployment.")) + // } + + // warn!("Failed verify submission: {:?}", resp); + // eprintln!( + // "Encountered an error verifying this contract:\nResponse: `{}`\nDetails: `{}`", + // resp.message, resp.result + // ); + // std::process::exit(1); + // } + + Ok(Some(resp)) }) .await?; - self.process_oklink_response(resp) + // if let Some(resp) = resp { + // println!( + // "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}", + // resp.message, + // resp.result, + // oklink.address_url(args.address) + // ); + + // if args.watch { + // let check_args = VerifyCheckArgs { + // id: resp.result, + // etherscan: args.etherscan, + // retry: RETRY_CHECK_ON_VERIFY, + // verifier: args.verifier, + // }; + // // return check_args.run().await + // return self.check(check_args).await + // } + // } else { + // println!("Contract source code already verified"); + // } + + Ok(()) } + + /// Executes the command to check verification status on Oklink async fn check(&self, args: VerifyCheckArgs) -> Result<()> { - let api_key = match args.etherscan.key.clone() { - None => eyre::bail!("OKLINK API KEY is not set"), - Some(key) => key - }; - debug!("api key {:?}", api_key); + let config = args.try_load_config_emit_warnings()?; + let oklink = self.client( + args.etherscan.chain.unwrap_or_default(), + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + )?; let retry: Retry = args.retry.into(); - let resp = retry + retry .run_async(|| { async { - let url = Url::from_str( - args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), - )?; - let query = format!( - "?module=contract&action=getabi&address={}", - args.id + let resp = oklink + .check_contract_verification_status(args.id.clone()) + .await + .wrap_err("Failed to request verification status")?; + + trace!(target: "forge::verify", ?resp, "Received verification response"); + + eprintln!( + "Contract verification status:\nResponse: `{}`\nDetails: `{}`", + resp.message, resp.result ); - let url = url.join(&query)?; - let client = reqwest::Client::new(); - debug!("url {:?}", url); - let response = client.get(url).header("Ok-Access-Key", &api_key).send().await?; - debug!("response: {:?}", response); - - if !response.status().is_success() { - eyre::bail!( - "Failed to request verification status with status code {}", - response.status() - ); - }; - let text = response.text().await?; - debug!("text {:?}", text); - Ok(Some(serde_json::from_str::(&text)?)) + if resp.result == "Pending in queue" { + return Err(eyre!("Verification is still pending...",)) + } + + if resp.result == "Unable to verify" { + return Err(eyre!("Unable to verify.",)) + } + + if resp.result == "Already Verified" { + println!("Contract source code already verified"); + return Ok(()) + } + + if resp.status == "0" { + println!("Contract failed to verify."); + std::process::exit(1); + } + + if resp.result == "Pass - Verified" { + println!("Contract successfully verified"); + } + + Ok(()) } .boxed() }) - .await?; - - self.process_oklink_response(resp) + .await + .wrap_err("Checking verification result failed:") } } impl OklinkVerificationProvider { - /// Configures the API request to the oklink API using the given [`VerifyArgs`]. - fn prepare_request(&self, args: &VerifyArgs) -> Result<(OklinkVerifyRequest, String)> { - let mut config = args.try_load_config_emit_warnings()?; - config.libraries.extend(args.libraries.clone()); - let api_key = match args.etherscan.key.clone() { - None => eyre::bail!("OKLINK API KEY is not set"), - Some(key) => key - }; - let project = config.project()?; + + fn source( + &self, + args: &VerifyArgs, + project: &Project, + target: &Path, + version: &Version, + ) -> Result<(String, String, CodeFormat)> { + let mut input: StandardJsonCompilerInput = project + .standard_json_input(target) + .wrap_err("Failed to get standard json input")? + .normalize_evm_version(version); + + input.settings.libraries.libs = input + .settings + .libraries + .libs + .into_iter() + .map(|(f, libs)| (f.strip_prefix(project.root()).unwrap_or(&f).to_path_buf(), libs)) + .collect(); + + // remove all incompatible settings + input.settings.sanitize(version); + + let source = + serde_json::to_string(&input).wrap_err("Failed to parse standard json input")?; - if !config.cache { - eyre::bail!("Cache is required for oklink verification.") + trace!(target: "forge::verify", standard_json=source, "determined standard json input"); + + let name = format!( + "{}:{}", + target.strip_prefix(project.root()).unwrap_or(target).display(), + args.contract.name.clone() + ); + Ok((source, name, CodeFormat::StandardJsonInput)) + } + + /// Return the memoized cache entry for the target contract. + /// Read the artifact from cache on first access. + fn cache_entry( + &mut self, + project: &Project, + contract: &ContractInfo, + ) -> Result<&(PathBuf, CacheEntry, CompactContract)> { + if let Some(ref entry) = self.cached_entry { + return Ok(entry) } let cache = project.read_cache_file()?; - let (path, entry) = get_cached_entry_by_name(&cache, &args.contract.name)?; + let (path, entry) = if let Some(path) = contract.path.as_ref() { + let path = project.root().join(path); + ( + path.clone(), + cache + .entry(&path) + .ok_or_else(|| { + eyre::eyre!(format!("Cache entry not found for {}", path.display())) + })? + .to_owned(), + ) + } else { + get_cached_entry_by_name(&cache, &contract.name)? + }; + let contract: CompactContract = cache.read_artifact(path.clone(), &contract.name)?; + Ok(self.cached_entry.insert((path, entry, contract))) + } + /// Configures the API request to the oklink API using the given [`VerifyArgs`]. + async fn prepare_request(&mut self, args: &VerifyArgs) -> Result<(Client, VerifyContract)> { + let config = args.try_load_config_emit_warnings()?; + let oklink = self.client( + args.etherscan.chain.unwrap_or_default(), + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + )?; + let verify_args = self.create_verify_request(args, Some(config)).await?; + Ok((oklink, verify_args)) + } - // the metadata is included in the contract's artifact file - let artifact_path = entry - .find_artifact_path(&args.contract.name) - .ok_or_else(|| eyre::eyre!("No artifact found for contract {}", args.contract.name))?; + /// Queries the oklink API to verify if the contract is already verified. + async fn is_contract_verified( + &self, + oklink: &Client, + verify_contract: &VerifyContract, + ) -> Result { + let check = oklink.contract_abi(verify_contract.address).await; - let artifact: ConfigurableContractArtifact = fs::read_json_file(artifact_path)?; - let compiler_version; - let optimization_used; - let runs; + if let Err(err) = check { + match err { + EtherscanError::ContractCodeNotVerified(_) => return Ok(false), + error => return Err(error.into()), + } + } - let evm_version; - let license_type; - let mut library_name:Vec = vec![]; - let mut library_address: Vec = vec![]; + Ok(true) + } - if let Some(metadata) = artifact.metadata { - compiler_version = metadata.compiler.version.clone(); - let settings = metadata.settings; - evm_version = match settings.evm_version { - Some(version) => Some(version.as_str().to_string()), - None => None, - }; - optimization_used = match settings.optimizer.enabled { - Some(enabled) => match enabled { - true => "1".to_string(), - false => "0".to_string(), - }, - None => "0".to_string(), - }; - runs = match settings.optimizer.runs { - Some(runs) => Some(runs.to_string()), - None => None, - }; - println!("{:?}",args.contract.path); - let contract_path = args.contract.path.clone().map_or(path.clone(), PathBuf::from); - let contract_path = contract_path.strip_prefix(project.root())?.to_string_lossy().to_string(); - println!("contract Path {:?}", contract_path); - // let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); - license_type = match metadata.sources.inner.get(&contract_path) { - Some(metadata_source) => metadata_source.license.clone(), - None => None, - }; - for (name, address) in settings.libraries.into_iter() { - library_name.push(name.clone()); - library_address.push(address.clone()); - } + /// Create an oklink client + pub(crate) fn client( + &self, + chain: Chain, + verifier_url: Option<&str>, + oklink_key: Option<&str>, + config: &Config, + ) -> Result { + let oklink_key = oklink_key.unwrap(); + + let mut builder = Client::builder(); + + builder = if let Some(api_url) = verifier_url { + let api_url = api_url.trim_end_matches('/'); + builder.with_api_url(api_url)?.with_url(OKLINK_URL)? } else { - eyre::bail!( - r#"No metadata found in artifact `{}` for contract {}. -Oklink requires contract metadata for verification. -metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.toml`"#, - artifact_path.display(), - args.contract.name - ) - } - let library_name = match library_name.len() { - 0 => None, - _ => Some(library_name.join(",")) - }; - let library_address = match library_address.len() { - 0 => None, - _ => Some(library_address.join(",")) + eyre::bail!("must pass the verifier URL") }; + debug!("{:?}", builder); - let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); - let source_code = fs::read_to_string(&contract_path)?; - - - let req = OklinkVerifyRequest { - sourceCode: source_code, - contractaddress: args.address.clone().to_string(), - // currently only single file supported - codeformat: CodeFormat::SingleFile.as_str().to_string(), - contractname: args.contract.name.clone(), - compilerversion: compiler_version, - optimizationUsed: optimization_used, - runs: runs, - constructorArguments: args.constructor_args.clone(), - evmversion: evm_version, - licenseType: license_type, - libraryname: library_name, - libraryaddress: library_address, + builder + .with_api_key(oklink_key) + .build() + .wrap_err("Failed to create oklink client") + } + + /// Creates the `VerifyContract` oklink request in order to verify the contract + /// + /// If `--flatten` is set to `true` then this will send with [`CodeFormat::SingleFile`] + /// otherwise this will use the [`CodeFormat::StandardJsonInput`] + pub async fn create_verify_request( + &mut self, + args: &VerifyArgs, + config: Option, + ) -> Result { + let mut config = + if let Some(config) = config { config } else { args.try_load_config_emit_warnings()? }; + + config.libraries.extend(args.libraries.clone()); + + let project = config.project()?; + + let contract_path = self.contract_path(args, &project)?; + let compiler_version = self.compiler_version(args, &config, &project)?; + let (source, contract_name, code_format) = + self.source(args, &project, &contract_path, &compiler_version)?; + + let compiler_version = format!("v{}", ensure_solc_build_metadata(compiler_version).await?); + let constructor_args = self.constructor_args(args, &project, &config).await?; + let mut verify_args = + VerifyContract::new(args.address, contract_name, source, compiler_version) + .constructor_arguments(constructor_args) + .code_format(code_format); + + if args.via_ir { + // we explicitly set this __undocumented__ argument to true if provided by the user, + // though this info is also available in the compiler settings of the standard json + // object if standard json is used + // unclear how oklink interprets this field in standard-json mode + verify_args = verify_args.via_ir(true); + } + + if code_format == CodeFormat::SingleFile { + verify_args = if let Some(optimizations) = args.num_of_optimizations { + verify_args.optimized().runs(optimizations as u32) + } else if config.optimizer { + verify_args.optimized().runs(config.optimizer_runs.try_into()?) + } else { + verify_args.not_optimized() + }; + } + + Ok(verify_args) + } + + /// Get the target contract path. If it wasn't provided, attempt a lookup + /// in cache. Validate the path indeed exists on disk. + fn contract_path(&mut self, args: &VerifyArgs, project: &Project) -> Result { + let path = if let Some(path) = args.contract.path.as_ref() { + project.root().join(path) + } else { + let (path, _, _) = self.cache_entry(project, &args.contract).wrap_err( + "If cache is disabled, contract info must be provided in the format :", + )?; + path.to_owned() }; - Ok((req, api_key)) + // check that the provided contract is part of the source dir + if !path.exists() { + eyre::bail!("Contract {:?} does not exist.", path); + } + + Ok(path) } - fn process_oklink_response( - &self, - response: Option, - ) -> Result<()> { - let Some(response) = response else { return Ok(()) }; - match response.status.as_str() { - "1" => match response.message.as_str() { - "OK" => { - if let Some(result) = &response.result { - println!("Contract source code already verified. the result is {result}"); - } else { - println!("Contract successfully verified"); - } - } - "NOTOK" => { - if let Some(result) = &response.result { - println!("Contract source code verified fail. the result is {result}") - } else { - println!("Contract verified fail") + /// Parse the compiler version. + /// The priority desc: + /// 1. Through CLI arg `--compiler-version` + /// 2. `solc` defined in foundry.toml + /// 3. The version contract was last compiled with. + fn compiler_version( + &mut self, + args: &VerifyArgs, + config: &Config, + project: &Project, + ) -> Result { + if let Some(ref version) = args.compiler_version { + return Ok(version.trim_start_matches('v').parse()?) + } + + if let Some(ref solc) = config.solc { + match solc { + SolcReq::Version(version) => return Ok(version.to_owned()), + SolcReq::Local(solc) => { + if solc.is_file() { + return Ok(Solc::new(solc).version()?) } - } - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), } - "0" => match response.message.as_str() { - "NOTOK" => { - if let Some(result) = &response.result { - println!("Contract source code verified fail. the result is '{result}'") - } else { - println!("Contract verified fail") - } - } - s => eyre::bail!("Unknown message from oklink. message: {s:?}"), + } + + let (_, entry, _) = self.cache_entry(project, &args.contract).wrap_err( + "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml" + )?; + let artifacts = entry.artifacts_versions().collect::>(); + + if artifacts.is_empty() { + eyre::bail!("No matching artifact found for {}", args.contract.name); + } + + // ensure we have a single version + let unique_versions = artifacts.iter().map(|a| a.0.to_string()).collect::>(); + if unique_versions.len() > 1 { + let versions = unique_versions.into_iter().collect::>(); + warn!("Ambiguous compiler versions found in cache: {}", versions.join(", ")); + eyre::bail!("Compiler version has to be set in `foundry.toml`. If the project was not deployed with foundry, specify the version through `--compiler-version` flag.") + } + + // we have a unique version + let mut version = artifacts[0].0.clone(); + version.build = match RE_BUILD_COMMIT.captures(version.build.as_str()) { + Some(cap) => BuildMetadata::new(cap.name("commit").unwrap().as_str())?, + _ => BuildMetadata::EMPTY, + }; + + Ok(version) + } + + /// Return the optional encoded constructor arguments. If the path to + /// constructor arguments was provided, read them and encode. Otherwise, + /// return whatever was set in the [VerifyArgs] args. + async fn constructor_args( + &mut self, + args: &VerifyArgs, + project: &Project, + config: &Config, + ) -> Result> { + if let Some(ref constructor_args_path) = args.constructor_args_path { + let (_, _, contract) = self.cache_entry(project, &args.contract).wrap_err( + "Cache must be enabled in order to use the `--constructor-args-path` option", + )?; + let abi = + contract.abi.as_ref().ok_or_else(|| eyre!("Can't find ABI in cached artifact."))?; + let constructor = abi + .constructor() + .ok_or_else(|| eyre!("Can't retrieve constructor info from artifact ABI."))?; + #[allow(deprecated)] + let func = Function { + name: "constructor".to_string(), + inputs: constructor.inputs.clone(), + outputs: vec![], + state_mutability: alloy_json_abi::StateMutability::NonPayable, + }; + let encoded_args = encode_function_args( + &func, + read_constructor_args_file(constructor_args_path.to_path_buf())?, + )?; + let encoded_args = hex::encode(encoded_args); + return Ok(Some(encoded_args[8..].into())) + } + if args.guess_constructor_args { + return Ok(Some(self.guess_constructor_args(args, project, config).await?)) + } + + Ok(args.constructor_args.clone()) + } + + /// Uses Oklink API to fetch contract creation transaction. + /// If transaction is a create transaction or a invocation of default CREATE2 deployer, tries to + /// match provided creation code with local bytecode of the target contract. + /// If bytecode match, returns latest bytes of on-chain creation code as constructor arguments. + async fn guess_constructor_args( + &mut self, + args: &VerifyArgs, + project: &Project, + config: &Config, + ) -> Result { + let provider = utils::get_provider(config)?; + let client = self.client( + args.etherscan.chain.unwrap_or_default(), + args.verifier.verifier_url.as_deref(), + args.etherscan.key.as_deref(), + config, + )?; + + let creation_data = client.contract_creation_data(args.address).await?; + let transaction = provider + .get_transaction(creation_data.transaction_hash.to_ethers()) + .await? + .ok_or_eyre("Couldn't fetch transaction data from RPC")?; + let receipt = provider + .get_transaction_receipt(creation_data.transaction_hash.to_ethers()) + .await? + .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?; + + let maybe_creation_code: &[u8]; + + if receipt.contract_address == Some(args.address.to_ethers()) { + maybe_creation_code = &transaction.input; + } else if transaction.to == Some(DEFAULT_CREATE2_DEPLOYER.to_ethers()) { + maybe_creation_code = &transaction.input[32..]; + } else { + eyre::bail!("Fetching of constructor arguments is not supported for contracts created by contracts") + } + + let contract_path = self.contract_path(args, project)?.to_string_lossy().into_owned(); + let output = project.compile()?; + let artifact = output + .find(contract_path, &args.contract.name) + .ok_or_eyre("Contract artifact wasn't found locally")?; + let bytecode = artifact + .get_bytecode_object() + .ok_or_eyre("Contract artifact does not contain bytecode")?; + + let bytecode = match bytecode.as_ref() { + BytecodeObject::Bytecode(bytes) => Ok(bytes), + BytecodeObject::Unlinked(_) => { + Err(eyre!("You have to provide correct libraries to use --guess-constructor-args")) } - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + }?; + + if maybe_creation_code.starts_with(bytecode) { + let constructor_args = &maybe_creation_code[bytecode.len()..]; + Ok(hex::encode(constructor_args)) + } else { + eyre::bail!("Local bytecode doesn't match on-chain bytecode") } - - Ok(()) } } -#[warn(dead_code)] -#[derive(Debug, Serialize)] -pub enum CodeFormat { - SingleFile, - JsonInput, - Vyper, -} -impl CodeFormat { - fn as_str(&self) -> &'static str { - match self { - CodeFormat::SingleFile => "solidity-single-file", - CodeFormat::JsonInput => "solidity-standard-json-input", - CodeFormat::Vyper => "Vyper", - } +/// Given any solc [Version] return a [Version] with build metadata +/// +/// # Example +/// +/// ```ignore +/// use semver::{BuildMetadata, Version}; +/// let version = Version::new(1, 2, 3); +/// let version = ensure_solc_build_metadata(version).await?; +/// assert_ne!(version.build, BuildMetadata::EMPTY); +/// ``` +async fn ensure_solc_build_metadata(version: Version) -> Result { + if version.build != BuildMetadata::EMPTY { + Ok(version) + } else { + Ok(lookup_compiler_version(&version).await?) } } -#[derive(Debug, Serialize)] -pub struct OklinkVerifyRequest { - sourceCode: String, - contractaddress: String, - codeformat: String, - contractname: String, - compilerversion: String, - optimizationUsed:String, - runs: Option, - constructorArguments: Option, - evmversion:Option, - licenseType:Option, - libraryname:Option, - libraryaddress:Option, +fn create_query( + api_key: &'static str, + module: &'static str, + action: &'static str, + other: T, +) -> Query<'static, T> { + Query { + apikey: Some(Cow::Borrowed(api_key)), + module: Cow::Borrowed(module), + action: Cow::Borrowed(action), + other, + } } +// fn sanitize_response(&self, res: impl AsRef) -> Result> { +// let res = res.as_ref(); +// let res: ResponseData = serde_json::from_str(res).map_err(|error| { +// error!(target: "etherscan", ?res, "Failed to deserialize response: {}", error); +// if res == "Page not found" { +// EtherscanError::PageNotFound +// } else if is_blocked_by_cloudflare_response(res) { +// EtherscanError::BlockedByCloudflare +// } else if is_cloudflare_security_challenge(res) { +// EtherscanError::CloudFlareSecurityChallenge +// } else { +// EtherscanError::Serde { error, content: res.to_string() } +// } +// })?; -// #[derive(Debug, Deserialize)] -// pub struct OklinkVerificationResponse { -// result: Vec, +// match res { +// ResponseData::Error { result, message, status } => { +// if let Some(ref result) = result { +// if result.starts_with("Max rate limit reached") { +// return Err(EtherscanError::RateLimitExceeded); +// } else if result.to_lowercase() == "invalid api key" { +// return Err(EtherscanError::InvalidApiKey); +// } +// } +// Err(EtherscanError::ErrorResponse { status, message, result }) +// } +// ResponseData::Success(res) => Ok(res), +// } // } -#[derive(Debug, Deserialize)] -pub struct OklinkResponseElement { - status: String, - message: String, - result: Option, -} - #[cfg(test)] mod tests { use super::*; + use clap::Parser; + use foundry_common::fs; + use foundry_test_utils::forgetest_async; + use tempfile::tempdir; #[test] - fn test_check_addresses_url() { - let url = Url::from_str("https://server-verify.hashscan.io").unwrap(); - let url = url.join("check-by-addresses?addresses=0x1234&chainIds=1").unwrap(); + fn can_extract_oklink_verify_config() { + let temp = tempdir().unwrap(); + let root = temp.path(); + + let config = r#" + [profile.default] + + [oklink] + mumbai = { key = "dummykey", chain = 80001, url = "https://api-testnet.polygonscan.com/" } + "#; + + let toml_file = root.join(Config::FILE_NAME); + fs::write(toml_file, config).unwrap(); + + let args: VerifyArgs = VerifyArgs::parse_from([ + "foundry-cli", + "0xd8509bee9c9bf012282ad33aba0d87241baf5064", + "src/Counter.sol:Counter", + "--chain", + "mumbai", + "--root", + root.as_os_str().to_str().unwrap(), + ]); + + let config = args.load_config(); + + let oklink = OklinkVerificationProvider::default(); + let client = oklink + .client( + args.etherscan.chain.unwrap_or_default(), + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + ) + .unwrap(); + assert_eq!(client.oklink_api_url().as_str(), "https://api-testnet.polygonscan.com/"); + + assert!(format!("{client:?}").contains("dummykey")); + + let args: VerifyArgs = VerifyArgs::parse_from([ + "foundry-cli", + "0xd8509bee9c9bf012282ad33aba0d87241baf5064", + "src/Counter.sol:Counter", + "--chain", + "mumbai", + "--verifier-url", + "https://verifier-url.com/", + "--root", + root.as_os_str().to_str().unwrap(), + ]); + + let config = args.load_config(); + + let oklink = OklinkVerificationProvider::default(); + let client = oklink + .client( + args.etherscan.chain.unwrap_or_default(), + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + ) + .unwrap(); + assert_eq!(client.oklink_api_url().as_str(), "https://verifier-url.com/"); + assert!(format!("{client:?}").contains("dummykey")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn fails_on_disabled_cache_and_missing_info() { + let temp = tempdir().unwrap(); + let root = temp.path(); + let root_path = root.as_os_str().to_str().unwrap(); + + let config = r" + [profile.default] + cache = false + "; + + let toml_file = root.join(Config::FILE_NAME); + fs::write(toml_file, config).unwrap(); + + let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064"; + let contract_name = "Counter"; + let src_dir = "src"; + fs::create_dir_all(root.join(src_dir)).unwrap(); + let contract_path = format!("{src_dir}/Counter.sol"); + fs::write(root.join(&contract_path), "").unwrap(); + + let mut oklink = OklinkVerificationProvider::default(); + + // No compiler argument + let args = VerifyArgs::parse_from([ + "foundry-cli", + address, + &format!("{contract_path}:{contract_name}"), + "--root", + root_path, + ]); + + let result = oklink.preflight_check(args).await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml" + ); + + // No contract path + let args = + VerifyArgs::parse_from(["foundry-cli", address, contract_name, "--root", root_path]); + + let result = oklink.preflight_check(args).await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "If cache is disabled, contract info must be provided in the format :" + ); + + // Constructor args path + let args = VerifyArgs::parse_from([ + "foundry-cli", + address, + &format!("{contract_path}:{contract_name}"), + "--constructor-args-path", + ".", + "--compiler-version", + "0.8.15", + "--root", + root_path, + ]); + + let result = oklink.preflight_check(args).await; + assert!(result.is_err()); assert_eq!( - url.as_str(), - "https://server-verify.hashscan.io/check-by-addresses?addresses=0x1234&chainIds=1" + result.unwrap_err().to_string(), + "Cache must be enabled in order to use the `--constructor-args-path` option", ); } + + forgetest_async!(respects_path_for_duplicate, |prj, cmd| { + prj.add_source("Counter1", "contract Counter {}").unwrap(); + prj.add_source("Counter2", "contract Counter {}").unwrap(); + + cmd.args(["build", "--force"]).ensure_execute_success().unwrap(); + + let args = VerifyArgs::parse_from([ + "foundry-cli", + "0x0000000000000000000000000000000000000000", + "src/Counter1.sol:Counter", + "--root", + &prj.root().to_string_lossy(), + ]); + + let mut oklink = OklinkVerificationProvider::default(); + oklink.preflight_check(args).await.unwrap(); + }); } diff --git a/crates/verify/src/oklink.rs.back b/crates/verify/src/oklink.rs.back new file mode 100644 index 000000000000..82bb2ec0b433 --- /dev/null +++ b/crates/verify/src/oklink.rs.back @@ -0,0 +1,321 @@ +use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; +use async_trait::async_trait; +use eyre::{Ok, Result}; +use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; +use foundry_common::{evm, fs, retry::Retry}; +use foundry_block_explorers::{ + Client, +}; +use foundry_compilers::ConfigurableContractArtifact; +use futures::FutureExt; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +pub static OKLINK_URL: &str = "https://www.oklink.com/api"; +/// The type that can verify a contract on `oklink` +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct OklinkVerificationProvider; + +#[async_trait] +impl VerificationProvider for OklinkVerificationProvider { + async fn preflight_check(&mut self, args: VerifyArgs) -> Result<()> { + let _ = self.prepare_request(&args)?; + Ok(()) + } + + async fn verify(&mut self, args: VerifyArgs) -> Result<()> { + let (body, api_key) = self.prepare_request(&args)?; + + debug!("submitting verification request {:?}", serde_json::to_string(&body)?); + debug!("api key {:?}", api_key); + + let client = reqwest::Client::new(); + + let retry: Retry = args.retry.into(); + let resp = retry + .run_async(|| { + async { + println!( + "\nSubmitting verification for [{}] {:?}.", + args.contract.name, + args.address.to_string() + ); + let response = client + .post(args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL)) + .header("Content-Type", "application/json") + .header("Ok-Access-Key", &api_key) + .body(serde_json::to_string(&body)?) + .send() + .await?; + debug!("response {:?}", response); + let status = response.status(); + if !status.is_success() { + let error: serde_json::Value = response.json().await?; + eyre::bail!( + "Oklink verification request for address ({}) failed with status code {status}\nDetails: {error:#}", + args.address, + ); + } + + let text = response.text().await?; + debug!("text {:?}", text); + Ok(Some(serde_json::from_str::(&text)?)) + } + .boxed() + }) + .await?; + + self.process_oklink_response(resp) + } + + async fn check(&self, args: VerifyCheckArgs) -> Result<()> { + let api_key = match args.etherscan.key.clone() { + None => eyre::bail!("OKLINK API KEY is not set"), + Some(key) => key + }; + debug!("api key {:?}", api_key); + let retry: Retry = args.retry.into(); + let resp = retry + .run_async(|| { + async { + let url = Url::from_str( + args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), + )?; + let query = format!( + "?module=contract&action=getabi&address={}", + args.id + ); + let url = url.join(&query)?; + let client = reqwest::Client::new(); + debug!("url {:?}", url); + let response = client.get(url).header("Ok-Access-Key", &api_key).send().await?; + debug!("response: {:?}", response); + + if !response.status().is_success() { + eyre::bail!( + "Failed to request verification status with status code {}", + response.status() + ); + }; + let text = response.text().await?; + debug!("text {:?}", text); + Ok(Some(serde_json::from_str::(&text)?)) + + } + .boxed() + }) + .await?; + + self.process_oklink_response(resp) + } +} + +impl OklinkVerificationProvider { + /// Configures the API request to the oklink API using the given [`VerifyArgs`]. + fn prepare_request(&self, args: &VerifyArgs) -> Result<(OklinkVerifyRequest, String)> { + let mut config = args.try_load_config_emit_warnings()?; + config.libraries.extend(args.libraries.clone()); + let api_key = match args.etherscan.key.clone() { + None => eyre::bail!("OKLINK API KEY is not set"), + Some(key) => key + }; + let project = config.project()?; + + if !config.cache { + eyre::bail!("Cache is required for oklink verification.") + } + + let cache = project.read_cache_file()?; + let (path, entry) = get_cached_entry_by_name(&cache, &args.contract.name)?; + + + + // the metadata is included in the contract's artifact file + let artifact_path = entry + .find_artifact_path(&args.contract.name) + .ok_or_else(|| eyre::eyre!("No artifact found for contract {}", args.contract.name))?; + + let artifact: ConfigurableContractArtifact = fs::read_json_file(artifact_path)?; + let compiler_version; + let optimization_used; + let runs; + + let evm_version; + let license_type; + let mut library_name:Vec = vec![]; + let mut library_address: Vec = vec![]; + + if let Some(metadata) = artifact.metadata { + compiler_version = metadata.compiler.version.clone(); + let settings = metadata.settings; + evm_version = match settings.evm_version { + Some(version) => Some(version.as_str().to_string()), + None => None, + }; + optimization_used = match settings.optimizer.enabled { + Some(enabled) => match enabled { + true => "1".to_string(), + false => "0".to_string(), + }, + None => "0".to_string(), + }; + runs = match settings.optimizer.runs { + Some(runs) => Some(runs.to_string()), + None => None, + }; + println!("{:?}",args.contract.path); + let contract_path = args.contract.path.clone().map_or(path.clone(), PathBuf::from); + let contract_path = contract_path.strip_prefix(project.root())?.to_string_lossy().to_string(); + println!("contract Path {:?}", contract_path); + // let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); + license_type = match metadata.sources.inner.get(&contract_path) { + Some(metadata_source) => metadata_source.license.clone(), + None => None, + }; + for (name, address) in settings.libraries.into_iter() { + library_name.push(name.clone()); + library_address.push(address.clone()); + } + + } else { + eyre::bail!( + r#"No metadata found in artifact `{}` for contract {}. +Oklink requires contract metadata for verification. +metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.toml`"#, + artifact_path.display(), + args.contract.name + ) + } + let library_name = match library_name.len() { + 0 => None, + _ => Some(library_name.join(",")) + }; + let library_address = match library_address.len() { + 0 => None, + _ => Some(library_address.join(",")) + }; + + let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); + let source_code = fs::read_to_string(&contract_path)?; + + + let req = OklinkVerifyRequest { + sourceCode: source_code, + contractaddress: args.address.clone().to_string(), + // currently only single file supported + codeformat: CodeFormat::SingleFile.as_str().to_string(), + contractname: args.contract.name.clone(), + compilerversion: compiler_version, + optimizationUsed: optimization_used, + runs: runs, + constructorArguments: args.constructor_args.clone(), + evmversion: evm_version, + licenseType: license_type, + libraryname: library_name, + libraryaddress: library_address, + }; + + Ok((req, api_key)) + } + + fn process_oklink_response( + &self, + response: Option, + ) -> Result<()> { + let Some(response) = response else { return Ok(()) }; + match response.status.as_str() { + "1" => match response.message.as_str() { + "OK" => { + if let Some(result) = &response.result { + println!("Contract source code already verified. the result is {result}"); + } else { + println!("Contract successfully verified"); + } + } + "NOTOK" => { + if let Some(result) = &response.result { + println!("Contract source code verified fail. the result is {result}") + } else { + println!("Contract verified fail") + } + + } + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + } + "0" => match response.message.as_str() { + "NOTOK" => { + if let Some(result) = &response.result { + println!("Contract source code verified fail. the result is '{result}'") + } else { + println!("Contract verified fail") + } + } + s => eyre::bail!("Unknown message from oklink. message: {s:?}"), + } + s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), + } + + Ok(()) + } +} + +#[warn(dead_code)] +#[derive(Debug, Serialize)] +pub enum CodeFormat { + SingleFile, + JsonInput, + Vyper, +} +impl CodeFormat { + fn as_str(&self) -> &'static str { + match self { + CodeFormat::SingleFile => "solidity-single-file", + CodeFormat::JsonInput => "solidity-standard-json-input", + CodeFormat::Vyper => "Vyper", + } + } +} +#[derive(Debug, Serialize)] +pub struct OklinkVerifyRequest { + sourceCode: String, + contractaddress: String, + codeformat: String, + contractname: String, + compilerversion: String, + optimizationUsed:String, + runs: Option, + constructorArguments: Option, + evmversion:Option, + licenseType:Option, + libraryname:Option, + libraryaddress:Option, +} + +// #[derive(Debug, Deserialize)] +// pub struct OklinkVerificationResponse { +// result: Vec, +// } + +#[derive(Debug, Deserialize)] +pub struct OklinkResponseElement { + status: String, + message: String, + result: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_check_addresses_url() { + let url = Url::from_str("https://server-verify.hashscan.io").unwrap(); + let url = url.join("check-by-addresses?addresses=0x1234&chainIds=1").unwrap(); + assert_eq!( + url.as_str(), + "https://server-verify.hashscan.io/check-by-addresses?addresses=0x1234&chainIds=1" + ); + } +} From 5c6c3b987253f8d7bc948f4d8289140cb3e083ac Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Sun, 7 Apr 2024 12:56:36 +0800 Subject: [PATCH 09/16] update --- crates/verify/src/oklink.rs | 407 ++++++++----------------------- crates/verify/src/oklink.rs.back | 321 ------------------------ 2 files changed, 98 insertions(+), 630 deletions(-) delete mode 100644 crates/verify/src/oklink.rs.back diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 71f1cfec85d6..8399ae7d89ab 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -1,13 +1,9 @@ use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; -use crate::{retry::RETRY_CHECK_ON_VERIFY}; use alloy_json_abi::Function; use ethers_providers::Middleware; use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ - errors::EtherscanError, - utils::lookup_compiler_version, - verify::{CodeFormat, VerifyContract}, - Client, + errors::EtherscanError, utils::lookup_compiler_version, verify::{CodeFormat, VerifyContract}, Client, Response, ResponseData }; use foundry_cli::utils::{self, get_cached_entry_by_name, read_constructor_args_file, LoadConfig}; use foundry_common::{abi::encode_function_args, retry::Retry, types::ToEthers}; @@ -23,9 +19,9 @@ use futures::FutureExt; use once_cell::sync::Lazy; use regex::Regex; use semver::{BuildMetadata, Version}; -use serde::Serialize; +use serde::{de::DeserializeOwned, Serialize}; use std::{ - borrow::Cow, fmt::Debug, path::{Path, PathBuf} + collections::HashMap, fmt::Debug, path::{Path, PathBuf} }; pub static OKLINK_URL: &str = "https://www.oklink.com/"; @@ -40,11 +36,11 @@ pub struct OklinkVerificationProvider { } #[derive(Clone, Debug, Serialize)] -struct Query<'a, T: Serialize> { +struct Query { #[serde(skip_serializing_if = "Option::is_none")] - apikey: Option>, - module: Cow<'a, str>, - action: Cow<'a, str>, + apikey: Option, + module: String, + action: String, #[serde(flatten)] other: T, } @@ -57,25 +53,10 @@ impl VerificationProvider for OklinkVerificationProvider { } async fn verify(&mut self, args: VerifyArgs) -> Result<()> { - let (oklink, verify_args) = self.prepare_request(&args).await?; - - // if !args.skip_is_verified_check && - // self.is_contract_verified(&oklink, &verify_args).await? - // { - // println!( - // "\nContract [{}] {:?} is already verified. Skipping verification.", - // verify_args.contract_name, - // verify_args.address.to_checksum(None) - // ); - - // return Ok(()) - // } - debug!("client {:?}", oklink); + let (_, verify_args) = self.prepare_request(&args).await?; trace!(target: "forge::verify", ?verify_args, "submitting verification request"); let client = reqwest::Client::new(); - - let retry: Retry = args.retry.into(); let resp = retry .run_async(|| async { @@ -83,7 +64,8 @@ impl VerificationProvider for OklinkVerificationProvider { "\nSubmitting verification for [{}] {}.", verify_args.contract_name, verify_args.address ); - let body = create_query("24b1a1b1-701f-4653-9d88-47d4f5c12512","contract", "verifysourcecode", &verify_args); + let api_key: Option = args.etherscan.key().clone(); + let body = create_query(api_key,"contract".to_string(), "verifysourcecode".to_string(), &verify_args); debug!("body {:?}", body); let resp = client .post(args.verifier.verifier_url.clone().unwrap()) @@ -94,66 +76,52 @@ impl VerificationProvider for OklinkVerificationProvider { .send() .await? .text() - .await?; - - // let resp = oklink - // .submit_contract_verification(&verify_args) - // .await - // .wrap_err_with(|| { - // // valid json - // let args = serde_json::to_string(&verify_args).unwrap(); - // error!(target: "forge::verify", ?args, "Failed to submit verification"); - // format!("Failed to submit contract verification, payload:\n{args}") - // })?; + .await + .wrap_err_with(|| { + // valid json + let args = serde_json::to_string(&verify_args).unwrap(); + error!(target: "forge::verify", ?args, "Failed to submit verification"); + format!("Failed to submit contract verification, payload:\n{args}") + })?; debug!(target: "forge::verify", ?resp, "Received verification response"); + let resp = sanitize_response::(&resp)?; + + if resp.status == "0" { + if resp.result == "Contract source code already verified" + // specific for blockscout response + || resp.result == "Smart-contract already verified." + { + return Ok(None) + } - // if resp.status == "0" { - // if resp.result == "Contract source code already verified" - // // specific for blockscout response - // || resp.result == "Smart-contract already verified." - // { - // return Ok(None) - // } - - // if resp.result.starts_with("Unable to locate ContractCode at") { - // warn!("{}", resp.result); - // return Err(eyre!("Oklink could not detect the deployment.")) - // } - - // warn!("Failed verify submission: {:?}", resp); - // eprintln!( - // "Encountered an error verifying this contract:\nResponse: `{}`\nDetails: `{}`", - // resp.message, resp.result - // ); - // std::process::exit(1); - // } + if resp.result.starts_with("Unable to locate ContractCode at") { + warn!("{}", resp.result); + return Err(eyre!("Oklink could not detect the deployment.")) + } + + warn!("Failed verify submission: {:?}", resp); + eprintln!( + "Encountered an error verifying this contract:\nResponse: `{}`\nDetails: `{}`", + resp.message, resp.result + ); + std::process::exit(1); + } Ok(Some(resp)) }) .await?; - // if let Some(resp) = resp { - // println!( - // "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}", - // resp.message, - // resp.result, - // oklink.address_url(args.address) - // ); - - // if args.watch { - // let check_args = VerifyCheckArgs { - // id: resp.result, - // etherscan: args.etherscan, - // retry: RETRY_CHECK_ON_VERIFY, - // verifier: args.verifier, - // }; - // // return check_args.run().await - // return self.check(check_args).await - // } - // } else { - // println!("Contract source code already verified"); - // } + if let Some(resp) = resp { + println!( + "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}", + resp.message, + resp.result, + OKLINK_URL + ); + } else { + println!("Contract source code already verified"); + } Ok(()) } @@ -161,23 +129,30 @@ impl VerificationProvider for OklinkVerificationProvider { /// Executes the command to check verification status on Oklink async fn check(&self, args: VerifyCheckArgs) -> Result<()> { - let config = args.try_load_config_emit_warnings()?; - let oklink = self.client( - args.etherscan.chain.unwrap_or_default(), - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - &config, - )?; + let retry: Retry = args.retry.into(); + let client = reqwest::Client::new(); + let api_key: Option = args.etherscan.key().clone(); + let body = create_query(api_key,"contract".to_string(), "checkverifystatus".to_string(), HashMap::from([("guid", args.id.clone())])); + debug!("body {:?}", body); + retry .run_async(|| { async { - let resp = oklink - .check_contract_verification_status(args.id.clone()) - .await - .wrap_err("Failed to request verification status")?; - - trace!(target: "forge::verify", ?resp, "Received verification response"); + let resp = client + .post(args.verifier.verifier_url.clone().unwrap()) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Ok-Access-Key", &args.etherscan.key().unwrap()) + .header("x-apiKey", &args.etherscan.key().unwrap()) + .form(&body) + .send() + .await? + .text() + .await + .wrap_err("Failed to request verification status")?; + + debug!(target: "forge::verify", ?resp, "Received verification response"); + let resp = sanitize_response::(&resp)?; eprintln!( "Contract verification status:\nResponse: `{}`\nDetails: `{}`", @@ -297,23 +272,6 @@ impl OklinkVerificationProvider { Ok((oklink, verify_args)) } - /// Queries the oklink API to verify if the contract is already verified. - async fn is_contract_verified( - &self, - oklink: &Client, - verify_contract: &VerifyContract, - ) -> Result { - let check = oklink.contract_abi(verify_contract.address).await; - - if let Err(err) = check { - match err { - EtherscanError::ContractCodeNotVerified(_) => return Ok(false), - error => return Err(error.into()), - } - } - - Ok(true) - } /// Create an oklink client pub(crate) fn client( @@ -584,209 +542,40 @@ async fn ensure_solc_build_metadata(version: Version) -> Result { } } fn create_query( - api_key: &'static str, - module: &'static str, - action: &'static str, + api_key: Option, + module: String, + action: String, other: T, -) -> Query<'static, T> { +) -> Query { Query { - apikey: Some(Cow::Borrowed(api_key)), - module: Cow::Borrowed(module), - action: Cow::Borrowed(action), + apikey: api_key, + module: module, + action: action, other, } } -// fn sanitize_response(&self, res: impl AsRef) -> Result> { -// let res = res.as_ref(); -// let res: ResponseData = serde_json::from_str(res).map_err(|error| { -// error!(target: "etherscan", ?res, "Failed to deserialize response: {}", error); -// if res == "Page not found" { -// EtherscanError::PageNotFound -// } else if is_blocked_by_cloudflare_response(res) { -// EtherscanError::BlockedByCloudflare -// } else if is_cloudflare_security_challenge(res) { -// EtherscanError::CloudFlareSecurityChallenge -// } else { -// EtherscanError::Serde { error, content: res.to_string() } -// } -// })?; - -// match res { -// ResponseData::Error { result, message, status } => { -// if let Some(ref result) = result { -// if result.starts_with("Max rate limit reached") { -// return Err(EtherscanError::RateLimitExceeded); -// } else if result.to_lowercase() == "invalid api key" { -// return Err(EtherscanError::InvalidApiKey); -// } -// } -// Err(EtherscanError::ErrorResponse { status, message, result }) -// } -// ResponseData::Success(res) => Ok(res), -// } -// } - -#[cfg(test)] -mod tests { - use super::*; - use clap::Parser; - use foundry_common::fs; - use foundry_test_utils::forgetest_async; - use tempfile::tempdir; - - #[test] - fn can_extract_oklink_verify_config() { - let temp = tempdir().unwrap(); - let root = temp.path(); - - let config = r#" - [profile.default] - - [oklink] - mumbai = { key = "dummykey", chain = 80001, url = "https://api-testnet.polygonscan.com/" } - "#; - - let toml_file = root.join(Config::FILE_NAME); - fs::write(toml_file, config).unwrap(); - - let args: VerifyArgs = VerifyArgs::parse_from([ - "foundry-cli", - "0xd8509bee9c9bf012282ad33aba0d87241baf5064", - "src/Counter.sol:Counter", - "--chain", - "mumbai", - "--root", - root.as_os_str().to_str().unwrap(), - ]); - - let config = args.load_config(); - - let oklink = OklinkVerificationProvider::default(); - let client = oklink - .client( - args.etherscan.chain.unwrap_or_default(), - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - &config, - ) - .unwrap(); - assert_eq!(client.oklink_api_url().as_str(), "https://api-testnet.polygonscan.com/"); - - assert!(format!("{client:?}").contains("dummykey")); - - let args: VerifyArgs = VerifyArgs::parse_from([ - "foundry-cli", - "0xd8509bee9c9bf012282ad33aba0d87241baf5064", - "src/Counter.sol:Counter", - "--chain", - "mumbai", - "--verifier-url", - "https://verifier-url.com/", - "--root", - root.as_os_str().to_str().unwrap(), - ]); - - let config = args.load_config(); - - let oklink = OklinkVerificationProvider::default(); - let client = oklink - .client( - args.etherscan.chain.unwrap_or_default(), - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - &config, - ) - .unwrap(); - assert_eq!(client.oklink_api_url().as_str(), "https://verifier-url.com/"); - assert!(format!("{client:?}").contains("dummykey")); - } - - #[tokio::test(flavor = "multi_thread")] - async fn fails_on_disabled_cache_and_missing_info() { - let temp = tempdir().unwrap(); - let root = temp.path(); - let root_path = root.as_os_str().to_str().unwrap(); - - let config = r" - [profile.default] - cache = false - "; - - let toml_file = root.join(Config::FILE_NAME); - fs::write(toml_file, config).unwrap(); - - let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064"; - let contract_name = "Counter"; - let src_dir = "src"; - fs::create_dir_all(root.join(src_dir)).unwrap(); - let contract_path = format!("{src_dir}/Counter.sol"); - fs::write(root.join(&contract_path), "").unwrap(); - - let mut oklink = OklinkVerificationProvider::default(); - - // No compiler argument - let args = VerifyArgs::parse_from([ - "foundry-cli", - address, - &format!("{contract_path}:{contract_name}"), - "--root", - root_path, - ]); - - let result = oklink.preflight_check(args).await; - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml" - ); - - // No contract path - let args = - VerifyArgs::parse_from(["foundry-cli", address, contract_name, "--root", root_path]); - - let result = oklink.preflight_check(args).await; - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "If cache is disabled, contract info must be provided in the format :" - ); - - // Constructor args path - let args = VerifyArgs::parse_from([ - "foundry-cli", - address, - &format!("{contract_path}:{contract_name}"), - "--constructor-args-path", - ".", - "--compiler-version", - "0.8.15", - "--root", - root_path, - ]); - - let result = oklink.preflight_check(args).await; - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "Cache must be enabled in order to use the `--constructor-args-path` option", - ); +fn sanitize_response(res: impl AsRef) -> Result> { + let res = res.as_ref(); + let res: ResponseData = serde_json::from_str(res).map_err(|error| { + error!(target: "etherscan", ?res, "Failed to deserialize response: {}", error); + if res == "Page not found" { + EtherscanError::PageNotFound + } else { + EtherscanError::Serde { error, content: res.to_string() } + } + })?; + + match res { + ResponseData::Error { result, message, status } => { + if let Some(ref result) = result { + if result.starts_with("Max rate limit reached") { + return Err(EtherscanError::RateLimitExceeded.into()); + } else if result.to_lowercase() == "invalid api key" { + return Err(EtherscanError::InvalidApiKey.into()); + } + } + Err(EtherscanError::ErrorResponse { status, message, result }.into()) + } + ResponseData::Success(res) => Ok(res), } - - forgetest_async!(respects_path_for_duplicate, |prj, cmd| { - prj.add_source("Counter1", "contract Counter {}").unwrap(); - prj.add_source("Counter2", "contract Counter {}").unwrap(); - - cmd.args(["build", "--force"]).ensure_execute_success().unwrap(); - - let args = VerifyArgs::parse_from([ - "foundry-cli", - "0x0000000000000000000000000000000000000000", - "src/Counter1.sol:Counter", - "--root", - &prj.root().to_string_lossy(), - ]); - - let mut oklink = OklinkVerificationProvider::default(); - oklink.preflight_check(args).await.unwrap(); - }); } diff --git a/crates/verify/src/oklink.rs.back b/crates/verify/src/oklink.rs.back deleted file mode 100644 index 82bb2ec0b433..000000000000 --- a/crates/verify/src/oklink.rs.back +++ /dev/null @@ -1,321 +0,0 @@ -use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; -use async_trait::async_trait; -use eyre::{Ok, Result}; -use foundry_cli::utils::{get_cached_entry_by_name, LoadConfig}; -use foundry_common::{evm, fs, retry::Retry}; -use foundry_block_explorers::{ - Client, -}; -use foundry_compilers::ConfigurableContractArtifact; -use futures::FutureExt; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::PathBuf, str::FromStr}; - -pub static OKLINK_URL: &str = "https://www.oklink.com/api"; -/// The type that can verify a contract on `oklink` -#[derive(Clone, Debug, Default)] -#[non_exhaustive] -pub struct OklinkVerificationProvider; - -#[async_trait] -impl VerificationProvider for OklinkVerificationProvider { - async fn preflight_check(&mut self, args: VerifyArgs) -> Result<()> { - let _ = self.prepare_request(&args)?; - Ok(()) - } - - async fn verify(&mut self, args: VerifyArgs) -> Result<()> { - let (body, api_key) = self.prepare_request(&args)?; - - debug!("submitting verification request {:?}", serde_json::to_string(&body)?); - debug!("api key {:?}", api_key); - - let client = reqwest::Client::new(); - - let retry: Retry = args.retry.into(); - let resp = retry - .run_async(|| { - async { - println!( - "\nSubmitting verification for [{}] {:?}.", - args.contract.name, - args.address.to_string() - ); - let response = client - .post(args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL)) - .header("Content-Type", "application/json") - .header("Ok-Access-Key", &api_key) - .body(serde_json::to_string(&body)?) - .send() - .await?; - debug!("response {:?}", response); - let status = response.status(); - if !status.is_success() { - let error: serde_json::Value = response.json().await?; - eyre::bail!( - "Oklink verification request for address ({}) failed with status code {status}\nDetails: {error:#}", - args.address, - ); - } - - let text = response.text().await?; - debug!("text {:?}", text); - Ok(Some(serde_json::from_str::(&text)?)) - } - .boxed() - }) - .await?; - - self.process_oklink_response(resp) - } - - async fn check(&self, args: VerifyCheckArgs) -> Result<()> { - let api_key = match args.etherscan.key.clone() { - None => eyre::bail!("OKLINK API KEY is not set"), - Some(key) => key - }; - debug!("api key {:?}", api_key); - let retry: Retry = args.retry.into(); - let resp = retry - .run_async(|| { - async { - let url = Url::from_str( - args.verifier.verifier_url.as_deref().unwrap_or(OKLINK_URL), - )?; - let query = format!( - "?module=contract&action=getabi&address={}", - args.id - ); - let url = url.join(&query)?; - let client = reqwest::Client::new(); - debug!("url {:?}", url); - let response = client.get(url).header("Ok-Access-Key", &api_key).send().await?; - debug!("response: {:?}", response); - - if !response.status().is_success() { - eyre::bail!( - "Failed to request verification status with status code {}", - response.status() - ); - }; - let text = response.text().await?; - debug!("text {:?}", text); - Ok(Some(serde_json::from_str::(&text)?)) - - } - .boxed() - }) - .await?; - - self.process_oklink_response(resp) - } -} - -impl OklinkVerificationProvider { - /// Configures the API request to the oklink API using the given [`VerifyArgs`]. - fn prepare_request(&self, args: &VerifyArgs) -> Result<(OklinkVerifyRequest, String)> { - let mut config = args.try_load_config_emit_warnings()?; - config.libraries.extend(args.libraries.clone()); - let api_key = match args.etherscan.key.clone() { - None => eyre::bail!("OKLINK API KEY is not set"), - Some(key) => key - }; - let project = config.project()?; - - if !config.cache { - eyre::bail!("Cache is required for oklink verification.") - } - - let cache = project.read_cache_file()?; - let (path, entry) = get_cached_entry_by_name(&cache, &args.contract.name)?; - - - - // the metadata is included in the contract's artifact file - let artifact_path = entry - .find_artifact_path(&args.contract.name) - .ok_or_else(|| eyre::eyre!("No artifact found for contract {}", args.contract.name))?; - - let artifact: ConfigurableContractArtifact = fs::read_json_file(artifact_path)?; - let compiler_version; - let optimization_used; - let runs; - - let evm_version; - let license_type; - let mut library_name:Vec = vec![]; - let mut library_address: Vec = vec![]; - - if let Some(metadata) = artifact.metadata { - compiler_version = metadata.compiler.version.clone(); - let settings = metadata.settings; - evm_version = match settings.evm_version { - Some(version) => Some(version.as_str().to_string()), - None => None, - }; - optimization_used = match settings.optimizer.enabled { - Some(enabled) => match enabled { - true => "1".to_string(), - false => "0".to_string(), - }, - None => "0".to_string(), - }; - runs = match settings.optimizer.runs { - Some(runs) => Some(runs.to_string()), - None => None, - }; - println!("{:?}",args.contract.path); - let contract_path = args.contract.path.clone().map_or(path.clone(), PathBuf::from); - let contract_path = contract_path.strip_prefix(project.root())?.to_string_lossy().to_string(); - println!("contract Path {:?}", contract_path); - // let filename = contract_path.file_name().unwrap().to_string_lossy().to_string(); - license_type = match metadata.sources.inner.get(&contract_path) { - Some(metadata_source) => metadata_source.license.clone(), - None => None, - }; - for (name, address) in settings.libraries.into_iter() { - library_name.push(name.clone()); - library_address.push(address.clone()); - } - - } else { - eyre::bail!( - r#"No metadata found in artifact `{}` for contract {}. -Oklink requires contract metadata for verification. -metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.toml`"#, - artifact_path.display(), - args.contract.name - ) - } - let library_name = match library_name.len() { - 0 => None, - _ => Some(library_name.join(",")) - }; - let library_address = match library_address.len() { - 0 => None, - _ => Some(library_address.join(",")) - }; - - let contract_path = args.contract.path.clone().map_or(path, PathBuf::from); - let source_code = fs::read_to_string(&contract_path)?; - - - let req = OklinkVerifyRequest { - sourceCode: source_code, - contractaddress: args.address.clone().to_string(), - // currently only single file supported - codeformat: CodeFormat::SingleFile.as_str().to_string(), - contractname: args.contract.name.clone(), - compilerversion: compiler_version, - optimizationUsed: optimization_used, - runs: runs, - constructorArguments: args.constructor_args.clone(), - evmversion: evm_version, - licenseType: license_type, - libraryname: library_name, - libraryaddress: library_address, - }; - - Ok((req, api_key)) - } - - fn process_oklink_response( - &self, - response: Option, - ) -> Result<()> { - let Some(response) = response else { return Ok(()) }; - match response.status.as_str() { - "1" => match response.message.as_str() { - "OK" => { - if let Some(result) = &response.result { - println!("Contract source code already verified. the result is {result}"); - } else { - println!("Contract successfully verified"); - } - } - "NOTOK" => { - if let Some(result) = &response.result { - println!("Contract source code verified fail. the result is {result}") - } else { - println!("Contract verified fail") - } - - } - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), - } - "0" => match response.message.as_str() { - "NOTOK" => { - if let Some(result) = &response.result { - println!("Contract source code verified fail. the result is '{result}'") - } else { - println!("Contract verified fail") - } - } - s => eyre::bail!("Unknown message from oklink. message: {s:?}"), - } - s => eyre::bail!("Unknown status from oklink. Status: {s:?}"), - } - - Ok(()) - } -} - -#[warn(dead_code)] -#[derive(Debug, Serialize)] -pub enum CodeFormat { - SingleFile, - JsonInput, - Vyper, -} -impl CodeFormat { - fn as_str(&self) -> &'static str { - match self { - CodeFormat::SingleFile => "solidity-single-file", - CodeFormat::JsonInput => "solidity-standard-json-input", - CodeFormat::Vyper => "Vyper", - } - } -} -#[derive(Debug, Serialize)] -pub struct OklinkVerifyRequest { - sourceCode: String, - contractaddress: String, - codeformat: String, - contractname: String, - compilerversion: String, - optimizationUsed:String, - runs: Option, - constructorArguments: Option, - evmversion:Option, - licenseType:Option, - libraryname:Option, - libraryaddress:Option, -} - -// #[derive(Debug, Deserialize)] -// pub struct OklinkVerificationResponse { -// result: Vec, -// } - -#[derive(Debug, Deserialize)] -pub struct OklinkResponseElement { - status: String, - message: String, - result: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_check_addresses_url() { - let url = Url::from_str("https://server-verify.hashscan.io").unwrap(); - let url = url.join("check-by-addresses?addresses=0x1234&chainIds=1").unwrap(); - assert_eq!( - url.as_str(), - "https://server-verify.hashscan.io/check-by-addresses?addresses=0x1234&chainIds=1" - ); - } -} From 01d4b87389b559774672b751870b5301353cf823 Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Sun, 7 Apr 2024 13:02:53 +0800 Subject: [PATCH 10/16] fmt --- crates/verify/src/oklink.rs | 73 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 8399ae7d89ab..b8117324ab83 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -3,7 +3,10 @@ use alloy_json_abi::Function; use ethers_providers::Middleware; use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ - errors::EtherscanError, utils::lookup_compiler_version, verify::{CodeFormat, VerifyContract}, Client, Response, ResponseData + errors::EtherscanError, + utils::lookup_compiler_version, + verify::{CodeFormat, VerifyContract}, + Client, Response, ResponseData, }; use foundry_cli::utils::{self, get_cached_entry_by_name, read_constructor_args_file, LoadConfig}; use foundry_common::{abi::encode_function_args, retry::Retry, types::ToEthers}; @@ -21,7 +24,9 @@ use regex::Regex; use semver::{BuildMetadata, Version}; use serde::{de::DeserializeOwned, Serialize}; use std::{ - collections::HashMap, fmt::Debug, path::{Path, PathBuf} + collections::HashMap, + fmt::Debug, + path::{Path, PathBuf}, }; pub static OKLINK_URL: &str = "https://www.oklink.com/"; @@ -115,9 +120,7 @@ impl VerificationProvider for OklinkVerificationProvider { if let Some(resp) = resp { println!( "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}", - resp.message, - resp.result, - OKLINK_URL + resp.message, resp.result, OKLINK_URL ); } else { println!("Contract source code already verified"); @@ -125,31 +128,34 @@ impl VerificationProvider for OklinkVerificationProvider { Ok(()) } - /// Executes the command to check verification status on Oklink async fn check(&self, args: VerifyCheckArgs) -> Result<()> { - let retry: Retry = args.retry.into(); let client = reqwest::Client::new(); let api_key: Option = args.etherscan.key().clone(); - let body = create_query(api_key,"contract".to_string(), "checkverifystatus".to_string(), HashMap::from([("guid", args.id.clone())])); + let body = create_query( + api_key, + "contract".to_string(), + "checkverifystatus".to_string(), + HashMap::from([("guid", args.id.clone())]), + ); debug!("body {:?}", body); retry .run_async(|| { async { let resp = client - .post(args.verifier.verifier_url.clone().unwrap()) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Ok-Access-Key", &args.etherscan.key().unwrap()) - .header("x-apiKey", &args.etherscan.key().unwrap()) - .form(&body) - .send() - .await? - .text() - .await - .wrap_err("Failed to request verification status")?; + .post(args.verifier.verifier_url.clone().unwrap()) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Ok-Access-Key", &args.etherscan.key().unwrap()) + .header("x-apiKey", &args.etherscan.key().unwrap()) + .form(&body) + .send() + .await? + .text() + .await + .wrap_err("Failed to request verification status")?; debug!(target: "forge::verify", ?resp, "Received verification response"); let resp = sanitize_response::(&resp)?; @@ -160,16 +166,16 @@ impl VerificationProvider for OklinkVerificationProvider { ); if resp.result == "Pending in queue" { - return Err(eyre!("Verification is still pending...",)) + return Err(eyre!("Verification is still pending...",)); } if resp.result == "Unable to verify" { - return Err(eyre!("Unable to verify.",)) + return Err(eyre!("Unable to verify.",)); } if resp.result == "Already Verified" { println!("Contract source code already verified"); - return Ok(()) + return Ok(()); } if resp.status == "0" { @@ -191,7 +197,6 @@ impl VerificationProvider for OklinkVerificationProvider { } impl OklinkVerificationProvider { - fn source( &self, args: &VerifyArgs, @@ -236,7 +241,7 @@ impl OklinkVerificationProvider { contract: &ContractInfo, ) -> Result<&(PathBuf, CacheEntry, CompactContract)> { if let Some(ref entry) = self.cached_entry { - return Ok(entry) + return Ok(entry); } let cache = project.read_cache_file()?; @@ -272,7 +277,6 @@ impl OklinkVerificationProvider { Ok((oklink, verify_args)) } - /// Create an oklink client pub(crate) fn client( &self, @@ -281,7 +285,6 @@ impl OklinkVerificationProvider { oklink_key: Option<&str>, config: &Config, ) -> Result { - let oklink_key = oklink_key.unwrap(); let mut builder = Client::builder(); @@ -294,10 +297,7 @@ impl OklinkVerificationProvider { }; debug!("{:?}", builder); - builder - .with_api_key(oklink_key) - .build() - .wrap_err("Failed to create oklink client") + builder.with_api_key(oklink_key).build().wrap_err("Failed to create oklink client") } /// Creates the `VerifyContract` oklink request in order to verify the contract @@ -381,7 +381,7 @@ impl OklinkVerificationProvider { project: &Project, ) -> Result { if let Some(ref version) = args.compiler_version { - return Ok(version.trim_start_matches('v').parse()?) + return Ok(version.trim_start_matches('v').parse()?); } if let Some(ref solc) = config.solc { @@ -389,7 +389,7 @@ impl OklinkVerificationProvider { SolcReq::Version(version) => return Ok(version.to_owned()), SolcReq::Local(solc) => { if solc.is_file() { - return Ok(Solc::new(solc).version()?) + return Ok(Solc::new(solc).version()?); } } } @@ -452,10 +452,10 @@ impl OklinkVerificationProvider { read_constructor_args_file(constructor_args_path.to_path_buf())?, )?; let encoded_args = hex::encode(encoded_args); - return Ok(Some(encoded_args[8..].into())) + return Ok(Some(encoded_args[8..].into())); } if args.guess_constructor_args { - return Ok(Some(self.guess_constructor_args(args, project, config).await?)) + return Ok(Some(self.guess_constructor_args(args, project, config).await?)); } Ok(args.constructor_args.clone()) @@ -547,12 +547,7 @@ fn create_query( action: String, other: T, ) -> Query { - Query { - apikey: api_key, - module: module, - action: action, - other, - } + Query { apikey: api_key, module, action, other } } fn sanitize_response(res: impl AsRef) -> Result> { let res = res.as_ref(); From 559c8ec34a6544810a494ee41f723b3dc64b1417 Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Sun, 7 Apr 2024 13:07:27 +0800 Subject: [PATCH 11/16] update --- crates/verify/src/oklink.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index b8117324ab83..62c87f83c616 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -555,6 +555,10 @@ fn sanitize_response(res: impl AsRef) -> Result(res: impl AsRef) -> Result Ok(res), } } + +/// etherscan/polyscan is protected by cloudflare, which can lead to html responses like `Sorry, you have been blocked` See also +/// +/// This returns true if the `txt` is a cloudflare error response +pub(crate) fn is_blocked_by_cloudflare_response(txt: &str) -> bool { + txt.to_lowercase().contains("sorry, you have been blocked") +} + +/// etherscan/polyscan is protected by cloudflare, which can require captchas to "review the +/// security of your connection before proceeding" +pub(crate) fn is_cloudflare_security_challenge(txt: &str) -> bool { + txt.contains("https://www.cloudflare.com?utm_source=challenge") + || txt.to_lowercase().contains("checking if the site connection is secure") +} \ No newline at end of file From e8f67d2af75e32ded8d02ec7454629f0464772eb Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Tue, 9 Apr 2024 18:41:07 +0800 Subject: [PATCH 12/16] fmt --- crates/verify/src/lib.rs | 2 +- crates/verify/src/oklink.rs | 6 +++--- crates/verify/src/provider.rs | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index 9b1f5071de3f..3f644a5f8efc 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -23,9 +23,9 @@ use etherscan::EtherscanVerificationProvider; pub mod provider; use provider::VerificationProvider; +mod oklink; pub mod retry; mod sourcify; -mod oklink; pub use retry::RetryArgs; diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index 62c87f83c616..a3ceefd6b29f 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -589,6 +589,6 @@ pub(crate) fn is_blocked_by_cloudflare_response(txt: &str) -> bool { /// etherscan/polyscan is protected by cloudflare, which can require captchas to "review the /// security of your connection before proceeding" pub(crate) fn is_cloudflare_security_challenge(txt: &str) -> bool { - txt.contains("https://www.cloudflare.com?utm_source=challenge") - || txt.to_lowercase().contains("checking if the site connection is secure") -} \ No newline at end of file + txt.contains("https://www.cloudflare.com?utm_source=challenge") || + txt.to_lowercase().contains("checking if the site connection is secure") +} diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index 707604c307a0..dad93462211b 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -1,7 +1,6 @@ use super::{ - etherscan::EtherscanVerificationProvider, sourcify::SourcifyVerificationProvider, VerifyArgs, - oklink::OklinkVerificationProvider, - VerifyCheckArgs, + etherscan::EtherscanVerificationProvider, oklink::OklinkVerificationProvider, + sourcify::SourcifyVerificationProvider, VerifyArgs, VerifyCheckArgs, }; use async_trait::async_trait; use eyre::Result; From e8f73ed78261e37e2a3bb0dcb1b8384a6dbb7584 Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Tue, 9 Apr 2024 18:54:00 +0800 Subject: [PATCH 13/16] udpate --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffef82ca5edc..9422d084d0d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,8 +2,6 @@ name: test on: push: - branches: - - master pull_request: concurrency: From dcb8a928560d9acadfe00e32ee02c008fdf78dcd Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Tue, 9 Apr 2024 18:58:48 +0800 Subject: [PATCH 14/16] update --- crates/verify/src/oklink.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index a3ceefd6b29f..f4d70f074bf8 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -280,10 +280,10 @@ impl OklinkVerificationProvider { /// Create an oklink client pub(crate) fn client( &self, - chain: Chain, + _chain: Chain, verifier_url: Option<&str>, oklink_key: Option<&str>, - config: &Config, + _config: &Config, ) -> Result { let oklink_key = oklink_key.unwrap(); From 4f692e6eba1d7b686d7213cf95b7d12358d0867c Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Wed, 10 Apr 2024 15:55:47 +0800 Subject: [PATCH 15/16] udpate --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9422d084d0d9..ffef82ca5edc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,8 @@ name: test on: push: + branches: + - master pull_request: concurrency: From 79e4d5b1d3cd74f521324f4ffa9880f529ef74ed Mon Sep 17 00:00:00 2001 From: "comcat.li" Date: Thu, 11 Apr 2024 11:39:51 +0800 Subject: [PATCH 16/16] replace the ethers to alloy --- crates/verify/src/lib.rs | 3 +-- crates/verify/src/oklink.rs | 15 ++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/crates/verify/src/lib.rs b/crates/verify/src/lib.rs index 6d71664a1bf1..5b4f3a2fa686 100644 --- a/crates/verify/src/lib.rs +++ b/crates/verify/src/lib.rs @@ -23,9 +23,8 @@ use etherscan::EtherscanVerificationProvider; pub mod provider; use provider::VerificationProvider; - -mod oklink; pub mod bytecode; +mod oklink; pub mod retry; mod sourcify; diff --git a/crates/verify/src/oklink.rs b/crates/verify/src/oklink.rs index f4d70f074bf8..36d3e23beaec 100644 --- a/crates/verify/src/oklink.rs +++ b/crates/verify/src/oklink.rs @@ -1,6 +1,6 @@ use super::{provider::VerificationProvider, VerifyArgs, VerifyCheckArgs}; use alloy_json_abi::Function; -use ethers_providers::Middleware; +use alloy_provider::Provider; use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ errors::EtherscanError, @@ -9,7 +9,7 @@ use foundry_block_explorers::{ Client, Response, ResponseData, }; use foundry_cli::utils::{self, get_cached_entry_by_name, read_constructor_args_file, LoadConfig}; -use foundry_common::{abi::encode_function_args, retry::Retry, types::ToEthers}; +use foundry_common::{abi::encode_function_args, retry::Retry}; use foundry_compilers::{ artifacts::{BytecodeObject, CompactContract, StandardJsonCompilerInput}, cache::CacheEntry, @@ -480,20 +480,17 @@ impl OklinkVerificationProvider { )?; let creation_data = client.contract_creation_data(args.address).await?; - let transaction = provider - .get_transaction(creation_data.transaction_hash.to_ethers()) - .await? - .ok_or_eyre("Couldn't fetch transaction data from RPC")?; + let transaction = provider.get_transaction_by_hash(creation_data.transaction_hash).await?; let receipt = provider - .get_transaction_receipt(creation_data.transaction_hash.to_ethers()) + .get_transaction_receipt(creation_data.transaction_hash) .await? .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?; let maybe_creation_code: &[u8]; - if receipt.contract_address == Some(args.address.to_ethers()) { + if receipt.contract_address == Some(args.address) { maybe_creation_code = &transaction.input; - } else if transaction.to == Some(DEFAULT_CREATE2_DEPLOYER.to_ethers()) { + } else if transaction.to == Some(DEFAULT_CREATE2_DEPLOYER) { maybe_creation_code = &transaction.input[32..]; } else { eyre::bail!("Fetching of constructor arguments is not supported for contracts created by contracts")