From 46cf4129acca6e2f09fe2377eba734097590cf0a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 20 Dec 2024 19:47:17 -0500 Subject: [PATCH] icepick workflow: it makes a signed transaction!!! --- crates/by-chain/icepick-solana/src/lib.rs | 5 +- crates/icepick/src/cli/mod.rs | 48 +++++--- crates/icepick/src/cli/workflow.rs | 135 +++++++++++++++++++++- icepick.toml | 16 +-- 4 files changed, 173 insertions(+), 31 deletions(-) diff --git a/crates/by-chain/icepick-solana/src/lib.rs b/crates/by-chain/icepick-solana/src/lib.rs index 3799321..b2a38dc 100644 --- a/crates/by-chain/icepick-solana/src/lib.rs +++ b/crates/by-chain/icepick-solana/src/lib.rs @@ -431,7 +431,10 @@ impl Module for Solana { Some((address, decimals)) => serde_json::json!({ "blob": { "token_address": address, - "token_decimals": decimals, + // forgive me father, for i have sinned + // see: https://git.distrust.co/public/icepick/issues/26 + // TransferToken { decimals: String } + "token_decimals": decimals.to_string(), } }), None => serde_json::json!({ diff --git a/crates/icepick/src/cli/mod.rs b/crates/icepick/src/cli/mod.rs index 5385490..ff5b64b 100644 --- a/crates/icepick/src/cli/mod.rs +++ b/crates/icepick/src/cli/mod.rs @@ -1,5 +1,6 @@ use clap::command; use icepick_module::help::*; +use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -17,6 +18,32 @@ pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) { } } +pub fn derive_keys( + algo: &DerivationAlgorithm, + path_prefix: &DerivationPath, + accounts: &[DerivationIndex], +) -> Vec> { + let mut derived_keys = vec![]; + let mut client = keyforkd_client::Client::discover_socket().expect("keyforkd started"); + for account in accounts { + let request = keyfork_derive_util::request::DerivationRequest::new( + algo.clone(), + &path_prefix.clone().chain_push(account.clone()), + ); + let request = keyforkd_models::Request::Derivation(request); + let response = client.request(&request).expect("valid derivation"); + match response { + keyforkd_models::Response::Derivation( + keyfork_derive_util::request::DerivationResponse { data, .. }, + ) => { + derived_keys.push(data.to_vec()); + } + _ => panic!("Unexpected response"), + } + } + derived_keys +} + #[derive(Serialize, Deserialize, Debug)] struct ModuleConfig { name: String, @@ -159,7 +186,7 @@ pub fn do_cli_thing() { .find(|(module, _)| module == module_name) .and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name)) .expect("workflow from CLI should match config"); - workflow.handle(matches, commands); + workflow.handle(matches, commands, &config.modules); return; } @@ -219,24 +246,7 @@ pub fn do_cli_thing() { let accounts: Vec = serde_json::from_value(accounts.clone()) .expect("valid derivation_accounts"); - let mut client = - keyforkd_client::Client::discover_socket().expect("keyforkd started"); - for account in accounts { - let request = keyfork_derive_util::request::DerivationRequest::new( - algo.clone(), - &path.clone().chain_push(account), - ); - let request = keyforkd_models::Request::Derivation(request); - let response = client.request(&request).expect("valid derivation"); - match response { - keyforkd_models::Response::Derivation( - keyfork_derive_util::request::DerivationResponse { data, .. }, - ) => { - derived_keys.push(data.to_vec()); - } - _ => panic!("Unexpected response"), - } - } + derived_keys.extend(derive_keys(&algo, &path, &accounts)); } let json = serde_json::json!({ diff --git a/crates/icepick/src/cli/workflow.rs b/crates/icepick/src/cli/workflow.rs index d6df17a..256f640 100644 --- a/crates/icepick/src/cli/workflow.rs +++ b/crates/icepick/src/cli/workflow.rs @@ -1,8 +1,13 @@ +use keyfork_derive_util::DerivationIndex; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + io::Write, + process::{Command, Stdio}, +}; -use super::{Commands, Operation}; +use super::{derive_keys, get_command, Commands, ModuleConfig, Operation}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Workflow { @@ -34,11 +39,61 @@ pub struct WorkflowStep { #[derive(Clone, Debug)] struct InvocableOperation { + module: String, name: String, binary: String, operation: Operation, } +// TODO: This should probably be migrated to an actual Result type, instead of +// currently just shoving everything in "blob". Probably done after derivation_accounts +// gets hoisted out of here. +#[derive(Serialize, Deserialize)] +struct OperationResult { + // All values returned from an operation. + blob: HashMap, + + // Any requested accounts from an operation. + // + // TODO: Move this to its own step. + #[serde(default)] + derivation_accounts: Vec, +} + +impl InvocableOperation { + fn invoke(&self, input: &HashMap, derived_keys: &[Vec]) -> OperationResult { + let (command, args) = get_command(&self.binary); + + let json = serde_json::json!({ + "operation": self.operation.name, + "values": input, + "derived_keys": derived_keys, + }); + + let mut child = Command::new(command) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut child_input = child.stdin.take().unwrap(); + serde_json::to_writer(&mut child_input, &json).unwrap(); + child_input + .write_all(b"\n{\"operation\": \"exit\"}\n") + .unwrap(); + + let result = child.wait_with_output().unwrap(); + if !result.status.success() { + panic!("Bad exit: {}", String::from_utf8_lossy(&result.stderr)); + } + + let output = result.stdout; + let json: OperationResult = serde_json::from_slice(&output).expect("valid json"); + json + } +} + impl Workflow { /// Generate a [`clap::Command`] for a [`Workflow`], where the inputs can be defined either by /// command-line arguments or via a JSON input file. @@ -121,7 +176,78 @@ impl Workflow { } } - pub fn handle(&self, matches: &clap::ArgMatches, modules: Commands) { + fn run_workflow( + &self, + mut data: HashMap, + operations: &[InvocableOperation], + config: &[ModuleConfig], + ) { + let mut derived_keys = vec![]; + let mut derivation_accounts = vec![]; + + for step in &self.steps { + let operation = operations + .iter() + .find(|op| op.name == step.r#type) + .expect("operation matched step type"); + + // Load keys from Keyfork, from previously requested workflow + let config = config + .iter() + .find(|module| module.name == operation.module) + .expect("could not find module config"); + let algo = &config.algorithm; + let path_prefix = &config.derivation_prefix; + derived_keys.extend(derive_keys(algo, path_prefix, &derivation_accounts)); + derivation_accounts.clear(); + + // Prepare all inputs for the operation invocation + // + // NOTE: this could be .clone().into_iter() but it would create an extra allocation of + // the HashMap, and an unnecessary alloc of the key. + let inputs: HashMap = data + .iter() + .map(|(k, v)| (k, v.clone())) + .filter_map(|(k, v)| { + // We have our stored name, `k`, which matches with this inner loop's `v`. We + // need to return our desired name, rather than our stored name, and the value + // in our storage, our current `v`. + let (desired, _stored) = step.inputs.iter().find(|(_, v)| k == *v)?; + Some((desired.clone(), v)) + }) + .chain( + step.values + .iter() + .map(|(k, v)| (k.clone(), Value::String(v.clone()))), + ) + .collect(); + let OperationResult { + blob, + derivation_accounts: new_accounts, + } = operation.invoke(&inputs, &derived_keys); + derived_keys.clear(); + derivation_accounts.extend(new_accounts); + data.extend(blob.into_iter().filter_map(|(k, v)| { + // We have our stored name, `k`, which matches with this inner loop's `v`. We + // need to return our desired name, rather than our stored name, and the value + // in our storage, our current `v`. + let (_given, stored) = step.outputs.iter().find(|(k1, _)| k == **k1)?; + Some((stored.clone(), v)) + })); + } + + let last_outputs = &self.steps.last().unwrap().outputs; + data.retain(|stored_name, _| { + last_outputs + .values() + .any(|storage_name| stored_name == storage_name) + }); + + let json_as_str = serde_json::to_string(&data).unwrap(); + println!("{json_as_str}"); + } + + pub fn handle(&self, matches: &clap::ArgMatches, modules: Commands, config: &[ModuleConfig]) { let inputs = self.load_inputs(matches); let data: HashMap = inputs .into_iter() @@ -134,6 +260,7 @@ impl Workflow { for operation in module_operations { let operation_name = &operation.name; let io = InvocableOperation { + module: module_name.clone(), name: format!("{module_name}-{operation_name}"), binary: module_binary.clone(), operation: operation.clone(), @@ -147,6 +274,6 @@ impl Workflow { return; } - todo!("Unsimulated transaction!"); + self.run_workflow(data, &operations, config); } } diff --git a/icepick.toml b/icepick.toml index 995bf1e..a060aa4 100644 --- a/icepick.toml +++ b/icepick.toml @@ -33,15 +33,23 @@ type = "sol-get-token-info" # The key is the key that is passed to the program in the # `values` field. The value is the item in storage. In this case, # we read a `token-name` from our input, but the operation expects `token`. -inputs = { token= "token_name" } +inputs = { token = "token_name" } # Because these two fields are currently unused in our storage, we can grab # them from the outputs of our module. The key is the key of the output value # we want to store, and the value is the name to be assigned in storage. outputs = { token_address = "token_address", token_decimals = "token_decimals" } +# Get a blockhash +[[module.workflow.step]] +type = "sol-get-blockhash" + +outputs = { blockhash = "blockhash" } + [[module.workflow.step]] # Generate an unsigned Transaction +# This step MUST run immediately before sol-sign, as in the current version of +# Icepick, keys are only held in memory in-between a single module invocation. type = "sol-transfer-token" # If using a lot of inputs, it may be best to use a non-inline table. @@ -59,12 +67,6 @@ from_address = "from_address" [module.workflow.step.outputs] transaction = "unsigned_transaction" -# Get a blockhash -[[module.workflow.step]] -type = "sol-get-blockhash" - -outputs = { blockhash = "blockhash" } - # Sign the transaction [[module.workflow.step]] type = "sol-sign"