Skip to content

Commit

Permalink
Resolve #1
Browse files Browse the repository at this point in the history
  • Loading branch information
ffakenz committed Apr 26, 2024
1 parent b952159 commit fff5b0f
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 156 deletions.
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Load environment variables from .env file
include .env

.PHONY: all
all: build

Expand Down Expand Up @@ -25,8 +28,13 @@ upgrade: build
.PHONY: test
.SILENT: test
test: install
dfx canister call backend get_icp_usd_exchange \
| grep '\[1682978460,5\.714,5\.718,5\.714,5\.714,243\.5678\]' && echo 'PASS'
# Call the backend canister to get the GitHub issue and capture the output
@echo "Calling backend canister..."
@TMP_FILE=$$(mktemp); \
dfx canister call backend get_gh_issue '("${GITHUB_TOKEN}")' > $$TMP_FILE; \
echo "get_gh_issue response:"; \
cat $$TMP_FILE; \
rm -f $$TMP_FILE

.PHONY: clean
.SILENT: clean
Expand Down
1 change: 0 additions & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ ic-cdk-macros = "0.6.0"
serde = "1.0.152"
serde_json = "1.0.93"
serde_bytes = "0.11.9"

14 changes: 12 additions & 2 deletions backend/backend.did
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);
}
237 changes: 98 additions & 139 deletions backend/src/lib.rs
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
}

0 comments on commit fff5b0f

Please sign in to comment.