-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
132 additions
and
156 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,4 +15,3 @@ ic-cdk-macros = "0.6.0" | |
serde = "1.0.152" | ||
serde_json = "1.0.93" | ||
serde_bytes = "0.11.9" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,13 @@ | ||
type issue = record { | ||
state: opt text; | ||
login: opt text; | ||
id: opt text; | ||
milestone_state: opt text; | ||
closed_at: opt text; | ||
}; | ||
|
||
type gh_token = text; | ||
|
||
service : { | ||
"get_icp_usd_exchange": () -> (text); | ||
} | ||
"get_gh_issue": (gh_token) -> (issue); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,170 +1,129 @@ | ||
//1. IMPORT IC MANAGEMENT CANISTER | ||
//This includes all methods and types needed | ||
use serde::{Deserialize, Serialize}; | ||
use ic_cdk::api::management_canister::http_request::{ | ||
http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, | ||
TransformContext, | ||
http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse | ||
}; | ||
|
||
use ic_cdk_macros::{self, query, update}; | ||
use serde::{Serialize, Deserialize}; | ||
use serde_json::{self, Value}; | ||
|
||
// This struct is legacy code and is not really used in the code. | ||
#[derive(Serialize, Deserialize)] | ||
struct Context { | ||
bucket_start_time_index: usize, | ||
closing_price_index: usize, | ||
use serde_json::Value; | ||
use candid::CandidType; | ||
|
||
// Define the IssueResponse struct to represent the transformed response | ||
#[derive(Debug, Serialize, Deserialize, CandidType)] | ||
struct IssueResponse { | ||
state: Option<String>, | ||
login: Option<String>, | ||
id: Option<String>, | ||
milestone_state: Option<String>, | ||
closed_at: Option<String>, | ||
} | ||
|
||
//Update method using the HTTPS outcalls feature | ||
#[ic_cdk::update] | ||
async fn get_icp_usd_exchange() -> String { | ||
//2. SETUP ARGUMENTS FOR HTTP GET request | ||
|
||
// 2.1 Setup the URL and its query parameters | ||
type Timestamp = u64; | ||
let start_timestamp: Timestamp = 1682978460; //May 1, 2023 22:01:00 GMT | ||
let seconds_of_time: u64 = 60; //we start with 60 seconds | ||
let host = "api.pro.coinbase.com"; | ||
async fn get_gh_issue(github_token: String) -> IssueResponse { | ||
// Setup the URL and its query parameters | ||
let owner = "input-output-hk"; | ||
let repo = "hydra"; | ||
let issue_nbr = 1404; | ||
let host = "api.github.com"; | ||
let url = format!( | ||
"https://{}/products/ICP-USD/candles?start={}&end={}&granularity={}", | ||
host, | ||
start_timestamp.to_string(), | ||
start_timestamp.to_string(), | ||
seconds_of_time.to_string() | ||
"https://{}/repos/{}/{}/issues/{}", | ||
host, owner, repo, issue_nbr | ||
); | ||
|
||
// 2.2 prepare headers for the system http_request call | ||
//Note that `HttpHeader` is declared in line 4 | ||
// Prepare headers for the system http_request call | ||
let request_headers = vec![ | ||
HttpHeader { | ||
name: "Host".to_string(), | ||
value: format!("{host}:443"), | ||
name: "Authorization".to_string(), | ||
value: format!("Bearer {}", github_token), | ||
}, | ||
HttpHeader { | ||
name: "Accept".to_string(), | ||
value: "application/vnd.github+json".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "User-Agent".to_string(), | ||
value: "exchange_rate_canister".to_string(), | ||
name: "X-GitHub-Api-Version".to_string(), | ||
value: "2022-11-28".to_string(), | ||
}, | ||
]; | ||
|
||
|
||
// This struct is legacy code and is not really used in the code. Need to be removed in the future | ||
// The "TransformContext" function does need a CONTEXT parameter, but this implementation is not necessary | ||
// the TransformContext(transform, context) below accepts this "context", but it does nothing with it in this implementation. | ||
// bucket_start_time_index and closing_price_index are meaninglesss | ||
let context = Context { | ||
bucket_start_time_index: 0, | ||
closing_price_index: 4, | ||
}; | ||
|
||
//note "CanisterHttpRequestArgument" and "HttpMethod" are declared in line 4 | ||
// Create the request argument | ||
let request = CanisterHttpRequestArgument { | ||
url: url.to_string(), | ||
method: HttpMethod::GET, | ||
body: None, //optional for request | ||
max_response_bytes: None, //optional for request | ||
// transform: None, //optional for request | ||
transform: Some(TransformContext::new(transform, serde_json::to_vec(&context).unwrap())), | ||
body: None, | ||
max_response_bytes: None, | ||
transform: None, // We'll handle transformation separately | ||
headers: request_headers, | ||
}; | ||
|
||
//3. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE | ||
|
||
//Note: in Rust, `http_request()` already sends the cycles needed | ||
//so no need for explicit Cycles.add() as in Motoko | ||
// Make the HTTP request and wait for the response | ||
match http_request(request).await { | ||
//4. DECODE AND RETURN THE RESPONSE | ||
|
||
//See:https://docs.rs/ic-cdk/latest/ic_cdk/api/management_canister/http_request/struct.HttpResponse.html | ||
Ok((response,)) => { | ||
//if successful, `HttpResponse` has this structure: | ||
// pub struct HttpResponse { | ||
// pub status: Nat, | ||
// pub headers: Vec<HttpHeader>, | ||
// pub body: Vec<u8>, | ||
// } | ||
|
||
//We need to decode that Vec<u8> that is the body into readable text. | ||
//To do this, we: | ||
// 1. Call `String::from_utf8()` on response.body | ||
// 3. We use a switch to explicitly call out both cases of decoding the Blob into ?Text | ||
let str_body = String::from_utf8(response.body) | ||
.expect("Transformed response is not UTF-8 encoded."); | ||
|
||
//The API response will looks like this: | ||
|
||
// ("[[1682978460,5.714,5.718,5.714,5.714,243.5678]]") | ||
|
||
//Which can be formatted as this | ||
// [ | ||
// [ | ||
// 1682978460, <-- start/timestamp | ||
// 5.714, <-- low | ||
// 5.718, <-- high | ||
// 5.714, <-- open | ||
// 5.714, <-- close | ||
// 243.5678 <-- volume | ||
// ], | ||
// ] | ||
// Parse the response body using the transform function | ||
let transformed_response = transform_response(response.clone()); | ||
|
||
// Print the transformed response for debugging | ||
println!("Transformed response: {:?}", transformed_response); | ||
|
||
//Return the body as a string and end the method | ||
str_body | ||
// Return the transformed response | ||
transformed_response | ||
} | ||
Err((r, m)) => { | ||
let message = | ||
format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); | ||
|
||
//Return the error as a string and end the method | ||
message | ||
Err((rejection_code, message)) => { | ||
panic!( | ||
"The http_request resulted in an error. RejectionCode: {:?}, Error: {}", | ||
rejection_code, message | ||
); | ||
} | ||
} | ||
} | ||
|
||
|
||
// Strips all data that is not needed from the original response. | ||
#[query] | ||
fn transform(raw: TransformArgs) -> HttpResponse { | ||
|
||
let headers = vec![ | ||
HttpHeader { | ||
name: "Content-Security-Policy".to_string(), | ||
value: "default-src 'self'".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "Referrer-Policy".to_string(), | ||
value: "strict-origin".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "Permissions-Policy".to_string(), | ||
value: "geolocation=(self)".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "Strict-Transport-Security".to_string(), | ||
value: "max-age=63072000".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "X-Frame-Options".to_string(), | ||
value: "DENY".to_string(), | ||
}, | ||
HttpHeader { | ||
name: "X-Content-Type-Options".to_string(), | ||
value: "nosniff".to_string(), | ||
}, | ||
]; | ||
|
||
let mut res = HttpResponse { | ||
status: raw.response.status.clone(), | ||
body: raw.response.body.clone(), | ||
headers, | ||
..Default::default() | ||
}; | ||
|
||
if res.status == 200 { | ||
|
||
res.body = raw.response.body; | ||
} else { | ||
ic_cdk::api::print(format!("Received an error from coinbase: err = {:?}", raw)); | ||
} | ||
res | ||
// Define a function to transform the response body | ||
fn transform_response( | ||
raw_response: HttpResponse, | ||
) -> IssueResponse { | ||
// Deserialize the raw response body into a serde_json::Value | ||
let parsed_response: Value = serde_json::from_slice(&raw_response.body) | ||
.unwrap_or_else(|e| panic!("Failed to parse JSON response: {}", e)); | ||
|
||
// Print the parsed response for debugging | ||
println!("Parsed response: {:?}", parsed_response); | ||
|
||
// Extract only the desired fields from the parsed response | ||
let transformed_response = parsed_response | ||
.as_object() | ||
.and_then(|obj| { | ||
// Extract fields from the object and construct a new object with only the desired fields | ||
let state = obj | ||
.get("state") | ||
.map(|value| value.as_str().map(|s| s.to_string())) | ||
.flatten(); | ||
let login = obj | ||
.get("closed_by") | ||
.and_then(|closed_by| closed_by.get("login")) | ||
.and_then(|value| value.as_str().map(|s| s.to_string())); | ||
let id = obj | ||
.get("closed_by") | ||
.and_then(|closed_by| closed_by.get("id")) | ||
.and_then(|value| value.as_str().map(|s| s.to_string())); | ||
let milestone_state = obj | ||
.get("milestone") | ||
.and_then(|milestone| milestone.get("state")) | ||
.and_then(|value| value.as_str().map(|s| s.to_string())); | ||
let closed_at = obj | ||
.get("milestone") | ||
.and_then(|milestone| milestone.get("closed_at")) | ||
.and_then(|value| value.as_str().map(|s| s.to_string())); | ||
|
||
// Construct the transformed response object | ||
Some(IssueResponse { | ||
state, | ||
login, | ||
id, | ||
milestone_state, | ||
closed_at, | ||
}) | ||
}) | ||
.unwrap_or_else(|| panic!("Failed to extract fields from parsed response")); | ||
|
||
// Print the transformed response for debugging | ||
println!("Transformed response: {:?}", transformed_response); | ||
|
||
transformed_response | ||
} |