icepick workflow: it makes a signed transaction!!!

This commit is contained in:
Ryan Heywood 2024-12-20 19:47:17 -05:00
parent fdeac3a313
commit 46cf4129ac
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
4 changed files with 173 additions and 31 deletions

View File

@ -431,7 +431,10 @@ impl Module for Solana {
Some((address, decimals)) => serde_json::json!({ Some((address, decimals)) => serde_json::json!({
"blob": { "blob": {
"token_address": address, "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!({ None => serde_json::json!({

View File

@ -1,5 +1,6 @@
use clap::command; use clap::command;
use icepick_module::help::*; use icepick_module::help::*;
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, 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<Vec<u8>> {
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)] #[derive(Serialize, Deserialize, Debug)]
struct ModuleConfig { struct ModuleConfig {
name: String, name: String,
@ -159,7 +186,7 @@ pub fn do_cli_thing() {
.find(|(module, _)| module == module_name) .find(|(module, _)| module == module_name)
.and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name)) .and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name))
.expect("workflow from CLI should match config"); .expect("workflow from CLI should match config");
workflow.handle(matches, commands); workflow.handle(matches, commands, &config.modules);
return; return;
} }
@ -219,24 +246,7 @@ pub fn do_cli_thing() {
let accounts: Vec<keyfork_derive_util::DerivationIndex> = let accounts: Vec<keyfork_derive_util::DerivationIndex> =
serde_json::from_value(accounts.clone()) serde_json::from_value(accounts.clone())
.expect("valid derivation_accounts"); .expect("valid derivation_accounts");
let mut client = derived_keys.extend(derive_keys(&algo, &path, &accounts));
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"),
}
}
} }
let json = serde_json::json!({ let json = serde_json::json!({

View File

@ -1,8 +1,13 @@
use keyfork_derive_util::DerivationIndex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; 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)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Workflow { pub struct Workflow {
@ -34,11 +39,61 @@ pub struct WorkflowStep {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct InvocableOperation { struct InvocableOperation {
module: String,
name: String, name: String,
binary: String, binary: String,
operation: Operation, 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<String, Value>,
// Any requested accounts from an operation.
//
// TODO: Move this to its own step.
#[serde(default)]
derivation_accounts: Vec<DerivationIndex>,
}
impl InvocableOperation {
fn invoke(&self, input: &HashMap<String, Value>, derived_keys: &[Vec<u8>]) -> 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 { impl Workflow {
/// Generate a [`clap::Command`] for a [`Workflow`], where the inputs can be defined either by /// Generate a [`clap::Command`] for a [`Workflow`], where the inputs can be defined either by
/// command-line arguments or via a JSON input file. /// 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<String, Value>,
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<String, Value> = 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 inputs = self.load_inputs(matches);
let data: HashMap<String, Value> = inputs let data: HashMap<String, Value> = inputs
.into_iter() .into_iter()
@ -134,6 +260,7 @@ impl Workflow {
for operation in module_operations { for operation in module_operations {
let operation_name = &operation.name; let operation_name = &operation.name;
let io = InvocableOperation { let io = InvocableOperation {
module: module_name.clone(),
name: format!("{module_name}-{operation_name}"), name: format!("{module_name}-{operation_name}"),
binary: module_binary.clone(), binary: module_binary.clone(),
operation: operation.clone(), operation: operation.clone(),
@ -147,6 +274,6 @@ impl Workflow {
return; return;
} }
todo!("Unsimulated transaction!"); self.run_workflow(data, &operations, config);
} }
} }

View File

@ -40,8 +40,16 @@ inputs = { token= "token_name" }
# we want to store, and the value is the name to be assigned in storage. # we want to store, and the value is the name to be assigned in storage.
outputs = { token_address = "token_address", token_decimals = "token_decimals" } 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]] [[module.workflow.step]]
# Generate an unsigned Transaction # 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" type = "sol-transfer-token"
# If using a lot of inputs, it may be best to use a non-inline table. # 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] [module.workflow.step.outputs]
transaction = "unsigned_transaction" transaction = "unsigned_transaction"
# Get a blockhash
[[module.workflow.step]]
type = "sol-get-blockhash"
outputs = { blockhash = "blockhash" }
# Sign the transaction # Sign the transaction
[[module.workflow.step]] [[module.workflow.step]]
type = "sol-sign" type = "sol-sign"