Skip to content

Commit

Permalink
feat: HTTP API for deferred
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Feb 17, 2024
1 parent 12ca23d commit a8a63da
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ num-bigint = "0.4"
num-traits = "0.2"
secp256k1 = "0.28"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
serde_json = "1"
sha2 = "0.10"
sha3 = "0.10"
Expand Down
71 changes: 71 additions & 0 deletions src/deferred/src/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
mod get_agencies;
mod get_contract;
mod get_token;

use did::{HttpRequest, HttpResponse};

use self::get_contract::{GetContractRequest, GetContractResponse};
use self::get_token::{GetTokenRequest, GetTokenResponse};
use crate::app::Deferred;

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() {
"getContracts" => Self::get_contracts(),
"getContract" => Self::get_contract(req),
"getToken" => Self::get_token(req),
"getAgencies" => Self::get_agencies(),
_ => HttpResponse::bad_request("unknown method".to_string()),
}
}

fn get_contracts() -> HttpResponse {
let response = get_contract::GetContractsResponse::from(Deferred::get_signed_contracts());

HttpResponse::ok(response)
}

fn get_contract(req: HttpRequest) -> HttpResponse {
let params = match req.decode_body::<GetContractRequest>() {
Ok(request) => request,
Err(response) => return response,
};
let response = GetContractResponse::from(Deferred::get_contract(&params.id));

HttpResponse::ok(response)
}

fn get_token(req: HttpRequest) -> HttpResponse {
let params = match req.decode_body::<GetTokenRequest>() {
Ok(request) => request,
Err(response) => return response,
};
let response = GetTokenResponse::from(Deferred::get_token(&params.id));

HttpResponse::ok(response)
}

fn get_agencies() -> HttpResponse {
let response = get_agencies::GetAgenciesResponse::from(Deferred::get_agencies());

HttpResponse::ok(response)
}
}
3 changes: 3 additions & 0 deletions src/deferred/src/http/get_agencies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use did::deferred::Agency;

pub type GetAgenciesResponse = Vec<Agency>;
21 changes: 21 additions & 0 deletions src/deferred/src/http/get_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use did::deferred::Contract;
use did::ID;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize)]
pub struct GetContractRequest {
pub id: ID,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GetContractResponse {
contract: Option<Contract>,
}

impl From<Option<Contract>> for GetContractResponse {
fn from(contract: Option<Contract>) -> Self {
Self { contract }
}
}

pub type GetContractsResponse = Vec<ID>;
18 changes: 18 additions & 0 deletions src/deferred/src/http/get_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use did::deferred::{TokenIdentifier, TokenInfo};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize)]
pub struct GetTokenRequest {
pub id: TokenIdentifier,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GetTokenResponse {
token: Option<TokenInfo>,
}

impl From<Option<TokenInfo>> for GetTokenResponse {
fn from(token: Option<TokenInfo>) -> Self {
Self { token }
}
}
10 changes: 9 additions & 1 deletion src/deferred/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ use candid::{candid_method, Nat, Principal};
use did::deferred::{
Agency, Contract, ContractRegistration, DeferredInitData, DeferredResult, Role, TokenInfo,
};
use did::ID;
use did::{HttpRequest, HttpResponse, ID};
use dip721::{Dip721 as _, GenericValue, TokenIdentifier};
use ic_cdk_macros::{init, post_upgrade, query, update};

mod app;
mod client;
mod constants;
mod http;
mod inspect;
mod utils;

Expand Down Expand Up @@ -360,6 +361,13 @@ pub fn total_transactions() -> Nat {
Deferred::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
2 changes: 2 additions & 0 deletions src/did/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ ic-stable-structures = { workspace = true }
icrc = { path = "../icrc" }
num-bigint = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
xrc = { path = "../xrc" }

Expand Down
2 changes: 2 additions & 0 deletions src/did/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
mod account;
mod h160;
mod http;
mod id;
mod nat;
mod principal;

pub use account::StorableAccount;
pub use h160::H160;
pub use http::{HttpApiRequest, HttpRequest, HttpResponse};
pub use id::ID;
pub use nat::StorableNat;
pub use principal::StorablePrincipal;
140 changes: 140 additions & 0 deletions src/did/src/common/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::borrow::Cow;
use std::collections::HashMap;

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

/// 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: 500,
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: 400,
headers: HashMap::from([("content-type".into(), "application/json".into())]),
body,
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(
200,
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(204, 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,
}
7 changes: 4 additions & 3 deletions src/did/src/deferred/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use candid::{CandidType, Decode, Deserialize, Encode, Principal};
pub use dip721::{GenericValue, TokenIdentifier};
use ic_stable_structures::storable::Bound;
use ic_stable_structures::Storable;
use serde::Serialize;

pub use crate::ID;

Expand All @@ -14,7 +15,7 @@ pub use info::TokenInfo;
pub use token::Token;

/// A sell contract for a building
#[derive(Clone, Debug, CandidType, Deserialize)]
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct Contract {
/// Contract ID
pub id: ID,
Expand Down Expand Up @@ -64,7 +65,7 @@ impl Storable for Contract {
pub type ContractProperties = Vec<(String, GenericValue)>;

/// A variant which defines the contract type
#[derive(Clone, Debug, CandidType, Deserialize)]
#[derive(Clone, Debug, CandidType, Serialize, Deserialize)]
pub enum ContractType {
Financing,
Sell,
Expand All @@ -73,7 +74,7 @@ pub enum ContractType {
/// A variant which defines a contract seller.
/// A contract may have more than one seller and the quota defines the percentage of the contract ownership.
/// The sum of all quotas must be 100.
#[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize, Serialize)]
pub struct Seller {
pub principal: Principal,
pub quota: u8,
Expand Down
6 changes: 3 additions & 3 deletions src/did/src/deferred/contract/agency.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use candid::{CandidType, Decode, Encode};
use ic_stable_structures::storable::Bound;
use ic_stable_structures::Storable;
use serde::Deserialize;
use serde::{Deserialize, Serialize};

/// A sell contract for a building
#[derive(Clone, Debug, CandidType, Deserialize, PartialEq)]
#[derive(Clone, Debug, CandidType, Deserialize, Serialize, PartialEq)]
pub struct Agency {
pub name: String,
pub address: String,
Expand All @@ -21,7 +21,7 @@ pub struct Agency {
pub logo: Option<String>,
}

#[derive(Clone, Debug, CandidType, Deserialize, Copy, PartialEq, Eq)]
#[derive(Clone, Debug, CandidType, Deserialize, Serialize, Copy, PartialEq, Eq)]
pub enum Continent {
Africa,
Antarctica,
Expand Down
4 changes: 2 additions & 2 deletions src/did/src/deferred/contract/info.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use candid::CandidType;
use serde::Deserialize;
use serde::{Deserialize, Serialize};

use super::{Contract, Token};

#[derive(Clone, Debug, CandidType, Deserialize)]
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
pub struct TokenInfo {
pub token: Token,
pub contract: Contract,
Expand Down
Loading

0 comments on commit a8a63da

Please sign in to comment.