diff --git a/src/did/Cargo.toml b/src/did/Cargo.toml index 846e8ef3..2babf995 100644 --- a/src/did/Cargo.toml +++ b/src/did/Cargo.toml @@ -25,6 +25,7 @@ jsonrpc-core = { workspace = true } log = { workspace = true } num = { workspace = true } serde = { workspace = true } +serde_bytes = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } sha2 = { workspace = true } diff --git a/src/did/src/error.rs b/src/did/src/error.rs index 1165a266..7337e4bc 100644 --- a/src/did/src/error.rs +++ b/src/did/src/error.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use alloy::eips::eip2718::Eip2718Error; +use alloy::primitives::SignatureError; use alloy::rlp::Error as DecoderError; use candid::{CandidType, Deserialize}; use jsonrpc_core::{Error, ErrorCode}; @@ -8,7 +9,7 @@ use serde::Serialize; use thiserror::Error; use crate::transaction::BlockId; -use crate::{BlockNumber, H160, U256}; +use crate::{BlockNumber, U256}; pub type Result = std::result::Result; @@ -253,10 +254,16 @@ impl From for EvmError { #[derive(Error, Debug, CandidType, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub enum SignatureVerificationError { - #[error("signature is not correct: expected: {expected}, recovered: {recovered}")] - RecoveryError { expected: H160, recovered: H160 }, + #[error("signature error: {0}")] + SignatureError(String), #[error("failed to verify signature: {0}")] InternalError(String), #[error("unauthorized principal")] Unauthorized, } + +impl From for SignatureVerificationError { + fn from(value: SignatureError) -> Self { + SignatureVerificationError::SignatureError(format!("{:?}", value)) + } +} diff --git a/src/did/src/http.rs b/src/did/src/http.rs new file mode 100644 index 00000000..4f7e008e --- /dev/null +++ b/src/did/src/http.rs @@ -0,0 +1,147 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use candid::CandidType; +use jsonrpc_core::{Error, Failure, Id, Version}; +use serde::de::DeserializeOwned; +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>>, + /// The response body. + pub body: ByteBuf, + /// Whether the query call should be upgraded to an update call. + pub upgrade: Option, +} + +impl HttpResponse { + pub fn new( + status_code: u16, + headers: HashMap, Cow<'static, str>>, + body: ByteBuf, + upgrade: Option, + ) -> Self { + Self { + status_code, + headers, + body, + upgrade, + } + } + + pub fn new_failure( + jsonrpc: Option, + id: Id, + error: Error, + status_code: HttpStatusCode, + ) -> Self { + let failure = Failure { jsonrpc, error, id }; + let body = match serde_json::to_vec(&failure) { + Ok(bytes) => ByteBuf::from(&bytes[..]), + Err(e) => ByteBuf::from(e.to_string().as_bytes()), + }; + + Self::new( + status_code as u16, + HashMap::from([("content-type".into(), "application/json".into())]), + body, + None, + ) + } + + /// 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 an OK response with the given body. + pub fn ok(body: ByteBuf) -> Self { + Self::new( + HttpStatusCode::Ok as u16, + HashMap::from([("content-type".into(), "application/json".into())]), + body, + 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: Cow<'static, str>, + /// The request headers. + pub headers: HashMap, Cow<'static, str>>, + /// The request body. + pub body: ByteBuf, +} + +impl HttpRequest { + pub fn new(data: &T) -> Self { + let mut headers = HashMap::new(); + headers.insert("content-type".into(), "application/json".into()); + Self { + method: "POST".into(), + url: "".into(), + headers, + body: ByteBuf::from(serde_json::to_vec(&data).unwrap()), + } + } + + pub fn decode_body(&self) -> Result> + where + T: DeserializeOwned, + { + serde_json::from_slice::(&self.body).map_err(|_| { + Box::new(HttpResponse::new_failure( + Some(Version::V2), + Id::Null, + Error::parse_error(), + HttpStatusCode::BadRequest, + )) + }) + } + + /// Returns an header value by matching the key with a case-insensitive comparison. + /// As IC HTTP headers are lowercased, this method cost is usually O(1) for matching lowercase inputs, and O(n) in any other case. + pub fn get_header_ignore_case(&self, header_name: &str) -> Option<&Cow<'static, str>> { + match self.headers.get(header_name) { + Some(ip) => Some(ip), + None => self + .headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(header_name)) + .map(|(_, v)| v), + } + } +} + +#[repr(u16)] +pub enum HttpStatusCode { + Ok = 200, + BadRequest = 400, + InternalServerError = 500, +} diff --git a/src/did/src/init.rs b/src/did/src/init.rs index a3e1718d..daac405d 100644 --- a/src/did/src/init.rs +++ b/src/did/src/init.rs @@ -2,7 +2,7 @@ use std::time::Duration; use candid::{CandidType, Nat, Principal}; use ic_log::LogSettings; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::permission::Permission; use crate::{H160, U256}; @@ -47,4 +47,12 @@ impl Default for EvmCanisterInitData { } /// These are the arguments which are taken by the signature verification canister init fn -pub type SignatureVerificationCanisterInitData = Vec; +#[derive(Debug, Clone, Serialize, CandidType, Deserialize)] +pub struct SignatureVerificationCanisterInitData { + /// Access list of principals that are allowed to send transactions to the EVM canisters + pub access_list: Vec, + /// EVM canister Principal + pub evm_canister: Principal, + /// Interval for pushing transactions to the EVM canisters + pub pushing_timer_interval: Duration, +} diff --git a/src/did/src/lib.rs b/src/did/src/lib.rs index e1a55449..3d128f03 100644 --- a/src/did/src/lib.rs +++ b/src/did/src/lib.rs @@ -24,6 +24,7 @@ pub mod state; pub mod transaction; pub mod fees; +pub mod http; #[cfg(test)] mod test_utils; diff --git a/src/did/src/permission.rs b/src/did/src/permission.rs index 636f7a28..4dc25d7e 100644 --- a/src/did/src/permission.rs +++ b/src/did/src/permission.rs @@ -18,6 +18,9 @@ pub enum Permission { UpdateLogsConfiguration, /// Allows caller to reset the EVM state ResetEvmState, + /// Allows the signature verification canister to send transaction to + /// the EVM Canister + PrivilegedSendTransaction, } #[derive(Debug, Clone, Default, CandidType, Deserialize, PartialEq, Eq, serde::Serialize)] diff --git a/src/ethereum-json-rpc-client/src/canister_client.rs b/src/ethereum-json-rpc-client/src/canister_client.rs index 6b7e7a27..635a2472 100644 --- a/src/ethereum-json-rpc-client/src/canister_client.rs +++ b/src/ethereum-json-rpc-client/src/canister_client.rs @@ -1,13 +1,10 @@ -use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use anyhow::Context; -use candid::{CandidType, Deserialize}; +use did::http::{HttpRequest, HttpResponse}; use ic_canister_client::CanisterClient; use jsonrpc_core::{Call, Request, Response}; -use serde::Serialize; -use serde_bytes::ByteBuf; use crate::{Client, ETH_SEND_RAW_TRANSACTION_METHOD}; @@ -33,7 +30,7 @@ impl Client for T { _ => false, }; - let args = HttpRequest::new(&request)?; + let args = HttpRequest::new(&request); let http_response: Result = if is_update_call { client.update("http_request_update", (args,)).await @@ -60,44 +57,6 @@ impl Client for T { } } -/// The important components of an HTTP request. -#[derive(Clone, Debug, CandidType)] -struct HttpRequest { - /// The HTTP method string. - pub method: &'static str, - /// The URL method string. - pub url: &'static str, - /// The request headers. - pub headers: HashMap<&'static str, &'static str>, - /// The request body. - pub body: ByteBuf, -} - -impl HttpRequest { - pub fn new(data: &T) -> anyhow::Result { - let mut headers = HashMap::new(); - headers.insert("content-type", "application/json"); - Ok(Self { - method: "POST", - headers, - url: "", - body: ByteBuf::from( - serde_json::to_vec(data).context("failed to serialize RPC request")?, - ), - }) - } -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct HttpResponse { - /// The HTTP status code. - pub status_code: u16, - /// The response header map. - pub headers: HashMap, - /// The response body. - pub body: ByteBuf, -} - #[inline] fn is_update_call(method: &str) -> bool { method.eq(ETH_SEND_RAW_TRANSACTION_METHOD) diff --git a/src/evm-canister-client/src/client.rs b/src/evm-canister-client/src/client.rs index 4012452e..92c75b58 100644 --- a/src/evm-canister-client/src/client.rs +++ b/src/evm-canister-client/src/client.rs @@ -844,4 +844,14 @@ impl EvmCanisterClient { ) -> CanisterClientResult> { self.client.update("revert_to_block", (args,)).await } + + /// Send a batch of Transactions and append them to the blockchain. + pub async fn send_transactions_unchecked( + &self, + transactions: Vec, + ) -> CanisterClientResult> { + self.client + .update("send_transactions_unchecked", (transactions,)) + .await + } } diff --git a/src/signature-verification-canister-client/src/client.rs b/src/signature-verification-canister-client/src/client.rs index a45e972e..eaf1aa48 100644 --- a/src/signature-verification-canister-client/src/client.rs +++ b/src/signature-verification-canister-client/src/client.rs @@ -39,7 +39,7 @@ impl SignatureVerificationCanisterClient { &self, principal: Principal, ) -> CanisterClientResult> { - self.client.update("add_access", (principal,)).await + self.client.update("admin_add_access", (principal,)).await } /// Remove principal from the access control list @@ -47,7 +47,9 @@ impl SignatureVerificationCanisterClient { &self, principal: Principal, ) -> CanisterClientResult> { - self.client.update("remove_access", (principal,)).await + self.client + .update("admin_remove_access", (principal,)) + .await } /// Get the owner of the canister @@ -60,7 +62,7 @@ impl SignatureVerificationCanisterClient { &self, principal: Principal, ) -> CanisterClientResult> { - self.client.update("set_owner", (principal,)).await + self.client.update("admin_set_owner", (principal,)).await } /// Get the access control list @@ -72,4 +74,39 @@ impl SignatureVerificationCanisterClient { pub async fn get_canister_build_data(&self) -> CanisterClientResult { self.client.query("get_canister_build_data", ()).await } + + /// Add evm canister to the access control list + pub async fn add_evm_canister_to_access_list( + &self, + principal: Principal, + ) -> CanisterClientResult> { + self.client + .update("admin_add_evm_canister", (principal,)) + .await + } + + /// Remove evm canister from the access control list + pub async fn remove_evm_canister_from_access_list( + &self, + principal: Principal, + ) -> CanisterClientResult> { + self.client + .update("admin_remove_evm_canister", (principal,)) + .await + } + + /// Interval for pushing transactions to the evm canisters + pub async fn get_pushing_timer_interval(&self) -> CanisterClientResult { + self.client.query("pushing_timer_interval", ()).await + } + + /// Set the interval for pushing transactions to the evm canisters + pub async fn set_pushing_timer_interval( + &self, + interval: u64, + ) -> CanisterClientResult> { + self.client + .update("admin_set_pushing_timer_interval", (interval,)) + .await + } }