use anda_core::{BoxError, BoxPinFut, HttpFeatures, RPCRequestRef, cbor_rpc};
use anda_engine::context::Web3ClientFeatures;
use candid::{
CandidType, Decode, Principal,
utils::{ArgumentEncoder, encode_args},
};
use ciborium::from_reader;
use ic_agent::identity::{AnonymousIdentity, BasicIdentity, Secp256k1Identity};
use ic_auth_types::deterministic_cbor_into_vec;
use ic_auth_verifier::envelope::SignedEnvelope;
use ic_cose::client::CoseSDK;
use ic_cose_types::{
CanisterCaller,
cose::{
ed25519::ed25519_verify,
k256::{secp256k1_verify_bip340, secp256k1_verify_ecdsa},
sha3_256,
},
};
use ic_tee_gateway_sdk::crypto;
use serde::{Serialize, de::DeserializeOwned};
use std::{sync::Arc, time::Duration};
pub use ic_agent::{Agent, Identity};
use anda_engine::APP_USER_AGENT;
#[derive(Clone)]
pub struct Client {
outer_http: reqwest::Client,
root_secret: [u8; 48],
identity: Arc<dyn Identity>,
agent: Agent,
cose_canister: Principal,
allow_http: bool,
}
#[non_exhaustive]
pub struct ClientBuilder {
ic_host: String,
root_secret: [u8; 48],
identity: Option<Arc<dyn Identity>>,
agent: Option<Agent>,
cose_canister: Principal,
outer_http: Option<reqwest::Client>,
allow_http: bool,
}
pub fn identity_from_secret(id_secret: [u8; 32]) -> Box<dyn Identity> {
Box::new(BasicIdentity::from_raw_key(&id_secret))
}
pub fn identity_from_pem(path: &str) -> Result<Box<dyn Identity>, BoxError> {
let content = std::fs::read_to_string(path)?;
match Secp256k1Identity::from_pem(content.as_bytes()) {
Ok(identity) => Ok(Box::new(identity)),
Err(_) => match BasicIdentity::from_pem(content.as_bytes()) {
Ok(identity) => Ok(Box::new(identity)),
Err(err) => Err(err.into()),
},
}
}
pub fn load_identity(id_secret_or_path: &str) -> Result<Box<dyn Identity>, BoxError> {
if id_secret_or_path == "Anonymous" {
return Ok(Box::new(AnonymousIdentity));
}
match identity_from_pem(id_secret_or_path) {
Ok(identity) => Ok(identity),
Err(_) => {
let id_secret = hex::decode(id_secret_or_path)?;
let id_secret: [u8; 32] = id_secret
.try_into()
.map_err(|_| format!("invalid id_secret: {id_secret_or_path:?}"))?;
Ok(identity_from_secret(id_secret))
}
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self {
ic_host: "https://icp-api.io".to_string(),
root_secret: [0; 48],
identity: None,
agent: None,
cose_canister: Principal::anonymous(),
outer_http: None,
allow_http: false,
}
}
}
impl ClientBuilder {
pub fn with_ic_host(mut self, ic_host: &str) -> Self {
self.ic_host = ic_host.to_string();
self
}
pub fn with_root_secret(mut self, root_secret: [u8; 48]) -> Self {
self.root_secret = root_secret;
self
}
pub fn with_cose_canister(mut self, cose_canister: Principal) -> Self {
self.cose_canister = cose_canister;
self
}
pub fn with_identity(mut self, identity: Arc<dyn Identity>) -> Self {
self.identity = Some(identity);
self
}
pub fn with_agent(mut self, agent: Agent) -> Self {
self.agent = Some(agent);
self
}
pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self {
self.outer_http = Some(http_client);
self
}
pub fn with_allow_http(mut self, allow_http: bool) -> Self {
self.allow_http = allow_http;
self
}
pub async fn build(self) -> Result<Client, BoxError> {
let identity = match self.identity {
Some(identity) => identity,
None => Arc::new(identity_from_secret(sha3_256(&self.root_secret))),
};
let agent = match self.agent {
Some(agent) => agent,
None => {
let agent = Agent::builder()
.with_url(self.ic_host.clone())
.with_arc_identity(identity.clone())
.with_verify_query_signatures(false)
.build()?;
if self.ic_host.starts_with("http://") {
let _ = agent.fetch_root_key().await;
}
agent
}
};
let outer_http = match self.outer_http {
Some(http_client) => http_client,
None => reqwest::Client::builder()
.use_rustls_tls()
.https_only(!self.allow_http)
.http2_keep_alive_interval(Some(Duration::from_secs(25)))
.http2_keep_alive_timeout(Duration::from_secs(15))
.http2_keep_alive_while_idle(true)
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(120))
.gzip(true)
.user_agent(APP_USER_AGENT)
.build()?,
};
Ok(Client {
outer_http,
root_secret: self.root_secret,
identity,
agent,
cose_canister: self.cose_canister,
allow_http: self.allow_http,
})
}
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn get_principal(&self) -> Principal {
self.identity
.sender()
.expect("Failed to get sender principal")
}
pub async fn sign_envelope(
&self,
message_digest: [u8; 32],
) -> Result<SignedEnvelope, BoxError> {
let se = SignedEnvelope::sign_digest(&self.identity, message_digest.into())?;
Ok(se)
}
}
impl Web3ClientFeatures for Client {
fn get_principal(&self) -> Principal {
self.identity
.sender()
.expect("Failed to get sender principal")
}
fn sign_envelope(
&self,
message_digest: [u8; 32],
) -> BoxPinFut<Result<SignedEnvelope, BoxError>> {
let identity = self.identity.clone();
Box::pin(async move {
let se = SignedEnvelope::sign_digest(&identity, message_digest.into())?;
Ok(se)
})
}
fn a256gcm_key(&self, derivation_path: Vec<Vec<u8>>) -> BoxPinFut<Result<[u8; 32], BoxError>> {
let res = crypto::a256gcm_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(Ok(res)))
}
fn ed25519_sign_message(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::ed25519_sign_message(&self.root_secret, derivation_path, message);
Box::pin(futures::future::ready(Ok(res)))
}
fn ed25519_verify(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::ed25519_public_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(
ed25519_verify(&res.0, message, signature).map_err(|e| e.into()),
))
}
fn ed25519_public_key(
&self,
derivation_path: Vec<Vec<u8>>,
) -> BoxPinFut<Result<[u8; 32], BoxError>> {
let res = crypto::ed25519_public_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(Ok(res.0)))
}
fn secp256k1_sign_message_bip340(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res =
crypto::secp256k1_sign_message_bip340(&self.root_secret, derivation_path, message);
Box::pin(futures::future::ready(Ok(res)))
}
fn secp256k1_verify_bip340(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(
secp256k1_verify_bip340(res.0.as_slice(), message, signature).map_err(|e| e.into()),
))
}
fn secp256k1_sign_message_ecdsa(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::secp256k1_sign_message_ecdsa(&self.root_secret, derivation_path, message);
Box::pin(futures::future::ready(Ok(res)))
}
fn secp256k1_sign_digest_ecdsa(
&self,
derivation_path: Vec<Vec<u8>>,
message_hash: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res =
crypto::secp256k1_sign_digest_ecdsa(&self.root_secret, derivation_path, message_hash);
Box::pin(futures::future::ready(Ok(res)))
}
fn secp256k1_verify_ecdsa(
&self,
derivation_path: Vec<Vec<u8>>,
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(
secp256k1_verify_ecdsa(res.0.as_slice(), message, signature).map_err(|e| e.into()),
))
}
fn secp256k1_public_key(
&self,
derivation_path: Vec<Vec<u8>>,
) -> BoxPinFut<Result<[u8; 33], BoxError>> {
let res = crypto::secp256k1_public_key(&self.root_secret, derivation_path);
Box::pin(futures::future::ready(Ok(res.0)))
}
fn canister_query_raw(
&self,
canister: Principal,
method: String,
args: Vec<u8>,
) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
let agent = self.agent.clone();
Box::pin(async move {
let res = agent.query(&canister, method).with_arg(args).call().await?;
Ok(res)
})
}
fn canister_update_raw(
&self,
canister: Principal,
method: String,
args: Vec<u8>,
) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
let agent = self.agent.clone();
Box::pin(async move {
let res = agent
.update(&canister, method)
.with_arg(args)
.call_and_wait()
.await?;
Ok(res)
})
}
fn https_call(
&self,
url: String,
method: http::Method,
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, ) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
if !self.allow_http && !url.starts_with("https://") {
return Box::pin(futures::future::ready(Err(
"Invalid url, must start with https://".into(),
)));
}
let outer_http = self.outer_http.clone();
Box::pin(async move {
let mut req = outer_http.request(method, url);
if let Some(headers) = headers {
req = req.headers(headers);
}
if let Some(body) = body {
req = req.body(body);
}
req.send().await.map_err(|e| e.into())
})
}
fn https_signed_call(
&self,
url: String,
method: http::Method,
message_digest: [u8; 32],
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, ) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
if !self.allow_http && !url.starts_with("https://") {
return Box::pin(futures::future::ready(Err(
"Invalid url, must start with https://".into(),
)));
}
let se = match SignedEnvelope::sign_digest(&self.identity, message_digest.into()) {
Ok(se) => se,
Err(err) => return Box::pin(futures::future::ready(Err(err.into()))),
};
let mut headers = headers.unwrap_or_default();
if let Err(err) = se.to_authorization(&mut headers) {
return Box::pin(futures::future::ready(Err(err.into())));
}
let outer_http = self.outer_http.clone();
Box::pin(async move {
let mut req = outer_http.request(method, url);
req = req.headers(headers);
if let Some(body) = body {
req = req.body(body);
}
req.send().await.map_err(|e| e.into())
})
}
fn https_signed_rpc_raw(
&self,
endpoint: String,
method: String,
args: Vec<u8>,
) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
if !self.allow_http && !endpoint.starts_with("https://") {
return Box::pin(futures::future::ready(Err(
"Invalid endpoint, must start with https://".into(),
)));
}
let req = RPCRequestRef {
method: &method,
params: &args.into(),
};
let body = match deterministic_cbor_into_vec(&req) {
Ok(body) => body,
Err(err) => return Box::pin(futures::future::ready(Err(err.into()))),
};
let digest: [u8; 32] = sha3_256(&body);
let se = match SignedEnvelope::sign_digest(&self.identity, digest.into()) {
Ok(se) => se,
Err(err) => return Box::pin(futures::future::ready(Err(err.into()))),
};
let mut headers = http::HeaderMap::new();
if let Err(err) = se.to_authorization(&mut headers) {
return Box::pin(futures::future::ready(Err(err.into())));
}
let outer_http = self.outer_http.clone();
Box::pin(async move {
let res = cbor_rpc(&outer_http, &endpoint, &method, Some(headers), body).await?;
Ok(res.into_vec())
})
}
}
impl HttpFeatures for Client {
async fn https_call(
&self,
url: &str,
method: http::Method,
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, ) -> Result<reqwest::Response, BoxError> {
if !self.allow_http && !url.starts_with("https://") {
return Err("Invalid url, must start with https://".into());
}
let mut req = self.outer_http.request(method, url);
if let Some(headers) = headers {
req = req.headers(headers);
}
if let Some(body) = body {
req = req.body(body);
}
req.send().await.map_err(|e| e.into())
}
async fn https_signed_call(
&self,
url: &str,
method: http::Method,
message_digest: [u8; 32],
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, ) -> Result<reqwest::Response, BoxError> {
if !self.allow_http && !url.starts_with("https://") {
return Err("Invalid url, must start with https://".into());
}
let se = SignedEnvelope::sign_digest(&self.identity, message_digest.into())?;
let mut headers = headers.unwrap_or_default();
se.to_authorization(&mut headers)?;
let mut req = self.outer_http.request(method, url);
req = req.headers(headers);
if let Some(body) = body {
req = req.body(body);
}
req.send().await.map_err(|e| e.into())
}
async fn https_signed_rpc<T>(
&self,
endpoint: &str,
method: &str,
args: impl Serialize + Send,
) -> Result<T, BoxError>
where
T: DeserializeOwned,
{
if !self.allow_http && !endpoint.starts_with("https://") {
return Err("Invalid endpoint, must start with https://".into());
}
let args = deterministic_cbor_into_vec(&args)?;
let req = RPCRequestRef {
method,
params: &args.into(),
};
let body = deterministic_cbor_into_vec(&req)?;
let digest: [u8; 32] = sha3_256(&body);
let se = SignedEnvelope::sign_digest(&self.identity, digest.into())?;
let mut headers = http::HeaderMap::new();
se.to_authorization(&mut headers)?;
let res = cbor_rpc(&self.outer_http, endpoint, &method, Some(headers), body).await?;
let res = from_reader(&res[..])?;
Ok(res)
}
}
impl CoseSDK for Client {
fn canister(&self) -> &Principal {
&self.cose_canister
}
}
impl CanisterCaller for Client {
async fn canister_query<
In: ArgumentEncoder + Send,
Out: CandidType + for<'a> candid::Deserialize<'a>,
>(
&self,
canister: &Principal,
method: &str,
args: In,
) -> Result<Out, BoxError> {
let input = encode_args(args)?;
let res = self
.agent
.query(canister, method)
.with_arg(input)
.call()
.await?;
let output = Decode!(res.as_slice(), Out)?;
Ok(output)
}
async fn canister_update<
In: ArgumentEncoder + Send,
Out: CandidType + for<'a> candid::Deserialize<'a>,
>(
&self,
canister: &Principal,
method: &str,
args: In,
) -> Result<Out, BoxError> {
let input = encode_args(args)?;
let res = self
.agent
.update(canister, method)
.with_arg(input)
.call_and_wait()
.await?;
let output = Decode!(res.as_slice(), Out)?;
Ok(output)
}
}