Compare commits
No commits in common. "main" and "anton/refactor-toml-to-yaml" have entirely different histories.
main
...
anton/refa
File diff suppressed because it is too large
Load Diff
|
@ -6,14 +6,9 @@ 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]
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
[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"
|
|
@ -1,91 +0,0 @@
|
|||
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(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
use icepick_module::Module;
|
||||
use icepick_ed25519::Ed25519;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ed25519::run_responder()
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
[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"] }
|
|
@ -1,172 +0,0 @@
|
|||
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!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
use icepick_module::Module;
|
||||
use icepick_spacemesh::Spacemesh;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Spacemesh::run_responder()
|
||||
}
|
|
@ -24,45 +24,15 @@ 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,
|
||||
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub inputs: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub inputs: Vec<Input>,
|
||||
pub optional_inputs: Vec<String>,
|
||||
|
||||
#[serde(rename = "step")]
|
||||
steps: Vec<WorkflowStep>,
|
||||
|
@ -99,8 +69,7 @@ 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>(
|
||||
|
@ -159,6 +128,15 @@ 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: StringMap<Value> = data
|
||||
.iter()
|
||||
|
@ -189,20 +167,13 @@ 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)
|
||||
|
|
|
@ -20,7 +20,6 @@ fn main() {
|
|||
|
||||
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());
|
||||
|
@ -29,7 +28,6 @@ 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() {
|
||||
|
|
|
@ -139,14 +139,6 @@ 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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use icepick_workflow::{Input, InvocableOperation, OperationResult, StringMap, Workflow};
|
||||
use icepick_workflow::{InvocableOperation, OperationResult, Workflow, StringMap};
|
||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||
use miniquorum::{Payload, PayloadVerification};
|
||||
|
@ -91,33 +91,31 @@ impl InvocableOperation for CLIOperation {
|
|||
}
|
||||
|
||||
pub fn generate_command(workflow: &Workflow) -> clap::Command {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
command.arg(clap::arg!(
|
||||
--"input-file" [FILE]
|
||||
"A file containing any inputs not passed on the command line"
|
||||
))
|
||||
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
|
||||
}
|
||||
|
||||
fn load_inputs<'a>(
|
||||
inputs: impl IntoIterator<Item = &'a Input>,
|
||||
fn load_inputs<T: AsRef<str> + Into<String> + std::fmt::Display>(
|
||||
inputs: impl IntoIterator<Item = T>,
|
||||
optional_inputs: impl IntoIterator<Item = T>,
|
||||
matches: &clap::ArgMatches,
|
||||
) -> StringMap {
|
||||
let mut map = StringMap::default();
|
||||
|
@ -126,25 +124,33 @@ fn load_inputs<'a>(
|
|||
.and_then(|p| std::fs::File::open(p).ok())
|
||||
.and_then(|f| serde_json::from_reader(f).ok());
|
||||
for input in inputs {
|
||||
let identifier = &input.name;
|
||||
match input
|
||||
.identifiers()
|
||||
.filter_map(|name| matches.get_one::<String>(name))
|
||||
.next()
|
||||
{
|
||||
match matches.get_one::<String>(input.as_ref()) {
|
||||
Some(value) => {
|
||||
map.insert(identifier.clone(), value.clone());
|
||||
map.insert(input.into(), value.clone());
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
if let Some(value) = input_file.as_ref().and_then(|f| f.get(identifier)) {
|
||||
map.insert(identifier.clone(), value.clone());
|
||||
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}");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,10 +188,13 @@ 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();
|
||||
|
||||
|
@ -201,19 +210,20 @@ 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();
|
||||
|
@ -241,7 +251,7 @@ pub fn handle(
|
|||
modules: Commands,
|
||||
config: &[ModuleConfig],
|
||||
) {
|
||||
let inputs = load_inputs(&workflow.inputs, matches);
|
||||
let inputs = load_inputs(&workflow.inputs, &workflow.optional_inputs, matches);
|
||||
let data: StringMap<Value> = inputs
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Value::String(v)))
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
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.
|
||||
- "nonce_address"
|
||||
- "chain_name"
|
||||
step:
|
||||
- type: "cosmos-get-chain-info"
|
||||
inputs:
|
||||
|
@ -40,4 +34,4 @@ step:
|
|||
status: "status"
|
||||
url: "url"
|
||||
error: "error"
|
||||
error_code: "error_code"
|
||||
error_code: "error_code"
|
|
@ -1,14 +1,8 @@
|
|||
name: generate-address
|
||||
description: |-
|
||||
Generate an address on a given Cosmos-based blockchain.
|
||||
inputs:
|
||||
- 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
|
||||
- chain_name
|
||||
optional_inputs:
|
||||
- account
|
||||
step:
|
||||
- type: cosmos-get-chain-info
|
||||
inputs:
|
||||
|
|
|
@ -1,27 +1,12 @@
|
|||
name: stake
|
||||
description: |-
|
||||
Stake coins on the provided chain.
|
||||
inputs:
|
||||
- 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
|
||||
- delegate_address
|
||||
- validator_address
|
||||
- chain_name
|
||||
- asset_name
|
||||
- asset_amount
|
||||
optional_inputs:
|
||||
- gas_factor
|
||||
step:
|
||||
- type: cosmos-get-chain-info
|
||||
inputs:
|
||||
|
|
|
@ -1,22 +1,10 @@
|
|||
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.
|
||||
- "from_address"
|
||||
- "to_address"
|
||||
- "asset_name"
|
||||
- "chain_name"
|
||||
- "asset_amount"
|
||||
step:
|
||||
- type: "cosmos-get-chain-info"
|
||||
inputs:
|
||||
|
@ -52,4 +40,4 @@ step:
|
|||
values:
|
||||
filename: "transaction.json"
|
||||
inputs:
|
||||
transaction: "signed_transaction"
|
||||
transaction: "signed_transaction"
|
|
@ -1,21 +1,10 @@
|
|||
name: withdraw-rewards
|
||||
description: |-
|
||||
Withdraw rewards gained from staking to a validator.
|
||||
inputs:
|
||||
- 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
|
||||
- delegate_address
|
||||
- validator_address
|
||||
- chain_name
|
||||
optional_inputs:
|
||||
- gas_factor
|
||||
step:
|
||||
- type: cosmos-get-chain-info
|
||||
inputs:
|
||||
|
|
|
@ -1,30 +1,12 @@
|
|||
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:
|
||||
- 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
|
||||
- delegate_address
|
||||
- validator_address
|
||||
- chain_name
|
||||
- asset_name
|
||||
- asset_amount
|
||||
optional_inputs:
|
||||
- gas_factor
|
||||
step:
|
||||
- type: cosmos-get-chain-info
|
||||
inputs:
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
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
|
||||
- "nonce_address"
|
||||
- "cluster"
|
||||
step:
|
||||
- type: "sol-get-nonce-account-data"
|
||||
inputs:
|
||||
|
@ -37,4 +29,4 @@ step:
|
|||
outputs:
|
||||
status: "status"
|
||||
url: "url"
|
||||
error: "error"
|
||||
error: "error"
|
|
@ -1,11 +1,6 @@
|
|||
name: generate-address
|
||||
description: |-
|
||||
Generate a Solana address.
|
||||
inputs:
|
||||
- name: account
|
||||
description: >-
|
||||
The account to use, if not the default account.
|
||||
optional: true
|
||||
optional_inputs:
|
||||
- account
|
||||
step:
|
||||
- type: sol-generate-wallet
|
||||
inputs:
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
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.
|
||||
- "cluster"
|
||||
- "authorization_address"
|
||||
step:
|
||||
- type: "sol-generate-wallet"
|
||||
- type: "sol-get-wallet-address"
|
||||
|
@ -72,4 +59,4 @@ step:
|
|||
status: "status"
|
||||
url: "url"
|
||||
nonce_account: "nonce_account"
|
||||
error: "error"
|
||||
error: "error"
|
|
@ -1,19 +1,9 @@
|
|||
name: transfer-token
|
||||
description: |-
|
||||
Transfer SPL tokens held on the Solana blockchain.
|
||||
inputs:
|
||||
- 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.
|
||||
- from_address
|
||||
- to_address
|
||||
- token_name
|
||||
- token_amount
|
||||
step:
|
||||
- type: sol-get-token-info
|
||||
inputs:
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
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.
|
||||
- "to_address"
|
||||
- "from_address"
|
||||
- "amount"
|
||||
step:
|
||||
- type: "internal-load-file"
|
||||
values:
|
||||
|
@ -46,4 +38,4 @@ step:
|
|||
values:
|
||||
filename: "transaction.json"
|
||||
inputs:
|
||||
transaction: "signed_transaction"
|
||||
transaction: "signed_transaction"
|
|
@ -1,25 +0,0 @@
|
|||
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
|
|
@ -1,26 +0,0 @@
|
|||
[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"] }
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
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
|
@ -1,42 +0,0 @@
|
|||
#![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
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
[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"
|
|
@ -1,530 +0,0 @@
|
|||
//! 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);
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
[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"] }
|
|
@ -1,58 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
//! 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],
|
||||
}
|
||||
}
|
14
icepick.toml
14
icepick.toml
|
@ -1,14 +0,0 @@
|
|||
[[module]]
|
||||
name = "sol"
|
||||
derivation_prefix = "m/44'/501'/0'"
|
||||
algorithm = "Ed25519"
|
||||
|
||||
[[module]]
|
||||
name = "cosmos"
|
||||
derivation_prefix = "m/44'/118'/0'"
|
||||
algorithm = "Secp256k1"
|
||||
|
||||
[[module]]
|
||||
name = "spacemesh"
|
||||
derivation_prefix = "m/44'/540'/0'/0'"
|
||||
algorithm = "Ed25519"
|
|
@ -0,0 +1,7 @@
|
|||
module:
|
||||
- name: "sol"
|
||||
derivation_prefix: "m/44'/501'/0'"
|
||||
algorithm: "Ed25519"
|
||||
- name: "cosmos"
|
||||
derivation_prefix: "m/44'/118'/0'"
|
||||
algorithm: "Secp256k1"
|
Loading…
Reference in New Issue