Compare commits

...

13 Commits

36 changed files with 2080 additions and 399 deletions

524
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,14 @@ members = [
"crates/icepick-workflow",
"crates/icepick-module",
"crates/builtins/icepick-internal",
"crates/builtins/icepick-ed25519",
"crates/by-chain/icepick-solana",
"crates/by-chain/icepick-cosmos",
"crates/miniquorum",
"crates/spacemesh/api-client",
"crates/spacemesh/codec",
"crates/spacemesh/spacemesh",
"crates/by-chain/icepick-spacemesh",
]
[workspace.dependencies]

View File

@ -0,0 +1,13 @@
[package]
name = "icepick-ed25519"
version = "0.1.0"
edition = "2021"
publish = ["distrust"]
[dependencies]
ed25519-dalek = "2.1.1"
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
serde.workspace = true
serde_json.workspace = true
smex = { version = "0.1.0", registry = "distrust" }
thiserror = "2.0.9"

View File

@ -0,0 +1,91 @@
use ed25519_dalek::Signer;
use icepick_module::Module;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "operation", content = "values", rename_all = "kebab-case")]
pub enum Operation {
GetPubkey {},
Sign { message: Vec<u8> },
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Request {
derived_keys: Option<Vec<[u8; 32]>>,
#[serde(flatten)]
operation: Operation,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {}
pub struct Ed25519;
impl Module for Ed25519 {
type Error = Error;
type Request = Request;
fn describe_operations() -> Vec<icepick_module::help::Operation> {
use icepick_module::help::*;
let message = Argument::builder()
.name("message")
.description("The message to sign, as an array of bytes.")
.r#type(ArgumentType::Required)
.build();
let get_pubkey = Operation::builder()
.name("get-pubkey")
.description("Get an Ed25519 public key from the provided private key.")
.build();
let sign = Operation::builder()
.name("sign")
.description("Sign a message using an Ed25519 private key.")
.build()
.argument(&message);
vec![get_pubkey, sign]
}
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
let Request {
derived_keys,
operation,
} = request;
match operation {
Operation::GetPubkey {} => {
let key = derived_keys
.iter()
.flatten()
.next()
.map(ed25519_dalek::SigningKey::from_bytes)
.unwrap();
let key = key.verifying_key().to_bytes();
Ok(serde_json::json!({
"blob": {
"pubkey": key,
}
}))
}
Operation::Sign { message } => {
let key = derived_keys
.iter()
.flatten()
.next()
.map(ed25519_dalek::SigningKey::from_bytes)
.unwrap();
let signature = key.sign(&message);
Ok(serde_json::json!({
"blob": {
"signature": signature.to_vec(),
}
}))
}
}
}
}

View File

@ -0,0 +1,6 @@
use icepick_module::Module;
use icepick_ed25519::Ed25519;
fn main() -> Result<(), Box<dyn std::error::Error>> {
Ed25519::run_responder()
}

View File

@ -705,8 +705,8 @@ impl Module for Solana {
"nonce_pubkey": keypair.pubkey().to_string(),
"nonce_privkey": [keypair.secret().to_bytes()],
"transaction": instructions,
"derivation_accounts": [0u32 | 1 << 31],
},
"derivation_accounts": [0u32 | 1 << 31],
}))
}
Operation::GetNonceAccountData(GetNonceAccountData {

View File

@ -0,0 +1,13 @@
[package]
name = "icepick-spacemesh"
version = "0.1.0"
edition = "2021"
publish = ["distrust"]
[dependencies]
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
serde.workspace = true
serde_json.workspace = true
spacemesh = { version = "0.1.0", path = "../../spacemesh/spacemesh" }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["rt", "net"] }

View File

@ -0,0 +1,172 @@
use icepick_module::Module;
use serde::{Deserialize, Serialize};
use spacemesh::bech32::{self, Hrp};
use std::str::FromStr;
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub enum Cluster {
Testnet,
#[default]
Mainnet,
}
impl Cluster {
fn hrp(&self) -> bech32::Hrp {
match self {
Cluster::Testnet => Hrp::parse("stest").unwrap(),
Cluster::Mainnet => Hrp::parse("sm").unwrap(),
}
}
}
impl std::str::FromStr for Cluster {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"testnet" => Ok(Self::Testnet),
"mainnet" => Ok(Self::Mainnet),
_ => Err("Invalid value"),
}
}
}
impl std::fmt::Display for Cluster {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Cluster::Testnet => f.write_str("testnet"),
Cluster::Mainnet => f.write_str("mainnet"),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {}
#[derive(Serialize, Deserialize, Debug)]
pub struct GenerateWallet {
account: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetWalletAddress {
pubkey: [u8; 32],
cluster: Option<Cluster>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetAccountData {
account: String,
cluster: Option<Cluster>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AwaitFunds {
address: String,
amount: String,
cluster: Option<Cluster>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "operation", content = "values", rename_all = "kebab-case")]
pub enum Operation {
GenerateWallet(GenerateWallet),
GetWalletAddress(GetWalletAddress),
AwaitFunds(AwaitFunds),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Request {
derived_keys: Option<Vec<[u8; 32]>>,
#[serde(flatten)]
operation: Operation,
}
pub fn run_async<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(f)
}
pub struct Spacemesh;
impl Module for Spacemesh {
type Error = Error;
type Request = Request;
fn describe_operations() -> Vec<icepick_module::help::Operation> {
use icepick_module::help::*;
let account = Argument::builder()
.name("account")
.description("The derivation index for the account.")
.r#type(ArgumentType::Optional)
.build();
let cluster = Argument::builder()
.name("cluster")
.description("Spacemesh cluster to interact with (mainnet, testnet).")
.r#type(ArgumentType::Required)
.build();
let generate_wallet = Operation::builder()
.name("generate-wallet")
.description("Generate a wallet for the given account.")
.build()
.argument(&account);
let get_wallet_address = Operation::builder()
.name("get-wallet-address")
.description("Get the address for a given wallet.")
.build()
.argument(&cluster)
.argument(
&Argument::builder()
.name("wallet_pubkey")
.description("Public key of the wallet.")
.r#type(ArgumentType::Required)
.build(),
);
vec![generate_wallet, get_wallet_address]
}
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
let Request {
operation,
derived_keys: _,
} = request;
match operation {
Operation::GenerateWallet(GenerateWallet { account }) => {
let account = u32::from_str(account.as_deref().unwrap_or("0")).unwrap();
Ok(serde_json::json!({
"blob": {},
"derivation_accounts": [(account | 1 << 31)],
}))
}
Operation::GetWalletAddress(GetWalletAddress { pubkey, cluster }) => {
use spacemesh::wallet::AsAddress;
let account = pubkey.as_address();
let hrp = cluster.unwrap_or_default().hrp();
let address = bech32::encode(hrp, &account).unwrap();
Ok(serde_json::json!({
"blob": {
"address": address,
},
"derivation_accounts": [],
}))
}
Operation::AwaitFunds(AwaitFunds {
address,
amount,
cluster,
}) => todo!(),
}
}
}

View File

@ -0,0 +1,6 @@
use icepick_module::Module;
use icepick_spacemesh::Spacemesh;
fn main() -> Result<(), Box<dyn std::error::Error>> {
Spacemesh::run_responder()
}

View File

@ -1,7 +1,7 @@
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashSet};
#[derive(thiserror::Error, Debug)]
pub enum SimulationError {
@ -24,21 +24,51 @@ pub enum WorkflowError {
InvocationError(String),
}
/// An input for a workflow argument. When inputs are read, they should be referenced by the first
/// name. Additional names can be provided as aliases, to allow chaining workflows together when
/// names may not make sense - such as a Solana address then being used as an authorization
/// address.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Input {
/// An input with a single identifier.
/// The name of the input.
pub name: String,
/// A description of the input.
pub description: String,
/// Aliases used when loading inputs.
#[serde(default)]
pub aliases: Vec<String>,
/// Whether the workflow input is optional.
pub optional: Option<bool>,
}
impl Input {
pub fn identifiers(&self) -> impl Iterator<Item = &String> {
[&self.name].into_iter().chain(self.aliases.iter())
}
pub fn is_required(&self) -> bool {
self.optional.is_some_and(|o| o)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Workflow {
pub name: String,
#[serde(default)]
pub inputs: Vec<String>,
pub description: String,
#[serde(default)]
pub optional_inputs: Vec<String>,
pub inputs: Vec<Input>,
#[serde(rename = "step")]
steps: Vec<WorkflowStep>,
}
pub type StringMap = HashMap<String, String>;
pub type StringMap<T = String> = BTreeMap<String, T>;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct WorkflowStep {
@ -60,7 +90,7 @@ pub struct WorkflowStep {
#[derive(Serialize, Deserialize)]
pub struct OperationResult {
// All values returned from an operation.
blob: HashMap<String, Value>,
blob: StringMap<Value>,
// Any requested accounts from an operation.
//
@ -69,7 +99,8 @@ pub struct OperationResult {
derivation_accounts: Vec<DerivationIndex>,
}
type DeriveKeys<'a> = &'a dyn Fn(&DerivationAlgorithm, &DerivationPath, &[DerivationIndex]) -> Vec<Vec<u8>>;
type DeriveKeys<'a> =
&'a dyn Fn(&DerivationAlgorithm, &DerivationPath, &[DerivationIndex]) -> Vec<Vec<u8>>;
impl Workflow {
pub fn simulate_workflow<T: InvocableOperation + Sized>(
@ -115,10 +146,10 @@ impl Workflow {
pub fn run_workflow<T: InvocableOperation>(
&self,
mut data: HashMap<String, Value>,
mut data: StringMap<Value>,
operations: &[T],
derive_keys: DeriveKeys,
) -> Result<HashMap<String, Value>, WorkflowError> {
) -> Result<StringMap<Value>, WorkflowError> {
let mut derived_keys = vec![];
let mut derivation_accounts = vec![];
@ -128,17 +159,8 @@ impl Workflow {
return Err(WorkflowError::InvocableOperationNotFound(step_type));
};
// Add requested derivation keys and clear derivation account requests.
if !derivation_accounts.is_empty() {
let Some((algo, path_prefix)) = operation.derivation_configuration() else {
return Err(WorkflowError::DerivationConfigurationNotFound(step_type));
};
derived_keys.extend(derive_keys(algo, path_prefix, &derivation_accounts));
}
derivation_accounts.clear();
// Prepare all inputs for the operation invocation
let inputs: HashMap<String, Value> = data
let inputs: StringMap<Value> = data
.iter()
.map(|(k, v)| (k, v.clone()))
.filter_map(|(k, v)| {
@ -167,13 +189,20 @@ impl Workflow {
let (_given, stored) = step.outputs.iter().find(|(k1, _)| k == **k1)?;
Some((stored.clone(), v))
}));
// Add requested derivation keys and clear derivation account requests.
if !derivation_accounts.is_empty() {
let Some((algo, path_prefix)) = operation.derivation_configuration() else {
return Err(WorkflowError::DerivationConfigurationNotFound(step_type));
};
derived_keys.extend(derive_keys(algo, path_prefix, &derivation_accounts));
}
derivation_accounts.clear();
}
if let Some(last_step) = &self.steps.last() {
let values = last_step.outputs.values().collect::<HashSet<_>>();
data.retain(|stored_name, _| {
values.contains(stored_name)
});
data.retain(|stored_name, _| values.contains(stored_name));
}
Ok(data)
@ -192,7 +221,7 @@ pub trait WorkflowHandler {
/// within themselves.
pub trait InvocableOperation {
/// Invoke the operation with the supplied inputs and derived keys.
fn invoke(&self, input: &HashMap<String, Value>, derived_keys: &[Vec<u8>]) -> OperationResult;
fn invoke(&self, input: &StringMap<Value>, derived_keys: &[Vec<u8>]) -> OperationResult;
/// The name of the operation.
fn name(&self) -> &String;

View File

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
bincode = "1.3.3"
chrono = { version = "0.4.39", default-features = false, features = ["now", "serde", "std"] }
clap = { version = "4.5.20", features = ["cargo", "derive", "string"] }
icepick-module = { version = "0.1.0", path = "../icepick-module" }
@ -17,9 +18,12 @@ miniquorum = { version = "0.1.0", path = "../miniquorum", default-features = fal
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["arbitrary_precision"] }
serde_yaml = "0.9.34"
smex = { version = "0.1.0", registry = "distrust" }
thiserror = "2.0.3"
toml = "0.8.19"
[build-dependencies]
bincode = "1.3.3"
icepick-workflow = { version = "0.1.0", path = "../icepick-workflow" }
serde_yaml = "0.9.34"
smex = { version = "0.1.0", registry = "distrust" }

View File

@ -1,5 +1,5 @@
use icepick_workflow::Workflow;
use std::{collections::HashMap, path::{PathBuf, Path}};
use std::{collections::BTreeMap, path::{PathBuf, Path}};
fn env_var(var: &'static str) -> String {
println!("cargo::rerun-if-env-changed={var}");
@ -11,15 +11,16 @@ fn track_path(path: &Path) {
}
fn main() {
let out_dir = env_var("CARGO_TARGET_DIR");
let out_dir = env_var("OUT_DIR");
let crate_dir = env_var("CARGO_MANIFEST_DIR");
let workflows_dir = PathBuf::from(crate_dir).join("workflows");
track_path(&workflows_dir);
let mut workflows_by_module: HashMap<String, Vec<Workflow>> = Default::default();
let mut workflows_by_module: BTreeMap<String, Vec<Workflow>> = Default::default();
for module_dir in std::fs::read_dir(&workflows_dir).unwrap() {
let module_dir = module_dir.unwrap();
dbg!(&module_dir);
let path = module_dir.path();
if !path.is_dir() {
panic!("found unexpected file {}", path.to_string_lossy());
@ -28,6 +29,7 @@ fn main() {
let mut workflows = vec![];
for workflow_file in std::fs::read_dir(&path).unwrap() {
dbg!(&workflow_file);
let workflow_file = workflow_file.unwrap();
let path = workflow_file.path();
if !path.is_file() {
@ -39,12 +41,15 @@ fn main() {
workflows.push(workflow);
}
workflows.sort_by(|a, b| a.name.cmp(&b.name));
workflows_by_module.insert(
module_dir.file_name().to_str().unwrap().to_owned(),
workflows,
);
}
let out_path = PathBuf::from(out_dir).join("workflows.yaml");
let out_file = std::fs::File::create(&out_path).unwrap();
serde_yaml::to_writer(out_file, &workflows_by_module).unwrap();
let out_path = PathBuf::from(out_dir).join("workflows.hex");
let result = bincode::serialize(&workflows_by_module).unwrap();
let hexed = smex::encode(&result);
std::fs::write(out_path, hexed).unwrap();
}

View File

@ -3,7 +3,7 @@ use icepick_module::help::*;
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
collections::{HashMap, BTreeMap},
io::{IsTerminal, Write},
path::PathBuf,
process::{Command, Stdio},
@ -113,6 +113,11 @@ struct Config {
// command name, invocable binary, operations
type Commands<'a> = &'a [(String, String, Vec<Operation>)];
fn default_workflows() -> HashMap<String, Vec<icepick_workflow::Workflow>> {
let workflows_hex = include_str!(concat!(env!("OUT_DIR"), "/workflows.hex"));
bincode::deserialize(&smex::decode(workflows_hex).unwrap()).unwrap()
}
pub fn do_cli_thing() {
/* parse config file to get module names */
let config_file = std::env::vars().find_map(|(k, v)| {
@ -121,7 +126,7 @@ pub fn do_cli_thing() {
}
None
});
let config_path = config_file.unwrap_or_else(|| "icepick.toml".to_string());
let config_path = config_file.unwrap_or_else(|| "/etc/icepick/icepick.toml".to_string());
let config_content = std::fs::read_to_string(config_path).expect("can't read config file");
let mut config: Config = match toml::from_str(&config_content) {
Ok(config) => config,
@ -134,6 +139,21 @@ pub fn do_cli_thing() {
derivation_prefix: Default::default(),
workflows: Default::default(),
});
config.modules.push(ModuleConfig {
name: "ed25519".to_string(),
command_name: Default::default(),
algorithm: Some(DerivationAlgorithm::Ed25519),
// TODO: impl Last
derivation_prefix: Default::default(),
workflows: Default::default(),
});
let workflows = default_workflows();
for module in &mut config.modules {
if let Some(module_workflows) = workflows.get(&module.name) {
module.workflows.extend(module_workflows.iter().cloned());
}
}
let workflows_file = std::env::vars().find_map(|(k, v)| {
if k == "ICEPICK_WORKFLOWS_FILE" {
@ -142,13 +162,14 @@ pub fn do_cli_thing() {
None
});
let workflows_path = workflows_file.unwrap_or_else(|| "workflows.yaml".to_string());
let workflows_content = std::fs::read(&workflows_path).expect("can't read workflows from file");
let workflows: HashMap<String, Vec<icepick_workflow::Workflow>> =
serde_yaml::from_slice(&workflows_content).unwrap();
for module in &mut config.modules {
if let Some(module_workflows) = workflows.get(&module.name) {
module.workflows.extend(module_workflows.iter().cloned());
if let Ok(content) = std::fs::read(&workflows_path) {
let workflows: HashMap<String, Vec<icepick_workflow::Workflow>> =
serde_yaml::from_slice(&content).unwrap();
for module in &mut config.modules {
if let Some(module_workflows) = workflows.get(&module.name) {
module.workflows.extend(module_workflows.iter().cloned());
}
}
}
@ -355,7 +376,7 @@ pub fn do_cli_thing() {
}
};
let inputs: HashMap<String, serde_json::Value> =
let inputs: BTreeMap<String, serde_json::Value> =
serde_json::from_value(inputs).unwrap();
let workflow = workflows

View File

@ -1,10 +1,9 @@
use icepick_workflow::{InvocableOperation, OperationResult, Workflow};
use icepick_workflow::{Input, InvocableOperation, OperationResult, StringMap, Workflow};
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
use keyfork_shard::{openpgp::OpenPGP, Format};
use miniquorum::{Payload, PayloadVerification};
use serde_json::Value;
use std::{
collections::HashMap,
io::Write,
process::{Command, Stdio},
};
@ -20,8 +19,6 @@ pub enum Purpose {
RunQuorum,
}
pub type StringMap = std::collections::HashMap<String, String>;
#[derive(Clone, Debug)]
struct CLIOperation {
/// The name of the operation (i.e. `transfer-token`).
@ -41,7 +38,7 @@ struct CLIOperation {
}
impl InvocableOperation for CLIOperation {
fn invoke(&self, input: &HashMap<String, Value>, derived_keys: &[Vec<u8>]) -> OperationResult {
fn invoke(&self, input: &StringMap<Value>, derived_keys: &[Vec<u8>]) -> OperationResult {
let (command, args) = get_command(&self.binary);
let json = serde_json::json!({
@ -94,31 +91,33 @@ impl InvocableOperation for CLIOperation {
}
pub fn generate_command(workflow: &Workflow) -> clap::Command {
let mut command = clap::Command::new(&workflow.name).arg(clap::arg!(
--"input-file" [FILE]
"A file containing any inputs not passed on the command line"
));
for input in &workflow.inputs {
// can also be included in the JSON file, so we won't mark this as required.
let arg = clap::Arg::new(input)
.required(false)
.long(input.replace('_', "-"))
.value_name(input.to_uppercase());
command = command.arg(arg);
let mut command = clap::Command::new(&workflow.name).about(&workflow.description);
// NOTE: all required inputs are still marked as .required(false) since they could be included
// in the `--input-file` argument.
for input in workflow.inputs.iter() {
for arg in input.identifiers() {
let arg = clap::Arg::new(arg)
.required(false)
.help(&input.description)
.long(arg.replace('_', "-"))
.value_name(arg.to_uppercase())
.conflicts_with_all(
input
.identifiers()
.filter(|name| *name != arg)
.collect::<Vec<_>>(),
);
command = command.arg(arg);
}
}
for input in &workflow.optional_inputs {
let arg = clap::Arg::new(input)
.required(false)
.long(input.replace('_', "-"))
.value_name(input.to_uppercase());
command = command.arg(arg);
}
command
command.arg(clap::arg!(
--"input-file" [FILE]
"A file containing any inputs not passed on the command line"
))
}
fn load_inputs<T: AsRef<str> + Into<String> + std::fmt::Display>(
inputs: impl IntoIterator<Item = T>,
optional_inputs: impl IntoIterator<Item = T>,
fn load_inputs<'a>(
inputs: impl IntoIterator<Item = &'a Input>,
matches: &clap::ArgMatches,
) -> StringMap {
let mut map = StringMap::default();
@ -127,33 +126,25 @@ fn load_inputs<T: AsRef<str> + Into<String> + std::fmt::Display>(
.and_then(|p| std::fs::File::open(p).ok())
.and_then(|f| serde_json::from_reader(f).ok());
for input in inputs {
match matches.get_one::<String>(input.as_ref()) {
let identifier = &input.name;
match input
.identifiers()
.filter_map(|name| matches.get_one::<String>(name))
.next()
{
Some(value) => {
map.insert(input.into(), value.clone());
map.insert(identifier.clone(), value.clone());
continue;
}
None => {
if let Some(value) = input_file.as_ref().and_then(|f| f.get(input.as_ref())) {
map.insert(input.into(), value.clone());
if let Some(value) = input_file.as_ref().and_then(|f| f.get(identifier)) {
map.insert(identifier.clone(), value.clone());
continue;
}
}
}
panic!("Required workflow input was not found: {input}");
}
for input in optional_inputs {
match matches.get_one::<String>(input.as_ref()) {
Some(value) => {
map.insert(input.into(), value.clone());
continue;
}
None => {
if let Some(value) = input_file.as_ref().and_then(|f| f.get(input.as_ref())) {
map.insert(input.into(), value.clone());
continue;
}
}
if input.is_required() {
panic!("Required workflow input was not found: {identifier}");
}
}
@ -191,13 +182,10 @@ pub fn parse_quorum_file(
let threshold = threshold.unwrap_or(u8::try_from(certs.len()).expect("too many certs!"));
let policy = match purpose {
Purpose::AddSignature => {
// All signatures must be valid, but we don't require a minimum.
// All signatures must be valid, but we don't require a minimum.
PayloadVerification::new().with_threshold(0)
}
Purpose::RunQuorum => {
PayloadVerification::new().with_threshold(threshold)
},
Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold),
};
payload.verify_signatures(&certs, &policy, None).unwrap();
@ -213,20 +201,19 @@ pub fn parse_quorum_with_shardfile(
let payload: Payload = serde_json::from_reader(payload_file).unwrap();
let opgp = OpenPGP;
let (threshold, certs) = opgp.decrypt_metadata_from_file(
None::<&std::path::Path>,
std::fs::File::open(shardfile_path).unwrap(),
keyfork_prompt::default_handler().unwrap(),
).unwrap();
let (threshold, certs) = opgp
.decrypt_metadata_from_file(
None::<&std::path::Path>,
std::fs::File::open(shardfile_path).unwrap(),
keyfork_prompt::default_handler().unwrap(),
)
.unwrap();
let policy = match purpose {
Purpose::AddSignature => {
// All signatures must be valid, but we don't require a minimum.
// All signatures must be valid, but we don't require a minimum.
PayloadVerification::new().with_threshold(0)
}
Purpose::RunQuorum => {
PayloadVerification::new().with_threshold(threshold)
},
Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold),
};
payload.verify_signatures(&certs, &policy, None).unwrap();
@ -236,7 +223,7 @@ pub fn parse_quorum_with_shardfile(
pub fn handle_payload(
workflow: &Workflow,
inputs: HashMap<String, Value>,
inputs: StringMap<Value>,
modules: Commands,
config: &[ModuleConfig],
) {
@ -254,8 +241,8 @@ pub fn handle(
modules: Commands,
config: &[ModuleConfig],
) {
let inputs = load_inputs(&workflow.inputs, &workflow.optional_inputs, matches);
let data: HashMap<String, Value> = inputs
let inputs = load_inputs(&workflow.inputs, matches);
let data: StringMap<Value> = inputs
.into_iter()
.map(|(k, v)| (k, Value::String(v)))
.collect();

View File

@ -0,0 +1,43 @@
name: "broadcast"
description: |-
Broadcast a transaction on a Cosmos-based blockchain.
inputs:
- name: "nonce_address"
description: >-
The address of the account used for the transaction nonce.
- name: "chain_name"
description: >-
The name of the Cosmos chain to broadcast a transaction on.
step:
- type: "cosmos-get-chain-info"
inputs:
chain_name: "chain_name"
outputs:
blockchain_config: "blockchain_config"
- type: "cosmos-get-account-data"
inputs:
account_id: "nonce_address"
blockchain_config: "blockchain_config"
outputs:
account_number: "account_number"
sequence_number: "sequence_number"
- type: "internal-save-file"
values:
filename: "account_info.json"
inputs:
account_number: "account_number"
sequence_number: "sequence_number"
- type: "internal-load-file"
values:
filename: "transaction.json"
outputs:
transaction: "transaction"
- type: "cosmos-broadcast"
inputs:
blockchain_config: "blockchain_config"
transaction: "transaction"
outputs:
status: "status"
url: "url"
error: "error"
error_code: "error_code"

View File

@ -1,8 +1,14 @@
name: generate-address
description: |-
Generate an address on a given Cosmos-based blockchain.
inputs:
- chain_name
optional_inputs:
- account
- name: chain_name
description: >-
The name of the Cosmos chain you'd like to generate an address for.
- name: account
description: >-
The account to use, if not the default account.
optional: true
step:
- type: cosmos-get-chain-info
inputs:

View File

@ -1,12 +1,27 @@
name: stake
description: |-
Stake coins on the provided chain.
inputs:
- delegate_address
- validator_address
- chain_name
- asset_name
- asset_amount
optional_inputs:
- gas_factor
- name: delegate_address
description: >-
Address holding the coins to be staked to a validator.
- name: validator_address
description: >-
Address of the validator operator.
- name: chain_name
description: >-
The name of the Cosmos-based chain.
- name: asset_name
description: >-
The name of the asset to stake.
- name: asset_amount
description: >-
The amount of the asset to stake.
- name: gas_factor
description: >-
An amount to multiply the required gas by; necessary if a chain requires
more gas for a specific operation.
optional: true
step:
- type: cosmos-get-chain-info
inputs:

View File

@ -0,0 +1,55 @@
name: "transfer"
description: |-
Transfer a Cosmos coin.
inputs:
- name: "from_address"
description: >-
The address from which to send coin.
- name: "to_address"
description: >-
The address to send coins to.
- name: "asset_name"
description: >-
The name of the asset to send.
- name: "chain_name"
description: >-
The name of the Cosmos chain the asset lives on.
- name: "asset_amount"
description: >-
The amount of the asset to send.
step:
- type: "cosmos-get-chain-info"
inputs:
chain_name: "chain_name"
outputs:
blockchain_config: "blockchain_config"
- type: "internal-load-file"
values:
filename: "account_info.json"
outputs:
account_number: "account_number"
sequence_number: "sequence_number"
- type: "cosmos-transfer"
inputs:
from_address: "from_address"
to_address: "to_address"
amount: "asset_amount"
denom: "asset_name"
blockchain_config: "blockchain_config"
outputs:
fee: "fee"
tx_messages: "tx_messages"
- type: "cosmos-sign"
inputs:
fee: "fee"
tx_messages: "tx_messages"
account_number: "account_number"
sequence_number: "sequence_number"
blockchain_config: "blockchain_config"
outputs:
transaction: "signed_transaction"
- type: "internal-save-file"
values:
filename: "transaction.json"
inputs:
transaction: "signed_transaction"

View File

@ -1,10 +1,21 @@
name: withdraw-rewards
description: |-
Withdraw rewards gained from staking to a validator.
inputs:
- delegate_address
- validator_address
- chain_name
optional_inputs:
- gas_factor
- name: delegate_address
description: >-
The owner of the staked coins; also, the recipient of rewards.
- name: validator_address
description: >-
The validator from whom coins are staked.
- name: chain_name
description: >-
The name of the Cosmos-based chain.
- name: gas_factor
description: >-
An amount to multiply the required gas by; necessary if a chain requires
more gas for a specific operation.
optional: true
step:
- type: cosmos-get-chain-info
inputs:

View File

@ -1,12 +1,30 @@
name: withdraw
description: |-
Withdraw staked coins from a validator.
Staked coins may be held for an unbonding period, depending on the chain upon
which they are staked.
inputs:
- delegate_address
- validator_address
- chain_name
- asset_name
- asset_amount
optional_inputs:
- gas_factor
- name: delegate_address
description: >-
The owner of the staked coins.
- name: validator_address
description: >-
The validator from whom coins are staked.
- name: chain_name
description: >-
The name of the Cosmos-based chain.
- name: asset_name
description: >-
The name of the asset to withdraw.
- name: asset_amount
description: >-
The amount of the asset to withdraw.
- name: gas_factor
description: >-
An amount to multiply the required gas by; necessary if a chain requires
more gas for a specific operation.
optional: true
step:
- type: cosmos-get-chain-info
inputs:

View File

@ -0,0 +1,40 @@
name: "broadcast"
description: |-
Broadcast a transaction on the Solana blockchain.
inputs:
- name: "nonce_address"
description: >-
The address of the nonce account.
- name: "cluster"
description: >-
The name of the Solana cluster to broadcast the transaction on, if not
mainnet-beta.
optional: true
step:
- type: "sol-get-nonce-account-data"
inputs:
nonce_address: "nonce_address"
cluster: "cluster"
outputs:
authority: "nonce_authority"
durable_nonce: "nonce"
- type: "internal-save-file"
values:
filename: "nonce.json"
inputs:
nonce_authority: "nonce_authority"
nonce_data: "nonce"
nonce_address: "nonce_address"
- type: "internal-load-file"
values:
filename: "transaction.json"
outputs:
transaction: "transaction"
- type: "sol-broadcast"
inputs:
cluster: "cluster"
transaction: "transaction"
outputs:
status: "status"
url: "url"
error: "error"

View File

@ -1,6 +1,11 @@
name: generate-address
optional_inputs:
- account
description: |-
Generate a Solana address.
inputs:
- name: account
description: >-
The account to use, if not the default account.
optional: true
step:
- type: sol-generate-wallet
inputs:

View File

@ -0,0 +1,75 @@
name: "generate-nonce-account"
description: |-
Using a temporary Keyfork instance, generate a nonce address for the given
authorization address.
inputs:
- name: "cluster"
description: >-
Name of the Solana cluster to generate the nonce account on, if not
mainnet-beta.
- name: "authorization_address"
description: >-
The address used to authorize advancing the nonce.
The authorization address (also called "address" or "pubkey" in other
workflows) is required to be a signer of the transaction, so the
authorization address is often the principal address - the one performing
the transaction.
step:
- type: "sol-generate-wallet"
- type: "sol-get-wallet-address"
outputs:
pubkey: "wallet_pubkey"
- type: "sol-await-funds"
inputs:
address: "wallet_pubkey"
cluster: "cluster"
values:
lamports: "1510000"
- type: "sol-get-blockhash"
inputs:
cluster: "cluster"
outputs:
blockhash: "blockhash"
- type: "sol-create-nonce-account-and-signing-key"
inputs:
from_address: "wallet_pubkey"
authorization_address: "authorization_address"
outputs:
transaction: "instructions"
nonce_pubkey: "nonce_pubkey"
nonce_privkey: "private_keys"
derivation_accounts: "derivation_accounts"
- type: "sol-compile"
inputs:
instructions: "instructions"
derivation_accounts: "derivation_accounts"
blockhash: "blockhash"
outputs:
transaction: "unsigned_transaction"
- type: "sol-sign"
inputs:
blockhash: "blockhash"
signing_keys: "private_keys"
transaction: "unsigned_transaction"
outputs:
transaction: "signed_transaction"
- type: "sol-broadcast"
inputs:
cluster: "cluster"
transaction: "signed_transaction"
outputs:
status: "status"
url: "url"
error: "error"
- type: "internal-cat"
inputs:
status: "status"
url: "url"
nonce_account: "nonce_pubkey"
error: "error"
outputs:
status: "status"
url: "url"
nonce_account: "nonce_account"
error: "error"

View File

@ -1,9 +1,19 @@
name: transfer-token
description: |-
Transfer SPL tokens held on the Solana blockchain.
inputs:
- from_address
- to_address
- token_name
- token_amount
- name: from_address
description: >-
The address from which to send tokens.
- name: to_address
description: >-
The address to send coins to.
- name: token_name
description: >-
The name of the token to transfer.
- name: token_amount
description: >-
The amount of the token to transfer.
step:
- type: sol-get-token-info
inputs:

View File

@ -0,0 +1,49 @@
name: "transfer"
description: |-
Transfer SOL from one address to another.
inputs:
- name: "to_address"
description: >-
The address to send SOL to.
- name: "from_address"
description: >-
The address to send SOL from.
- name: "amount"
description: >-
The amount of SOL to send.
step:
- type: "internal-load-file"
values:
filename: "nonce.json"
outputs:
nonce_authority: "nonce_authority"
nonce_data: "nonce_data"
nonce_address: "nonce_address"
- type: "sol-transfer"
inputs:
from_address: "from_address"
to_address: "to_address"
amount: "amount"
outputs:
instructions: "instructions"
derivation_accounts: "derivation_accounts"
- type: "sol-compile"
inputs:
instructions: "instructions"
derivation_accounts: "derivation_accounts"
nonce_address: "nonce_address"
nonce_authority: "nonce_authority"
nonce_data: "nonce_data"
outputs:
transaction: "unsigned_transaction"
- type: "sol-sign"
inputs:
blockhash: "nonce_data"
transaction: "unsigned_transaction"
outputs:
transaction: "signed_transaction"
- type: "internal-save-file"
values:
filename: "transaction.json"
inputs:
transaction: "signed_transaction"

View File

@ -0,0 +1,25 @@
name: generate-address
description: |-
Generate a Spacemesh address
inputs:
- name: account
description: >-
The account to use, if not the default account.
optional: true
- name: cluster
description: >-
The Spacemesh cluster to use, if not the mainnet.
optional: true
step:
- type: spacemesh-generate-wallet
inputs:
account: account
- type: ed25519-get-pubkey
outputs:
pubkey: pubkey
- type: spacemesh-get-wallet-address
inputs:
pubkey: pubkey
cluster: cluster
outputs:
address: address

View File

@ -0,0 +1,26 @@
[package]
name = "spacemesh-api-client"
version = "0.1.0"
edition = "2021"
publish = ["distrust"]
[dependencies]
futures = "0.3"
progenitor-client = { git = "https://github.com/geoffreygarrett/progenitor", rev = "8726ea91eb19f92e1357f1ceeeab507477dcfeb6" }
reqwest = { version = "0.11", features = ["json", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
base64 = "0.22.1"
smex = { version = "0.1.0", registry = "distrust" }
[build-dependencies]
prettyplease = "0.2.22"
progenitor = { git = "https://github.com/geoffreygarrett/progenitor", rev = "8726ea91eb19f92e1357f1ceeeab507477dcfeb6" }
serde_json = "1.0"
syn = "2.0"
[dev-dependencies]
base64 = "0.22.1"
smex = { version = "0.1.0", registry = "distrust" }
tokio = { version = "1.43.0", features = ["macros", "net", "rt", "test-util"] }

View File

@ -0,0 +1,22 @@
/*
curl -X 'GET' \
'https://converter.swagger.io/api/convert?url=https%3A%2F%2Fmainnet-api-docs.spacemesh.network%2Fv1.7.12%2Fapi.swagger.json' \
-H 'accept: application/json'
*/
fn main() {
let src = "openapi.json";
println!("cargo:rerun-if-changed={}", src);
let file = std::fs::File::open(src).unwrap();
let spec = serde_json::from_reader(file).unwrap();
let mut generator = progenitor::Generator::default();
let tokens = generator.generate_tokens(&spec).unwrap();
let ast = syn::parse2(tokens).unwrap();
let content = prettyplease::unparse(&ast);
let mut out_file = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen.rs");
std::fs::write(out_file, content).unwrap();
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
#![allow(warnings, unused)]
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
// NOTE: The RPC API requires base64-encoded transaction IDs rather than hex-encoded.
// That was confusing, after all their branding is `0x` based.
pub fn encode_transaction_id(txid: impl AsRef<str>) -> Result<String, smex::DecodeError> {
use base64::prelude::*;
let tx = smex::decode(txid)?;
Ok(BASE64_STANDARD.encode(tx))
}
#[cfg(test)]
mod tests {
use super::*;
use base64::prelude::*;
#[tokio::test]
async fn it_works() {
let client = Client::new("https://mainnet-api.spacemesh.network");
let txid = "638442a2033f20b5a7280b9a4f2bfc73022f6e7ec64b1497b85335444381d99d";
let txid = smex::decode(txid).unwrap();
let txid = BASE64_STANDARD.encode(txid);
let result = client
.transaction_service_list(&types::Spacemeshv2alpha1TransactionRequest {
txid: vec![txid],
limit: Some(100.to_string()),
..Default::default()
})
.await
.unwrap()
.into_inner();
let result = match result {
types::GooglerpcStatusOrSpacemeshv2alpha1TransactionList::GooglerpcStatus(googlerpc_status) => panic!("{:?}", googlerpc_status.message),
types::GooglerpcStatusOrSpacemeshv2alpha1TransactionList::Spacemeshv2alpha1TransactionList(transaction_list) => {
transaction_list
},
};
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "spacemesh-codec"
version = "0.1.0"
edition = "2021"
publish = ["distrust"]
[dependencies]
parity-scale-codec = { version = "3.6.12", features = ["derive"] }
[dev-dependencies]
base64 = "0.22.1"
bech32 = "0.11.0"

View File

@ -0,0 +1,530 @@
//! Spacemesh transaction encoding and decoding.
//! Based loosely on: <https://github.com/spacemeshos/sm-codec/>.
//!
//! # Encoding Transactions
//!
//! ```rust
//! let principal = [0u8; 24];
//! let destination = [1u8; 24];
//!
//! let single_sig_spend = Spend {
//! header: TxHeader {
//! principal,
//! },
//! payload: SpendPayload {
//! nonce: Compact(2),
//! gas_price: Compact(1),
//! arguments: SpendArguments {
//! destination,
//! amount: Compact(100000),
//! },
//! },
//! // unsigned transaction
//! signature: [0; 64],
//! };
//! ```
//!
//! # Decoding Transactions
//!
//! Transactions can be decoded to bytes using the [`base64`][base64] crate. Using the Spacemesh
//! client, the transaction should also include `template` and `method` values. With those values,
//! [`tx_types::decode_by_address_and_method()`] can be used to attempt to parse the transaction.
//!
//! ```rust
//! use base64::prelude::*;
//!
//! let encoded_tx = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAIBAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAYIaBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let raw_tx = BASE64_STANDARD.decode(encoded_tx).unwrap();
//! let spend = tx_types::single_signature::Spend::decode(&mut &raw_tx[..]).unwrap();
//! ```
//!
//! [base64]: https://docs.rs/base64/latest/base64/
pub use parity_scale_codec::{Compact, Decode, Encode};
pub mod constants {
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/codecs/constants.ts
/// The length of an address.
pub const ADDRESS_BYTES_LENGTH: usize = 24;
}
pub mod core {
use super::*;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/codecs/core.ts
// NOTE: Encoding an array doesn't encode length, matching the same functionality
// as Bytes in scale-ts.
pub type Address = [u8; constants::ADDRESS_BYTES_LENGTH];
pub type PublicKey = [u8; 32];
pub type Nonce = Compact<u64>;
pub type GasPrice = Compact<u64>;
}
pub mod signatures {
use super::*;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/codecs/signatures.ts
pub type SingleSig = [u8; 64];
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct MultiSigPart {
pub r#ref: Compact<u8>,
pub sig: SingleSig,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MultiSig {
pub parts: Vec<MultiSigPart>,
}
impl Encode for MultiSig {
fn size_hint(&self) -> usize {
self.parts.len() * std::mem::size_of::<SingleSig>()
}
fn encode(&self) -> Vec<u8> {
// NOTE: No inline length is included.
let mut r = Vec::with_capacity(self.size_hint());
for sig in &self.parts {
sig.encode_to(&mut r);
}
r
}
}
impl Decode for MultiSig {
fn decode<I: parity_scale_codec::Input>(
input: &mut I,
) -> Result<Self, parity_scale_codec::Error> {
let mut parts = vec![];
// NOTE: We can't rely on the length of the input. It may not be available.
// Unfortunately, we also don't have enough context to know if the reason it can't
// decode is because we ran out of input, or because there was a format error.
while let Ok(part) = MultiSigPart::decode(input) {
parts.push(part);
}
Ok(Self { parts })
}
}
}
pub mod tx {
use super::*;
pub trait TransactionMethod {
fn method_selector() -> u8;
}
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/codecs/tx.ts
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TxHeader<const M: u8> {
// should always be 0
// pub transaction_type: Compact<u8>,
pub principal: core::Address,
// covered by const M
// pub method_selector: Compact<u8>,
}
impl<const M: u8> Encode for TxHeader<M> {
fn encode(&self) -> Vec<u8> {
let mut r = Vec::with_capacity(self.size_hint());
let transaction_type = Compact(0u8);
transaction_type.encode_to(&mut r);
self.principal.encode_to(&mut r);
let method_selector = Compact(M);
method_selector.encode_to(&mut r);
r
}
}
impl<const M: u8> Decode for TxHeader<M> {
fn decode<I: parity_scale_codec::Input>(
input: &mut I,
) -> Result<Self, parity_scale_codec::Error> {
let transaction_type = Compact::<u8>::decode(input)?;
if transaction_type.0 != 0 {
return Err("transaction_type != 0".into());
}
let principal = core::Address::decode(input)?;
let method_selector = Compact::<u8>::decode(input)?;
if method_selector.0 != M {
return Err("method_selector != M".into());
}
Ok(Self {
principal,
})
}
}
// NOTE: This is used in place of `withTemplateAddress()`.
// The original source implementation placed `template_address` as the last field,
// but I don't think that's correct based on the implementation of `withTemplateAddress()`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpawnTxHeader<const M: u8> {
pub template_address: core::Address,
// should always be 0
// pub transaction_type: Compact<u8>,
pub principal: core::Address,
// covered by const M
// pub method_selector: Compact<u8>,
}
impl<const M: u8> Encode for SpawnTxHeader<M> {
fn encode(&self) -> Vec<u8> {
let mut r = Vec::with_capacity(self.size_hint());
self.template_address.encode_to(&mut r);
let transaction_type = Compact(0u8);
transaction_type.encode_to(&mut r);
self.principal.encode_to(&mut r);
let method_selector = Compact(M);
method_selector.encode_to(&mut r);
r
}
}
impl<const M: u8> Decode for SpawnTxHeader<M> {
fn decode<I: parity_scale_codec::Input>(
input: &mut I,
) -> Result<Self, parity_scale_codec::Error> {
let template_address = core::Address::decode(input)?;
let transaction_type = Compact::<u8>::decode(input)?;
if transaction_type.0 != 0 {
return Err("transaction_type != 0".into());
}
let principal = core::Address::decode(input)?;
let method_selector = Compact::<u8>::decode(input)?;
if method_selector.0 != M {
return Err("method_selector != M".into());
}
Ok(Self {
template_address,
principal,
})
}
}
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/transaction.ts
mod sealed {
use super::signatures;
pub trait Signature {}
impl Signature for signatures::SingleSig {}
impl Signature for signatures::MultiSig {}
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpawnTransaction<Payload, Signature: sealed::Signature, const M: u8> {
pub header: SpawnTxHeader<M>,
pub payload: Payload,
pub signature: Signature,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct Transaction<Payload, Signature: sealed::Signature, const M: u8> {
pub header: TxHeader<M>,
pub payload: Payload,
pub signature: Signature,
}
}
pub mod tx_types {
use super::*;
pub type DecodeResult<T> = Option<Result<T, parity_scale_codec::Error>>;
pub mod common {
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/std/common.ts
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct TxPayload<Arguments> {
pub nonce: core::Nonce,
pub gas_price: core::GasPrice,
pub arguments: Arguments,
}
}
pub mod vault {
use super::*;
use common::TxPayload;
use signatures::SingleSig;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/std/vault.ts
pub const VAULT_TEMPLATE_ADDRESS: core::Address = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
];
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpawnArguments {
pub owner: core::Address,
pub total_amount: Compact<u64>,
pub initial_unlock_amount: Compact<u64>,
pub vesting_start: Compact<u32>,
pub vesting_end: Compact<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpendArguments {
pub destination: core::Address,
pub amount: Compact<u64>,
}
pub type SpawnPayload = TxPayload<SpawnArguments>;
pub type SpendPayload = TxPayload<SpendArguments>;
pub type Spawn = tx::SpawnTransaction<SpawnPayload, SingleSig, 0>;
pub type Spend = tx::Transaction<SpendPayload, SingleSig, 16>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Spawn(Spawn),
Spend(Spend),
}
pub fn decode_by_method(method: u8, input: &[u8]) -> DecodeResult<Method> {
match method {
0 => Some(Spawn::decode(&mut &*input).map(Method::Spawn)),
16 => Some(Spend::decode(&mut &*input).map(Method::Spend)),
_ => None,
}
}
}
pub mod vesting {
use super::*;
use common::TxPayload;
use signatures::MultiSig;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/std/vesting.ts
pub const VESTING_TEMPLATE_ADDRESS: core::Address = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
];
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpawnArguments {
pub required: Compact<u8>,
pub public_keys: Vec<core::PublicKey>,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpendArguments {
pub destination: core::Address,
pub amount: Compact<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct DrainVaultArguments {
pub vault: core::Address,
pub destination: core::Address,
pub amount: Compact<u64>,
}
pub type SpawnPayload = TxPayload<SpawnArguments>;
pub type SpendPayload = TxPayload<SpendArguments>;
pub type DrainVaultPayload = TxPayload<DrainVaultArguments>;
pub type Spawn = tx::SpawnTransaction<SpawnPayload, MultiSig, 0>;
pub type Spend = tx::Transaction<SpendPayload, MultiSig, 16>;
pub type DrainVault = tx::Transaction<DrainVaultPayload, MultiSig, 17>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Spawn(Spawn),
Spend(Spend),
DrainVault(DrainVault),
}
pub fn decode_by_method(method: u8, input: &[u8]) -> DecodeResult<Method> {
match method {
0 => Some(Spawn::decode(&mut &*input).map(Method::Spawn)),
16 => Some(Spend::decode(&mut &*input).map(Method::Spend)),
17 => Some(DrainVault::decode(&mut &*input).map(Method::DrainVault)),
_ => None,
}
}
}
pub mod single_signature {
use super::*;
use common::TxPayload;
use signatures::SingleSig;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/std/singlesig.ts
pub const SINGLE_SIG_TEMPLATE_ADDRESS: core::Address = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
];
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpawnArguments {
pub public_key: core::PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpendArguments {
pub destination: core::Address,
pub amount: Compact<u64>,
}
pub type SpawnPayload = TxPayload<SpawnArguments>;
pub type SpendPayload = TxPayload<SpendArguments>;
pub type Spawn = tx::SpawnTransaction<SpawnPayload, SingleSig, 0>;
pub type Spend = tx::Transaction<SpendPayload, SingleSig, 16>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Spawn(Spawn),
Spend(Spend),
}
pub fn decode_by_method(method: u8, input: &[u8]) -> DecodeResult<Method> {
match method {
0 => Some(Spawn::decode(&mut &*input).map(Method::Spawn)),
16 => Some(Spend::decode(&mut &*input).map(Method::Spend)),
_ => None,
}
}
}
pub mod multi_signature {
use super::*;
use common::TxPayload;
use signatures::MultiSig;
// ref: https://github.com/spacemeshos/sm-codec/blob/master/src/std/singlesig.ts
pub const MULTI_SIG_TEMPLATE_ADDRESS: core::Address = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
];
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpawnArguments {
pub required: Compact<u8>,
pub public_key: Vec<core::PublicKey>,
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct SpendArguments {
pub destination: core::Address,
pub amount: Compact<u64>,
}
pub type SpawnPayload = TxPayload<SpawnArguments>;
pub type SpendPayload = TxPayload<SpendArguments>;
pub type Spawn = tx::SpawnTransaction<SpawnPayload, MultiSig, 0>;
pub type Spend = tx::Transaction<SpendPayload, MultiSig, 16>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Spawn(Spawn),
Spend(Spend),
}
pub fn decode_by_method(method: u8, input: &[u8]) -> DecodeResult<Method> {
match method {
0 => Some(Spawn::decode(&mut &*input).map(Method::Spawn)),
16 => Some(Spend::decode(&mut &*input).map(Method::Spend)),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModuleMethod {
Vault(vault::Method),
Vesting(vesting::Method),
SingleSig(single_signature::Method),
MultiSig(multi_signature::Method),
}
#[rustfmt::skip]
pub fn decode_by_address_and_method(
address: core::Address,
method: u8,
input: &[u8],
) -> DecodeResult<ModuleMethod> {
match address {
vault::VAULT_TEMPLATE_ADDRESS =>
vault::decode_by_method(method, input)
.map(|method| method.map(ModuleMethod::Vault)),
vesting::VESTING_TEMPLATE_ADDRESS =>
vesting::decode_by_method(method, input)
.map(|method| method.map(ModuleMethod::Vesting)),
single_signature::SINGLE_SIG_TEMPLATE_ADDRESS => {
single_signature::decode_by_method(method, input)
.map(|method| method.map(ModuleMethod::SingleSig))
}
multi_signature::MULTI_SIG_TEMPLATE_ADDRESS => {
multi_signature::decode_by_method(method, input)
.map(|method| method.map(ModuleMethod::MultiSig))
}
_ => {
unimplemented!()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
use base64::prelude::*;
use bech32::Bech32;
let (hrp, data) =
bech32::decode("sm1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg56ypy7").unwrap();
assert_eq!(hrp.as_str(), "sm");
assert_eq!(
&data,
&tx_types::single_signature::SINGLE_SIG_TEMPLATE_ADDRESS
);
let encoded_tx = "AAAAAAAvqmgSN6hBGS16FVNfNDURojTRU0AQBAAAAABJThXbKEnjnty59ht5e/5EkjDK8AeANolPDOAiIHlzj7CIG60FzFRpuR/fLVRQsmzRbApYBryfg4RKcnZgmmWPywafADHyuVjkLNGup0gpvhnXAHICeSXveAs=";
let raw_tx = BASE64_STANDARD.decode(encoded_tx).unwrap();
let spend = tx_types::single_signature::Spend::decode(&mut &raw_tx[..]).unwrap();
let equivalence = spend.encode();
assert_eq!(raw_tx, equivalence);
let recipient_address =
bech32::encode::<Bech32>(hrp, &spend.payload.arguments.destination).unwrap();
assert_eq!(
recipient_address,
"sm1qqqqqqzffc2ak2zfuw0dew0krduhhljyjgcv4uqdt6nrd"
);
}
#[test]
fn recode() {
use tx::*;
use tx_types::single_signature::*;
let principal = [0u8; 24];
let single_sig_spend = Spend {
header: TxHeader {
principal,
},
payload: SpendPayload {
nonce: Compact(2),
gas_price: Compact(1),
arguments: SpendArguments {
destination: [1; 24],
amount: Compact(100000),
},
},
signature: [0; 64],
};
let encoded = single_sig_spend.encode();
let recoded = Spend::decode(&mut &*encoded).unwrap();
assert_eq!(single_sig_spend, recoded);
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "spacemesh"
version = "0.1.0"
edition = "2021"
publish = ["distrust"]
[dependencies]
bech32 = "0.11.0"
spacemesh-api-client = { version = "0.1.0", path = "../api-client" }
spacemesh-codec = { version = "0.1.0", path = "../codec" }
[dev-dependencies]
base64 = "0.22.1"
bech32 = "0.11.0"
smex = { version = "0.1.0", registry = "distrust" }
tokio = { version = "1.43.0", features = ["net", "rt", "macros"] }

View File

@ -0,0 +1,58 @@
pub use spacemesh_api_client as client;
pub use spacemesh_api_client::Client;
pub use spacemesh_codec as codec;
pub use spacemesh_codec::tx_types as transaction;
pub mod wallet;
pub mod bech32 {
pub use bech32::*;
pub fn encode(hrp: Hrp, input: &[u8]) -> Result<String, EncodeError> {
bech32::encode::<Bech32>(hrp, input)
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::prelude::*;
use spacemesh_api_client::*;
#[tokio::test]
async fn decodes_live_transaction() {
let client = Client::new("https://mainnet-api.spacemesh.network");
let txid = "638442a2033f20b5a7280b9a4f2bfc73022f6e7ec64b1497b85335444381d99d";
let txid = smex::decode(txid).unwrap();
let txid = BASE64_STANDARD.encode(txid);
let result = client
.transaction_service_list(&types::Spacemeshv2alpha1TransactionRequest {
txid: vec![txid],
limit: Some(100.to_string()),
..Default::default()
})
.await
.unwrap()
.into_inner();
let mut result = match result {
types::GooglerpcStatusOrSpacemeshv2alpha1TransactionList::GooglerpcStatus(googlerpc_status) => panic!("{:?}", googlerpc_status.message),
types::GooglerpcStatusOrSpacemeshv2alpha1TransactionList::Spacemeshv2alpha1TransactionList(transaction_list) => {
transaction_list
},
};
let tx = result.transactions.pop().unwrap().tx.unwrap();
let (_hrp, address) = bech32::decode(&tx.template.unwrap()).unwrap();
let tx_raw = BASE64_STANDARD.decode(tx.raw.unwrap()).unwrap();
let decoded = transaction::decode_by_address_and_method(
address.try_into().unwrap(),
tx.method.unwrap() as u8,
&tx_raw,
)
.unwrap()
.unwrap();
drop(decoded);
}
}

View File

@ -0,0 +1,77 @@
//! Spacemesh wallet management.
pub use crate::codec::core::Address;
use crate::codec::tx::*;
use crate::codec::Compact;
use crate::transaction::single_signature;
const ADDRESS_RESERVED: usize = 4;
mod sealed {
pub trait Sealed {}
}
pub trait AsAddress: sealed::Sealed {
fn as_address(&self) -> Address;
}
impl sealed::Sealed for Address {}
impl AsAddress for Address {
#[inline(always)]
fn as_address(&self) -> Address {
*self
}
}
impl sealed::Sealed for [u8; 32] {}
impl AsAddress for [u8; 32] {
#[inline(always)]
fn as_address(&self) -> Address {
let mut output = [0u8; std::mem::size_of::<Address>()];
const START: usize = 32 - std::mem::size_of::<Address>() + ADDRESS_RESERVED;
output[ADDRESS_RESERVED..].copy_from_slice(
&self[START..],
);
output
}
}
pub fn spawn(principal: [u8; 32], nonce: u64, gas_price: u64) -> single_signature::Spawn {
single_signature::Spawn {
header: SpawnTxHeader {
principal: principal.as_address(),
template_address: single_signature::SINGLE_SIG_TEMPLATE_ADDRESS,
},
payload: single_signature::SpawnPayload {
nonce: Compact(nonce),
gas_price: Compact(gas_price),
arguments: single_signature::SpawnArguments {
public_key: principal,
},
},
signature: [0u8; 64],
}
}
pub fn transfer(
principal: impl AsAddress,
recipient: impl AsAddress,
amount: u64,
nonce: u64,
gas_price: u64,
) -> single_signature::Spend {
single_signature::Spend {
header: TxHeader {
principal: principal.as_address(),
},
payload: single_signature::SpendPayload {
nonce: Compact(nonce),
gas_price: Compact(gas_price),
arguments: single_signature::SpendArguments {
destination: recipient.as_address(),
amount: Compact(amount),
},
},
signature: [0u8; 64],
}
}

View File

@ -3,209 +3,12 @@ name = "sol"
derivation_prefix = "m/44'/501'/0'"
algorithm = "Ed25519"
# NOTE: To get a nonce address, the `generate-nonce-account` workflow should be
# run. It is the only workflow that uses a blockhash, which is why a
# `broadcast-with-blockhash` or similar is not, and should not be, implemented.
[[module.workflow]]
name = "broadcast"
inputs = ["nonce_address", "cluster"]
[[module.workflow.step]]
type = "sol-get-nonce-account-data"
inputs = { nonce_address = "nonce_address", cluster = "cluster" }
outputs = { authority = "nonce_authority", durable_nonce = "nonce" }
[[module.workflow.step]]
type = "internal-save-file"
values = { filename = "nonce.json" }
inputs = { nonce_authority = "nonce_authority", nonce_data = "nonce", nonce_address = "nonce_address" }
[[module.workflow.step]]
type = "internal-load-file"
values = { filename = "transaction.json" }
outputs = { transaction = "transaction" }
[[module.workflow.step]]
type = "sol-broadcast"
inputs = { cluster = "cluster", transaction = "transaction" }
outputs = { status = "status", url = "url", error = "error" }
[[module.workflow]]
name = "generate-nonce-account"
inputs = ["cluster", "authorization_address"]
[[module.workflow.step]]
type = "sol-generate-wallet"
[[module.workflow.step]]
type = "sol-get-wallet-address"
outputs = { pubkey = "wallet_pubkey" }
[[module.workflow.step]]
type = "sol-await-funds"
inputs = { address = "wallet_pubkey", cluster = "cluster" }
# enough to cover two signatures and the 1_500_000 approx. rent fee
values = { lamports = "1510000" }
[[module.workflow.step]]
type = "sol-get-blockhash"
inputs = { cluster = "cluster" }
outputs = { blockhash = "blockhash" }
[[module.workflow.step]]
type = "sol-create-nonce-account-and-signing-key"
[module.workflow.step.inputs]
from_address = "wallet_pubkey"
authorization_address = "authorization_address"
[module.workflow.step.outputs]
transaction = "unsigned_transaction"
nonce_pubkey = "nonce_pubkey"
nonce_privkey = "private_keys"
[[module.workflow.step]]
type = "sol-sign"
[module.workflow.step.inputs]
blockhash = "blockhash"
signing_keys = "private_keys"
transaction = "unsigned_transaction"
[module.workflow.step.outputs]
transaction = "signed_transaction"
[[module.workflow.step]]
type = "sol-broadcast"
inputs = { cluster = "cluster", transaction = "signed_transaction" }
outputs = { status = "status", url = "url" }
[[module.workflow.step]]
type = "internal-cat"
inputs = { status = "status", url = "url", nonce_account = "nonce_pubkey" }
outputs = { status = "status", url = "url", nonce_account = "nonce_account" }
[[module.workflow]]
# Transfer SOL from one address to another.
name = "transfer"
inputs = ["to_address", "from_address", "amount"]
[[module.workflow.step]]
type = "internal-load-file"
values = { filename = "nonce.json" }
outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" }
[[module.workflow.step]]
type = "sol-transfer"
inputs = { from_address = "from_address", to_address = "to_address", amount = "amount" }
outputs = { instructions = "instructions", derivation_accounts = "derivation_accounts" }
[[module.workflow.step]]
type = "sol-compile"
[module.workflow.step.inputs]
instructions = "instructions"
derivation_accounts = "derivation_accounts"
nonce_address = "nonce_address"
nonce_authority = "nonce_authority"
nonce_data = "nonce_data"
[module.workflow.step.outputs]
transaction = "unsigned_transaction"
[[module.workflow.step]]
type = "sol-sign"
inputs = { blockhash = "nonce_data", transaction = "unsigned_transaction" }
outputs = { transaction = "signed_transaction" }
[[module.workflow.step]]
type = "internal-save-file"
values = { filename = "transaction.json" }
inputs = { transaction = "signed_transaction" }
[[module]]
name = "cosmos"
derivation_prefix = "m/44'/118'/0'"
algorithm = "Secp256k1"
[[module.workflow]]
name = "transfer"
inputs = ["from_address", "to_address", "asset_name", "chain_name", "asset_amount"]
[[module.workflow.step]]
# NOTE: chain_name can't be discoverable by filtering from asset_name, since
# some asset devnets reuse the name. There's no difference between KYVE on Kyve
# or Korellia (devnet).
type = "cosmos-get-chain-info"
inputs = { chain_name = "chain_name" }
outputs = { blockchain_config = "blockchain_config" }
[[module.workflow.step]]
type = "internal-load-file"
values = { filename = "account_info.json" }
outputs = { account_number = "account_number", sequence_number = "sequence_number" }
[[module.workflow.step]]
type = "cosmos-transfer"
[module.workflow.step.inputs]
from_address = "from_address"
to_address = "to_address"
amount = "asset_amount"
denom = "asset_name"
blockchain_config = "blockchain_config"
[module.workflow.step.outputs]
fee = "fee"
tx_messages = "tx_messages"
[[module.workflow.step]]
type = "cosmos-sign"
[module.workflow.step.inputs]
fee = "fee"
tx_messages = "tx_messages"
account_number = "account_number"
sequence_number = "sequence_number"
blockchain_config = "blockchain_config"
[module.workflow.step.outputs]
transaction = "signed_transaction"
[[module.workflow.step]]
type = "internal-save-file"
values = { filename = "transaction.json" }
inputs = { transaction = "signed_transaction" }
[[module.workflow]]
name = "broadcast"
# NOTE: For the purpose of Cosmos, the nonce is a direct part of the signer's
# account.
inputs = ["nonce_address", "chain_name"]
[[module.workflow.step]]
type = "cosmos-get-chain-info"
inputs = { chain_name = "chain_name" }
outputs = { blockchain_config = "blockchain_config" }
[[module.workflow.step]]
type = "cosmos-get-account-data"
inputs = { account_id = "nonce_address", blockchain_config = "blockchain_config" }
outputs = { account_number = "account_number", sequence_number = "sequence_number" }
[[module.workflow.step]]
type = "internal-save-file"
values = { filename = "account_info.json" }
inputs = { account_number = "account_number", sequence_number = "sequence_number" }
[[module.workflow.step]]
type = "internal-load-file"
values = { filename = "transaction.json" }
outputs = { transaction = "transaction" }
[[module.workflow.step]]
type = "cosmos-broadcast"
inputs = { blockchain_config = "blockchain_config", transaction = "transaction" }
outputs = { status = "status", url = "url", error = "error", error_code = "error_code" }
[[module]]
name = "spacemesh"
derivation_prefix = "m/44'/540'/0'/0'"
algorithm = "Ed25519"