diff --git a/Cargo.lock b/Cargo.lock index 23b070c..3a1d2e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1460,10 +1460,20 @@ dependencies = [ "keyforkd-models", "serde", "serde_json", - "thiserror 2.0.3", + "thiserror 2.0.9", "toml 0.8.19", ] +[[package]] +name = "icepick-internal" +version = "0.1.0" +dependencies = [ + "icepick-module", + "serde", + "serde_json", + "thiserror 2.0.9", +] + [[package]] name = "icepick-module" version = "0.1.0" @@ -1486,7 +1496,7 @@ dependencies = [ "spl-associated-token-account", "spl-token", "spl-token-2022", - "thiserror 2.0.3", + "thiserror 2.0.9", ] [[package]] @@ -4116,11 +4126,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.9", ] [[package]] @@ -4136,9 +4146,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5c5111b..7039e12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "crates/icepick", "crates/icepick-module", + "crates/builtins/icepick-internal", "crates/by-chain/icepick-solana", ] diff --git a/crates/builtins/icepick-internal/Cargo.toml b/crates/builtins/icepick-internal/Cargo.toml new file mode 100644 index 0000000..6e2ce98 --- /dev/null +++ b/crates/builtins/icepick-internal/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "icepick-internal" +version = "0.1.0" +edition = "2021" + +[dependencies] +icepick-module = { version = "0.1.0", path = "../../icepick-module" } +serde.workspace = true +serde_json.workspace = true +thiserror = "2.0.9" diff --git a/crates/builtins/icepick-internal/src/lib.rs b/crates/builtins/icepick-internal/src/lib.rs new file mode 100644 index 0000000..7e87a47 --- /dev/null +++ b/crates/builtins/icepick-internal/src/lib.rs @@ -0,0 +1,105 @@ +use icepick_module::{ + help::{Argument, ArgumentType}, + Module, +}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +fn path_for_filename(filename: &Path) -> PathBuf { + PathBuf::from( + std::env::vars() + .find(|(k, _)| k == "ICEPICK_DATA_DIRECTORY") + .map(|(_, v)| v) + .as_deref() + .unwrap_or("/media/external"), + ) + .join(filename) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "operation", content = "values", rename_all = "kebab-case")] +pub enum Request { + LoadFile { + filename: PathBuf, + }, + + SaveFile { + filename: PathBuf, + + #[serde(flatten)] + values: serde_json::Value, + }, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error {} + +pub struct Internal; + +impl Module for Internal { + type Error = Error; + + type Request = Request; + + fn describe_operations() -> Vec { + let filename = Argument { + name: "filename".to_string(), + description: "The file to load or save data to.".to_string(), + r#type: ArgumentType::Required, + }; + vec![ + icepick_module::help::Operation { + name: "load-file".to_string(), + description: "Load data from a JSON file.".to_string(), + arguments: vec![filename.clone()], + }, + icepick_module::help::Operation { + name: "save-file".to_string(), + description: "Save data from a JSON file.".to_string(), + arguments: vec![filename.clone()], + }, + ] + } + + fn handle_request(request: Self::Request) -> Result { + match request { + Request::LoadFile { filename } => { + let path = path_for_filename(&filename); + + let mut attempt = 0; + while !std::fs::exists(&path).is_ok_and(|v| v) { + if attempt % 10 == 0 { + eprintln!( + "Waiting for {path} to be populated...", + path = path.to_string_lossy() + ); + } + attempt += 1; + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + // if we ran at least once, we should have previously printed a message. write a + // confirmation that we are no longer waiting. if we haven't, we've never printed + // a message, therefore we don't need to confirm the prior reading. + if attempt > 0 { + eprintln!("File contents loaded."); + } + + let file = std::fs::File::open(path).unwrap(); + let json: serde_json::Value = serde_json::from_reader(file).unwrap(); + Ok(serde_json::json!({ + "blob": json, + })) + } + Request::SaveFile { filename, values } => { + let path = path_for_filename(&filename); + let file = std::fs::File::create(path).unwrap(); + serde_json::to_writer(file, &values).unwrap(); + + Ok(serde_json::json!({ + "blob": {}, + })) + } + } + } +} diff --git a/crates/builtins/icepick-internal/src/main.rs b/crates/builtins/icepick-internal/src/main.rs new file mode 100644 index 0000000..d4ab2d8 --- /dev/null +++ b/crates/builtins/icepick-internal/src/main.rs @@ -0,0 +1,6 @@ +use icepick_module::Module; +use icepick_internal::Internal; + +fn main() -> Result<(), Box> { + Internal::run_responder() +} diff --git a/crates/icepick/src/cli/mod.rs b/crates/icepick/src/cli/mod.rs index b540375..c312bf7 100644 --- a/crates/icepick/src/cli/mod.rs +++ b/crates/icepick/src/cli/mod.rs @@ -59,12 +59,13 @@ struct ModuleConfig { /// The bip32 derivation algorithm. This is currently used for deriving keys from Keyfork, but /// may be passed to modules within the workflow to provide additional context, such as the /// algorithm for a generic signer. + #[serde(default)] algorithm: Option, /// The bip44 derivation prefix. This is currently used for deriving keys from Keyfork directly /// within Icepick, but may be passed to modules within the workflow to provide additional /// context, such as a module for deriving keys. - #[serde(with = "serde_derivation")] + #[serde(with = "serde_derivation", default)] derivation_prefix: Option, /// All workflows for a module. @@ -121,7 +122,14 @@ pub fn do_cli_thing() { }); let config_path = config_file.unwrap_or_else(|| "icepick.toml".to_string()); let config_content = std::fs::read_to_string(config_path).expect("can't read config file"); - let config: Config = toml::from_str(&config_content).expect("config file had invalid toml"); + let mut config: Config = toml::from_str(&config_content).expect("config file had invalid toml"); + config.modules.push(ModuleConfig { + name: "internal".to_string(), + command_name: Default::default(), + algorithm: Default::default(), + derivation_prefix: Default::default(), + workflows: Default::default(), + }); let mut commands = vec![]; let mut icepick_command = command!(); diff --git a/crates/icepick/src/cli/workflow.rs b/crates/icepick/src/cli/workflow.rs index 8d857ac..35c0bae 100644 --- a/crates/icepick/src/cli/workflow.rs +++ b/crates/icepick/src/cli/workflow.rs @@ -198,14 +198,16 @@ impl Workflow { .expect("could not find module config"); let algo = &config.algorithm; let path_prefix = &config.derivation_prefix; - derived_keys.extend(derive_keys( - algo.as_ref() - .expect("a module requested keys but didn't provide algorithm"), - path_prefix - .as_ref() - .expect("a module requested keys but didn't provide prefix"), - &derivation_accounts, - )); + if !derivation_accounts.is_empty() { + derived_keys.extend(derive_keys( + algo.as_ref() + .expect("a module requested keys but didn't provide algorithm"), + path_prefix + .as_ref() + .expect("a module requested keys but didn't provide prefix"), + &derivation_accounts, + )); + } derivation_accounts.clear(); // Prepare all inputs for the operation invocation diff --git a/e2e-tests/solana/base.Containerfile b/e2e-tests/solana/base.Containerfile index fa23b9b..5d90a17 100644 --- a/e2e-tests/solana/base.Containerfile +++ b/e2e-tests/solana/base.Containerfile @@ -44,8 +44,12 @@ RUN < /data/output.json.tmp -mv /data/output.json.tmp /data/output.json +icepick workflow sol transfer-token --from-address "$from_address" --to-address "$to_address" --token-name "$token_name" --token-amount "$token_amount" diff --git a/e2e-tests/solana/online.sh b/e2e-tests/solana/online.sh index 5f64102..988a30f 100644 --- a/e2e-tests/solana/online.sh +++ b/e2e-tests/solana/online.sh @@ -1,32 +1,24 @@ printf "%s" "Public key of the sender address: " -read from_address +read -r from_address printf "%s" "Public key of the recipient address: " -read to_address +read -r to_address printf "%s" "Name of the token to transfer: " -read token_name +read -r token_name printf "%s" "Amount of token to transfer: " -read token_amount +read -r token_amount -echo "Acquiring blockhash..." -blockhash="$(icepick sol get-blockhash --cluster devnet | jq -r .blob.blockhash)" - -echo "Saving information to file" +echo "Saving inputs to file" cat < /data/input.json { "from_address": "$from_address", "to_address": "$to_address", "token_name": "$token_name", - "token_amount": "$token_amount", - "blockhash": "$blockhash" + "token_amount": "$token_amount" } EOF -echo "Waiting for signed transaction..." -while test ! -f /data/output.json; do sleep 1; done - -echo "Broadcasting transaction" -icepick sol broadcast --cluster devnet < /data/output.json | jq . +icepick workflow sol broadcast --cluster devnet diff --git a/icepick.toml b/icepick.toml index 228a169..1882329 100644 --- a/icepick.toml +++ b/icepick.toml @@ -13,18 +13,7 @@ name = "transfer-token" # be serialized by serde_json::Value. # These values can also be loaded using "internal-load-file", using some form # of later-defined signature validation. -inputs = ["from_address", "to_address", "token_name", "token_amount", "cluster"] - -## Load the Blockhash from the SD card -#[[module.workflow.step]] -#type = "internal-load-file" -# -## Pre-defined values to be passed to the module -#values = { filename = "blockhash.json" } -# -## This value is marked to be saved in-memory, and can be used as an input for -## later steps. -#outputs = { blockhash = "blockhash" } +inputs = ["from_address", "to_address", "token_name", "token_amount"] # Get the token address and token decimals for the given token [[module.workflow.step]] @@ -40,12 +29,15 @@ inputs = { token = "token_name" } # 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 +# Load the Blockhash from the SD card [[module.workflow.step]] -type = "sol-get-blockhash" +type = "internal-load-file" -inputs = { cluster = "cluster" } +# Pre-defined values to be passed to the module +values = { filename = "blockhash.json" } +# This value is marked to be saved in-memory, and can be used as an input for +# later steps. outputs = { blockhash = "blockhash" } [[module.workflow.step]] @@ -80,21 +72,38 @@ blockhash = "blockhash" [module.workflow.step.outputs] transaction = "signed_transaction" -# Broadcast the transaction +# Write the signed transaction to a file +[[module.workflow.step]] +type = "internal-save-file" + +# We are using a static filename here, so we use `values` instead of `inputs`. +values = { filename = "transaction.json" } + +# All fields in both `inputs` and `values`, other than `filename`, will be +# persisted to the file. In this case, the `transaction` field of the file will +# contain the signed transaction. +inputs = { transaction = "signed_transaction" } + +[[module.workflow]] +name = "broadcast" +inputs = ["cluster"] + +[[module.workflow.step]] +type = "sol-get-blockhash" +inputs = { cluster = "cluster" } +outputs = { blockhash = "blockhash" } + +[[module.workflow.step]] +type = "internal-save-file" +values = { filename = "blockhash.json" } +inputs = { blockhash = "blockhash" } + +[[module.workflow.step]] +type = "internal-load-file" +values = { filename = "transaction.json" } +outputs = { transaction = "transaction" } + [[module.workflow.step]] type = "sol-broadcast" - -inputs = { cluster = "cluster", transaction = "signed_transaction" } +inputs = { cluster = "cluster", transaction = "transaction" } outputs = { status = "status", url = "url" } - -## Write the signed transaction to a file -#[[module.workflow.step]] -#type = "internal-save-file" -# -## We are using a static filename here, so we use `values` instead of `inputs`. -#values = { filename = "transaction.json" } -# -## All fields in both `inputs` and `values`, other than `filename`, will be -## persisted to the file. In this case, the `transaction` field of the file will -## contain the signed transaction. -#inputs = { transaction = "signed_transaction" }