Skip to content

Commit

Permalink
offline mode send (#72)
Browse files Browse the repository at this point in the history
* output all offline calls to messages.json; add -i option to enter repl after script

* checkpoint

* works

* fix

* fix

* add send function

* fix
  • Loading branch information
chenyan-dfinity authored Aug 11, 2023
1 parent d88f85d commit 39a0bec
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 73 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ic-repl"
version = "0.4.1"
version = "0.4.2"
authors = ["DFINITY Team"]
edition = "2021"
default-run = "ic-repl"
Expand Down Expand Up @@ -38,6 +38,7 @@ ring = "0.16"
rpassword = "7.2"
serde = "1.0"
serde_json = "1.0"
serde_cbor = "0.11"
hex = { version = "0.4", features = ["serde"] }
sha2 = "0.10"
crc32fast = "1.3"
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Canister REPL

```
ic-repl [--replica [local|ic|url] | --offline [--format [ascii|png]]] --config <dhall config> [script file]
ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --config <dhall config> [script file]
```

## Commands
Expand Down Expand Up @@ -47,7 +47,6 @@ You cannot define recursive functions, as there is no control flow in the langua
We also provide some built-in functions:
* `account(principal)`: convert principal to account id.
* `neuron_account(principal, nonce)`: convert (principal, nonce) to account in the governance canister.
* `metadata(principal, path)`: fetch the HTTP endpoint of `canister/<principal>/<path>`.
* `file(path)`: load external file as a blob value.
* `gzip(blob)`: gzip a blob value.
* `stringify(exp1, exp2, exp3, ...)`: convert all expressions to string and concat. Only supports primitive types.
Expand All @@ -57,6 +56,11 @@ We also provide some built-in functions:
* `concat(e1, e2)`: concatenate two vec/record/text together.
* `add/sub/mul/div(e1, e2)`: addition/subtraction/multiplication/division of two integer/float numbers. If one of the arguments is float32/float64, the result is float64; otherwise, the result is integer. You can use type annotation to get the integer part of the float number. For example `div((mul(div(1, 3.0), 1000) : nat), 100.0)` returns `3.33`.

The following functions are only available in non-offline mode:
* `metadata(principal, path)`: fetch the HTTP endpoint of `canister/<principal>/<path>`.
* `send(blob)`: send signed JSON messages generated from offline mode. The function can take a single message or an array of messages. Most likely use is `send(file("messages.json"))`. The return result is the return results of all calls. Alternatively, you can use `ic-repl -s messages.json -r ic`.


## Object methods

For `vec`, `record` or `text` value, we provide some built-in methods for value transformation:
Expand Down
46 changes: 35 additions & 11 deletions src/exp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use candid::{
utils::check_unique,
Principal, TypeEnv,
};
use ic_agent::Agent;
use std::collections::BTreeMap;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -127,7 +126,7 @@ impl Exp {
}
_ => return Err(anyhow!("neuron_account expects (principal, nonce)")),
},
"metadata" => match args.as_slice() {
"metadata" if helper.offline.is_none() => match args.as_slice() {
[IDLValue::Principal(id), IDLValue::Text(path)] => {
let res = fetch_metadata(&helper.agent, *id, path)?;
IDLValue::Vec(res.into_iter().map(IDLValue::Nat8).collect())
Expand Down Expand Up @@ -164,6 +163,23 @@ impl Exp {
}
_ => return Err(anyhow!("gzip expects blob")),
},
"send" if helper.offline.is_none() => match args.as_slice() {
[IDLValue::Vec(blob)] => {
use crate::offline::{send, send_messages};
let blob: Vec<u8> = blob.iter().filter_map(|v| match v {
IDLValue::Nat8(n) => Some(*n),
_ => None,
}).collect();
let json = std::str::from_utf8(&blob)?;
let res = match json.trim_start().chars().next() {
Some('{') => send(helper, &serde_json::from_str(json)?)?,
Some('[') => send_messages(helper, &serde_json::from_str(json)?)?,
_ => return Err(anyhow!("not a valid json message")),
};
args_to_value(res)
}
_ => return Err(anyhow!("send expects a json blob")),
},
"wasm_profiling" => match args.as_slice() {
[IDLValue::Text(file)] | [IDLValue::Text(file), IDLValue::Vec(_)] => {
let path = resolve_path(&helper.base_path, file);
Expand Down Expand Up @@ -360,7 +376,7 @@ impl Exp {
0
};
let res = call(
&helper.agent,
helper,
&info.canister_id,
&method.method,
&bytes,
Expand Down Expand Up @@ -482,7 +498,7 @@ pub struct MethodInfo {
pub profiling: Option<BTreeMap<u16, String>>,
}
impl Method {
fn get_info(&self, helper: &MyHelper) -> Result<MethodInfo> {
pub fn get_info(&self, helper: &MyHelper) -> Result<MethodInfo> {
let canister_id = str_to_principal(&self.canister, helper)?;
let agent = &helper.agent;
let mut map = helper.canister_map.borrow_mut();
Expand Down Expand Up @@ -528,14 +544,15 @@ impl Method {

#[tokio::main]
async fn call(
agent: &Agent,
helper: &MyHelper,
canister_id: &Principal,
method: &str,
args: &[u8],
opt_func: &Option<(TypeEnv, Function)>,
offline: &Option<OfflineOutput>,
) -> anyhow::Result<IDLArgs> {
use crate::offline::*;
let agent = &helper.agent;
let effective_id = get_effective_canister_id(*canister_id, method, args)?;
let is_query = opt_func
.as_ref()
Expand All @@ -547,12 +564,17 @@ async fn call(
.with_arg(args)
.with_effective_canister_id(effective_id);
if let Some(offline) = offline {
let mut msgs = helper.messages.borrow_mut();
let signed = builder.sign()?;
let message = Ingress {
call_type: "query".to_owned(),
request_id: None,
content: hex::encode(signed.signed_query),
let message = IngressWithStatus {
ingress: Ingress {
call_type: "query".to_owned(),
request_id: None,
content: hex::encode(signed.signed_query),
},
request_status: None,
};
msgs.push(message.clone());
output_message(serde_json::to_string(&message)?, offline)?;
return Ok(IDLArgs::new(&[]));
} else {
Expand All @@ -564,6 +586,7 @@ async fn call(
.with_arg(args)
.with_effective_canister_id(effective_id);
if let Some(offline) = offline {
let mut msgs = helper.messages.borrow_mut();
let signed = builder.sign()?;
let status = agent.sign_request_status(effective_id, signed.request_id)?;
let message = IngressWithStatus {
Expand All @@ -572,12 +595,13 @@ async fn call(
request_id: Some(hex::encode(signed.request_id.as_slice())),
content: hex::encode(signed.signed_update),
},
request_status: RequestStatus {
request_status: Some(RequestStatus {
canister_id: status.effective_canister_id,
request_id: hex::encode(status.request_id.as_slice()),
content: hex::encode(status.signed_request_status),
},
}),
};
msgs.push(message.clone());
output_message(serde_json::to_string(&message)?, offline)?;
return Ok(IDLArgs::new(&[]));
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub struct MyHelper {
pub func_env: FuncEnv,
pub base_path: std::path::PathBuf,
pub history: Vec<String>,
pub messages: RefCell<Vec<crate::offline::IngressWithStatus>>,
}

impl MyHelper {
Expand All @@ -108,6 +109,7 @@ impl MyHelper {
agent: self.agent.clone(),
agent_url: self.agent_url.clone(),
offline: self.offline.clone(),
messages: self.messages.clone(),
}
}
pub fn new(agent: Agent, agent_url: String, offline: Option<OfflineOutput>) -> Self {
Expand All @@ -125,6 +127,7 @@ impl MyHelper {
func_env: FuncEnv::default(),
base_path: std::env::current_dir().unwrap(),
history: Vec::new(),
messages: Vec::new().into(),
agent,
agent_url,
offline,
Expand Down Expand Up @@ -186,6 +189,9 @@ impl MyHelper {
};
Ok(())
}
pub fn dump_ingress(&self) -> anyhow::Result<()> {
crate::offline::dump_ingress(&self.messages.borrow())
}
}

#[derive(Debug, PartialEq, Clone)]
Expand Down
14 changes: 0 additions & 14 deletions src/lib.rs

This file was deleted.

79 changes: 50 additions & 29 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use ansi_term::Color;
use clap::Parser;
use ic_agent::agent::http_transport::ReqwestHttpReplicaV2Transport as V2Transport;
use ic_agent::Agent;
use rustyline::error::ReadlineError;
use rustyline::CompletionType;
Expand Down Expand Up @@ -38,8 +39,8 @@ fn repl(opts: Opts) -> anyhow::Result<()> {
.url
.unwrap_or_else(|| "https://qhmh2-niaaa-aaaab-qadta-cai.raw.icp0.io/?msg=".to_string());
Some(match opts.format.as_deref() {
Some("json") => OfflineOutput::Json,
None | Some("ascii") => OfflineOutput::Ascii(send_url),
None | Some("json") => OfflineOutput::Json,
Some("ascii") => OfflineOutput::Ascii(send_url),
Some("png") => OfflineOutput::Png(send_url),
Some("png_no_url") => OfflineOutput::PngNoUrl,
Some("ascii_no_url") => OfflineOutput::AsciiNoUrl,
Expand All @@ -55,9 +56,7 @@ fn repl(opts: Opts) -> anyhow::Result<()> {
};
println!("Ping {url}...");
let agent = Agent::builder()
.with_transport(
ic_agent::agent::http_transport::ReqwestHttpReplicaV2Transport::create(url)?,
)
.with_transport(V2Transport::create(url)?)
.build()?;

println!("Canister REPL");
Expand All @@ -66,6 +65,13 @@ fn repl(opts: Opts) -> anyhow::Result<()> {
.completion_type(CompletionType::List)
.build();
let h = MyHelper::new(agent, url.to_string(), offline);
if let Some(file) = opts.send {
use crate::offline::{send_messages, Messages};
let json = std::fs::read_to_string(file)?;
let msgs = serde_json::from_str::<Messages>(&json)?;
send_messages(&h, &msgs)?;
return Ok(());
}
let mut rl = rustyline::Editor::with_config(config)?;
rl.set_helper(Some(h));
if rl.load_history("./.history").is_err() {
Expand All @@ -75,36 +81,45 @@ fn repl(opts: Opts) -> anyhow::Result<()> {
let config = std::fs::read_to_string(file)?;
rl.helper_mut().unwrap().config = candid::parser::configs::Configs::from_dhall(&config)?;
}

let enter_repl = opts.script.is_none() || opts.interactive;
if let Some(file) = opts.script {
let cmd = Command::Load(file);
let helper = rl.helper_mut().unwrap();
return cmd.run(helper);
cmd.run(helper)?;
}

let mut count = 1;
loop {
let identity = &rl.helper().unwrap().current_identity;
let p = format!("{identity}@{replica} {count}> ");
rl.helper_mut().unwrap().colored_prompt = format!("{}", Color::Green.bold().paint(&p));
let input = rl.readline(&p);
match input {
Ok(line) => {
rl.add_history_entry(&line)?;
unwrap(pretty_parse::<Command>("stdin", &line), |cmd| {
let helper = rl.helper_mut().unwrap();
helper.history.push(line.clone());
unwrap(cmd.run(helper), |_| {});
});
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
Err(err) => {
eprintln!("Error: {err:?}");
break;
if enter_repl {
let mut count = 1;
loop {
let identity = &rl.helper().unwrap().current_identity;
let p = format!("{identity}@{replica} {count}> ");
rl.helper_mut().unwrap().colored_prompt = format!("{}", Color::Green.bold().paint(&p));
let input = rl.readline(&p);
match input {
Ok(line) => {
rl.add_history_entry(&line)?;
unwrap(pretty_parse::<Command>("stdin", &line), |cmd| {
let helper = rl.helper_mut().unwrap();
helper.history.push(line.clone());
unwrap(cmd.run(helper), |_| {});
});
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
Err(err) => {
eprintln!("Error: {err:?}");
break;
}
}
count += 1;
}
rl.save_history("./.history")?;
}
if opts.offline {
let helper = rl.helper().unwrap();
if !helper.messages.borrow().is_empty() {
helper.dump_ingress()?;
}
count += 1;
}
rl.save_history("./.history")?;
Ok(())
}

Expand All @@ -115,7 +130,7 @@ struct Opts {
/// Specifies replica URL, possible values: local, ic, URL
replica: Option<String>,
#[clap(short, long, conflicts_with("replica"))]
/// Offline mode to be run in air-gap machines
/// Offline mode to be run in air-gap machines. All signed messages will be stored in messages.json
offline: bool,
#[clap(short, long, requires("offline"), value_parser = ["ascii", "json", "png", "ascii_no_url", "png_no_url"])]
/// Offline output format
Expand All @@ -128,6 +143,12 @@ struct Opts {
config: Option<String>,
/// ic-repl script file
script: Option<String>,
#[clap(short, long, requires("script"))]
/// Enter repl once the script is finished
interactive: bool,
#[clap(short, long, conflicts_with("script"), conflicts_with("offline"))]
/// Send signed messages
send: Option<String>,
}

fn main() -> anyhow::Result<()> {
Expand Down
Loading

0 comments on commit 39a0bec

Please sign in to comment.