From af9babe526d9811e1ecb54e056a844e0648427f5 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 18 Feb 2025 00:30:52 -0500 Subject: [PATCH] add descriptions for workflows and inputs --- crates/icepick-workflow/src/lib.rs | 36 ++- crates/icepick/build.rs | 2 + crates/icepick/src/cli/workflow.rs | 106 ++++----- .../workflows/cosmos/generate-address.yaml | 12 +- crates/icepick/workflows/cosmos/stake.yaml | 29 ++- .../workflows/cosmos/withdraw-rewards.yaml | 21 +- crates/icepick/workflows/cosmos/withdraw.yaml | 32 ++- .../workflows/sol/generate-address.yaml | 9 +- .../icepick/workflows/sol/transfer-token.yaml | 18 +- .../workflows/spacemesh/generate-wallet.yaml | 14 +- icepick.toml | 214 ------------------ 11 files changed, 187 insertions(+), 306 deletions(-) diff --git a/crates/icepick-workflow/src/lib.rs b/crates/icepick-workflow/src/lib.rs index bb52e55..c24ed04 100644 --- a/crates/icepick-workflow/src/lib.rs +++ b/crates/icepick-workflow/src/lib.rs @@ -24,15 +24,45 @@ pub enum WorkflowError { InvocationError(String), } +/// An input for a workflow argument. When inputs are read, they should be referenced by the first +/// name. Additional names can be provided as aliases, to allow chaining workflows together when +/// names may not make sense - such as a Solana address then being used as an authorization +/// address. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Input { + /// An input with a single identifier. + /// The name of the input. + pub name: String, + + /// A description of the input. + pub description: String, + + /// Aliases used when loading inputs. + #[serde(default)] + pub aliases: Vec, + + /// Whether the workflow input is optional. + pub optional: Option, +} + +impl Input { + pub fn identifiers(&self) -> impl Iterator { + [&self.name].into_iter().chain(self.aliases.iter()) + } + + pub fn is_required(&self) -> bool { + self.optional.is_some_and(|o| o) + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Workflow { pub name: String, - #[serde(default)] - pub inputs: Vec, + pub description: String, #[serde(default)] - pub optional_inputs: Vec, + pub inputs: Vec, #[serde(rename = "step")] steps: Vec, diff --git a/crates/icepick/build.rs b/crates/icepick/build.rs index 4b38224..62a6333 100644 --- a/crates/icepick/build.rs +++ b/crates/icepick/build.rs @@ -20,6 +20,7 @@ fn main() { for module_dir in std::fs::read_dir(&workflows_dir).unwrap() { let module_dir = module_dir.unwrap(); + dbg!(&module_dir); let path = module_dir.path(); if !path.is_dir() { panic!("found unexpected file {}", path.to_string_lossy()); @@ -28,6 +29,7 @@ fn main() { let mut workflows = vec![]; for workflow_file in std::fs::read_dir(&path).unwrap() { + dbg!(&workflow_file); let workflow_file = workflow_file.unwrap(); let path = workflow_file.path(); if !path.is_file() { diff --git a/crates/icepick/src/cli/workflow.rs b/crates/icepick/src/cli/workflow.rs index 1402f60..ff6ec02 100644 --- a/crates/icepick/src/cli/workflow.rs +++ b/crates/icepick/src/cli/workflow.rs @@ -1,4 +1,4 @@ -use icepick_workflow::{InvocableOperation, OperationResult, Workflow, StringMap}; +use icepick_workflow::{Input, InvocableOperation, OperationResult, StringMap, Workflow}; use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath}; use keyfork_shard::{openpgp::OpenPGP, Format}; use miniquorum::{Payload, PayloadVerification}; @@ -91,31 +91,33 @@ impl InvocableOperation for CLIOperation { } pub fn generate_command(workflow: &Workflow) -> clap::Command { - let mut command = clap::Command::new(&workflow.name).arg(clap::arg!( - --"input-file" [FILE] - "A file containing any inputs not passed on the command line" - )); - for input in &workflow.inputs { - // can also be included in the JSON file, so we won't mark this as required. - let arg = clap::Arg::new(input) - .required(false) - .long(input.replace('_', "-")) - .value_name(input.to_uppercase()); - command = command.arg(arg); + let mut command = clap::Command::new(&workflow.name).about(&workflow.description); + // NOTE: all required inputs are still marked as .required(false) since they could be included + // in the `--input-file` argument. + for input in workflow.inputs.iter() { + for arg in input.identifiers() { + let arg = clap::Arg::new(arg) + .required(false) + .help(&input.description) + .long(arg.replace('_', "-")) + .value_name(arg.to_uppercase()) + .conflicts_with_all( + input + .identifiers() + .filter(|name| *name != arg) + .collect::>(), + ); + command = command.arg(arg); + } } - for input in &workflow.optional_inputs { - let arg = clap::Arg::new(input) - .required(false) - .long(input.replace('_', "-")) - .value_name(input.to_uppercase()); - command = command.arg(arg); - } - command + command.arg(clap::arg!( + --"input-file" [FILE] + "A file containing any inputs not passed on the command line" + )) } -fn load_inputs + Into + std::fmt::Display>( - inputs: impl IntoIterator, - optional_inputs: impl IntoIterator, +fn load_inputs<'a>( + inputs: impl IntoIterator, matches: &clap::ArgMatches, ) -> StringMap { let mut map = StringMap::default(); @@ -124,33 +126,25 @@ fn load_inputs + Into + std::fmt::Display>( .and_then(|p| std::fs::File::open(p).ok()) .and_then(|f| serde_json::from_reader(f).ok()); for input in inputs { - match matches.get_one::(input.as_ref()) { + let identifier = &input.name; + match input + .identifiers() + .filter_map(|name| matches.get_one::(name)) + .next() + { Some(value) => { - map.insert(input.into(), value.clone()); + map.insert(identifier.clone(), value.clone()); continue; } None => { - if let Some(value) = input_file.as_ref().and_then(|f| f.get(input.as_ref())) { - map.insert(input.into(), value.clone()); + if let Some(value) = input_file.as_ref().and_then(|f| f.get(identifier)) { + map.insert(identifier.clone(), value.clone()); continue; } } } - panic!("Required workflow input was not found: {input}"); - } - - for input in optional_inputs { - match matches.get_one::(input.as_ref()) { - Some(value) => { - map.insert(input.into(), value.clone()); - continue; - } - None => { - if let Some(value) = input_file.as_ref().and_then(|f| f.get(input.as_ref())) { - map.insert(input.into(), value.clone()); - continue; - } - } + if input.is_required() { + panic!("Required workflow input was not found: {identifier}"); } } @@ -188,13 +182,10 @@ pub fn parse_quorum_file( let threshold = threshold.unwrap_or(u8::try_from(certs.len()).expect("too many certs!")); let policy = match purpose { Purpose::AddSignature => { - // All signatures must be valid, but we don't require a minimum. + // All signatures must be valid, but we don't require a minimum. PayloadVerification::new().with_threshold(0) } - Purpose::RunQuorum => { - PayloadVerification::new().with_threshold(threshold) - - }, + Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold), }; payload.verify_signatures(&certs, &policy, None).unwrap(); @@ -210,20 +201,19 @@ pub fn parse_quorum_with_shardfile( let payload: Payload = serde_json::from_reader(payload_file).unwrap(); let opgp = OpenPGP; - let (threshold, certs) = opgp.decrypt_metadata_from_file( - None::<&std::path::Path>, - std::fs::File::open(shardfile_path).unwrap(), - keyfork_prompt::default_handler().unwrap(), - ).unwrap(); + let (threshold, certs) = opgp + .decrypt_metadata_from_file( + None::<&std::path::Path>, + std::fs::File::open(shardfile_path).unwrap(), + keyfork_prompt::default_handler().unwrap(), + ) + .unwrap(); let policy = match purpose { Purpose::AddSignature => { - // All signatures must be valid, but we don't require a minimum. + // All signatures must be valid, but we don't require a minimum. PayloadVerification::new().with_threshold(0) } - Purpose::RunQuorum => { - PayloadVerification::new().with_threshold(threshold) - - }, + Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold), }; payload.verify_signatures(&certs, &policy, None).unwrap(); @@ -251,7 +241,7 @@ pub fn handle( modules: Commands, config: &[ModuleConfig], ) { - let inputs = load_inputs(&workflow.inputs, &workflow.optional_inputs, matches); + let inputs = load_inputs(&workflow.inputs, matches); let data: StringMap = inputs .into_iter() .map(|(k, v)| (k, Value::String(v))) diff --git a/crates/icepick/workflows/cosmos/generate-address.yaml b/crates/icepick/workflows/cosmos/generate-address.yaml index 07a44c6..7cd8311 100644 --- a/crates/icepick/workflows/cosmos/generate-address.yaml +++ b/crates/icepick/workflows/cosmos/generate-address.yaml @@ -1,8 +1,14 @@ name: generate-address +description: |- + Generate an address on a given Cosmos-based blockchain. inputs: -- chain_name -optional_inputs: -- account +- name: chain_name + description: >- + The name of the Cosmos chain you'd like to generate an address for. +- name: account + description: >- + The account to use, if not the default account. + optional: true step: - type: cosmos-get-chain-info inputs: diff --git a/crates/icepick/workflows/cosmos/stake.yaml b/crates/icepick/workflows/cosmos/stake.yaml index 4bb8edb..9b0b608 100644 --- a/crates/icepick/workflows/cosmos/stake.yaml +++ b/crates/icepick/workflows/cosmos/stake.yaml @@ -1,12 +1,27 @@ name: stake +description: |- + Stake coins on the provided chain. inputs: -- delegate_address -- validator_address -- chain_name -- asset_name -- asset_amount -optional_inputs: -- gas_factor +- name: delegate_address + description: >- + Address holding the coins to be staked to a validator. +- name: validator_address + description: >- + Address of the validator operator. +- name: chain_name + description: >- + The name of the Cosmos-based chain. +- name: asset_name + description: >- + The name of the asset to stake. +- name: asset_amount + description: >- + The amount of the asset to stake. +- name: gas_factor + description: >- + An amount to multiply the required gas by; necessary if a chain requires + more gas for a specific operation. + optional: true step: - type: cosmos-get-chain-info inputs: diff --git a/crates/icepick/workflows/cosmos/withdraw-rewards.yaml b/crates/icepick/workflows/cosmos/withdraw-rewards.yaml index bca9ac6..10c6cbd 100644 --- a/crates/icepick/workflows/cosmos/withdraw-rewards.yaml +++ b/crates/icepick/workflows/cosmos/withdraw-rewards.yaml @@ -1,10 +1,21 @@ name: withdraw-rewards +description: |- + Withdraw rewards gained from staking to a validator. inputs: -- delegate_address -- validator_address -- chain_name -optional_inputs: -- gas_factor +- name: delegate_address + description: >- + The owner of the staked coins; also, the recipient of rewards. +- name: validator_address + description: >- + The validator from whom coins are staked. +- name: chain_name + description: >- + The name of the Cosmos-based chain. +- name: gas_factor + description: >- + An amount to multiply the required gas by; necessary if a chain requires + more gas for a specific operation. + optional: true step: - type: cosmos-get-chain-info inputs: diff --git a/crates/icepick/workflows/cosmos/withdraw.yaml b/crates/icepick/workflows/cosmos/withdraw.yaml index 9f9c6cc..bbd4888 100644 --- a/crates/icepick/workflows/cosmos/withdraw.yaml +++ b/crates/icepick/workflows/cosmos/withdraw.yaml @@ -1,12 +1,30 @@ name: withdraw +description: |- + Withdraw staked coins from a validator. + + Staked coins may be held for an unbonding period, depending on the chain upon + which they are staked. inputs: -- delegate_address -- validator_address -- chain_name -- asset_name -- asset_amount -optional_inputs: -- gas_factor +- name: delegate_address + description: >- + The owner of the staked coins. +- name: validator_address + description: >- + The validator from whom coins are staked. +- name: chain_name + description: >- + The name of the Cosmos-based chain. +- name: asset_name + description: >- + The name of the asset to withdraw. +- name: asset_amount + description: >- + The amount of the asset to withdraw. +- name: gas_factor + description: >- + An amount to multiply the required gas by; necessary if a chain requires + more gas for a specific operation. + optional: true step: - type: cosmos-get-chain-info inputs: diff --git a/crates/icepick/workflows/sol/generate-address.yaml b/crates/icepick/workflows/sol/generate-address.yaml index c63c627..844849e 100644 --- a/crates/icepick/workflows/sol/generate-address.yaml +++ b/crates/icepick/workflows/sol/generate-address.yaml @@ -1,6 +1,11 @@ name: generate-address -optional_inputs: -- account +description: |- + Generate a Solana address. +inputs: +- name: account + description: >- + The account to use, if not the default account. + optional: true step: - type: sol-generate-wallet inputs: diff --git a/crates/icepick/workflows/sol/transfer-token.yaml b/crates/icepick/workflows/sol/transfer-token.yaml index ac914f5..eee24c6 100644 --- a/crates/icepick/workflows/sol/transfer-token.yaml +++ b/crates/icepick/workflows/sol/transfer-token.yaml @@ -1,9 +1,19 @@ name: transfer-token +description: |- + Transfer SPL tokens held on the Solana blockchain. inputs: -- from_address -- to_address -- token_name -- token_amount +- name: from_address + description: >- + The address from which to send tokens. +- name: to_address + description: >- + The address to send coins to. +- name: token_name + description: >- + The name of the token to transfer. +- name: token_amount + description: >- + The amount of the token to transfer. step: - type: sol-get-token-info inputs: diff --git a/crates/icepick/workflows/spacemesh/generate-wallet.yaml b/crates/icepick/workflows/spacemesh/generate-wallet.yaml index 4f9b647..19c82b1 100644 --- a/crates/icepick/workflows/spacemesh/generate-wallet.yaml +++ b/crates/icepick/workflows/spacemesh/generate-wallet.yaml @@ -1,7 +1,15 @@ name: generate-address -optional_inputs: -- account -- cluster +description: |- + Generate a Spacemesh address +inputs: +- name: account + description: >- + The account to use, if not the default account. + optional: true +- name: cluster + description: >- + The Spacemesh cluster to use, if not the mainnet. + optional: true step: - type: spacemesh-generate-wallet inputs: diff --git a/icepick.toml b/icepick.toml index e7103e1..c64f8e4 100644 --- a/icepick.toml +++ b/icepick.toml @@ -3,225 +3,11 @@ name = "sol" derivation_prefix = "m/44'/501'/0'" algorithm = "Ed25519" -# NOTE: To get a nonce address, the `generate-nonce-account` workflow should be -# run. It is the only workflow that uses a blockhash, which is why a -# `broadcast-with-blockhash` or similar is not, and should not be, implemented. -[[module.workflow]] -name = "broadcast" -inputs = ["nonce_address", "cluster"] - -[[module.workflow.step]] -type = "sol-get-nonce-account-data" -inputs = { nonce_address = "nonce_address", cluster = "cluster" } -outputs = { authority = "nonce_authority", durable_nonce = "nonce" } - -[[module.workflow.step]] -type = "internal-save-file" -values = { filename = "nonce.json" } -inputs = { nonce_authority = "nonce_authority", nonce_data = "nonce", nonce_address = "nonce_address" } - -[[module.workflow.step]] -type = "internal-load-file" -values = { filename = "transaction.json" } -outputs = { transaction = "transaction" } - -[[module.workflow.step]] -type = "sol-broadcast" -inputs = { cluster = "cluster", transaction = "transaction" } -outputs = { status = "status", url = "url", error = "error" } - -[[module.workflow]] -name = "generate-nonce-account" -inputs = ["cluster", "authorization_address"] - -[[module.workflow.step]] -type = "sol-generate-wallet" - -[[module.workflow.step]] -type = "sol-get-wallet-address" -outputs = { pubkey = "wallet_pubkey" } - -[[module.workflow.step]] -type = "sol-await-funds" -inputs = { address = "wallet_pubkey", cluster = "cluster" } -# enough to cover two signatures and the 1_500_000 approx. rent fee -values = { lamports = "1510000" } - -[[module.workflow.step]] -type = "sol-get-blockhash" -inputs = { cluster = "cluster" } -outputs = { blockhash = "blockhash" } - -[[module.workflow.step]] -type = "sol-create-nonce-account-and-signing-key" - -[module.workflow.step.inputs] -from_address = "wallet_pubkey" -authorization_address = "authorization_address" - -[module.workflow.step.outputs] -transaction = "instructions" -nonce_pubkey = "nonce_pubkey" -nonce_privkey = "private_keys" -derivation_accounts = "derivation_accounts" - -[[module.workflow.step]] -type = "sol-compile" - -[module.workflow.step.inputs] -instructions = "instructions" -derivation_accounts = "derivation_accounts" -blockhash = "blockhash" - -[module.workflow.step.outputs] -transaction = "unsigned_transaction" - -[[module.workflow.step]] -type = "sol-sign" - -[module.workflow.step.inputs] -blockhash = "blockhash" -signing_keys = "private_keys" -transaction = "unsigned_transaction" - -[module.workflow.step.outputs] -transaction = "signed_transaction" - -[[module.workflow.step]] -type = "sol-broadcast" -inputs = { cluster = "cluster", transaction = "signed_transaction" } -outputs = { status = "status", url = "url", error = "error" } - -[[module.workflow.step]] -type = "internal-cat" -inputs = { status = "status", url = "url", nonce_account = "nonce_pubkey", error = "error" } -outputs = { status = "status", url = "url", nonce_account = "nonce_account", error = "error" } - -[[module.workflow]] -# Transfer SOL from one address to another. -name = "transfer" -inputs = ["to_address", "from_address", "amount"] - -[[module.workflow.step]] -type = "internal-load-file" -values = { filename = "nonce.json" } -outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" } - -[[module.workflow.step]] -type = "sol-transfer" -inputs = { from_address = "from_address", to_address = "to_address", amount = "amount" } -outputs = { instructions = "instructions", derivation_accounts = "derivation_accounts" } - -[[module.workflow.step]] -type = "sol-compile" - -[module.workflow.step.inputs] -instructions = "instructions" -derivation_accounts = "derivation_accounts" -nonce_address = "nonce_address" -nonce_authority = "nonce_authority" -nonce_data = "nonce_data" - -[module.workflow.step.outputs] -transaction = "unsigned_transaction" - -[[module.workflow.step]] -type = "sol-sign" - -inputs = { blockhash = "nonce_data", transaction = "unsigned_transaction" } -outputs = { transaction = "signed_transaction" } - -[[module.workflow.step]] -type = "internal-save-file" - -values = { filename = "transaction.json" } -inputs = { transaction = "signed_transaction" } - [[module]] name = "cosmos" derivation_prefix = "m/44'/118'/0'" algorithm = "Secp256k1" -[[module.workflow]] -name = "transfer" -inputs = ["from_address", "to_address", "asset_name", "chain_name", "asset_amount"] - -[[module.workflow.step]] -# NOTE: chain_name can't be discoverable by filtering from asset_name, since -# some asset devnets reuse the name. There's no difference between KYVE on Kyve -# or Korellia (devnet). -type = "cosmos-get-chain-info" -inputs = { chain_name = "chain_name" } -outputs = { blockchain_config = "blockchain_config" } - -[[module.workflow.step]] -type = "internal-load-file" -values = { filename = "account_info.json" } -outputs = { account_number = "account_number", sequence_number = "sequence_number" } - -[[module.workflow.step]] -type = "cosmos-transfer" - -[module.workflow.step.inputs] -from_address = "from_address" -to_address = "to_address" -amount = "asset_amount" -denom = "asset_name" -blockchain_config = "blockchain_config" - -[module.workflow.step.outputs] -fee = "fee" -tx_messages = "tx_messages" - -[[module.workflow.step]] -type = "cosmos-sign" - -[module.workflow.step.inputs] -fee = "fee" -tx_messages = "tx_messages" -account_number = "account_number" -sequence_number = "sequence_number" -blockchain_config = "blockchain_config" - -[module.workflow.step.outputs] -transaction = "signed_transaction" - -[[module.workflow.step]] -type = "internal-save-file" -values = { filename = "transaction.json" } -inputs = { transaction = "signed_transaction" } - -[[module.workflow]] -name = "broadcast" -# NOTE: For the purpose of Cosmos, the nonce is a direct part of the signer's -# account. -inputs = ["nonce_address", "chain_name"] - -[[module.workflow.step]] -type = "cosmos-get-chain-info" -inputs = { chain_name = "chain_name" } -outputs = { blockchain_config = "blockchain_config" } - -[[module.workflow.step]] -type = "cosmos-get-account-data" -inputs = { account_id = "nonce_address", blockchain_config = "blockchain_config" } -outputs = { account_number = "account_number", sequence_number = "sequence_number" } - -[[module.workflow.step]] -type = "internal-save-file" -values = { filename = "account_info.json" } -inputs = { account_number = "account_number", sequence_number = "sequence_number" } - -[[module.workflow.step]] -type = "internal-load-file" -values = { filename = "transaction.json" } -outputs = { transaction = "transaction" } - -[[module.workflow.step]] -type = "cosmos-broadcast" -inputs = { blockchain_config = "blockchain_config", transaction = "transaction" } -outputs = { status = "status", url = "url", error = "error", error_code = "error_code" } - [[module]] name = "spacemesh" derivation_prefix = "m/44'/540'/0'/0'"