Skip to content

Commit

Permalink
feat: HTTP api
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Apr 24, 2024
1 parent 99882fa commit 7b5445e
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 5 deletions.
6 changes: 4 additions & 2 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ itertools = "0.12"
num-bigint = "0.4"
num-traits = "0.2"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
serde_json = "1"
thiserror = "1.0"

[profile.dev]
Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy_local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ deploy_dip721 "reinstall" "local" "$ADMIN_PRINCIPAL" "$SUPPORTED_INTERFACES" "$N

set +e

dfx stop
# dfx stop

exit 0
2 changes: 2 additions & 0 deletions src/dip721_canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ itertools = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
Expand Down
161 changes: 160 additions & 1 deletion src/dip721_canister/src/did.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::borrow::Cow;
use std::collections::HashMap;

use candid::{CandidType, Principal};
use dip721_rs::SupportedInterface;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

#[derive(Debug, CandidType, Deserialize, PartialEq)]
pub struct CanisterInitData {
Expand All @@ -10,3 +14,158 @@ pub struct CanisterInitData {
pub symbol: String,
pub logo: Option<String>,
}

const HTTP_OK: u16 = 200;
const HTTP_UPGRADE: u16 = 204;
const HTTP_BAD_REQUEST: u16 = 400;
const HTTP_NOT_FOUND: u16 = 404;
const HTTP_INTERNAL_ERROR: u16 = 500;

/// A HTTP response.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct HttpResponse {
/// The HTTP status code.
pub status_code: u16,
/// The response header map.
pub headers: HashMap<Cow<'static, str>, Cow<'static, str>>,
/// The response body.
pub body: ByteBuf,
/// Whether the query call should be upgraded to an update call.
pub upgrade: Option<bool>,
}

impl HttpResponse {
pub fn new(
status_code: u16,
headers: HashMap<Cow<'static, str>, Cow<'static, str>>,
body: ByteBuf,
upgrade: Option<bool>,
) -> Self {
Self {
status_code,
headers,
body,
upgrade,
}
}

/// Returns a new `HttpResponse` intended to be used for internal errors.
pub fn internal_error(e: String) -> Self {
let body = match serde_json::to_vec(&e) {
Ok(bytes) => ByteBuf::from(&bytes[..]),
Err(e) => ByteBuf::from(e.to_string().as_bytes()),
};

Self {
status_code: HTTP_INTERNAL_ERROR,
headers: HashMap::from([("content-type".into(), "application/json".into())]),
body,
upgrade: None,
}
}

/// Returns a new `HttpResponse` intended to be used for bad request
pub fn bad_request(e: String) -> Self {
let body = match serde_json::to_vec(&e) {
Ok(bytes) => ByteBuf::from(&bytes[..]),
Err(e) => ByteBuf::from(e.to_string().as_bytes()),
};

Self {
status_code: HTTP_BAD_REQUEST,
headers: HashMap::from([("content-type".into(), "application/json".into())]),
body,
upgrade: None,
}
}

/// Returns a new `HttpResponse` intended to be used for not found
pub fn not_found() -> Self {
Self {
status_code: HTTP_NOT_FOUND,
headers: HashMap::from([("content-type".into(), "application/json".into())]),
body: ByteBuf::from("Not Found".as_bytes()),
upgrade: None,
}
}

/// Returns an OK response with the given body.
pub fn ok<S>(body: S) -> Self
where
S: Serialize,
{
let body = match serde_json::to_string(&body) {
Ok(body) => body,
Err(e) => return HttpResponse::internal_error(e.to_string()),
};
Self::new(
HTTP_OK,
HashMap::from([("content-type".into(), "application/json".into())]),
ByteBuf::from(body.as_bytes()),
None,
)
}

/// Upgrade response to update call.
pub fn upgrade_response() -> Self {
Self::new(
HTTP_UPGRADE,
HashMap::default(),
ByteBuf::default(),
Some(true),
)
}
}

/// The important components of an HTTP request.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct HttpRequest {
/// The HTTP method string.
pub method: Cow<'static, str>,
/// The URL that was visited.
pub url: String,
/// The request headers.
pub headers: HashMap<Cow<'static, str>, Cow<'static, str>>,
/// The request body.
pub body: ByteBuf,
}

