add WIP progress towards Solana transfer
This commit is contained in:
parent
dc91b0bee8
commit
499edce086
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
|
@ -1,8 +1,14 @@
|
|||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/icepick",
|
||||
members = [ "crates/by-chain/icepick-solana",
|
||||
"crates/coin/icepick-coin",
|
||||
"crates/coin/icepick-eth",
|
||||
"crates/coin/icepick-sol",
|
||||
"crates/icepick", "crates/icepick-module",
|
||||
"vendor/solana-sdk",
|
||||
]
|
||||
|
||||
[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 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-coin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = "4.5.21"
|
||||
thiserror = "2.0.3"
|
|
@ -0,0 +1,37 @@
|
|||
use clap::{Command, ArgMatches};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("The requested operation is unsupported or not yet implemented")]
|
||||
pub struct Unsupported;
|
||||
|
||||
pub trait Coin {
|
||||
/// Return the name for the coin, in lowercase.
|
||||
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. May be a float.
|
||||
/// * `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, Unsupported> {
|
||||
Err(Unsupported)
|
||||
}
|
||||
|
||||
/// Given a Command, add any arguments relevant to the Stake operation.
|
||||
fn build_stake_command(&self, _command: Command) -> Result<Command, Unsupported> {
|
||||
Err(Unsupported)
|
||||
}
|
||||
|
||||
/// Perform a Transfer
|
||||
///
|
||||
/// TODO: This should later be split into "make a transfer",
|
||||
/// "verify a transfer", "sign a transfer", and "broadcast the transfer".
|
||||
#[allow(unused_variables)]
|
||||
fn run_transfer_command(&self, matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err(Box::new(Unsupported))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "icepick-eth"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = "4.5.21"
|
||||
icepick-coin = { version = "0.1.0", path = "../icepick-coin" }
|
|
@ -0,0 +1,24 @@
|
|||
use clap::arg;
|
||||
use icepick_coin::{Coin, Unsupported};
|
||||
|
||||
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, Unsupported> {
|
||||
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, Unsupported> {
|
||||
Err(Unsupported)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "icepick-sol"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = "4.5.21"
|
||||
icepick-coin = { version = "0.1.0", path = "../icepick-coin" }
|
||||
solana-sdk = { version = "2.1.1", features = [], default-features = false }
|
|
@ -0,0 +1,90 @@
|
|||
use clap::{arg, ArgMatches, Command};
|
||||
use icepick_coin::{Coin, Unsupported};
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, system_instruction};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct SOL;
|
||||
|
||||
impl Coin for SOL {
|
||||
fn coin_name(&self) -> &'static str {
|
||||
"sol"
|
||||
}
|
||||
|
||||
fn coin_description(&self) -> &'static str {
|
||||
"Bring blockchain to the people. Solana supports experiences for power users, new consumers, and everyone in between."
|
||||
}
|
||||
|
||||
fn build_transfer_command(&self, command: Command) -> Result<Command, Unsupported> {
|
||||
Ok(command
|
||||
.arg(arg!(--"fee-payer" [FEE_PAYER] "Use an alternative derivation account for paying fees."))
|
||||
.arg(arg!(--"blockhash" <BLOCKHASH> "A recent blockhash to include in the Transaction")))
|
||||
}
|
||||
|
||||
fn build_stake_command(&self, _command: Command) -> Result<Command, Unsupported> {
|
||||
Err(Unsupported)
|
||||
}
|
||||
|
||||
fn run_transfer_command(&self, matches: &ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = matches
|
||||
.get_one::<String>("amount")
|
||||
.and_then(|amount| u64::from_str(amount).ok())
|
||||
.expect("amount: valid int");
|
||||
let to_address = matches
|
||||
.get_one::<String>("to-address")
|
||||
.and_then(|pk| Pubkey::from_str(pk).ok())
|
||||
.expect("to-address: valid public key");
|
||||
let from_account = matches
|
||||
.get_one::<String>("from-account")
|
||||
.and_then(|index| u32::from_str(index).ok())
|
||||
.filter(|index| *index < (1 << 31));
|
||||
let fee_payer = matches.get_one::<String>("fee-payer");
|
||||
let blockhash = matches.get_one::<String>("blockhash");
|
||||
// We need: pubkey 1, pubkey 2, lamports,
|
||||
let instruction = system_instruction::transfer(&to_address, &to_address, amount);
|
||||
print_instruction(&instruction);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_instruction(i: &Instruction) -> Option<String> {
|
||||
let data = &i.data;
|
||||
let instruction = data[..4]
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(u32::from_le_bytes)?;
|
||||
let system_program = solana_sdk::system_program::ID;
|
||||
|
||||
let entries = [(system_program, 2u32, |data: &[u8]| {
|
||||
let lamports = data[..8]
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(u64::from_le_bytes)
|
||||
.expect("unable to parse data into lamport amount");
|
||||
format!("transfer {lamports} lamports")
|
||||
})];
|
||||
|
||||
entries
|
||||
.iter()
|
||||
.find(|entry| entry.0 == i.program_id && entry.1 == instruction)
|
||||
.map(|entry| entry.2(&data[4..]))
|
||||
}
|
||||
|
||||
fn print_instruction(instruction: &Instruction) {
|
||||
println!("Program ID: {}", instruction.program_id);
|
||||
if !instruction.accounts.is_empty() {
|
||||
println!("Associated Accounts:");
|
||||
for account in &instruction.accounts {
|
||||
println!("|- Public Key: {}", account.pubkey);
|
||||
println!("|- Is Signer: {}", account.is_signer);
|
||||
println!("\\- Is Writable: {}", account.is_writable);
|
||||
}
|
||||
match guess_instruction(instruction) {
|
||||
Some(guess) => {
|
||||
println!("Instruction: {guess}");
|
||||
}
|
||||
None => {
|
||||
println!("Unknown instruction: {:?}", instruction.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
[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
|
||||
mod icepick_coin {
|
||||
use clap::Command;
|
||||
pub trait Coin {
|
||||
/// Return the name for the coin, in lowercase.
|
||||
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(())
|
||||
}
|
||||
pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) {
|
||||
if std::env::vars().any(|(k, _)| &k == "ICEPICK_USE_CARGO") {
|
||||
("cargo", vec!["run", "--bin", bin_name, "--"])
|
||||
} else {
|
||||
(bin_name, vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
pub fn do_cli_thing() {
|
||||
let mut commands = vec![];
|
||||
let mut icepick_command = command!();
|
||||
for coin_bin in guessed_bins() {
|
||||
// try to run the "help" operation on any bins.
|
||||
let bin = format!("icepick-{coin_bin}");
|
||||
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();
|
||||
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 {
|
||||
"The leading platform for innovative apps and blockchain networks."
|
||||
}
|
||||
let stdin = std::io::stdin();
|
||||
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, ()> {
|
||||
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(())
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
fn main() {
|
||||
let command = cli::build_command();
|
||||
let matches = command.get_matches();
|
||||
cli::handle_matches(matches).unwrap();
|
||||
println!("Hello, world!");
|
||||
// Yoinked from keyfork-bin
|
||||
// TODO: make public in keyfork-bin, make a new release
|
||||
fn report_err<E: std::error::Error>(e: E) {
|
||||
eprintln!("Unable to run command: {e}");
|
||||
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