diff --git a/Cargo.lock b/Cargo.lock index 5cdd242f2..1b123be33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,6 +1229,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "url", ] [[package]] diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index b978cd1d8..073e89ffc 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -18,6 +18,7 @@ rand.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["io-std", "macros"] } tracing.workspace = true +url.workspace = true calimero-context = { path = "../context" } calimero-blobstore = { path = "../store/blobs" } diff --git a/crates/node/src/interactive_cli.rs b/crates/node/src/interactive_cli.rs index a5eaaf139..2d720da4e 100644 --- a/crates/node/src/interactive_cli.rs +++ b/crates/node/src/interactive_cli.rs @@ -16,30 +16,30 @@ use clap::{Parser, Subcommand}; use crate::Node; #[derive(Debug, Parser)] +#[command(multicall = true, bin_name = "{repl}")] #[non_exhaustive] pub struct RootCommand { #[command(subcommand)] - pub action: SubCommands, + pub action: SubCommand, } #[derive(Debug, Subcommand)] #[non_exhaustive] -pub enum SubCommands { +pub enum SubCommand { + #[command(alias = "app")] Application(applications::ApplicationCommand), Call(call::CallCommand), Context(context::ContextCommand), Identity(identity::IdentityCommand), Peers(peers::PeersCommand), - Store(store::StoreCommand), + // Store(store::StoreCommand), State(state::StateCommand), } pub async fn handle_line(node: &mut Node, line: String) -> eyre::Result<()> { - // IMPORTANT: Parser needs first string to be binary name - let mut args = vec!["{repl}"]; - args.extend(line.split_whitespace()); + let mut args = line.split_whitespace().peekable(); - if args.len() == 1 { + if args.peek().is_none() { return Ok(()); } @@ -52,13 +52,13 @@ pub async fn handle_line(node: &mut Node, line: String) -> eyre::Result<()> { }; match command.action { - SubCommands::Application(application) => application.run(node).await?, - SubCommands::Call(call) => call.run(node).await?, - SubCommands::Context(context) => context.run(node).await?, - SubCommands::Identity(identity) => identity.run(node)?, - SubCommands::Peers(peers) => peers.run(node.network_client.clone().into()).await?, - SubCommands::State(state) => state.run(node)?, - SubCommands::Store(store) => store.run(node)?, + SubCommand::Application(application) => application.run(node).await?, + SubCommand::Call(call) => call.run(node).await?, + SubCommand::Context(context) => context.run(node).await?, + SubCommand::Identity(identity) => identity.run(node)?, + SubCommand::Peers(peers) => peers.run(node.network_client.clone().into()).await?, + SubCommand::State(state) => state.run(node)?, + // SubCommand::Store(store) => store.run(node)?, } Ok(()) diff --git a/crates/node/src/interactive_cli/applications.rs b/crates/node/src/interactive_cli/applications.rs index 9ac4d1d15..4062b93c8 100644 --- a/crates/node/src/interactive_cli/applications.rs +++ b/crates/node/src/interactive_cli/applications.rs @@ -1,10 +1,13 @@ +use calimero_primitives::hash::Hash; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use eyre::Result; use owo_colors::OwoColorize; +use url::Url; use crate::Node; +/// Manage applications #[derive(Debug, Parser)] pub struct ApplicationCommand { #[command(subcommand)] @@ -13,40 +16,58 @@ pub struct ApplicationCommand { #[derive(Debug, Subcommand)] enum ApplicationSubcommand { + /// List installed applications + Ls, + /// Install an application Install { - #[arg(value_enum)] - type_: InstallType, - resource: String, - metadata: Option, + #[command(subcommand)] + resource: Resource, }, - Ls, } -#[derive(Debug, clap::ValueEnum, Clone)] -enum InstallType { - Url, - File, +#[derive(Debug, Subcommand)] +enum Resource { + /// Install an application from a URL + Url { + /// The URL to download the application from + url: Url, + /// The hash of the application (bs58 encoded) + hash: Option, + /// Metadata to associate with the application + metadata: Option, + }, + /// Install an application from a file + File { + /// The file path to the application + path: Utf8PathBuf, + /// Metadata to associate with the application + metadata: Option, + }, } impl ApplicationCommand { pub async fn run(self, node: &Node) -> Result<()> { let ind = ">>".blue(); match self.command { - ApplicationSubcommand::Install { - type_, - resource, - metadata, - } => { - let application_id = match type_ { - InstallType::Url => { - let url = resource.parse()?; + ApplicationSubcommand::Install { resource } => { + let application_id = match resource { + Resource::Url { + url, + hash, + metadata, + } => { println!("{ind} Downloading application.."); node.ctx_manager - .install_application_from_url(url, vec![], None) + .install_application_from_url( + url, + metadata + .map(|x| x.as_bytes().to_owned()) + .unwrap_or_default(), + hash, + ) .await? } - InstallType::File => { - let path = Utf8PathBuf::from(resource); + Resource::File { path, metadata } => { if let Ok(application_id) = node .ctx_manager .install_application_from_path( diff --git a/crates/node/src/interactive_cli/call.rs b/crates/node/src/interactive_cli/call.rs index 2327013b1..6efc85947 100644 --- a/crates/node/src/interactive_cli/call.rs +++ b/crates/node/src/interactive_cli/call.rs @@ -6,11 +6,16 @@ use serde_json::Value; use crate::Node; +/// Call a method on a context #[derive(Debug, Parser)] pub struct CallCommand { + /// The context ID to call the method on context_id: ContextId, + /// The method to call method: String, + /// The payload to send to the method payload: Value, + /// The public key of the executor executor_key: PublicKey, } diff --git a/crates/node/src/interactive_cli/context.rs b/crates/node/src/interactive_cli/context.rs index d50de5655..645a69e60 100644 --- a/crates/node/src/interactive_cli/context.rs +++ b/crates/node/src/interactive_cli/context.rs @@ -1,15 +1,17 @@ -use core::mem::replace; -use core::str::FromStr; - +use calimero_primitives::application::ApplicationId; +use calimero_primitives::context::{ContextId, ContextInvitationPayload}; use calimero_primitives::hash::Hash; +use calimero_primitives::identity::{PrivateKey, PublicKey}; use calimero_store::key::ContextMeta as ContextMetaKey; use clap::{Parser, Subcommand}; use eyre::Result; use owo_colors::OwoColorize; +use serde_json::Value; use tokio::sync::oneshot; use crate::Node; +/// Manage contexts #[derive(Debug, Parser)] pub struct ContextCommand { #[command(subcommand)] @@ -18,26 +20,43 @@ pub struct ContextCommand { #[derive(Debug, Subcommand)] enum Commands { + /// List contexts Ls, - Join { - private_key: String, - invitation_payload: String, - }, - Leave { - context_id: String, - }, + /// Create a context Create { - application_id: String, - context_seed: Option, - params: Option, + /// The application ID to create the context with + application_id: ApplicationId, + /// The initialization parameters for the context + params: Option, + /// The seed for the context (to derive a deterministic context ID) + #[clap(long = "seed")] + context_seed: Option, }, + /// Invite a user to a context Invite { - context_id: String, - inviter_id: String, - invitee_id: String, + /// The context ID to invite the user to + context_id: ContextId, + /// The ID of the inviter + inviter_id: PublicKey, + /// The ID of the invitee + invitee_id: PublicKey, + }, + /// Join a context + Join { + /// The private key of the user + private_key: PrivateKey, + /// The invitation payload from the inviter + invitation_payload: ContextInvitationPayload, + }, + /// Leave a context + Leave { + /// The context ID to leave + context_id: ContextId, }, + /// Delete a context Delete { - context_id: String, + /// The context ID to delete + context_id: ContextId, }, } @@ -76,9 +95,6 @@ impl ContextCommand { private_key, invitation_payload, } => { - let private_key = private_key.parse()?; - let invitation_payload = invitation_payload.parse()?; - let response = node .ctx_manager .join_context(private_key, invitation_payload) @@ -95,7 +111,6 @@ impl ContextCommand { } } Commands::Leave { context_id } => { - let context_id = context_id.parse()?; if node.ctx_manager.delete_context(&context_id).await? { println!("{ind} Successfully deleted context {context_id}"); } else { @@ -105,39 +120,20 @@ impl ContextCommand { } Commands::Create { application_id, + params, context_seed, - mut params, } => { - let application_id = application_id.parse()?; - - let (context_seed, params) = 'infer: { - let Some(context_seed) = context_seed else { - break 'infer (None, None); - }; - let context_seed_clone = context_seed.clone(); - - if let Ok(context_seed) = context_seed.parse::() { - break 'infer (Some(context_seed), params); - }; - - match replace(&mut params, Some(context_seed)) - .map(|arg0| FromStr::from_str(&arg0)) - { - Some(Ok(context_seed)) => break 'infer (Some(context_seed), params), - None => break 'infer (None, params), - _ => {} - }; - println!("{ind} Invalid context seed: {context_seed_clone}"); - return Err(eyre::eyre!("Invalid context seed")); - }; - let (tx, rx) = oneshot::channel(); node.ctx_manager.create_context( context_seed.map(Into::into), application_id, None, - params.map(|x| x.as_bytes().to_owned()).unwrap_or_default(), + params + .as_ref() + .map(serde_json::to_vec) + .transpose()? + .unwrap_or_default(), tx, )?; @@ -159,10 +155,6 @@ impl ContextCommand { inviter_id, invitee_id, } => { - let context_id = context_id.parse()?; - let inviter_id = inviter_id.parse()?; - let invitee_id = invitee_id.parse()?; - if let Some(invitation_payload) = node .ctx_manager .invite_to_context(context_id, inviter_id, invitee_id) @@ -175,7 +167,6 @@ impl ContextCommand { } } Commands::Delete { context_id } => { - let context_id = context_id.parse()?; let _ = node.ctx_manager.delete_context(&context_id).await?; println!("{ind} Deleted context {context_id}"); } diff --git a/crates/node/src/interactive_cli/identity.rs b/crates/node/src/interactive_cli/identity.rs index cebeeea72..88e5e2228 100644 --- a/crates/node/src/interactive_cli/identity.rs +++ b/crates/node/src/interactive_cli/identity.rs @@ -8,6 +8,7 @@ use owo_colors::OwoColorize; use crate::Node; +/// Manage identities #[derive(Debug, Parser)] pub struct IdentityCommand { #[command(subcommand)] @@ -16,7 +17,12 @@ pub struct IdentityCommand { #[derive(Debug, Subcommand)] enum IdentitySubcommands { - Ls { context_id: String }, + /// List identities in a context + Ls { + /// The context ID to list identities in + context_id: String, + }, + /// Create a new identity New, } diff --git a/crates/node/src/interactive_cli/peers.rs b/crates/node/src/interactive_cli/peers.rs index a6f58f15e..6a8e65660 100644 --- a/crates/node/src/interactive_cli/peers.rs +++ b/crates/node/src/interactive_cli/peers.rs @@ -7,9 +7,11 @@ use eyre::Result; use libp2p::gossipsub::TopicHash; use owo_colors::OwoColorize; +/// List the peers in the network #[derive(Copy, Clone, Debug, Parser)] pub struct PeersCommand { - topic: Option, + /// The context ID to list the peers for + context_id: Option, } impl PeersCommand { @@ -20,8 +22,8 @@ impl PeersCommand { network_client.peer_count().await.cyan() ); - if let Some(topic) = self.topic { - let topic = TopicHash::from_raw(topic); + if let Some(context_id) = self.context_id { + let topic = TopicHash::from_raw(context_id); println!( "{ind} Peers (Session) for Topic {}: {:#?}", topic.clone(), diff --git a/crates/node/src/interactive_cli/state.rs b/crates/node/src/interactive_cli/state.rs index 0210f016f..6c9d986b5 100644 --- a/crates/node/src/interactive_cli/state.rs +++ b/crates/node/src/interactive_cli/state.rs @@ -7,44 +7,51 @@ use owo_colors::OwoColorize; use crate::Node; -#[derive(Debug, Parser)] +/// View the raw state of contexts +#[derive(Copy, Clone, Debug, Parser)] pub struct StateCommand { - context_id: String, + /// The context ID to view the state for + context_id: Option, } + impl StateCommand { pub fn run(self, node: &Node) -> Result<()> { let ind = ">>".blue(); + let handle = node.store.handle(); let mut iter = handle.iter::()?; - println!("{ind} {:44} | {:44}", "State Key", "Value"); - - let context_id = match self.context_id.parse::() { - Ok(id) => id, - Err(e) => eyre::bail!("{} Failed to parse context_id: {}", ind.red(), e), - }; - - let first = 'first: { - let Some(k) = iter - .seek(ContextStateKey::new(context_id, [0; 32])) - .transpose() - else { - break 'first None; - }; + println!( + "{ind} {c1:44} | {c2:44} | Value", + c1 = "Context ID", + c2 = "State Key", + ); - Some((k, iter.read().map(|s| s.value.into_boxed()))) - }; + let first = self.context_id.and_then(|s| { + Some(( + iter.seek(ContextStateKey::new(s, [0; 32])).transpose()?, + iter.read().map(|v| v.value.into_boxed()), + )) + }); let rest = iter .entries() - .map(|(k, v)| (k, v.map(|s| s.value.into_boxed()))); + .map(|(k, v)| (k, v.map(|v| v.value.into_boxed()))); for (k, v) in first.into_iter().chain(rest) { let (k, v) = (k?, v?); - if k.context_id() != context_id { - break; + + let (cx, state_key) = (k.context_id(), k.state_key()); + + if let Some(context_id) = self.context_id { + if cx != context_id { + break; + } } - let entry = format!("{:44} | {:?}", Hash::from(k.state_key()), v); + + let sk = Hash::from(state_key); + + let entry = format!("{c1:44} | {c2:44} | {c3:?}", c1 = cx, c2 = sk, c3 = v); for line in entry.lines() { println!("{ind} {}", line.cyan()); } diff --git a/crates/node/src/interactive_cli/store.rs b/crates/node/src/interactive_cli/store.rs index 34af2839c..712e03ff7 100644 --- a/crates/node/src/interactive_cli/store.rs +++ b/crates/node/src/interactive_cli/store.rs @@ -1,6 +1,4 @@ -use calimero_primitives::hash::Hash; -use calimero_store::key::ContextState as ContextStateKey; -use clap::Parser; +use clap::{Parser, Subcommand}; use eyre::Result; use owo_colors::OwoColorize; @@ -8,31 +6,64 @@ use crate::Node; #[derive(Debug, Parser)] #[non_exhaustive] -pub struct StoreCommand; +pub struct StoreCommand { + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Ls, + Set, + Get, +} impl StoreCommand { // todo! revisit: get specific context state - pub fn run(self, node: &Node) -> Result<()> { - println!("Executing Store command"); + pub fn run(self, _node: &Node) -> Result<()> { let ind = ">>".blue(); - println!( - "{ind} {c1:44} | {c2:44} | Value", - c1 = "Context ID", - c2 = "State Key", - ); - - let handle = node.store.handle(); - - for (k, v) in handle.iter::()?.entries() { - let (k, v) = (k?, v?); - let (cx, state_key) = (k.context_id(), k.state_key()); - let sk = Hash::from(state_key); - let entry = format!("{c1:44} | {c2:44}| {c3:?}", c1 = cx, c2 = sk, c3 = v.value); - for line in entry.lines() { - println!("{ind} {}", line.cyan()); - } - } + println!("{ind} Not implemented yet",); + + // println!( + // "{ind} {c1:44} | {c2:44} | Value", + // c1 = "Context ID", + // c2 = "State Key", + // ); + + // let handle = node.store.handle(); + + // let mut iter = handle.iter::()?; + + // let first = self.context_id.and_then(|s| { + // Some(( + // iter.seek(ContextStateKey::new(s, [0; 32])).transpose()?, + // iter.read().map(|v| v.value.into_boxed()), + // )) + // }); + + // let rest = iter + // .entries() + // .map(|(k, v)| (k, v.map(|v| v.value.into_boxed()))); + + // for (k, v) in first.into_iter().chain(rest) { + // let (k, v) = (k?, v?); + + // let (cx, state_key) = (k.context_id(), k.state_key()); + + // if let Some(context_id) = self.context_id { + // if context_id != cx { + // break; + // } + // } + + // let sk = Hash::from(state_key); + + // let entry = format!("{c1:44} | {c2:44} | {c3:?}", c1 = cx, c2 = sk, c3 = v); + // for line in entry.lines() { + // println!("{ind} {}", line.cyan()); + // } + // } Ok(()) }