impl HttpRequest {
pub fn new(data: &[u8]) -> Self {
let mut headers = HashMap::new();
headers.insert("content-type".into(), "application/json".into());
Self {
method: "POST".into(),
url: "".into(),
headers,
body: ByteBuf::from(data),
}
}

pub fn decode_body<S>(&self) -> Result<S, HttpResponse>
where
S: serde::de::DeserializeOwned,
{
serde_json::from_slice::<HttpApiRequest<S>>(&self.body)
.map_err(|_| HttpResponse::bad_request("Invalid request body".to_string()))
.map(|m| m.params)
}

pub fn decode_method(&self) -> Result<String, HttpResponse> {
serde_json::from_slice::<HttpApiMethod>(&self.body)
.map_err(|_| HttpResponse::bad_request("Invalid request body".to_string()))
.map(|m| m.method)
}
}

#[derive(Clone, Debug, Deserialize)]
struct HttpApiMethod {
pub method: String,
}

/// The important components of an HTTP request.
#[derive(Clone, Debug, Deserialize)]
pub struct HttpApiRequest<S> {
pub method: String,
pub params: S,
}
80 changes: 80 additions & 0 deletions src/dip721_canister/src/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use candid::Nat;
use dip721_rs::Dip721;
use serde::Deserialize;

use crate::{
app::App,
did::{HttpRequest, HttpResponse},
};

#[derive(Deserialize)]
struct TokenIdentifierReq {
pub id: Nat,
}

pub struct HttpApi;

impl HttpApi {
/// Handles an HTTP request
pub async fn handle_http_request(req: HttpRequest) -> HttpResponse {
// must be a GET request
if req.method != "GET" {
return HttpResponse::bad_request("expected GET method".to_string());
}
// Must be a JSON-RPC request
if req.headers.get("content-type").map(|s| s.as_ref()) != Some("application/json") {
return HttpResponse::bad_request(
"expected content-type: application/json".to_string(),
);
}
let method = match req.decode_method() {
Ok(request) => request,
Err(response) => return response,
};

match method.as_str() {
"dip721_metadata" => Self::dip721_metadata(),
"dip721_name" => Self::dip721_name(),
"dip721_symbol" => Self::dip721_symbol(),
"dip721_logo" => Self::dip721_logo(),
"dip721_total_unique_holders" => Self::dip721_total_unique_holders(),
"dip721_token_metadata" => Self::dip721_token_metadata(req),
"dip721_total_supply" => Self::dip721_total_supply(),
_ => HttpResponse::bad_request("unknown method".to_string()),
}
}

fn dip721_metadata() -> HttpResponse {
HttpResponse::ok(App::dip721_metadata())
}

fn dip721_name() -> HttpResponse {
HttpResponse::ok(App::dip721_name())
}

fn dip721_symbol() -> HttpResponse {
HttpResponse::ok(App::dip721_symbol())
}

fn dip721_logo() -> HttpResponse {
HttpResponse::ok(App::dip721_logo())
}

fn dip721_total_unique_holders() -> HttpResponse {
HttpResponse::ok(App::dip721_total_unique_holders())
}

fn dip721_token_metadata(req: HttpRequest) -> HttpResponse {
let params = match req.decode_body::<TokenIdentifierReq>() {
Ok(request) => request,
Err(response) => return response,
};
App::dip721_token_metadata(params.id)
.map(HttpResponse::ok)
.unwrap_or_else(|_| HttpResponse::not_found())
}

fn dip721_total_supply() -> HttpResponse {
HttpResponse::ok(App::dip721_total_supply())
}
}
10 changes: 9 additions & 1 deletion src/dip721_canister/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! # DIP721 canister
use candid::{candid_method, Nat, Principal};
use did::CanisterInitData;
use did::{CanisterInitData, HttpRequest, HttpResponse};
use dip721_rs::Dip721 as _;
use ic_cdk_macros::{init, post_upgrade, query, update};

mod app;
pub mod did;
mod http;
mod inspect;
mod storable;
mod utils;
Expand Down Expand Up @@ -257,6 +258,13 @@ pub fn dip721_total_transactions() -> Nat {
App::dip721_total_transactions()
}

// HTTP endpoint
#[query]
#[candid_method(query)]
pub async fn http_request(req: HttpRequest) -> HttpResponse {
http::HttpApi::handle_http_request(req).await
}

#[allow(dead_code)]
fn main() {
// The line below generates did types and service definition from the
Expand Down

0 comments on commit 7b5445e

Please sign in to comment.