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 ed25519_consensus::SigningKey;
use ic_agent::identity::{AnonymousIdentity, BasicIdentity, Secp256k1Identity};
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,
},
to_cbor_bytes,
};
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],
id: Principal,
identity: Arc<dyn Identity>,
agent: Arc<Agent>,
cose_canister: Principal,
allow_http: bool,
}
pub struct ClientBuilder {
ic_host: String,
root_secret: [u8; 48],
identity: Arc<dyn Identity>,
cose_canister: Principal,
outer_http: reqwest::Client,
allow_http: bool,
}
pub fn identity_from_secret(id_secret: [u8; 32]) -> Box<dyn Identity> {
let sk = SigningKey::from(id_secret);
Box::new(BasicIdentity::from_signing_key(sk))
}
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 = const_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: Arc::new(AnonymousIdentity),
cose_canister: Principal::anonymous(),
outer_http: reqwest::Client::builder()
.use_rustls_tls()
.https_only(true)
.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(360))
.gzip(true)
.user_agent(APP_USER_AGENT)
.build()
.expect("Could not create HTTP client"),
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_identity(mut self, identity: Arc<dyn Identity>) -> Self {
self.identity = identity;
self
}
pub fn with_cose_canister(mut self, cose_canister: Principal) -> Self {
self.cose_canister = cose_canister;
self
}
pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self {
self.outer_http = http_client;
self
}
pub fn with_allow_http(
mut self,
allow_http: bool,
http_client: Option<reqwest::Client>,
) -> Self {
self.allow_http = allow_http;
self.outer_http = http_client.unwrap_or_else(|| {
reqwest::Client::builder()
.use_rustls_tls()
.https_only(!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(360))
.user_agent(APP_USER_AGENT)
.build()
.expect("Could not create HTTP client")
});
self
}
pub async fn build(self) -> Result<Client, BoxError> {
let agent = Agent::builder()
.with_url(&self.ic_host)
.with_verify_query_signatures(false)
.with_arc_identity(self.identity.clone())
.with_http_client(self.outer_http.clone())
.build()?;
if self.ic_host.starts_with("http://") {
agent.fetch_root_key().await?;
}
Ok(Client {
outer_http: self.outer_http,
root_secret: self.root_secret,
id: self
.identity
.sender()
.expect("Failed to get sender principal"),
identity: self.identity,
agent: Arc::new(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.id
}
}
impl Web3ClientFeatures for Client {
fn a256gcm_key(&self, derivation_path: &[&[u8]]) -> BoxPinFut<Result<[u8; 32], BoxError>> {
let res = crypto::a256gcm_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
Box::pin(futures::future::ready(Ok(res.into_array())))
}
fn ed25519_sign_message(
&self,
derivation_path: &[&[u8]],
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::ed25519_sign_message(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
message,
);
Box::pin(futures::future::ready(Ok(res.into_array())))
}
fn ed25519_verify(
&self,
derivation_path: &[&[u8]],
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::ed25519_public_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
Box::pin(futures::future::ready(
ed25519_verify(&res.0, message, signature).map_err(|e| e.into()),
))
}
fn ed25519_public_key(
&self,
derivation_path: &[&[u8]],
) -> BoxPinFut<Result<[u8; 32], BoxError>> {
let res = crypto::ed25519_public_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
Box::pin(futures::future::ready(Ok(res.0.into_array())))
}
fn secp256k1_sign_message_bip340(
&self,
derivation_path: &[&[u8]],
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::secp256k1_sign_message_bip340(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
message,
);
Box::pin(futures::future::ready(Ok(res.into_array())))
}
fn secp256k1_verify_bip340(
&self,
derivation_path: &[&[u8]],
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::secp256k1_public_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
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: &[&[u8]],
message: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::secp256k1_sign_message_ecdsa(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
message,
);
Box::pin(futures::future::ready(Ok(res.into_array())))
}
fn secp256k1_sign_digest_ecdsa(
&self,
derivation_path: &[&[u8]],
message_hash: &[u8],
) -> BoxPinFut<Result<[u8; 64], BoxError>> {
let res = crypto::secp256k1_sign_digest_ecdsa(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
message_hash,
);
Box::pin(futures::future::ready(Ok(res.into_array())))
}
fn secp256k1_verify_ecdsa(
&self,
derivation_path: &[&[u8]],
message: &[u8],
signature: &[u8],
) -> BoxPinFut<Result<(), BoxError>> {
let res = crypto::secp256k1_public_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
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: &[&[u8]],
) -> BoxPinFut<Result<[u8; 33], BoxError>> {
let res = crypto::secp256k1_public_key(
&self.root_secret,
derivation_path.iter().map(|v| v.to_vec()).collect(),
);
Box::pin(futures::future::ready(Ok(res.0.into_array())))
}
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.as_ref(), 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_headers(&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 = to_cbor_bytes(&req);
let digest: [u8; 32] = sha3_256(&body);
let se = match SignedEnvelope::sign_digest(self.identity.as_ref(), 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_headers(&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.as_ref(), message_digest.into())?;
let mut headers = headers.unwrap_or_default();
se.to_headers(&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 = to_cbor_bytes(&args);
let req = RPCRequestRef {
method,
params: &args.into(),
};
let body = to_cbor_bytes(&req);
let digest: [u8; 32] = sha3_256(&body);
let se = SignedEnvelope::sign_digest(self.identity.as_ref(), digest.into())?;
let mut headers = http::HeaderMap::new();
se.to_headers(&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)
}
}