Skip to content

Commit

Permalink
chore: improve anda_web3_client
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Feb 15, 2025
1 parent 829baae commit 65fe86e
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 35 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.

6 changes: 3 additions & 3 deletions anda_engine/src/context/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,17 +634,17 @@ impl HttpFeatures for AgentCtx {
/// # Arguments
/// * `endpoint` - URL endpoint to send the request to
/// * `method` - RPC method name to call
/// * `params` - Parameters to serialize as CBOR and send with the request
/// * `args` - Arguments to serialize as CBOR and send with the request
async fn https_signed_rpc<T>(
&self,
endpoint: &str,
method: &str,
params: impl Serialize + Send,
args: impl Serialize + Send,
) -> Result<T, BoxError>
where
T: DeserializeOwned,
{
self.base.https_signed_rpc(endpoint, method, params).await
self.base.https_signed_rpc(endpoint, method, args).await
}
}

Expand Down
10 changes: 5 additions & 5 deletions anda_engine/src/context/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,22 +605,22 @@ impl HttpFeatures for BaseCtx {
/// # Arguments
/// * `endpoint` - URL endpoint to send the request to
/// * `method` - RPC method name to call
/// * `params` - Parameters to serialize as CBOR and send with the request
/// * `args` - Arguments to serialize as CBOR and send with the request
async fn https_signed_rpc<T>(
&self,
endpoint: &str,
method: &str,
params: impl Serialize + Send,
args: impl Serialize + Send,
) -> Result<T, BoxError>
where
T: DeserializeOwned,
{
match self.web3.as_ref() {
Web3SDK::Tee(cli) => cli.https_signed_rpc(endpoint, method, params).await,
Web3SDK::Tee(cli) => cli.https_signed_rpc(endpoint, method, args).await,
Web3SDK::Web3(Web3Client { client: cli }) => {
let params = to_cbor_bytes(&params);
let args = to_cbor_bytes(&args);
let res = cli
.https_signed_rpc_raw(endpoint.to_string(), method.to_string(), params)
.https_signed_rpc_raw(endpoint.to_string(), method.to_string(), args)
.await?;
let res = from_reader(&res[..])?;
Ok(res)
Expand Down
4 changes: 2 additions & 2 deletions anda_engine/src/context/web3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ pub trait Web3ClientFeatures: Send + Sync + 'static {
/// # Arguments
/// * `endpoint` - URL endpoint to send the request to
/// * `method` - RPC method name to call
/// * `params` - Parameters to serialize as CBOR and send with the request
/// * `args` - Arguments to serialize as CBOR and send with the request
fn https_signed_rpc_raw(
&self,
endpoint: String,
method: String,
params: Vec<u8>,
args: Vec<u8>,
) -> BoxPinFut<Result<Vec<u8>, BoxError>>;
}

Expand Down
2 changes: 1 addition & 1 deletion anda_engine_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "anda_engine_cli"
description = "The command line interface for Anda engine server."
repository = "https://github.com/ldclabs/anda/tree/main/anda_engine_cli"
publish = false # can't publish this crate because `anda_web3_client` is not published
version = "0.1.0"
version = "0.1.1"
edition.workspace = true
keywords.workspace = true
categories.workspace = true
Expand Down
19 changes: 7 additions & 12 deletions anda_engine_cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use anda_core::{AgentOutput, BoxError};
use anda_engine::context::Web3ClientFeatures;
use anda_core::{AgentOutput, BoxError, HttpFeatures};
use anda_web3_client::client::Client as Web3Client;
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use ciborium::from_reader;
use clap::{Parser, Subcommand};
use ic_cose_types::to_cbor_bytes;
use rand::{thread_rng, RngCore};

#[derive(Parser)]
Expand Down Expand Up @@ -88,11 +85,10 @@ async fn main() -> Result<(), BoxError> {
let web3 =
Web3Client::new(&cli.ic_host, id_secret, [0u8; 48], None, Some(true)).await?;
println!("principal: {}", web3.get_principal());
let args = to_cbor_bytes(&(&name, &prompt, None::<Vec<u8>>));
let res = web3
.https_signed_rpc_raw(endpoint.to_owned(), "agent_run".to_string(), args)

let res: AgentOutput = web3
.https_signed_rpc(endpoint, "agent_run", &(&name, &prompt, None::<Vec<u8>>))
.await?;
let res: AgentOutput = from_reader(&res[..])?;
println!("{:?}", res);
}

Expand All @@ -107,11 +103,10 @@ async fn main() -> Result<(), BoxError> {
let web3 =
Web3Client::new(&cli.ic_host, id_secret, [0u8; 48], None, Some(true)).await?;
println!("principal: {}", web3.get_principal());
let args = to_cbor_bytes(&(&name, &args));
let res = web3
.https_signed_rpc_raw(endpoint.to_owned(), "tool_call".to_string(), args)

let res: (String, bool) = web3
.https_signed_rpc(endpoint, "tool_call", &(&name, &args))
.await?;
let res: (String, bool) = from_reader(&res[..])?;
println!("{:?}", res);
}

Expand Down
4 changes: 3 additions & 1 deletion anda_web3_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "anda_web3_client"
description = "The Rust SDK for Web3 integration in non-TEE environments."
repository = "https://github.com/ldclabs/anda/tree/main/anda_web3_client"
publish = false # can't publish this crate because `ic-crypto-secp256k1` and `ic-crypto-ed25519` are not published
version = "0.1.0"
version = "0.1.1"
edition.workspace = true
keywords.workspace = true
categories.workspace = true
Expand All @@ -15,6 +15,8 @@ anda_engine = { path = "../anda_engine", version = "0.4" }
futures = { workspace = true }
http = { workspace = true }
candid = { workspace = true }
ciborium = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
ic-agent = { workspace = true }
ic_cose = { workspace = true }
Expand Down
124 changes: 115 additions & 9 deletions anda_web3_client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use anda_core::{cbor_rpc, BoxError, BoxPinFut, RPCRequest};
use anda_core::{cbor_rpc, BoxError, BoxPinFut, HttpFeatures, RPCRequest};
use anda_engine::context::Web3ClientFeatures;
use candid::{
utils::{encode_args, ArgumentEncoder},
CandidType, Decode, Principal,
};
use ciborium::from_reader;
use ed25519_consensus::SigningKey;
use ic_agent::identity::BasicIdentity;
use ic_cose::client::CoseSDK;
Expand All @@ -16,6 +17,7 @@ use ic_cose_types::{
to_cbor_bytes, CanisterCaller,
};
use ic_tee_agent::http::sign_digest_to_headers;
use serde::{de::DeserializeOwned, Serialize};
use std::{sync::Arc, time::Duration};

pub use ic_agent::{Agent, Identity};
Expand Down Expand Up @@ -56,7 +58,7 @@ impl Client {
cose_canister: Option<Principal>,
is_dev: Option<bool>,
) -> Result<Self, BoxError> {
let is_dev = is_dev.unwrap_or(false);
let is_dev = is_dev.unwrap_or(ic_host.starts_with("http://"));
let outer_http = reqwest::Client::builder()
.use_rustls_tls()
.https_only(!is_dev)
Expand Down Expand Up @@ -322,11 +324,14 @@ impl Web3ClientFeatures for Client {
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, // default is empty
) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
if !self.is_dev && !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 {
if !url.starts_with("https://") {
return Err("Invalid URL, must start with https://".into());
}
let mut req = outer_http.request(method, url);
if let Some(headers) = headers {
req = req.headers(headers);
Expand All @@ -347,6 +352,12 @@ impl Web3ClientFeatures for Client {
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, // default is empty
) -> BoxPinFut<Result<reqwest::Response, BoxError>> {
if !self.is_dev && !url.starts_with("https://") {
return Box::pin(futures::future::ready(Err(
"Invalid url, must start with https://".into(),
)));
}

let mut headers = headers.unwrap_or_default();
if let Err(err) =
sign_digest_to_headers(self.identity.as_ref(), &mut headers, &message_digest)
Expand All @@ -355,11 +366,7 @@ impl Web3ClientFeatures for Client {
}

let outer_http = self.outer_http.clone();
let is_dev = self.is_dev;
Box::pin(async move {
if !is_dev && !url.starts_with("https://") {
return Err("Invalid URL, must start with https://".into());
}
let mut req = outer_http.request(method, url);
req = req.headers(headers);
if let Some(body) = body {
Expand All @@ -376,6 +383,12 @@ impl Web3ClientFeatures for Client {
method: String,
args: Vec<u8>,
) -> BoxPinFut<Result<Vec<u8>, BoxError>> {
if !self.is_dev && !endpoint.starts_with("https://") {
return Box::pin(futures::future::ready(Err(
"Invalid endpoint, must start with https://".into(),
)));
}

let req = RPCRequest {
method: &method,
params: &args.into(),
Expand All @@ -394,6 +407,99 @@ impl Web3ClientFeatures for Client {
}
}

impl HttpFeatures for Client {
/// Makes an HTTPs request
///
/// # Arguments
/// * `url` - Target URL, should start with `https://`
/// * `method` - HTTP method (GET, POST, etc.)
/// * `headers` - Optional HTTP headers
/// * `body` - Optional request body (default empty)
async fn https_call(
&self,
url: &str,
method: http::Method,
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, // default is empty
) -> Result<reqwest::Response, BoxError> {
if !self.is_dev && !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())
}

/// Makes a signed HTTPs request with message authentication
///
/// # Arguments
/// * `url` - Target URL
/// * `method` - HTTP method (GET, POST, etc.)
/// * `message_digest` - 32-byte message digest for signing
/// * `headers` - Optional HTTP headers
/// * `body` - Optional request body (default empty)
async fn https_signed_call(
&self,
url: &str,
method: http::Method,
message_digest: [u8; 32],
headers: Option<http::HeaderMap>,
body: Option<Vec<u8>>, // default is empty
) -> Result<reqwest::Response, BoxError> {
if !self.is_dev && !url.starts_with("https://") {
return Err("Invalid url, must start with https://".into());
}
let mut headers = headers.unwrap_or_default();
sign_digest_to_headers(self.identity.as_ref(), &mut headers, &message_digest)?;

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())
}

/// Makes a signed CBOR-encoded RPC call
///
/// # Arguments
/// * `endpoint` - URL endpoint to send the request to
/// * `method` - RPC method name to call
/// * `args` - Arguments to serialize as CBOR and send with the request
async fn https_signed_rpc<T>(
&self,
endpoint: &str,
method: &str,
args: impl Serialize + Send,
) -> Result<T, BoxError>
where
T: DeserializeOwned,
{
if !self.is_dev && !endpoint.starts_with("https://") {
return Err("Invalid endpoint, must start with https://".into());
}
let args = to_cbor_bytes(&args);
let req = RPCRequest {
method,
params: &args.into(),
};
let body = to_cbor_bytes(&req);
let digest: [u8; 32] = sha3_256(&body);
let mut headers = http::HeaderMap::new();
sign_digest_to_headers(self.identity.as_ref(), &mut headers, &digest)?;
let res = cbor_rpc(&self.outer_http, endpoint, &method, Some(headers), body).await?;
let res = from_reader(&res[..])?;
Ok(res)
}
}

/// Implements the `CoseSDK` trait for Client to enable IC-COSE canister API calls
///
/// This implementation provides the necessary interface to interact with the
Expand Down

0 comments on commit 65fe86e

Please sign in to comment.