Compare commits
No commits in common. "bfe345a8f901e1f2fbcf428ab134d2479b0ca5cc" and "dc91b0bee89c73a33e6a841d6494e8173b2f6594" have entirely different histories.
bfe345a8f9
...
dc91b0bee8
|
@ -1,2 +0,0 @@
|
||||||
[registries.distrust]
|
|
||||||
index = "https://git.distrust.co/public/_cargo-index.git"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,10 +3,6 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/icepick",
|
"crates/icepick",
|
||||||
"crates/icepick-module",
|
|
||||||
"crates/by-chain/icepick-solana",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version= "1.0.195", features = ["derive"] }
|
|
||||||
serde_json = "1.0.111"
|
|
||||||
|
|
16
README.md
16
README.md
|
@ -1,19 +1,3 @@
|
||||||
# Icepick
|
|
||||||
|
|
||||||
Icepick is a framework for rapidly developing applications to perform transfer
|
|
||||||
and staking cryptocurrency operations.
|
|
||||||
|
|
||||||
## Implementing a New Coin
|
|
||||||
|
|
||||||
Coins are implemented using the `icepick_coin::Coin` trait. Implementing the
|
|
||||||
trait should act as a "flow" for implementing the coin.
|
|
||||||
|
|
||||||
The crate will need the `clap` dependency for generating the CLI command and
|
|
||||||
the `icepick-coin` dependency for implementing the `Coin` trait.
|
|
||||||
|
|
||||||
`Coin` should be implemented on marker structs that can be easily placed in the
|
|
||||||
main Icepick program.
|
|
||||||
|
|
||||||
## Bitcoin
|
## Bitcoin
|
||||||
|
|
||||||
Bitcoin is a native coin, and does not require any kind of custom token support
|
Bitcoin is a native coin, and does not require any kind of custom token support
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "icepick-solana"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
serde_json.workspace = true
|
|
||||||
solana-sdk = { version = "2.1.1", path = "../../../vendor/solana-sdk" }
|
|
||||||
thiserror = "2.0.3"
|
|
|
@ -1,6 +0,0 @@
|
||||||
use icepick_module::Module;
|
|
||||||
use icepick_solana::Solana;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
Solana::run_responder()
|
|
||||||
}
|
|
|
@ -1,207 +0,0 @@
|
||||||
use icepick_module::{
|
|
||||||
help::{Argument, ArgumentType},
|
|
||||||
Module,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// How does this not exist in solana_sdk.
|
|
||||||
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum Error {}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct Transfer {
|
|
||||||
amount: String,
|
|
||||||
blockhash: String,
|
|
||||||
to_address: String,
|
|
||||||
from_account: Option<String>,
|
|
||||||
from_address: String,
|
|
||||||
fee: Option<String>,
|
|
||||||
fee_payer: Option<String>,
|
|
||||||
fee_payer_address: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct Sign {}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct Request {
|
|
||||||
// NOTE: Can't use the proper XPrv type from Keyfork because Solana's a big stinky
|
|
||||||
// and adds in its own derivation constructs that cause type conflicts.
|
|
||||||
derived_keys: Option<Vec<[u8; 32]>>,
|
|
||||||
|
|
||||||
// NOTE: This is an opaque type that can be deserialized inside an Operation
|
|
||||||
blob: Option<serde_json::Value>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
operation: Operation,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[serde(tag = "operation", content = "values", rename_all = "kebab-case")]
|
|
||||||
pub enum Operation {
|
|
||||||
Transfer(Transfer),
|
|
||||||
Sign(Sign),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Solana;
|
|
||||||
|
|
||||||
impl Module for Solana {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
type Request = Request;
|
|
||||||
|
|
||||||
fn describe_operations() -> Vec<icepick_module::help::Operation> {
|
|
||||||
let account = Argument {
|
|
||||||
name: "from-account".to_string(),
|
|
||||||
description: "The derivation account used for the transaction.".to_string(),
|
|
||||||
r#type: ArgumentType::Optional,
|
|
||||||
};
|
|
||||||
let fee = Argument {
|
|
||||||
name: "fee".to_string(),
|
|
||||||
description: "A custom fee for the transaction".to_string(),
|
|
||||||
r#type: ArgumentType::Optional,
|
|
||||||
};
|
|
||||||
let fee_payer_address = Argument {
|
|
||||||
name: "fee-payer-address".to_string(),
|
|
||||||
description: "The address used to pay the fee.".to_string(),
|
|
||||||
r#type: ArgumentType::Optional,
|
|
||||||
};
|
|
||||||
let fee_payer = Argument {
|
|
||||||
name: "fee-payer".to_string(),
|
|
||||||
description: "The derivation account used to pay the fee.".to_string(),
|
|
||||||
r#type: ArgumentType::Optional,
|
|
||||||
};
|
|
||||||
let blockhash = Argument {
|
|
||||||
name: "blockhash".to_string(),
|
|
||||||
description: "A recent blockhash".to_string(),
|
|
||||||
r#type: ArgumentType::Required,
|
|
||||||
};
|
|
||||||
let from_address = Argument {
|
|
||||||
name: "from-address".to_string(),
|
|
||||||
description: concat!(
|
|
||||||
"The address to send SOL from; will be used to verify ",
|
|
||||||
"the derivation account."
|
|
||||||
)
|
|
||||||
.to_string(),
|
|
||||||
r#type: ArgumentType::Required,
|
|
||||||
};
|
|
||||||
vec![
|
|
||||||
icepick_module::help::Operation {
|
|
||||||
name: "transfer".to_string(),
|
|
||||||
description: "Transfer SOL from a Keyfork wallet to an external wallet."
|
|
||||||
.to_string(),
|
|
||||||
arguments: vec![
|
|
||||||
Argument {
|
|
||||||
name: "amount".to_string(),
|
|
||||||
description: "The amount of SOL to transfer.".to_string(),
|
|
||||||
r#type: ArgumentType::Required,
|
|
||||||
},
|
|
||||||
account.clone(),
|
|
||||||
Argument {
|
|
||||||
name: "to-address".to_string(),
|
|
||||||
description: "The address to send SOL to.".to_string(),
|
|
||||||
r#type: ArgumentType::Required,
|
|
||||||
},
|
|
||||||
from_address.clone(),
|
|
||||||
blockhash.clone(),
|
|
||||||
fee.clone(),
|
|
||||||
fee_payer.clone(),
|
|
||||||
fee_payer_address.clone(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
icepick_module::help::Operation {
|
|
||||||
name: "stake".to_string(),
|
|
||||||
description: "Stake SOL to earn rewards.".to_string(),
|
|
||||||
arguments: vec![
|
|
||||||
Argument {
|
|
||||||
name: "amount".to_string(),
|
|
||||||
description: "The amount of SOL to stake.".to_string(),
|
|
||||||
r#type: ArgumentType::Required,
|
|
||||||
},
|
|
||||||
account.clone(),
|
|
||||||
from_address.clone(),
|
|
||||||
blockhash.clone(),
|
|
||||||
fee.clone(),
|
|
||||||
fee_payer.clone(),
|
|
||||||
fee_payer_address.clone(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
icepick_module::help::Operation {
|
|
||||||
name: "sign".to_string(),
|
|
||||||
description: "Sign a previously-generated transaction.".to_string(),
|
|
||||||
arguments: vec![],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
|
|
||||||
match request.operation {
|
|
||||||
Operation::Transfer(Transfer {
|
|
||||||
amount,
|
|
||||||
from_account,
|
|
||||||
to_address,
|
|
||||||
from_address,
|
|
||||||
blockhash,
|
|
||||||
fee: _,
|
|
||||||
fee_payer,
|
|
||||||
fee_payer_address,
|
|
||||||
}) => {
|
|
||||||
// TODO:
|
|
||||||
// parse address for to_address
|
|
||||||
|
|
||||||
let amount = f64::from_str(&amount).expect("float amount");
|
|
||||||
let amount: u64 = (amount * LAMPORTS_PER_SOL as f64) as u64;
|
|
||||||
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
let to_pk = Pubkey::from_str(&to_address).unwrap();
|
|
||||||
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
|
||||||
let payer_account_and_pk = {
|
|
||||||
// If a fee payer is given, a fee payer address must also be given, since the
|
|
||||||
// address must be known before signing the transaction.
|
|
||||||
match (&fee_payer, &fee_payer_address) {
|
|
||||||
(Some(payer), Some(address)) => {
|
|
||||||
// Use the provided account
|
|
||||||
Some((payer.clone(), Pubkey::from_str_const(address)))
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
// Use the transaction account
|
|
||||||
None
|
|
||||||
}
|
|
||||||
_ => panic!("Invalid combination of fee_payer and fee_payer_address"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let instruction =
|
|
||||||
solana_sdk::system_instruction::transfer(&from_pk, &to_pk, amount);
|
|
||||||
let hash = solana_sdk::hash::Hash::from_str(&blockhash).unwrap();
|
|
||||||
let message = solana_sdk::message::Message::new_with_blockhash(
|
|
||||||
&[instruction],
|
|
||||||
payer_account_and_pk.map(|v| v.1).as_ref(),
|
|
||||||
&hash,
|
|
||||||
);
|
|
||||||
let transaction = solana_sdk::transaction::Transaction::new_unsigned(message);
|
|
||||||
let mut required_derivation_indices = vec![];
|
|
||||||
// TODO: error handling from_str
|
|
||||||
let from_account = from_account.and_then(|a| u32::from_str(&a).ok()).unwrap_or(0);
|
|
||||||
required_derivation_indices.push(from_account);
|
|
||||||
Ok(serde_json::json!({
|
|
||||||
"blob": transaction,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
Operation::Sign(Sign {}) => {
|
|
||||||
let blob = request.blob.expect("passed in instruction blob");
|
|
||||||
let transaction: solana_sdk::transaction::Transaction =
|
|
||||||
serde_json::from_value(blob).expect("valid message blob");
|
|
||||||
dbg!(transaction);
|
|
||||||
Ok(serde_json::json!({
|
|
||||||
"blob": []
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "icepick-module"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
serde_json.workspace = true
|
|
|
@ -1,118 +0,0 @@
|
||||||
/// The types used to build the `help` documentation for a module, as well as determine the
|
|
||||||
/// arguments that can be automatically detected by Icepick.
|
|
||||||
pub mod help {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// An operation that can be exposed to a frontend.
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct Operation {
|
|
||||||
/// The name of the argument, in the format expected by the deserializer.
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// A description of what the operation does.
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
/// The arguments to be provided to the operation's deserializer.
|
|
||||||
pub arguments: Vec<Argument>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
/// The context of whether a signature is signed, needs to be signed, or has been signed.
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub enum SigningContext {
|
|
||||||
/// This operation accepts a signed blob.
|
|
||||||
Signed,
|
|
||||||
|
|
||||||
/// This operation accepts an unsigned blob and will return a signed blob.
|
|
||||||
Signer,
|
|
||||||
|
|
||||||
/// This operation will return an unsigned blob.
|
|
||||||
Unsigned,
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ArgumentType {
|
|
||||||
Required,
|
|
||||||
Optional,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An argument to an operation.
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct Argument {
|
|
||||||
/// The name of the argument, in the format expected by the deserializer.
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// A description of the format and parameters of an argument.
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
/// The type of argument - this may affect how it displays in the frontend.
|
|
||||||
pub r#type: ArgumentType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation methods for Icepick Modules, performed over command I/O using JSON.
|
|
||||||
pub trait Module {
|
|
||||||
/// The error type returned by an operation.
|
|
||||||
type Error: std::error::Error + 'static;
|
|
||||||
|
|
||||||
/// The request type. See [`Module::handle_request`] for more information.
|
|
||||||
type Request: serde::de::DeserializeOwned + std::fmt::Debug + 'static;
|
|
||||||
|
|
||||||
/// Describe the operations interface of RPC calls. This information will be used to generate a
|
|
||||||
/// frontend for users to perform requests.
|
|
||||||
fn describe_operations() -> Vec<help::Operation>;
|
|
||||||
|
|
||||||
/// Handle an incoming request. Requests can often be formed using the following schema:
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// #[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
||||||
/// #[serde(tag = "operation", rename_all="kebab-case")]
|
|
||||||
/// enum Request {
|
|
||||||
/// Transfer {
|
|
||||||
/// // This is the amount of the "primary" currency you'd be receiving, such as Ether,
|
|
||||||
/// // Bitcoin, or Sol.
|
|
||||||
/// amount: f64,
|
|
||||||
///
|
|
||||||
/// // This would be your native wallet address type. In this example, we'll just use a
|
|
||||||
/// // String.
|
|
||||||
/// to_address: String,
|
|
||||||
///
|
|
||||||
/// // This is the address of the derivation account.
|
|
||||||
/// from_account: u32,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Stake {
|
|
||||||
/// amount: f64,
|
|
||||||
/// from_account: u32,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Sign {
|
|
||||||
/// blob:
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error>;
|
|
||||||
|
|
||||||
fn run_responder() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut lines = std::io::stdin().lines();
|
|
||||||
while let Some(line) = lines.next().transpose()? {
|
|
||||||
let value: serde_json::Value = serde_json::from_str(&line)?;
|
|
||||||
if let Some(serde_json::Value::String(operation)) = value.get("operation") {
|
|
||||||
if operation == "help" {
|
|
||||||
println!("{}", serde_json::to_string(&Self::describe_operations())?);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if operation == "exit" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: error handling
|
|
||||||
let request: Self::Request = serde_json::from_value(value).expect("good value");
|
|
||||||
let response = Self::handle_request(request)?;
|
|
||||||
println!("{}", serde_json::to_string(&response).unwrap());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,4 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.20", features = ["cargo", "derive", "string"] }
|
clap = { version = "4.5.20", features = ["cargo", "derive"] }
|
||||||
icepick-module = { version = "0.1.0", path = "../icepick-module" }
|
|
||||||
serde_json.workspace = true
|
|
||||||
thiserror = "2.0.3"
|
|
||||||
|
|
|
@ -1,114 +1,139 @@
|
||||||
use clap::command;
|
#![allow(clippy::upper_case_acronyms)]
|
||||||
use icepick_module::help::*;
|
|
||||||
use std::{
|
|
||||||
io::{IsTerminal, Write},
|
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn guessed_bins() -> Vec<String> {
|
use clap::{Arg, Command};
|
||||||
"sol".split_whitespace().map(|s| s.to_owned()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) {
|
// TODO: extract as icepick_coin
|
||||||
if std::env::vars().any(|(k, _)| &k == "ICEPICK_USE_CARGO") {
|
mod icepick_coin {
|
||||||
("cargo", vec!["run", "--bin", bin_name, "--"])
|
use clap::Command;
|
||||||
} else {
|
pub trait Coin {
|
||||||
(bin_name, vec![])
|
/// Return the name for the coin, in lowercase.
|
||||||
}
|
fn coin_name(&self) -> &'static str;
|
||||||
}
|
|
||||||
|
|
||||||
pub fn do_cli_thing() {
|
/// Return a description of the coin.
|
||||||
let mut commands = vec![];
|
fn coin_description(&self) -> &'static str;
|
||||||
let mut icepick_command = command!();
|
|
||||||
for coin_bin in guessed_bins() {
|
/// Given a Command, add any arguments relevant to the Transfer operation.
|
||||||
// try to run the "help" operation on any bins.
|
///
|
||||||
let bin = format!("icepick-{coin_bin}");
|
/// All commands by default have the following values:
|
||||||
let (command, args) = get_command(&bin);
|
/// * `amount`: The amount of the currency to transfer.
|
||||||
let mut child = Command::new(command)
|
/// * `to`: The account to transfer the currency to.
|
||||||
.args(args)
|
/// * `from_account`: The non-default derivation account to send currency from.
|
||||||
.stdin(Stdio::piped())
|
fn build_transfer_command(&self, _command: Command) -> Result<Command, ()> {
|
||||||
.stdout(Stdio::piped())
|
Err(())
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
let mut input = child.stdin.take().unwrap();
|
|
||||||
input
|
|
||||||
.write_all("{\"operation\": \"help\"}\n".as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
input
|
|
||||||
.write_all("{\"operation\": \"exit\"}\n".as_bytes())
|
|
||||||
.unwrap();
|
|
||||||
let output = child.wait_with_output().unwrap().stdout;
|
|
||||||
let operations: Vec<Operation> = serde_json::from_slice::<Vec<Operation>>(&output)
|
|
||||||
.expect("successful deserialization of operation");
|
|
||||||
commands.push((coin_bin, operations));
|
|
||||||
}
|
|
||||||
let commands = commands.leak();
|
|
||||||
for command in commands.iter() {
|
|
||||||
let mut subcommand = clap::Command::new(command.0.as_str());
|
|
||||||
for op in &command.1 {
|
|
||||||
let mut op_command = clap::Command::new(&op.name).about(&op.description);
|
|
||||||
for arg in &op.arguments {
|
|
||||||
let mut op_arg = clap::Arg::new(&arg.name).help(arg.description.as_str());
|
|
||||||
op_arg = match arg.r#type {
|
|
||||||
ArgumentType::Required => op_arg.required(true),
|
|
||||||
ArgumentType::Optional => op_arg.required(false).long(&arg.name),
|
|
||||||
};
|
|
||||||
op_command = op_command.arg(op_arg);
|
|
||||||
}
|
|
||||||
subcommand = subcommand.subcommand(op_command);
|
|
||||||
}
|
}
|
||||||
icepick_command = icepick_command.subcommand(subcommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdin = std::io::stdin();
|
/// Given a Command, add any arguments relevant to the Stake operation.
|
||||||
let mut cli_input: Option<serde_json::Value> = None;
|
fn build_stake_command(&self, _command: Command) -> Result<Command, ()> {
|
||||||
// HACK: Allow good UX when running from CLI without piping in a blob.
|
Err(())
|
||||||
// This is because we have CLI arguments _only_ for the module. We _could_ add a global
|
|
||||||
// argument `--input` instead of reading input.
|
|
||||||
if !stdin.is_terminal() {
|
|
||||||
cli_input = serde_json::from_reader(stdin).ok();
|
|
||||||
}
|
|
||||||
dbg!(&cli_input);
|
|
||||||
let blob = cli_input.as_ref().and_then(|json| {
|
|
||||||
json.get("blob")
|
|
||||||
});
|
|
||||||
|
|
||||||
let matches = icepick_command.get_matches();
|
|
||||||
if let Some((module, matches)) = matches.subcommand() {
|
|
||||||
if let Some((subcommand, matches)) = matches.subcommand() {
|
|
||||||
if let Some(operation) = commands
|
|
||||||
.iter()
|
|
||||||
.find(|(name, _)| name == module)
|
|
||||||
.and_then(|(_, operations)| operations.iter().find(|o| o.name == subcommand))
|
|
||||||
{
|
|
||||||
let mut args = std::collections::HashMap::<&String, Option<&String>>::with_capacity(
|
|
||||||
operation.arguments.len(),
|
|
||||||
);
|
|
||||||
for arg in &operation.arguments {
|
|
||||||
args.insert(&arg.name, matches.get_one::<String>(&arg.name));
|
|
||||||
}
|
|
||||||
let json = serde_json::json!({
|
|
||||||
"operation": subcommand,
|
|
||||||
"values": args,
|
|
||||||
"derived-keys": [],
|
|
||||||
"blob": blob,
|
|
||||||
});
|
|
||||||
let bin = format!("icepick-{module}");
|
|
||||||
let (command, args) = get_command(&bin);
|
|
||||||
let mut child = Command::new(command)
|
|
||||||
.args(args)
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
let mut input = child.stdin.take().unwrap();
|
|
||||||
serde_json::to_writer(&mut input, &json).unwrap();
|
|
||||||
input.write_all(b"\n{\"operation\": \"exit\"}\n").unwrap();
|
|
||||||
let output = child.wait_with_output().unwrap().stdout;
|
|
||||||
let json: serde_json::Value = serde_json::from_slice(&output).expect("valid json");
|
|
||||||
let json_as_str = serde_json::to_string(&json).unwrap();
|
|
||||||
println!("{json_as_str}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: extract as icepick_eth
|
||||||
|
mod eth {
|
||||||
|
use super::icepick_coin::Coin;
|
||||||
|
use clap::arg;
|
||||||
|
|
||||||
|
pub struct ETH;
|
||||||
|
|
||||||
|
impl Coin for ETH {
|
||||||
|
fn coin_name(&self) -> &'static str {
|
||||||
|
"eth"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coin_description(&self) -> &'static str {
|
||||||
|
"The leading platform for innovative apps and blockchain networks."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_transfer_command(&self, command: clap::Command) -> Result<clap::Command, ()> {
|
||||||
|
Ok(command
|
||||||
|
.arg(arg!(--gas <GAS> "Specify custom gas for the transaction."))
|
||||||
|
.arg(arg!(--"gas-payer" <ACCOUNT> "Use an alternative derivation account for paying gas.")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_stake_command(&self, _command: clap::Command) -> Result<clap::Command, ()> {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use icepick_coin::Coin;
|
||||||
|
|
||||||
|
fn build_transfer_command() -> Command {
|
||||||
|
Command::new("transfer")
|
||||||
|
.about("Transfer coins from one wallet to another.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_stake_command() -> Command {
|
||||||
|
Command::new("stake")
|
||||||
|
.about("Stake coins.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_transfer_subcommand(coin: &dyn Coin) -> Command {
|
||||||
|
let name = coin.coin_name();
|
||||||
|
let description = coin.coin_description();
|
||||||
|
let uppercase_name = name.to_uppercase();
|
||||||
|
Command::new(name)
|
||||||
|
.about(description)
|
||||||
|
.arg(Arg::new("amount").help(format!("Amount of {uppercase_name} to transfer")))
|
||||||
|
.arg(Arg::new("to_address").help(format!("Account to send {uppercase_name} to")))
|
||||||
|
.arg(
|
||||||
|
Arg::new("from_account")
|
||||||
|
.help(format!(
|
||||||
|
"Specify a non-default derivation account to send {uppercase_name} from"
|
||||||
|
))
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_stake_subcommand(coin: &dyn Coin) -> Command {
|
||||||
|
let name = coin.coin_name();
|
||||||
|
let description = coin.coin_description();
|
||||||
|
let uppercase_name = name.to_uppercase();
|
||||||
|
Command::new(name)
|
||||||
|
.about(description)
|
||||||
|
.arg(Arg::new("amount").help(format!("Amount of {uppercase_name} to stake")))
|
||||||
|
.arg(
|
||||||
|
Arg::new("from_account")
|
||||||
|
.help(format!(
|
||||||
|
"Specify a non-default derivation account to send {uppercase_name} from"
|
||||||
|
))
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_command() -> Command {
|
||||||
|
let command = clap::command!();
|
||||||
|
let mut transfer_subcommand = build_transfer_command();
|
||||||
|
let mut stake_subcommand = build_stake_command();
|
||||||
|
// TODO: this will break. how do i support multiple types here?
|
||||||
|
#[allow(clippy::single_element_loop)]
|
||||||
|
for value in &[eth::ETH] {
|
||||||
|
let res = value.build_transfer_command(build_transfer_subcommand(value));
|
||||||
|
match res {
|
||||||
|
Ok(subcommand) => {
|
||||||
|
transfer_subcommand = transfer_subcommand.subcommand(subcommand);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// This coin does not support the given operation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = value.build_stake_command(build_stake_subcommand(value));
|
||||||
|
match res {
|
||||||
|
Ok(subcommand) => {
|
||||||
|
stake_subcommand = stake_subcommand.subcommand(subcommand);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// This coin does not support the given operation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command
|
||||||
|
.subcommand(transfer_subcommand)
|
||||||
|
.subcommand(stake_subcommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_matches(_matches: clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,24 +1,8 @@
|
||||||
use std::process::ExitCode;
|
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
// Yoinked from keyfork-bin
|
fn main() {
|
||||||
// TODO: make public in keyfork-bin, make a new release
|
let command = cli::build_command();
|
||||||
fn report_err<E: std::error::Error>(e: E) {
|
let matches = command.get_matches();
|
||||||
eprintln!("Unable to run command: {e}");
|
cli::handle_matches(matches).unwrap();
|
||||||
let mut source = e.source();
|
println!("Hello, world!");
|
||||||
while let Some(new_error) = source.take() {
|
|
||||||
eprintln!("- Caused by: {new_error}");
|
|
||||||
source = new_error.source();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
cli::do_cli_thing();
|
|
||||||
if let Err(e) = Ok::<_, Box<dyn std::error::Error>>(()) {
|
|
||||||
report_err(&*e);
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
|
|
||||||
}
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue