Compare commits
4 Commits
merge-blob
...
main
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 6a3ab8a64b | |
Ryan Heywood | 46cf4129ac | |
Ryan Heywood | fdeac3a313 | |
Ryan Heywood | c77f460176 |
|
@ -72,7 +72,7 @@ const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {}
|
pub enum Error {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum Cluster {
|
pub enum Cluster {
|
||||||
Devnet,
|
Devnet,
|
||||||
|
@ -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!({
|
||||||
|
@ -623,7 +626,10 @@ impl Module for Solana {
|
||||||
"derivation_accounts": [0u32 | 1 << 31],
|
"derivation_accounts": [0u32 | 1 << 31],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Operation::Sign(Sign { blockhash, mut transaction }) => {
|
Operation::Sign(Sign {
|
||||||
|
blockhash,
|
||||||
|
mut transaction,
|
||||||
|
}) => {
|
||||||
let keys = request
|
let keys = request
|
||||||
.derived_keys
|
.derived_keys
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
@ -641,7 +647,10 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Operation::Broadcast(Broadcast { cluster, transaction }) => {
|
Operation::Broadcast(Broadcast {
|
||||||
|
cluster,
|
||||||
|
transaction,
|
||||||
|
}) => {
|
||||||
let cluster = cluster.unwrap_or(Cluster::MainnetBeta);
|
let cluster = cluster.unwrap_or(Cluster::MainnetBeta);
|
||||||
let cluster_url = format!("https://api.{cluster}.solana.com");
|
let cluster_url = format!("https://api.{cluster}.solana.com");
|
||||||
|
|
||||||
|
@ -649,12 +658,20 @@ impl Module for Solana {
|
||||||
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
||||||
let _simulated_response = client.simulate_transaction(&transaction).unwrap();
|
let _simulated_response = client.simulate_transaction(&transaction).unwrap();
|
||||||
let response = client.send_and_confirm_transaction(&transaction);
|
let response = client.send_and_confirm_transaction(&transaction);
|
||||||
|
let cluster_suffix = {
|
||||||
|
if cluster == Cluster::MainnetBeta {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("?cluster={cluster}")
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(match response {
|
Ok(match response {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"blob": {
|
"blob": {
|
||||||
"status": "send_and_confirm",
|
"status": "send_and_confirm",
|
||||||
"succcess": s.to_string(),
|
"succcess": s.to_string(),
|
||||||
|
"url": format!("https://explorer.solana.com/tx/{s}{cluster_suffix}"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,35 @@ pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn derive_keys(
|
||||||
|
algo: &DerivationAlgorithm,
|
||||||
|
path_prefix: &DerivationPath,
|
||||||
|
accounts: &[DerivationIndex],
|
||||||
|
) -> Vec<Vec<u8>> {
|
||||||
|
if accounts.is_empty() {
|
||||||
|
return 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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
struct ModuleConfig {
|
struct ModuleConfig {
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -159,7 +189,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 +249,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!({
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,4 @@ echo "Waiting for signed transaction..."
|
||||||
while test ! -f /data/output.json; do sleep 1; done
|
while test ! -f /data/output.json; do sleep 1; done
|
||||||
|
|
||||||
echo "Broadcasting transaction"
|
echo "Broadcasting transaction"
|
||||||
icepick sol broadcast --cluster devnet < /data/output.json
|
icepick sol broadcast --cluster devnet < /data/output.json | jq .
|
||||||
|
|
16
icepick.toml
16
icepick.toml
|
@ -33,15 +33,23 @@ type = "sol-get-token-info"
|
||||||
# The key is the key that is passed to the program in the
|
# 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,
|
# `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`.
|
# 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
|
# 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
|
# 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.
|
# 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"
|
||||||
|
|
Loading…
Reference in New Issue