Compare commits
3 Commits
dc91b0bee8
...
bfe345a8f9
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | bfe345a8f9 | |
Ryan Heywood | 971c58ade3 | |
Ryan Heywood | 499edce086 |
|
@ -0,0 +1,2 @@
|
||||||
|
[registries.distrust]
|
||||||
|
index = "https://git.distrust.co/public/_cargo-index.git"
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,10 @@
|
||||||
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,3 +1,19 @@
|
||||||
|
# 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
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
[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"
|
|
@ -0,0 +1,6 @@
|
||||||
|
use icepick_module::Module;
|
||||||
|
use icepick_solana::Solana;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Solana::run_responder()
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
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": []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "icepick-module"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
|
@ -0,0 +1,118 @@
|
||||||
|
/// 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,4 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.20", features = ["cargo", "derive"] }
|
clap = { version = "4.5.20", features = ["cargo", "derive", "string"] }
|
||||||
|
icepick-module = { version = "0.1.0", path = "../icepick-module" }
|
||||||
|
serde_json.workspace = true
|
||||||
|
thiserror = "2.0.3"
|
||||||
|
|
|
@ -1,139 +1,114 @@
|
||||||
#![allow(clippy::upper_case_acronyms)]
|
use clap::command;
|
||||||
|
use icepick_module::help::*;
|
||||||
|
use std::{
|
||||||
|
io::{IsTerminal, Write},
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
pub fn guessed_bins() -> Vec<String> {
|
||||||
|
"sol".split_whitespace().map(|s| s.to_owned()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: extract as icepick_coin
|
pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) {
|
||||||
mod icepick_coin {
|
if std::env::vars().any(|(k, _)| &k == "ICEPICK_USE_CARGO") {
|
||||||
use clap::Command;
|
("cargo", vec!["run", "--bin", bin_name, "--"])
|
||||||
pub trait Coin {
|
} else {
|
||||||
/// Return the name for the coin, in lowercase.
|
(bin_name, vec![])
|
||||||
fn coin_name(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Return a description of the coin.
|
|
||||||
fn coin_description(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Given a Command, add any arguments relevant to the Transfer operation.
|
|
||||||
///
|
|
||||||
/// All commands by default have the following values:
|
|
||||||
/// * `amount`: The amount of the currency to transfer.
|
|
||||||
/// * `to`: The account to transfer the currency to.
|
|
||||||
/// * `from_account`: The non-default derivation account to send currency from.
|
|
||||||
fn build_transfer_command(&self, _command: Command) -> Result<Command, ()> {
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a Command, add any arguments relevant to the Stake operation.
|
|
||||||
fn build_stake_command(&self, _command: Command) -> Result<Command, ()> {
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: extract as icepick_eth
|
pub fn do_cli_thing() {
|
||||||
mod eth {
|
let mut commands = vec![];
|
||||||
use super::icepick_coin::Coin;
|
let mut icepick_command = command!();
|
||||||
use clap::arg;
|
for coin_bin in guessed_bins() {
|
||||||
|
// try to run the "help" operation on any bins.
|
||||||
pub struct ETH;
|
let bin = format!("icepick-{coin_bin}");
|
||||||
|
let (command, args) = get_command(&bin);
|
||||||
impl Coin for ETH {
|
let mut child = Command::new(command)
|
||||||
fn coin_name(&self) -> &'static str {
|
.args(args)
|
||||||
"eth"
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn coin_description(&self) -> &'static str {
|
let stdin = std::io::stdin();
|
||||||
"The leading platform for innovative apps and blockchain networks."
|
let mut cli_input: Option<serde_json::Value> = None;
|
||||||
|
// HACK: Allow good UX when running from CLI without piping in a blob.
|
||||||
|
// 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")
|
||||||
|
});
|
||||||
|
|
||||||
fn build_transfer_command(&self, command: clap::Command) -> Result<clap::Command, ()> {
|
let matches = icepick_command.get_matches();
|
||||||
Ok(command
|
if let Some((module, matches)) = matches.subcommand() {
|
||||||
.arg(arg!(--gas <GAS> "Specify custom gas for the transaction."))
|
if let Some((subcommand, matches)) = matches.subcommand() {
|
||||||
.arg(arg!(--"gas-payer" <ACCOUNT> "Use an alternative derivation account for paying gas.")))
|
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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
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,8 +1,24 @@
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
fn main() {
|
// Yoinked from keyfork-bin
|
||||||
let command = cli::build_command();
|
// TODO: make public in keyfork-bin, make a new release
|
||||||
let matches = command.get_matches();
|
fn report_err<E: std::error::Error>(e: E) {
|
||||||
cli::handle_matches(matches).unwrap();
|
eprintln!("Unable to run command: {e}");
|
||||||
println!("Hello, world!");
|
let mut source = e.source();
|
||||||
|
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