Compare commits
5 Commits
anton/refa
...
main
Author | SHA1 | Date |
---|---|---|
|
3e9490644a | |
|
79cef4d01a | |
|
af9babe526 | |
|
4832300098 | |
|
097bacbdea |
File diff suppressed because it is too large
Load Diff
|
@ -6,9 +6,14 @@ members = [
|
||||||
"crates/icepick-workflow",
|
"crates/icepick-workflow",
|
||||||
"crates/icepick-module",
|
"crates/icepick-module",
|
||||||
"crates/builtins/icepick-internal",
|
"crates/builtins/icepick-internal",
|
||||||
|
"crates/builtins/icepick-ed25519",
|
||||||
"crates/by-chain/icepick-solana",
|
"crates/by-chain/icepick-solana",
|
||||||
"crates/by-chain/icepick-cosmos",
|
"crates/by-chain/icepick-cosmos",
|
||||||
"crates/miniquorum",
|
"crates/miniquorum",
|
||||||
|
"crates/spacemesh/api-client",
|
||||||
|
"crates/spacemesh/codec",
|
||||||
|
"crates/spacemesh/spacemesh",
|
||||||
|
"crates/by-chain/icepick-spacemesh",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
@ -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"
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
use icepick_module::Module;
|
||||||
|
use icepick_ed25519::Ed25519;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Ed25519::run_responder()
|
||||||
|
}
|
|
@ -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"] }
|
|
@ -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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
use icepick_module::Module;
|
||||||
|
use icepick_spacemesh::Spacemesh;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Spacemesh::run_responder()
|
||||||
|
}
|
|
@ -24,15 +24,45 @@ pub enum WorkflowError {
|
||||||
InvocationError(String),
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Workflow {
|
pub struct Workflow {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[serde(default)]
|
pub description: String,
|
||||||
pub inputs: Vec<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub optional_inputs: Vec<String>,
|
pub inputs: Vec<Input>,
|
||||||
|
|
||||||
#[serde(rename = "step")]
|
#[serde(rename = "step")]
|
||||||
steps: Vec<WorkflowStep>,
|
steps: Vec<WorkflowStep>,
|
||||||
|
@ -69,7 +99,8 @@ pub struct OperationResult {
|
||||||
derivation_accounts: Vec<DerivationIndex>,
|
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 {
|
impl Workflow {
|
||||||
pub fn simulate_workflow<T: InvocableOperation + Sized>(
|
pub fn simulate_workflow<T: InvocableOperation + Sized>(
|
||||||
|
@ -128,15 +159,6 @@ impl Workflow {
|
||||||
return Err(WorkflowError::InvocableOperationNotFound(step_type));
|
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
|
// Prepare all inputs for the operation invocation
|
||||||
let inputs: StringMap<Value> = data
|
let inputs: StringMap<Value> = data
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -167,13 +189,20 @@ impl Workflow {
|
||||||
let (_given, stored) = step.outputs.iter().find(|(k1, _)| k == **k1)?;
|
let (_given, stored) = step.outputs.iter().find(|(k1, _)| k == **k1)?;
|
||||||
Some((stored.clone(), v))
|
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() {
|
if let Some(last_step) = &self.steps.last() {
|
||||||
let values = last_step.outputs.values().collect::<HashSet<_>>();
|
let values = last_step.outputs.values().collect::<HashSet<_>>();
|
||||||
data.retain(|stored_name, _| {
|
data.retain(|stored_name, _| values.contains(stored_name));
|
||||||
values.contains(stored_name)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
|
|
|
@ -20,6 +20,7 @@ fn main() {
|
||||||
|
|
||||||
for module_dir in std::fs::read_dir(&workflows_dir).unwrap() {
|
for module_dir in std::fs::read_dir(&workflows_dir).unwrap() {
|
||||||
let module_dir = module_dir.unwrap();
|
let module_dir = module_dir.unwrap();
|
||||||
|
dbg!(&module_dir);
|
||||||
let path = module_dir.path();
|
let path = module_dir.path();
|
||||||
if !path.is_dir() {
|
if !path.is_dir() {
|
||||||
panic!("found unexpected file {}", path.to_string_lossy());
|
panic!("found unexpected file {}", path.to_string_lossy());
|
||||||
|
@ -28,6 +29,7 @@ fn main() {
|
||||||
let mut workflows = vec![];
|
let mut workflows = vec![];
|
||||||
|
|
||||||
for workflow_file in std::fs::read_dir(&path).unwrap() {
|
for workflow_file in std::fs::read_dir(&path).unwrap() {
|
||||||
|
dbg!(&workflow_file);
|
||||||
let workflow_file = workflow_file.unwrap();
|
let workflow_file = workflow_file.unwrap();
|
||||||
let path = workflow_file.path();
|
let path = workflow_file.path();
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
|
|
|
@ -139,6 +139,14 @@ pub fn do_cli_thing() {
|
||||||
derivation_prefix: Default::default(),
|
derivation_prefix: Default::default(),
|
||||||
workflows: 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();
|
let workflows = default_workflows();
|
||||||
for module in &mut config.modules {
|
for module in &mut config.modules {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use icepick_workflow::{InvocableOperation, OperationResult, Workflow, StringMap};
|
use icepick_workflow::{Input, InvocableOperation, OperationResult, StringMap, Workflow};
|
||||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationPath};
|
||||||
use keyfork_shard::{openpgp::OpenPGP, Format};
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
use miniquorum::{Payload, PayloadVerification};
|
use miniquorum::{Payload, PayloadVerification};
|
||||||
|
@ -91,31 +91,33 @@ impl InvocableOperation for CLIOperation {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_command(workflow: &Workflow) -> clap::Command {
|
pub fn generate_command(workflow: &Workflow) -> clap::Command {
|
||||||
let mut command = clap::Command::new(&workflow.name).arg(clap::arg!(
|
let mut command = clap::Command::new(&workflow.name).about(&workflow.description);
|
||||||
--"input-file" [FILE]
|
// NOTE: all required inputs are still marked as .required(false) since they could be included
|
||||||
"A file containing any inputs not passed on the command line"
|
// in the `--input-file` argument.
|
||||||
));
|
for input in workflow.inputs.iter() {
|
||||||
for input in &workflow.inputs {
|
for arg in input.identifiers() {
|
||||||
// can also be included in the JSON file, so we won't mark this as required.
|
let arg = clap::Arg::new(arg)
|
||||||
let arg = clap::Arg::new(input)
|
.required(false)
|
||||||
.required(false)
|
.help(&input.description)
|
||||||
.long(input.replace('_', "-"))
|
.long(arg.replace('_', "-"))
|
||||||
.value_name(input.to_uppercase());
|
.value_name(arg.to_uppercase())
|
||||||
command = command.arg(arg);
|
.conflicts_with_all(
|
||||||
|
input
|
||||||
|
.identifiers()
|
||||||
|
.filter(|name| *name != arg)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
command = command.arg(arg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for input in &workflow.optional_inputs {
|
command.arg(clap::arg!(
|
||||||
let arg = clap::Arg::new(input)
|
--"input-file" [FILE]
|
||||||
.required(false)
|
"A file containing any inputs not passed on the command line"
|
||||||
.long(input.replace('_', "-"))
|
))
|
||||||
.value_name(input.to_uppercase());
|
|
||||||
command = command.arg(arg);
|
|
||||||
}
|
|
||||||
command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_inputs<T: AsRef<str> + Into<String> + std::fmt::Display>(
|
fn load_inputs<'a>(
|
||||||
inputs: impl IntoIterator<Item = T>,
|
inputs: impl IntoIterator<Item = &'a Input>,
|
||||||
optional_inputs: impl IntoIterator<Item = T>,
|
|
||||||
matches: &clap::ArgMatches,
|
matches: &clap::ArgMatches,
|
||||||
) -> StringMap {
|
) -> StringMap {
|
||||||
let mut map = StringMap::default();
|
let mut map = StringMap::default();
|
||||||
|
@ -124,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(|p| std::fs::File::open(p).ok())
|
||||||
.and_then(|f| serde_json::from_reader(f).ok());
|
.and_then(|f| serde_json::from_reader(f).ok());
|
||||||
for input in inputs {
|
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) => {
|
Some(value) => {
|
||||||
map.insert(input.into(), value.clone());
|
map.insert(identifier.clone(), value.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if let Some(value) = input_file.as_ref().and_then(|f| f.get(input.as_ref())) {
|
if let Some(value) = input_file.as_ref().and_then(|f| f.get(identifier)) {
|
||||||
map.insert(input.into(), value.clone());
|
map.insert(identifier.clone(), value.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panic!("Required workflow input was not found: {input}");
|
if input.is_required() {
|
||||||
}
|
panic!("Required workflow input was not found: {identifier}");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,13 +182,10 @@ pub fn parse_quorum_file(
|
||||||
let threshold = threshold.unwrap_or(u8::try_from(certs.len()).expect("too many certs!"));
|
let threshold = threshold.unwrap_or(u8::try_from(certs.len()).expect("too many certs!"));
|
||||||
let policy = match purpose {
|
let policy = match purpose {
|
||||||
Purpose::AddSignature => {
|
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)
|
PayloadVerification::new().with_threshold(0)
|
||||||
}
|
}
|
||||||
Purpose::RunQuorum => {
|
Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold),
|
||||||
PayloadVerification::new().with_threshold(threshold)
|
|
||||||
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
payload.verify_signatures(&certs, &policy, None).unwrap();
|
payload.verify_signatures(&certs, &policy, None).unwrap();
|
||||||
|
|
||||||
|
@ -210,20 +201,19 @@ pub fn parse_quorum_with_shardfile(
|
||||||
let payload: Payload = serde_json::from_reader(payload_file).unwrap();
|
let payload: Payload = serde_json::from_reader(payload_file).unwrap();
|
||||||
|
|
||||||
let opgp = OpenPGP;
|
let opgp = OpenPGP;
|
||||||
let (threshold, certs) = opgp.decrypt_metadata_from_file(
|
let (threshold, certs) = opgp
|
||||||
None::<&std::path::Path>,
|
.decrypt_metadata_from_file(
|
||||||
std::fs::File::open(shardfile_path).unwrap(),
|
None::<&std::path::Path>,
|
||||||
keyfork_prompt::default_handler().unwrap(),
|
std::fs::File::open(shardfile_path).unwrap(),
|
||||||
).unwrap();
|
keyfork_prompt::default_handler().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let policy = match purpose {
|
let policy = match purpose {
|
||||||
Purpose::AddSignature => {
|
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)
|
PayloadVerification::new().with_threshold(0)
|
||||||
}
|
}
|
||||||
Purpose::RunQuorum => {
|
Purpose::RunQuorum => PayloadVerification::new().with_threshold(threshold),
|
||||||
PayloadVerification::new().with_threshold(threshold)
|
|
||||||
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
payload.verify_signatures(&certs, &policy, None).unwrap();
|
payload.verify_signatures(&certs, &policy, None).unwrap();
|
||||||
|
@ -251,7 +241,7 @@ pub fn handle(
|
||||||
modules: Commands,
|
modules: Commands,
|
||||||
config: &[ModuleConfig],
|
config: &[ModuleConfig],
|
||||||
) {
|
) {
|
||||||
let inputs = load_inputs(&workflow.inputs, &workflow.optional_inputs, matches);
|
let inputs = load_inputs(&workflow.inputs, matches);
|
||||||
let data: StringMap<Value> = inputs
|
let data: StringMap<Value> = inputs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| (k, Value::String(v)))
|
.map(|(k, v)| (k, Value::String(v)))
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
name: "broadcast"
|
name: "broadcast"
|
||||||
|
description: |-
|
||||||
|
Broadcast a transaction on a Cosmos-based blockchain.
|
||||||
inputs:
|
inputs:
|
||||||
- "nonce_address"
|
- name: "nonce_address"
|
||||||
- "chain_name"
|
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:
|
step:
|
||||||
- type: "cosmos-get-chain-info"
|
- type: "cosmos-get-chain-info"
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
name: generate-address
|
name: generate-address
|
||||||
|
description: |-
|
||||||
|
Generate an address on a given Cosmos-based blockchain.
|
||||||
inputs:
|
inputs:
|
||||||
- chain_name
|
- name: chain_name
|
||||||
optional_inputs:
|
description: >-
|
||||||
- account
|
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:
|
step:
|
||||||
- type: cosmos-get-chain-info
|
- type: cosmos-get-chain-info
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
name: stake
|
name: stake
|
||||||
|
description: |-
|
||||||
|
Stake coins on the provided chain.
|
||||||
inputs:
|
inputs:
|
||||||
- delegate_address
|
- name: delegate_address
|
||||||
- validator_address
|
description: >-
|
||||||
- chain_name
|
Address holding the coins to be staked to a validator.
|
||||||
- asset_name
|
- name: validator_address
|
||||||
- asset_amount
|
description: >-
|
||||||
optional_inputs:
|
Address of the validator operator.
|
||||||
- gas_factor
|
- 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:
|
step:
|
||||||
- type: cosmos-get-chain-info
|
- type: cosmos-get-chain-info
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,10 +1,22 @@
|
||||||
name: "transfer"
|
name: "transfer"
|
||||||
|
description: |-
|
||||||
|
Transfer a Cosmos coin.
|
||||||
inputs:
|
inputs:
|
||||||
- "from_address"
|
- name: "from_address"
|
||||||
- "to_address"
|
description: >-
|
||||||
- "asset_name"
|
The address from which to send coin.
|
||||||
- "chain_name"
|
- name: "to_address"
|
||||||
- "asset_amount"
|
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:
|
step:
|
||||||
- type: "cosmos-get-chain-info"
|
- type: "cosmos-get-chain-info"
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
name: withdraw-rewards
|
name: withdraw-rewards
|
||||||
|
description: |-
|
||||||
|
Withdraw rewards gained from staking to a validator.
|
||||||
inputs:
|
inputs:
|
||||||
- delegate_address
|
- name: delegate_address
|
||||||
- validator_address
|
description: >-
|
||||||
- chain_name
|
The owner of the staked coins; also, the recipient of rewards.
|
||||||
optional_inputs:
|
- name: validator_address
|
||||||
- gas_factor
|
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:
|
step:
|
||||||
- type: cosmos-get-chain-info
|
- type: cosmos-get-chain-info
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
name: withdraw
|
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:
|
inputs:
|
||||||
- delegate_address
|
- name: delegate_address
|
||||||
- validator_address
|
description: >-
|
||||||
- chain_name
|
The owner of the staked coins.
|
||||||
- asset_name
|
- name: validator_address
|
||||||
- asset_amount
|
description: >-
|
||||||
optional_inputs:
|
The validator from whom coins are staked.
|
||||||
- gas_factor
|
- 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:
|
step:
|
||||||
- type: cosmos-get-chain-info
|
- type: cosmos-get-chain-info
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
name: "broadcast"
|
name: "broadcast"
|
||||||
|
description: |-
|
||||||
|
Broadcast a transaction on the Solana blockchain.
|
||||||
inputs:
|
inputs:
|
||||||
- "nonce_address"
|
- name: "nonce_address"
|
||||||
- "cluster"
|
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:
|
step:
|
||||||
- type: "sol-get-nonce-account-data"
|
- type: "sol-get-nonce-account-data"
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
name: generate-address
|
name: generate-address
|
||||||
optional_inputs:
|
description: |-
|
||||||
- account
|
Generate a Solana address.
|
||||||
|
inputs:
|
||||||
|
- name: account
|
||||||
|
description: >-
|
||||||
|
The account to use, if not the default account.
|
||||||
|
optional: true
|
||||||
step:
|
step:
|
||||||
- type: sol-generate-wallet
|
- type: sol-generate-wallet
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
name: "generate-nonce-account"
|
name: "generate-nonce-account"
|
||||||
|
description: |-
|
||||||
|
Using a temporary Keyfork instance, generate a nonce address for the given
|
||||||
|
authorization address.
|
||||||
inputs:
|
inputs:
|
||||||
- "cluster"
|
- name: "cluster"
|
||||||
- "authorization_address"
|
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:
|
step:
|
||||||
- type: "sol-generate-wallet"
|
- type: "sol-generate-wallet"
|
||||||
- type: "sol-get-wallet-address"
|
- type: "sol-get-wallet-address"
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
name: transfer-token
|
name: transfer-token
|
||||||
|
description: |-
|
||||||
|
Transfer SPL tokens held on the Solana blockchain.
|
||||||
inputs:
|
inputs:
|
||||||
- from_address
|
- name: from_address
|
||||||
- to_address
|
description: >-
|
||||||
- token_name
|
The address from which to send tokens.
|
||||||
- token_amount
|
- 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:
|
step:
|
||||||
- type: sol-get-token-info
|
- type: sol-get-token-info
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
name: "transfer"
|
name: "transfer"
|
||||||
|
description: |-
|
||||||
|
Transfer SOL from one address to another.
|
||||||
inputs:
|
inputs:
|
||||||
- "to_address"
|
- name: "to_address"
|
||||||
- "from_address"
|
description: >-
|
||||||
- "amount"
|
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:
|
step:
|
||||||
- type: "internal-load-file"
|
- type: "internal-load-file"
|
||||||
values:
|
values:
|
||||||
|
|
|
@ -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
|
|
@ -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"] }
|
|
@ -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
|
@ -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
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"] }
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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],
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
[[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"
|
|
@ -1,7 +0,0 @@
|
||||||
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