diff --git a/crates/afj-rest/src/cloudagent/connection.rs b/crates/afj-rest/src/cloudagent/connection.rs index c6724e1b..7c58da80 100644 --- a/crates/afj-rest/src/cloudagent/connection.rs +++ b/crates/afj-rest/src/cloudagent/connection.rs @@ -9,7 +9,7 @@ use siera_agent::modules::connection::{ }; /// Create invitation response -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Response { /// Invitation url diff --git a/crates/agent/src/modules/connection.rs b/crates/agent/src/modules/connection.rs index a14d7940..97993041 100644 --- a/crates/agent/src/modules/connection.rs +++ b/crates/agent/src/modules/connection.rs @@ -32,7 +32,7 @@ pub struct ConnectionGetAllOptions { } /// Create invitation response -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Invitation { /// Invitation url #[serde(alias = "invitationUrl")] @@ -46,7 +46,7 @@ pub struct Invitation { } /// A single connection structure -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Connection { /// The connection id used for further functionality #[serde(alias = "connection_id")] diff --git a/crates/agent/src/modules/mod.rs b/crates/agent/src/modules/mod.rs index 853c9be1..749f590b 100644 --- a/crates/agent/src/modules/mod.rs +++ b/crates/agent/src/modules/mod.rs @@ -25,5 +25,8 @@ pub mod schema; /// Multitenancy module for a generic cloudagent pub mod multitenancy; +/// wallet module for a generic cloudagent +pub mod wallet; + /// webhook module for a generic cloudagent pub mod webhook; diff --git a/crates/agent/src/modules/wallet.rs b/crates/agent/src/modules/wallet.rs new file mode 100644 index 00000000..9ef1904a --- /dev/null +++ b/crates/agent/src/modules/wallet.rs @@ -0,0 +1,105 @@ +use crate::error::Result; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +/// Options that are supplied when querying a wallet for DIDs +#[derive(Debug, Deserialize, Serialize)] +pub struct Did { + /// The DID of interest + pub did: Option, + + // TODO: enum + /// The key type to query for eg. ed25519, bls12381g2 + pub key_type: Option, + + // TODO: enum + /// DID method to query for. e.g. sov to only fetch indy/sov DIDs Available values : key, sov + pub method: Option, + + // TODO: enum + /// The DID posture specifying whether the DID is + /// the current public DID, + /// posted to ledger but current public DID, + /// or local to the wallet + /// Available values : public, posted, wallet_only + pub posture: Option, + + /// The verification key of interest + pub verkey: Option, +} + +/// Response from the cloudagent when requesting info about dids +/// of a wallet +#[derive(Debug, Deserialize, Serialize)] +pub struct DidList(Vec); + +/// Response from the cloudagent when requesting info about dids +/// of a wallet +#[derive(Debug, Deserialize, Serialize)] +pub struct DidResult(Did); + +/// Key type in a JSON format k,v pair +#[derive(Debug, Deserialize, Serialize)] +pub struct KeyType { + // TODO: enum + /// The key type to query for eg. ed25519, bls12381g2 + pub key_type: String, +} + +/// Options that are supplied when querying a wallet for DIDs +#[derive(Debug, Deserialize, Serialize)] +pub struct CreateLocalDidOptions { + /// DID method to query for. e.g. sov to only fetch indy/sov DIDs Available values : key, sov + pub method: String, + + /// The key type to query for eg. ed25519, bls12381g2 + pub options: KeyType, +} + +/// Options that are supplied when querying a wallet for DIDs +#[derive(Debug, Deserialize, Serialize)] +pub struct DidEndpoint { + /// The DID of interest + pub did: String, + + /// The endpoint url + pub endpoint: String, +} + +/// Options that are supplied when querying a wallet for DIDs +#[derive(Debug, Deserialize, Serialize)] +pub struct SetDidEndpointOptions { + /// The DID of interest + pub did: String, + + /// The endpoint url + pub endpoint: String, + + ///The endpoint type eg. 'Endpoint' + pub endpoint_type: String, +} + +/// Generic cloudagent basic message module +#[async_trait] +pub trait WalletModule { + /// Query a wallet for DIDs + async fn get_wallet_dids(&self, options: Did) -> Result; + + /// Create a local DID + async fn create_local_did(&self, options: CreateLocalDidOptions) -> Result; + + /// Rotate key pair + async fn rotate_keypair(&self, did: String) -> Result<()>; + + /// Fetch public did + async fn fetch_public_did(&self) -> Result; + + /// Assign the current public DID + async fn assign_public_did(&self, did: String) -> Result; + + /// Query DID endpoint of wallet + async fn fetch_did_endpoint(&self, did: String) -> Result; + + /// Set DID endpoint of wallet + async fn set_did_endpoint(&self, options: SetDidEndpointOptions) -> Result<()>; +} diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 74af1882..474857c2 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -10,7 +10,7 @@ use crate::modules::{ basic_message::BasicMessageOptions, configuration::ConfigurationOptions, connection::ConnectionOptions, credential::CredentialOptions, credential_definition::CredentialDefinitionOptions, feature::FeaturesOptions, oob::OobOptions, - proof::ProofOptions, schema::SchemaOptions, webhook::WebhookOptions, + proof::ProofOptions, schema::SchemaOptions, wallet::WalletOptions, webhook::WebhookOptions, }; /// Main command with options, flags and subcommands @@ -97,6 +97,9 @@ pub enum Commands { /// Multitenancy subcommands Multitenancy(MultitenancyOptions), + + /// Wallet subcommands + Wallet(WalletOptions), } impl From for String { @@ -114,6 +117,7 @@ impl From for String { Commands::Configuration(_) => "Configuration", Commands::Proof(_) => "Proof", Commands::Multitenancy(_) => "Multitenancy", + Commands::Wallet(_) => "Wallet", }; Self::from(s) diff --git a/crates/cli/src/help_strings.rs b/crates/cli/src/help_strings.rs index 719b39e2..14fbcf65 100644 --- a/crates/cli/src/help_strings.rs +++ b/crates/cli/src/help_strings.rs @@ -126,6 +126,25 @@ pub enum HelpStrings { AutomationCreateCredentialDefinitionName, AutomationCreateCredentialDefinitionAttributes, AutomationCreateCredentialDefinitionVersion, + + // Wallet + Wallet, + WalletCreate, + WalletCreateMethod, + WalletCreateOptions, + WalletEndpoint, + WalletEndpointType, + WalletFetchDidEndpoint, + WalletGetPublic, + WalletList, + WalletListDid, + WalletListKeyType, + WalletListMethod, + WalletListPosture, + WalletListVerkey, + WalletRotateKeypair, + WalletSetEndpoint, + WalletSetPublic, } impl From for Option<&str> { @@ -251,6 +270,24 @@ impl HelpStrings { Self::MultitenancyCreate => "Create a new sub agent", Self::MultitenancyRemove => "Remove a sub agent", Self::MultitenancyRemoveWalletId => "Remove the wallet by id of a sub agent", + + Self::Wallet => "Interacts with a wallet", + Self::WalletCreate => "Create a local DID", + Self::WalletCreateMethod => "The did method. One of 'key' or 'sov'", + Self::WalletCreateOptions => "Key types are e.g. ed25519, bls12381g2", + Self::WalletEndpoint => "The endpoint url", + Self::WalletEndpointType => "The endpoint type. E.g. 'Endpoint'", + Self::WalletFetchDidEndpoint => "Get the endpoint information associated with a DID", + Self::WalletGetPublic => "Get the public DID of the wallet", + Self::WalletList => "Query for DID associated with a wallet", + Self::WalletListDid => "A DID to query for", + Self::WalletListKeyType => "Key types are e.g. ed25519, bls12381g2", + Self::WalletListMethod => "DID method to query for. e.g. sov to only fetch indy/sov DIDs Available values : key, sov", + Self::WalletListPosture => "The DID posture specifying whether the DID is the current public DID, posted to ledger but current public DID, or local to the wallet. Available values : public, posted, wallet_only", + Self::WalletListVerkey => "The verification key of interest", + Self::WalletRotateKeypair => "Rotate the keypair for a DID", + Self::WalletSetEndpoint => "Set the endpoint information for a DID", + Self::WalletSetPublic => "Set the public DID of the wallet", } } } diff --git a/crates/cli/src/modules/mod.rs b/crates/cli/src/modules/mod.rs index 77d8a48b..66a4c78f 100644 --- a/crates/cli/src/modules/mod.rs +++ b/crates/cli/src/modules/mod.rs @@ -31,5 +31,8 @@ pub mod schema; /// Module for multitenancy pub mod multitenancy; +/// Module for wallet +pub mod wallet; + /// Module for webhook pub mod webhook; diff --git a/crates/cli/src/modules/wallet.rs b/crates/cli/src/modules/wallet.rs new file mode 100644 index 00000000..49874b30 --- /dev/null +++ b/crates/cli/src/modules/wallet.rs @@ -0,0 +1,196 @@ +use crate::error::Result; +use crate::help_strings::HelpStrings; +use crate::utils::loader::{Loader, LoaderVariant}; +use clap::{Args, Subcommand}; +use siera_agent::modules::wallet::{ + CreateLocalDidOptions, Did, DidList, KeyType, SetDidEndpointOptions, WalletModule, +}; +use siera_logger::pretty_stringify_obj; + +/// Schema options and flags +#[derive(Args)] +#[clap(about = HelpStrings::Wallet)] +pub struct WalletOptions { + /// All the subcommands of the schema cli + #[clap(subcommand)] + pub commands: WalletSubcommands, +} + +/// Wallet subcommands +#[derive(Subcommand, Debug)] +pub enum WalletSubcommands { + /// Get associated did for wallet + #[clap(about = HelpStrings::WalletList)] + List { + /// A DID in question + #[clap(short, long, help=HelpStrings::WalletListDid, required = false)] + did: Option, + + /// The key type of the wallet e.g. ed25519, bls12381g2 + #[clap(short, long, help=HelpStrings::WalletListKeyType, required = false, possible_values=&["ed25519", "bls12381g2"])] + key_type: Option, + + /// The did method to query for + #[clap(short, long, help=HelpStrings::WalletListMethod, required = false, possible_values=&["did", "sov"])] + method: Option, + + /// Available values : public, posted, wallet_only + #[clap(short, long, help=HelpStrings::WalletListPosture, required = false, possible_values=&["posted", "public", "wallet_only"])] + posture: Option, + + /// The verification key of interest + #[clap(short, long, help=HelpStrings::WalletListVerkey, required = false)] + verkey: Option, + }, + + /// Create a local DID + #[clap(about = HelpStrings::WalletCreate)] + CreateLocalDid { + /// The method to be used did or sov + #[clap(long, short, help=HelpStrings::WalletCreateMethod, required = true, default_value="did", possible_values=&["did", "sov"])] + method: String, + + /// The key type e.g. ed25519 or bls12381g2 + #[clap(long, short, help=HelpStrings::WalletListKeyType, required = true, default_value="ed25519", possible_values=&["ed25519", "bls12381g2"])] + key_type: String, + }, + + /// Rotate the wallets key pair + #[clap(about = HelpStrings::WalletRotateKeypair)] + RotateKeyPair { + /// The did to rotate the keypair for + #[clap(short, long, help=HelpStrings::WalletListDid)] + did: String, + }, + + /// Fetch the wallets public did + #[clap(about = HelpStrings::WalletGetPublic)] + FetchPublicDid {}, + + /// Assign a DID as public + #[clap(about = HelpStrings::WalletSetPublic)] + AssignPublicDid { + /// The DID to assign + #[clap(long, short, help=HelpStrings::WalletListDid)] + did: String, + }, + + /// Get the DID endpoint + #[clap(about = HelpStrings::WalletFetchDidEndpoint)] + FetchDidEndpoint { + /// The DID to assign + #[clap(long, short, help=HelpStrings::WalletListDid)] + did: String, + }, + + /// Query DID endpoint of wallet + #[clap(about = HelpStrings::WalletSetEndpoint)] + SetDidEndpoint { + /// The DID to assign + #[clap(long, short, help=HelpStrings::WalletListDid)] + did: String, + + /// The endpoint url for the did + #[clap(long, short, help=HelpStrings::WalletEndpoint)] + endpoint: String, + + /// The endpoint type + #[clap(long, short, help=HelpStrings::WalletEndpointType, default_value="Endpoint")] + endpoint_type: String, + }, +} + +/// Subcommand Schema parser +pub async fn parse_wallet_args( + options: &WalletOptions, + agent: impl WalletModule + Send + Sync, +) -> Result<()> { + let loader = Loader::start(&LoaderVariant::default()); + match &options.commands { + WalletSubcommands::List { + did, + key_type, + method, + posture, + verkey, + } => { + let options = Did { + did: did.clone(), + key_type: key_type.clone(), + method: method.clone(), + posture: posture.clone(), + verkey: verkey.clone(), + }; + agent + .get_wallet_dids(options) + .await + .map(|response: DidList| { + loader.stop(); + log_info!("Found the following DID information for your query: ",); + log!("{}", pretty_stringify_obj(&response)); + copy!("{}", pretty_stringify_obj(&response)); + }) + } + WalletSubcommands::CreateLocalDid { method, key_type } => { + let options = CreateLocalDidOptions { + method: method.clone(), + options: KeyType { + key_type: key_type.clone(), + }, + }; + agent.create_local_did(options).await.map(|response| { + loader.stop(); + log_info!("Successfully created local DID: {:?}", response.did); + copy!("{}", pretty_stringify_obj(&response)); + log!("{}", pretty_stringify_obj(response)); + }) + } + WalletSubcommands::RotateKeyPair { did } => { + agent.rotate_keypair(did.clone()).await.map(|response| { + loader.stop(); + log_info!("Successfully rotated keypair for did DID {}: ", did); + copy!("{}", pretty_stringify_obj(response)); + log!("{}", pretty_stringify_obj(response)); + }) + } + WalletSubcommands::FetchPublicDid {} => agent.fetch_public_did().await.map(|response| { + loader.stop(); + log_info!("Wallet public DID: "); + copy!("{}", pretty_stringify_obj(&response)); + log!("{}", pretty_stringify_obj(response)); + }), + WalletSubcommands::AssignPublicDid { did } => { + agent.assign_public_did(did.clone()).await.map(|response| { + loader.stop(); + log_info!("Successfully assigned public DID: "); + copy!("{}", pretty_stringify_obj(&response)); + log!("{}", pretty_stringify_obj(response)); + }) + } + WalletSubcommands::FetchDidEndpoint { did } => { + agent.fetch_did_endpoint(did.clone()).await.map(|response| { + loader.stop(); + log_info!("DID endpoint for DID {}: ", did); + copy!("{}", pretty_stringify_obj(&response)); + log!("{}", pretty_stringify_obj(response)); + }) + } + WalletSubcommands::SetDidEndpoint { + did, + endpoint, + endpoint_type, + } => { + let options = SetDidEndpointOptions { + did: did.clone(), + endpoint: endpoint.clone(), + endpoint_type: endpoint_type.clone(), + }; + agent.set_did_endpoint(options).await.map(|response| { + loader.stop(); + log_info!("Set DID endpoint for DID {}: ", did); + log!("{}", pretty_stringify_obj(response)); + copy!("{}", pretty_stringify_obj(response)); + }) + } + } +} diff --git a/crates/cli/src/register.rs b/crates/cli/src/register.rs index 4cf14120..c60365f1 100644 --- a/crates/cli/src/register.rs +++ b/crates/cli/src/register.rs @@ -11,6 +11,7 @@ use crate::modules::multitenancy::parse_multitenancy_args; use crate::modules::oob::parse_oob_args; use crate::modules::proof::parse_proof_args; use crate::modules::schema::parse_schema_args; +use crate::modules::wallet::parse_wallet_args; use crate::modules::webhook::parse_webhook_args; use crate::utils::config::{get_config_from_path, get_config_path}; use clap::Parser; @@ -63,23 +64,24 @@ pub async fn register() -> Result<()> { let agent = CloudAgentPython::new(agent_url, version, api_key, auth_token); // Commands that require the agent match &cli.commands { - Commands::Schema(options) => parse_schema_args(options, agent).await, - Commands::Feature(_) => parse_features_args(agent).await, - Commands::Message(options) => parse_basic_message_args(options, agent).await, - Commands::CredentialDefinition(options) => { - parse_credential_definition_args(options, agent).await - } + Commands::Automate(options) => parse_automation_args(options, agent).await, Commands::Connection(options) => parse_connection_args(options, agent).await, Commands::Credential(options) => { parse_credentials_args(&options.commands, agent).await } - Commands::Proof(options) => parse_proof_args(&options.commands, agent).await, + Commands::CredentialDefinition(options) => { + parse_credential_definition_args(options, agent).await + } + Commands::Feature(_) => parse_features_args(agent).await, + Commands::Message(options) => parse_basic_message_args(options, agent).await, Commands::Multitenancy(options) => { parse_multitenancy_args(options, agent).await } Commands::Oob(options) => parse_oob_args(options, agent).await, + Commands::Proof(options) => parse_proof_args(&options.commands, agent).await, + Commands::Schema(options) => parse_schema_args(options, agent).await, + Commands::Wallet(options) => parse_wallet_args(options, agent).await, Commands::Webhook(_) => parse_webhook_args(agent).await, - Commands::Automate(options) => parse_automation_args(options, agent).await, _ => Err( Error::SubcommandNotRegisteredForAgent(cli.commands.into(), "aca-py") .into(), diff --git a/crates/cloudagent-python/src/cloudagent/mod.rs b/crates/cloudagent-python/src/cloudagent/mod.rs index b1159533..ec7c1631 100644 --- a/crates/cloudagent-python/src/cloudagent/mod.rs +++ b/crates/cloudagent-python/src/cloudagent/mod.rs @@ -24,5 +24,8 @@ mod schema; /// Module for multitenancy specific for an Aries cloudagent Python mod multitenancy; +/// Module for wallet specific for an Aries Cloudagent Python +mod wallet; + /// Module for listening to webhook for an Aries Cloudagent Python mod webhook; diff --git a/crates/cloudagent-python/src/cloudagent/schema.rs b/crates/cloudagent-python/src/cloudagent/schema.rs index cf50cabf..6a44dabb 100644 --- a/crates/cloudagent-python/src/cloudagent/schema.rs +++ b/crates/cloudagent-python/src/cloudagent/schema.rs @@ -7,7 +7,7 @@ use siera_agent::modules::schema::{ Schema, SchemaCreateOptions, SchemaModule, SchemasGetAllResponse, }; -/// Reponse from the cloudagent that contains the wrapped schema +/// Response from the cloudagent that contains the wrapped schema #[derive(Serialize, Deserialize, Debug)] struct Response { /// Schema wrapper diff --git a/crates/cloudagent-python/src/cloudagent/wallet.rs b/crates/cloudagent-python/src/cloudagent/wallet.rs new file mode 100644 index 00000000..0f6f3811 --- /dev/null +++ b/crates/cloudagent-python/src/cloudagent/wallet.rs @@ -0,0 +1,84 @@ +use crate::agent::CloudAgentPython; +use crate::fill_query; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use siera_agent::error::Result; +use siera_agent::modules::wallet::{ + CreateLocalDidOptions, Did, DidEndpoint, DidList, SetDidEndpointOptions, WalletModule, +}; + +/// Response from the cloudagent that contains the wrapped Did result +#[derive(Serialize, Deserialize, Debug)] +struct Response { + /// Wallet wrapper + result: Did, +} + +/// Response from the cloudagent when requesting info about dids +/// of a wallet specific to Aca-py +#[derive(Debug, Deserialize, Serialize)] +pub struct DidListResults { + /// The aca-py structure returning results + results: DidList, +} + +#[async_trait] +impl WalletModule for CloudAgentPython { + async fn get_wallet_dids(&self, options: Did) -> Result { + let url = self.create_url(&["wallet", "did"])?; + + let query = fill_query!(options, did, key_type, method, posture, verkey); + + let did_list: DidListResults = self.get(url, Some(query)).await?; + + Ok(did_list.results) + } + + async fn create_local_did(&self, options: CreateLocalDidOptions) -> Result { + let url = self.create_url(&["wallet", "did", "create"])?; + + let body = json!({ + "method": options.method, + "options": options.options + }); + + self.post(url, None, Some(body)).await + } + + async fn rotate_keypair(&self, did: String) -> Result<()> { + let url = self.create_url(&["wallet", "did", "local", "rotate-keypair"])?; + + self.patch(url, Some(Vec::from([("did", did)]))).await + } + + async fn fetch_public_did(&self) -> Result { + let url = self.create_url(&["wallet", "did", "public"])?; + + self.get(url, None).await + } + + async fn assign_public_did(&self, did: String) -> Result { + let url = self.create_url(&["wallet", "did", "public"])?; + + self.post(url, Some(Vec::from([("did", did)])), None).await + } + + async fn fetch_did_endpoint(&self, did: String) -> Result { + let url = self.create_url(&["wallet", "fetch-did-endpoint"])?; + + self.get(url, Some(Vec::from([("did", did)]))).await + } + + async fn set_did_endpoint(&self, options: SetDidEndpointOptions) -> Result<()> { + let url = self.create_url(&["wallet", "set-did-endpoint"])?; + + let body = json!({ + "did": options.did, + "endpoint": options.endpoint, + "endpoint_type": options.endpoint_type + }); + + self.post(url, None, Some(body)).await + } +} diff --git a/crates/cloudagent-python/src/web.rs b/crates/cloudagent-python/src/web.rs index 7231ab62..13c6d316 100644 --- a/crates/cloudagent-python/src/web.rs +++ b/crates/cloudagent-python/src/web.rs @@ -29,6 +29,27 @@ impl CloudAgentPython { self.send::(client).await } + /// Builds a patch request and calls the sender + /// + /// # Errors + /// + /// When it could not fulfill a PATCH request + pub async fn patch( + &self, + url: Url, + query: Option>, + ) -> Result { + let client = match &query { + Some(q) => Client::new().patch(url).query(&q), + None => Client::new().patch(url), + }; + + log_trace!("Patch request query:"); + log_trace!("{:#?}", query); + + self.send::(client).await + } + /// Builds a post request and calls the sender /// /// # Errors diff --git a/crates/logger/src/lib.rs b/crates/logger/src/lib.rs index e365fefa..babf125d 100644 --- a/crates/logger/src/lib.rs +++ b/crates/logger/src/lib.rs @@ -117,7 +117,7 @@ pub fn pretty_stringify_obj(obj: impl Serialize) -> String { /// /// # Panics /// -/// When the clipoard provider could not be found +/// When the clipboard provider could not be found pub fn copy_to_clipboard(string: impl AsRef) { let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); ctx.set_contents(string.as_ref().to_owned()).unwrap();