Compare commits
15 Commits
anton/refa
...
main
Author | SHA1 | Date |
---|---|---|
|
1b5d168647 | |
|
47f79aa62b | |
|
adf1e68006 | |
|
f8f33a72ed | |
|
d3165929fa | |
|
3d14a34321 | |
|
719f695d3a | |
|
e12d655245 | |
|
08d11019c1 | |
|
3712fc2b51 | |
|
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()
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ impl Bech32Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_similar_prefix(prefix: &'static str) -> Self {
|
fn with_similar_prefix(prefix: &'static str) -> Self {
|
||||||
|
#[allow(clippy::useless_format)]
|
||||||
Self {
|
Self {
|
||||||
account_address_prefix: format!("{prefix}"),
|
account_address_prefix: format!("{prefix}"),
|
||||||
account_address_public_prefix: format!("{prefix}pub"),
|
account_address_public_prefix: format!("{prefix}pub"),
|
||||||
|
@ -167,7 +168,7 @@ fn seda_chains() -> Vec<Blockchain> {
|
||||||
let mut chains = vec![];
|
let mut chains = vec![];
|
||||||
|
|
||||||
let aseda = Currency::builder()
|
let aseda = Currency::builder()
|
||||||
.coin_denom("seda")
|
.coin_denom("SEDA")
|
||||||
.coin_minimal_denom("aseda")
|
.coin_minimal_denom("aseda")
|
||||||
.coin_decimals(18)
|
.coin_decimals(18)
|
||||||
.coin_gecko_id("ID")
|
.coin_gecko_id("ID")
|
||||||
|
@ -194,8 +195,26 @@ fn seda_chains() -> Vec<Blockchain> {
|
||||||
.fee_currencies(&[CurrencyWithGas::builder()
|
.fee_currencies(&[CurrencyWithGas::builder()
|
||||||
.currency(aseda.clone())
|
.currency(aseda.clone())
|
||||||
.gas_price_step(aseda_gas.clone()).build()])
|
.gas_price_step(aseda_gas.clone()).build()])
|
||||||
.gas_price_step(aseda_gas)
|
.gas_price_step(aseda_gas.clone())
|
||||||
.stake_currency(aseda)
|
.stake_currency(aseda.clone())
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
chains.push(
|
||||||
|
Blockchain::builder()
|
||||||
|
.chain_id("seda-1")
|
||||||
|
.chain_name("seda")
|
||||||
|
.rpc_url("https://rpc.seda.xyz")
|
||||||
|
.rest_url("https://lcd.seda.xyz")
|
||||||
|
.explorer_url_format("https://explorer.seda.xyz/txs/%s")
|
||||||
|
.bip44_config(Bip44Config::builder().coin_type(118).build())
|
||||||
|
.bech32_config(Bech32Config::with_similar_prefix("seda"))
|
||||||
|
.currencies(&[aseda.clone()])
|
||||||
|
.fee_currencies(&[CurrencyWithGas::builder()
|
||||||
|
.currency(aseda.clone())
|
||||||
|
.gas_price_step(aseda_gas.clone()).build()])
|
||||||
|
.gas_price_step(aseda_gas.clone())
|
||||||
|
.stake_currency(aseda.clone())
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -217,6 +236,18 @@ fn kyve_chains() -> Vec<Blockchain> {
|
||||||
.high(0.03)
|
.high(0.03)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let ukyve = Currency::builder()
|
||||||
|
.coin_denom("KYVE")
|
||||||
|
.coin_minimal_denom("ukyve")
|
||||||
|
.coin_decimals(6)
|
||||||
|
.coin_gecko_id("unknown")
|
||||||
|
.build();
|
||||||
|
let ukyve_gas = GasPriceStep::builder()
|
||||||
|
.low(0.01)
|
||||||
|
.average(0.025)
|
||||||
|
.high(0.03)
|
||||||
|
.build();
|
||||||
|
|
||||||
chains.push(
|
chains.push(
|
||||||
Blockchain::builder()
|
Blockchain::builder()
|
||||||
.chain_id("korellia-2")
|
.chain_id("korellia-2")
|
||||||
|
@ -236,6 +267,44 @@ fn kyve_chains() -> Vec<Blockchain> {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
chains.push(
|
||||||
|
Blockchain::builder()
|
||||||
|
.chain_id("kaon-1")
|
||||||
|
.chain_name("kaon")
|
||||||
|
.rpc_url("https://rpc.kaon.kyve.network")
|
||||||
|
.rest_url("https://api.kaon.kyve.network")
|
||||||
|
.explorer_url_format("https://explorer.kyve.network/kaon/tx/%s")
|
||||||
|
.bip44_config(Bip44Config::builder().coin_type(118).build())
|
||||||
|
.bech32_config(Bech32Config::with_similar_prefix("kyve"))
|
||||||
|
.currencies(&[tkyve.clone()])
|
||||||
|
.fee_currencies(&[CurrencyWithGas::builder()
|
||||||
|
.currency(tkyve.clone())
|
||||||
|
.gas_price_step(tkyve_gas.clone())
|
||||||
|
.build()])
|
||||||
|
.gas_price_step(tkyve_gas.clone())
|
||||||
|
.stake_currency(tkyve.clone())
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
chains.push(
|
||||||
|
Blockchain::builder()
|
||||||
|
.chain_id("kyve-1")
|
||||||
|
.chain_name("kyve")
|
||||||
|
.rpc_url("https://rpc.kyve.network")
|
||||||
|
.rest_url("https://api.kyve.network")
|
||||||
|
.explorer_url_format("https://explorer.kyve.network/kyve/tx/%s")
|
||||||
|
.bip44_config(Bip44Config::builder().coin_type(118).build())
|
||||||
|
.bech32_config(Bech32Config::with_similar_prefix("kyve"))
|
||||||
|
.currencies(&[ukyve.clone()])
|
||||||
|
.fee_currencies(&[CurrencyWithGas::builder()
|
||||||
|
.currency(ukyve.clone())
|
||||||
|
.gas_price_step(ukyve_gas.clone())
|
||||||
|
.build()])
|
||||||
|
.gas_price_step(ukyve_gas.clone())
|
||||||
|
.stake_currency(ukyve.clone())
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
chains
|
chains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,8 +175,6 @@ pub struct GetTokenInfo {
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct CreateNonceAccountAndSigningKey {
|
pub struct CreateNonceAccountAndSigningKey {
|
||||||
authorization_address: String,
|
authorization_address: String,
|
||||||
from_account: Option<String>,
|
|
||||||
from_address: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -226,7 +224,7 @@ pub struct TransferToken {
|
||||||
pub struct Compile {
|
pub struct Compile {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
hashable: Hashable,
|
hashable: Hashable,
|
||||||
derivation_accounts: Vec<u32>,
|
derivation_accounts: Option<Vec<u32>>,
|
||||||
instructions: Vec<solana_sdk::instruction::Instruction>,
|
instructions: Vec<solana_sdk::instruction::Instruction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,9 +236,11 @@ pub struct Inspect {
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Sign {
|
pub struct Sign {
|
||||||
blockhash: String,
|
blockhash: String,
|
||||||
transaction: solana_sdk::transaction::Transaction,
|
instructions: Vec<solana_sdk::instruction::Instruction>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
signing_keys: Vec<[u8; Keypair::SECRET_KEY_LENGTH]>,
|
signing_keys: Vec<[u8; Keypair::SECRET_KEY_LENGTH]>,
|
||||||
|
#[serde(default)]
|
||||||
|
payer_address: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -669,8 +669,6 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
Operation::CreateNonceAccountAndSigningKey(CreateNonceAccountAndSigningKey {
|
Operation::CreateNonceAccountAndSigningKey(CreateNonceAccountAndSigningKey {
|
||||||
authorization_address,
|
authorization_address,
|
||||||
from_account,
|
|
||||||
from_address,
|
|
||||||
}) => {
|
}) => {
|
||||||
// NOTE: Since this transaction is meant to be run on an online system with a
|
// NOTE: Since this transaction is meant to be run on an online system with a
|
||||||
// freshly generated mnemonic, only designed to live to make the nonce account, we
|
// freshly generated mnemonic, only designed to live to make the nonce account, we
|
||||||
|
@ -683,16 +681,12 @@ impl Module for Solana {
|
||||||
// this uses OsRng, which sources from getrandom() if available, which pulls from
|
// this uses OsRng, which sources from getrandom() if available, which pulls from
|
||||||
// /dev/urandom, or sources from `/dev/urandom` directly.
|
// /dev/urandom, or sources from `/dev/urandom` directly.
|
||||||
let keypair = Keypair::new();
|
let keypair = Keypair::new();
|
||||||
|
let payer_keypair = Keypair::new();
|
||||||
|
|
||||||
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
|
||||||
let authorization_pk = Pubkey::from_str(&authorization_address).unwrap();
|
let authorization_pk = Pubkey::from_str(&authorization_address).unwrap();
|
||||||
|
|
||||||
if from_account.is_some() {
|
|
||||||
unimplemented!("alternative derivation accounts are not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
let instructions = system_instruction::create_nonce_account(
|
let instructions = system_instruction::create_nonce_account(
|
||||||
&from_pk,
|
&payer_keypair.pubkey(),
|
||||||
&keypair.pubkey(),
|
&keypair.pubkey(),
|
||||||
&authorization_pk,
|
&authorization_pk,
|
||||||
// just above the approximate rent necessary for a nonce account
|
// just above the approximate rent necessary for a nonce account
|
||||||
|
@ -704,8 +698,13 @@ impl Module for Solana {
|
||||||
"blob": {
|
"blob": {
|
||||||
"nonce_pubkey": keypair.pubkey().to_string(),
|
"nonce_pubkey": keypair.pubkey().to_string(),
|
||||||
"nonce_privkey": [keypair.secret().to_bytes()],
|
"nonce_privkey": [keypair.secret().to_bytes()],
|
||||||
|
"payer_pubkey": payer_keypair.pubkey().to_string(),
|
||||||
|
"payer_privkey": [payer_keypair.secret().to_bytes()],
|
||||||
|
"privkeys": [
|
||||||
|
keypair.secret().to_bytes(),
|
||||||
|
payer_keypair.secret().to_bytes()
|
||||||
|
],
|
||||||
"transaction": instructions,
|
"transaction": instructions,
|
||||||
"derivation_accounts": [0u32 | 1 << 31],
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -987,9 +986,9 @@ impl Module for Solana {
|
||||||
derivation_accounts,
|
derivation_accounts,
|
||||||
mut instructions,
|
mut instructions,
|
||||||
}) => {
|
}) => {
|
||||||
use solana_sdk::{hash::Hash, message::Message, transaction::Transaction};
|
use solana_sdk::hash::Hash;
|
||||||
|
|
||||||
let (hash, transaction) = match hashable {
|
let hash = match hashable {
|
||||||
// We already have the account from GetNonceAccountData,
|
// We already have the account from GetNonceAccountData,
|
||||||
// which also gives us the authority and the nonce itself.
|
// which also gives us the authority and the nonce itself.
|
||||||
Hashable::Nonce {
|
Hashable::Nonce {
|
||||||
|
@ -1005,23 +1004,16 @@ impl Module for Solana {
|
||||||
system_instruction::advance_nonce_account(&account_pk, &authority_pk);
|
system_instruction::advance_nonce_account(&account_pk, &authority_pk);
|
||||||
|
|
||||||
instructions.insert(0, increment_nonce);
|
instructions.insert(0, increment_nonce);
|
||||||
let message = Message::new(&instructions, None);
|
hash
|
||||||
let transaction = Transaction::new_unsigned(message);
|
|
||||||
(hash, transaction)
|
|
||||||
}
|
|
||||||
Hashable::Blockhash { blockhash } => {
|
|
||||||
let blockhash = Hash::from_str(&blockhash).unwrap();
|
|
||||||
let message = Message::new(&instructions, None);
|
|
||||||
let transaction = Transaction::new_unsigned(message);
|
|
||||||
(blockhash, transaction)
|
|
||||||
}
|
}
|
||||||
|
Hashable::Blockhash { blockhash } => Hash::from_str(&blockhash).unwrap(),
|
||||||
};
|
};
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": {
|
"blob": {
|
||||||
"hash": hash,
|
"hash": hash,
|
||||||
"transaction": transaction,
|
"instructions": instructions,
|
||||||
},
|
},
|
||||||
"derivation_accounts": derivation_accounts,
|
"derivation_accounts": derivation_accounts.as_deref().unwrap_or(&[]),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Operation::Inspect(Inspect { transaction }) => {
|
Operation::Inspect(Inspect { transaction }) => {
|
||||||
|
@ -1035,9 +1027,12 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
Operation::Sign(Sign {
|
Operation::Sign(Sign {
|
||||||
blockhash,
|
blockhash,
|
||||||
mut transaction,
|
instructions,
|
||||||
signing_keys,
|
signing_keys,
|
||||||
|
payer_address,
|
||||||
}) => {
|
}) => {
|
||||||
|
use solana_sdk::{message::Message, transaction::Transaction};
|
||||||
|
|
||||||
let keys = request
|
let keys = request
|
||||||
.derived_keys
|
.derived_keys
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
@ -1046,10 +1041,21 @@ impl Module for Solana {
|
||||||
.map(|k| Self::keypair_from_bytes(*k))
|
.map(|k| Self::keypair_from_bytes(*k))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let payer_pk = payer_address
|
||||||
|
.as_deref()
|
||||||
|
.map(Pubkey::from_str)
|
||||||
|
.transpose()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let message =
|
||||||
|
Message::new(&instructions, Some(&payer_pk.unwrap_or(keys[0].pubkey())));
|
||||||
|
let mut transaction = Transaction::new_unsigned(message);
|
||||||
|
|
||||||
let hash = solana_sdk::hash::Hash::from_str(&blockhash).unwrap();
|
let hash = solana_sdk::hash::Hash::from_str(&blockhash).unwrap();
|
||||||
transaction
|
transaction
|
||||||
.try_sign(&keys, hash)
|
.try_sign(&keys, hash)
|
||||||
.expect("not enough keys provided");
|
.expect("not enough keys provided");
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": {
|
"blob": {
|
||||||
"transaction": transaction,
|
"transaction": transaction,
|
||||||
|
@ -1065,7 +1071,15 @@ impl Module for Solana {
|
||||||
|
|
||||||
transaction.verify().expect("invalid signatures");
|
transaction.verify().expect("invalid signatures");
|
||||||
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
||||||
let _simulated_response = client.simulate_transaction(&transaction).unwrap();
|
let simulated_response = client.simulate_transaction(&transaction).unwrap();
|
||||||
|
if let Some(err) = simulated_response.value.err {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"status": "simulate_transaction",
|
||||||
|
"error": err.to_string(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
let response = client.send_and_confirm_transaction(&transaction);
|
let response = client.send_and_confirm_transaction(&transaction);
|
||||||
let cluster_suffix = {
|
let cluster_suffix = {
|
||||||
if cluster == Cluster::MainnetBeta {
|
if cluster == Cluster::MainnetBeta {
|
||||||
|
|
|
@ -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,30 @@ 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);
|
||||||
|
// 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() {
|
||||||
|
let name = &input.name;
|
||||||
|
let arg = clap::Arg::new(name)
|
||||||
|
.required(false)
|
||||||
|
.help(&input.description)
|
||||||
|
.long(name.replace('_', "-"))
|
||||||
|
.value_name(name.to_uppercase())
|
||||||
|
.visible_aliases(&input.aliases);
|
||||||
|
command = command.arg(arg);
|
||||||
|
}
|
||||||
|
command.arg(
|
||||||
|
clap::arg!(
|
||||||
--"input-file" [FILE]
|
--"input-file" [FILE]
|
||||||
"A file containing any inputs not passed on the command line"
|
"A file containing any inputs not passed on the command line"
|
||||||
));
|
)
|
||||||
for input in &workflow.inputs {
|
.value_parser(clap::value_parser!(std::path::PathBuf)),
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
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<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 +123,26 @@ 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 matches.get_one::<String>(identifier) {
|
||||||
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())) {
|
for aliasable_identifier in input.identifiers() {
|
||||||
map.insert(input.into(), value.clone());
|
if let Some(value) = input_file
|
||||||
continue;
|
.as_ref()
|
||||||
|
.and_then(|f| f.get(aliasable_identifier))
|
||||||
|
{
|
||||||
|
map.insert(identifier.clone(), value.clone());
|
||||||
|
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 +180,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 +199,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 +239,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:
|
||||||
|
@ -34,4 +40,4 @@ step:
|
||||||
status: "status"
|
status: "status"
|
||||||
url: "url"
|
url: "url"
|
||||||
error: "error"
|
error: "error"
|
||||||
error_code: "error_code"
|
error_code: "error_code"
|
||||||
|
|
|
@ -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,27 @@
|
||||||
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.
|
||||||
|
- 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:
|
||||||
|
@ -40,4 +57,4 @@ step:
|
||||||
values:
|
values:
|
||||||
filename: "transaction.json"
|
filename: "transaction.json"
|
||||||
inputs:
|
inputs:
|
||||||
transaction: "signed_transaction"
|
transaction: "signed_transaction"
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -29,4 +37,4 @@ step:
|
||||||
outputs:
|
outputs:
|
||||||
status: "status"
|
status: "status"
|
||||||
url: "url"
|
url: "url"
|
||||||
error: "error"
|
error: "error"
|
||||||
|
|
|
@ -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,18 +1,26 @@
|
||||||
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.
|
||||||
|
aliases:
|
||||||
|
- address
|
||||||
|
- primary_address
|
||||||
|
- principal_address
|
||||||
|
- pubkey
|
||||||
step:
|
step:
|
||||||
- type: "sol-generate-wallet"
|
|
||||||
- type: "sol-get-wallet-address"
|
|
||||||
outputs:
|
|
||||||
pubkey: "wallet_pubkey"
|
|
||||||
- type: "sol-await-funds"
|
|
||||||
inputs:
|
|
||||||
address: "wallet_pubkey"
|
|
||||||
cluster: "cluster"
|
|
||||||
values:
|
|
||||||
lamports: "1510000"
|
|
||||||
- type: "sol-get-blockhash"
|
- type: "sol-get-blockhash"
|
||||||
inputs:
|
inputs:
|
||||||
cluster: "cluster"
|
cluster: "cluster"
|
||||||
|
@ -20,25 +28,30 @@ step:
|
||||||
blockhash: "blockhash"
|
blockhash: "blockhash"
|
||||||
- type: "sol-create-nonce-account-and-signing-key"
|
- type: "sol-create-nonce-account-and-signing-key"
|
||||||
inputs:
|
inputs:
|
||||||
from_address: "wallet_pubkey"
|
|
||||||
authorization_address: "authorization_address"
|
authorization_address: "authorization_address"
|
||||||
outputs:
|
outputs:
|
||||||
transaction: "instructions"
|
transaction: "instructions"
|
||||||
nonce_pubkey: "nonce_pubkey"
|
nonce_pubkey: "nonce_pubkey"
|
||||||
nonce_privkey: "private_keys"
|
payer_pubkey: "payer_pubkey"
|
||||||
derivation_accounts: "derivation_accounts"
|
privkeys: "private_keys"
|
||||||
|
- type: "sol-await-funds"
|
||||||
|
inputs:
|
||||||
|
address: "payer_pubkey"
|
||||||
|
cluster: "cluster"
|
||||||
|
values:
|
||||||
|
lamports: "1510000"
|
||||||
- type: "sol-compile"
|
- type: "sol-compile"
|
||||||
inputs:
|
inputs:
|
||||||
instructions: "instructions"
|
instructions: "instructions"
|
||||||
derivation_accounts: "derivation_accounts"
|
derivation_accounts: "derivation_accounts"
|
||||||
blockhash: "blockhash"
|
blockhash: "blockhash"
|
||||||
outputs:
|
outputs:
|
||||||
transaction: "unsigned_transaction"
|
instructions: "nonced_instructions"
|
||||||
- type: "sol-sign"
|
- type: "sol-sign"
|
||||||
inputs:
|
inputs:
|
||||||
blockhash: "blockhash"
|
blockhash: "blockhash"
|
||||||
signing_keys: "private_keys"
|
signing_keys: "private_keys"
|
||||||
transaction: "unsigned_transaction"
|
instructions: "nonced_instructions"
|
||||||
outputs:
|
outputs:
|
||||||
transaction: "signed_transaction"
|
transaction: "signed_transaction"
|
||||||
- type: "sol-broadcast"
|
- type: "sol-broadcast"
|
||||||
|
@ -53,10 +66,10 @@ step:
|
||||||
inputs:
|
inputs:
|
||||||
status: "status"
|
status: "status"
|
||||||
url: "url"
|
url: "url"
|
||||||
nonce_account: "nonce_pubkey"
|
nonce_address: "nonce_pubkey"
|
||||||
error: "error"
|
error: "error"
|
||||||
outputs:
|
outputs:
|
||||||
status: "status"
|
status: "status"
|
||||||
url: "url"
|
url: "url"
|
||||||
nonce_account: "nonce_account"
|
nonce_address: "nonce_address"
|
||||||
error: "error"
|
error: "error"
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -36,10 +46,10 @@ step:
|
||||||
nonce_authority: nonce_authority
|
nonce_authority: nonce_authority
|
||||||
nonce_data: nonce_data
|
nonce_data: nonce_data
|
||||||
outputs:
|
outputs:
|
||||||
transaction: unsigned_transaction
|
instructions: nonced_instructions
|
||||||
- type: sol-sign
|
- type: sol-sign
|
||||||
inputs:
|
inputs:
|
||||||
transaction: unsigned_transaction
|
instructions: nonced_instructions
|
||||||
blockhash: nonce_data
|
blockhash: nonce_data
|
||||||
outputs:
|
outputs:
|
||||||
transaction: transaction
|
transaction: transaction
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -27,15 +35,15 @@ step:
|
||||||
nonce_authority: "nonce_authority"
|
nonce_authority: "nonce_authority"
|
||||||
nonce_data: "nonce_data"
|
nonce_data: "nonce_data"
|
||||||
outputs:
|
outputs:
|
||||||
transaction: "unsigned_transaction"
|
instructions: "nonced_instructions"
|
||||||
- type: "sol-sign"
|
- type: "sol-sign"
|
||||||
inputs:
|
inputs:
|
||||||
blockhash: "nonce_data"
|
blockhash: "nonce_data"
|
||||||
transaction: "unsigned_transaction"
|
instructions: "nonced_instructions"
|
||||||
outputs:
|
outputs:
|
||||||
transaction: "signed_transaction"
|
transaction: "signed_transaction"
|
||||||
- type: "internal-save-file"
|
- type: "internal-save-file"
|
||||||
values:
|
values:
|
||||||
filename: "transaction.json"
|
filename: "transaction.json"
|
||||||
inputs:
|
inputs:
|
||||||
transaction: "signed_transaction"
|
transaction: "signed_transaction"
|
||||||
|
|
|
@ -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
|
|
@ -61,6 +61,26 @@ pub enum BaseError {
|
||||||
/// The JSON object is not a valid value.
|
/// The JSON object is not a valid value.
|
||||||
#[error("the JSON object is not a valid value")]
|
#[error("the JSON object is not a valid value")]
|
||||||
InvalidJSONValue,
|
InvalidJSONValue,
|
||||||
|
|
||||||
|
/// No signing key was found on smartcard.
|
||||||
|
#[error("no signing key was found on smartcard")]
|
||||||
|
NoSigningKey,
|
||||||
|
|
||||||
|
/// A signature exists for the current smartcard.
|
||||||
|
#[error("a signature exists for the key on the current smartcard: {0}")]
|
||||||
|
ConflictingSignature(openpgp::Fingerprint),
|
||||||
|
|
||||||
|
/// A bad packet type was encountered.
|
||||||
|
#[error("a bad OpenPGP packet was encountered: {0}")]
|
||||||
|
BadOpenPGPPacket(openpgp::packet::Tag),
|
||||||
|
|
||||||
|
/// A signature could not have been added; a smartcard might not have been pluggedi n.
|
||||||
|
#[error("a signature could not be added")]
|
||||||
|
NoSignatureAdded,
|
||||||
|
|
||||||
|
/// The signature matched a key that was already used to verify another signature.
|
||||||
|
#[error("signature {1} matched key {0} previously used to sign signature {2}")]
|
||||||
|
DuplicateSignature(openpgp::Fingerprint, usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BaseError {
|
impl BaseError {
|
||||||
|
@ -151,7 +171,11 @@ impl PayloadVerification {
|
||||||
|
|
||||||
/// Set a threshold for required signatures.
|
/// Set a threshold for required signatures.
|
||||||
pub fn with_threshold(self, threshold: u8) -> Self {
|
pub fn with_threshold(self, threshold: u8) -> Self {
|
||||||
Self { one_each: false, threshold, ..self }
|
Self {
|
||||||
|
one_each: false,
|
||||||
|
threshold,
|
||||||
|
..self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Require a single valid signature; other signatures may be invalid.
|
/// Require a single valid signature; other signatures may be invalid.
|
||||||
|
@ -259,6 +283,12 @@ impl Payload {
|
||||||
///
|
///
|
||||||
/// The method may error if a signature could not be created.
|
/// The method may error if a signature could not be created.
|
||||||
pub fn add_signature(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn add_signature(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let signatures = self
|
||||||
|
.signatures
|
||||||
|
.iter()
|
||||||
|
.map(|signature_text| Packet::from_bytes(signature_text.as_bytes()).map_err(Into::into))
|
||||||
|
.collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;
|
||||||
|
|
||||||
let unhashed = unhashed(serde_json::to_value(&self)?)?;
|
let unhashed = unhashed(serde_json::to_value(&self)?)?;
|
||||||
let builder =
|
let builder =
|
||||||
SignatureBuilder::new(SignatureType::Binary).set_hash_algo(HashAlgorithm::SHA512);
|
SignatureBuilder::new(SignatureType::Binary).set_hash_algo(HashAlgorithm::SHA512);
|
||||||
|
@ -268,10 +298,26 @@ impl Payload {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut has_signed_any = false;
|
||||||
for backend in card_backend_pcsc::PcscBackend::cards(None)? {
|
for backend in card_backend_pcsc::PcscBackend::cards(None)? {
|
||||||
let mut card = Card::<Open>::new(backend?)?;
|
let mut card = Card::<Open>::new(backend?)?;
|
||||||
let mut transaction = card.transaction()?;
|
let mut transaction = card.transaction()?;
|
||||||
|
|
||||||
|
let key_fps = transaction.fingerprints()?;
|
||||||
|
let signing_key_fp = key_fps.signature().ok_or(BaseError::NoSigningKey)?;
|
||||||
|
|
||||||
|
for packet in &signatures {
|
||||||
|
let Packet::Signature(signature) = packet else {
|
||||||
|
return Err(BaseError::BadOpenPGPPacket(packet.tag()).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
for issuer_fp in signature.issuer_fingerprints() {
|
||||||
|
if issuer_fp.as_bytes() == signing_key_fp.as_bytes() {
|
||||||
|
return Err(BaseError::ConflictingSignature(issuer_fp.clone()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cardholder_name = format_name(transaction.cardholder_name()?);
|
let cardholder_name = format_name(transaction.cardholder_name()?);
|
||||||
let card_id = transaction.application_identifier()?.ident();
|
let card_id = transaction.application_identifier()?.ident();
|
||||||
let mut pin = None;
|
let mut pin = None;
|
||||||
|
@ -329,9 +375,14 @@ impl Payload {
|
||||||
writer.finalize()?;
|
writer.finalize()?;
|
||||||
|
|
||||||
self.signatures.push(String::from_utf8(armored_signature)?);
|
self.signatures.push(String::from_utf8(armored_signature)?);
|
||||||
|
has_signed_any = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
if has_signed_any {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(BaseError::NoSignatureAdded.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the keychain and certificates using either a Key ID or an OpenPGP card.
|
/// Verify the keychain and certificates using either a Key ID or an OpenPGP card.
|
||||||
|
@ -370,13 +421,23 @@ impl Payload {
|
||||||
threshold = certs.len() as u8;
|
threshold = certs.len() as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
for signature in &self.signatures {
|
let mut seen = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for (index, signature) in self.signatures.iter().enumerate() {
|
||||||
|
dbg!(&index);
|
||||||
let packet = Packet::from_bytes(signature.as_bytes())?;
|
let packet = Packet::from_bytes(signature.as_bytes())?;
|
||||||
let Packet::Signature(signature) = packet else {
|
let Packet::Signature(signature) = packet else {
|
||||||
panic!("bad packet found: {}", packet.tag());
|
panic!("bad packet found: {}", packet.tag());
|
||||||
};
|
};
|
||||||
let mut signature_matched = false;
|
let mut signature_matched = false;
|
||||||
for issuer in signature.get_issuers() {
|
// NOTE: It is allowable, by the specification, to have a packet that doesn't include
|
||||||
|
// an issuer fingerprint, but instead just a key ID. However, filtering by both key ID
|
||||||
|
// and by fingerprint triggers the "duplicate signature" mechanism. For that reason, we
|
||||||
|
// are only going to filter over fingerprints.
|
||||||
|
//
|
||||||
|
// Any program that makes these signatures should be using fingerprints.
|
||||||
|
for issuer in signature.issuer_fingerprints() {
|
||||||
|
let mut currently_seen = std::collections::HashMap::new();
|
||||||
for cert in &certs {
|
for cert in &certs {
|
||||||
match cert
|
match cert
|
||||||
.with_policy(&policy, None)?
|
.with_policy(&policy, None)?
|
||||||
|
@ -387,13 +448,29 @@ impl Payload {
|
||||||
.next()
|
.next()
|
||||||
.map(|signing_key| signature.verify_hash(&signing_key, hashed.clone()))
|
.map(|signing_key| signature.verify_hash(&signing_key, hashed.clone()))
|
||||||
{
|
{
|
||||||
Some(Ok(())) => {
|
Some(result) => {
|
||||||
// key found, signature matched
|
// matching key found, check for duplicates
|
||||||
signature_matched = true;
|
if let Some(seen_index) = seen.get(&cert.fingerprint()) {
|
||||||
}
|
return Err(BaseError::DuplicateSignature(
|
||||||
Some(Err(e)) => {
|
cert.fingerprint(),
|
||||||
if error_on_invalid {
|
index,
|
||||||
return Err(e)?;
|
*seen_index,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
signature_matched = true;
|
||||||
|
|
||||||
|
// mark the cert as seen, so it isn't reusable
|
||||||
|
currently_seen.insert(cert.fingerprint(), index);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if error_on_invalid {
|
||||||
|
return Err(e)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -401,6 +478,7 @@ impl Payload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
seen.extend(currently_seen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if signature_matched {
|
if signature_matched {
|
||||||
|
|
|
@ -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