Compare commits
No commits in common. "main" and "merge-blob-and-values" have entirely different histories.
main
...
merge-blob
File diff suppressed because it is too large
Load Diff
|
@ -4,8 +4,7 @@ resolver = "2"
|
|||
members = [
|
||||
"crates/icepick",
|
||||
"crates/icepick-module",
|
||||
"crates/builtins/icepick-internal",
|
||||
"crates/by-chain/icepick-solana", "crates/by-chain/icepick-cosmos",
|
||||
"crates/by-chain/icepick-solana",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "icepick-internal"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror = "2.0.9"
|
|
@ -1,121 +0,0 @@
|
|||
use icepick_module::{
|
||||
help::{Argument, ArgumentType},
|
||||
Module,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn path_for_filename(filename: &Path) -> PathBuf {
|
||||
PathBuf::from(
|
||||
std::env::vars()
|
||||
.find(|(k, _)| k == "ICEPICK_DATA_DIRECTORY")
|
||||
.map(|(_, v)| v)
|
||||
.as_deref()
|
||||
.unwrap_or("/media/external"),
|
||||
)
|
||||
.join(filename)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "operation", content = "values", rename_all = "kebab-case")]
|
||||
pub enum Request {
|
||||
LoadFile {
|
||||
filename: PathBuf,
|
||||
},
|
||||
|
||||
SaveFile {
|
||||
filename: PathBuf,
|
||||
|
||||
#[serde(flatten)]
|
||||
values: serde_json::Value,
|
||||
},
|
||||
|
||||
Cat {
|
||||
#[serde(flatten)]
|
||||
values: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {}
|
||||
|
||||
pub struct Internal;
|
||||
|
||||
impl Module for Internal {
|
||||
type Error = Error;
|
||||
|
||||
type Request = Request;
|
||||
|
||||
fn describe_operations() -> Vec<icepick_module::help::Operation> {
|
||||
let filename = Argument {
|
||||
name: "filename".to_string(),
|
||||
description: "The file to load or save data to.".to_string(),
|
||||
r#type: ArgumentType::Required,
|
||||
};
|
||||
vec![
|
||||
icepick_module::help::Operation {
|
||||
name: "load-file".to_string(),
|
||||
description: "Load data from a JSON file.".to_string(),
|
||||
arguments: vec![filename.clone()],
|
||||
},
|
||||
icepick_module::help::Operation {
|
||||
name: "save-file".to_string(),
|
||||
description: "Save data from a JSON file.".to_string(),
|
||||
arguments: vec![filename.clone()],
|
||||
},
|
||||
icepick_module::help::Operation {
|
||||
name: "cat".to_string(),
|
||||
description: "Return all inputs. Usable in workflows to sum up all desired outputs"
|
||||
.to_string(),
|
||||
arguments: vec![],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
|
||||
match request {
|
||||
Request::LoadFile { filename } => {
|
||||
let path = path_for_filename(&filename);
|
||||
|
||||
let mut attempt = 0;
|
||||
while !std::fs::exists(&path).is_ok_and(|v| v) {
|
||||
if attempt % 10 == 0 {
|
||||
eprintln!(
|
||||
"Waiting for {path} to be populated...",
|
||||
path = path.to_string_lossy()
|
||||
);
|
||||
}
|
||||
attempt += 1;
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
|
||||
// if we ran at least once, we should have previously printed a message. write a
|
||||
// confirmation that we are no longer waiting. if we haven't, we've never printed
|
||||
// a message, therefore we don't need to confirm the prior reading.
|
||||
if attempt > 0 {
|
||||
eprintln!("File contents loaded.");
|
||||
}
|
||||
|
||||
let file = std::fs::File::open(path).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_reader(file).unwrap();
|
||||
Ok(serde_json::json!({
|
||||
"blob": json,
|
||||
}))
|
||||
}
|
||||
Request::SaveFile { filename, values } => {
|
||||
let path = path_for_filename(&filename);
|
||||
let file = std::fs::File::create(path).unwrap();
|
||||
serde_json::to_writer(file, &values).unwrap();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"blob": {},
|
||||
}))
|
||||
}
|
||||
Request::Cat { values } => {
|
||||
Ok(serde_json::json!({
|
||||
"blob": values,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
use icepick_module::Module;
|
||||
use icepick_internal::Internal;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Internal::run_responder()
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "icepick-cosmos"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bon = "3.3.2"
|
||||
cosmrs = { version = "0.21.0", features = ["rpc", "tokio"] }
|
||||
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror = "2.0.9"
|
||||
tokio = { version = "1.43.0", features = ["rt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmrs = { version = "0.21.0", features = ["dev"] }
|
|
@ -1,198 +0,0 @@
|
|||
use bon::{bon, Builder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Bip44Config {
|
||||
pub coin_type: u32,
|
||||
}
|
||||
|
||||
// NOTE: Are `public` variants used?
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Bech32Config {
|
||||
#[serde(rename = "bech32PrefixAccAddress")]
|
||||
pub account_address_prefix: String,
|
||||
|
||||
#[serde(rename = "bech32PrefixAccPub")]
|
||||
pub account_address_public_prefix: String,
|
||||
|
||||
#[serde(rename = "bech32PrefixValOper")]
|
||||
pub validator_operator_prefix: String,
|
||||
|
||||
#[serde(rename = "bech32PrefixValPub")]
|
||||
pub validator_operator_public_prefix: String,
|
||||
|
||||
#[serde(rename = "bech32PrefixConsAddr")]
|
||||
pub consensus_node_prefix: String,
|
||||
|
||||
#[serde(rename = "bech32PrefixConsPub")]
|
||||
pub consensus_node_public_prefix: String,
|
||||
}
|
||||
|
||||
#[bon]
|
||||
impl Bech32Config {
|
||||
#[builder]
|
||||
fn new(
|
||||
account_address_prefix: &'static str,
|
||||
account_address_public_prefix: &'static str,
|
||||
validator_operator_prefix: &'static str,
|
||||
validator_operator_public_prefix: &'static str,
|
||||
consensus_node_prefix: &'static str,
|
||||
consensus_node_public_prefix: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
account_address_prefix: account_address_prefix.to_string(),
|
||||
account_address_public_prefix: account_address_public_prefix.to_string(),
|
||||
validator_operator_prefix: validator_operator_prefix.to_string(),
|
||||
validator_operator_public_prefix: validator_operator_public_prefix.to_string(),
|
||||
consensus_node_prefix: consensus_node_prefix.to_string(),
|
||||
consensus_node_public_prefix: consensus_node_public_prefix.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
|
||||
pub struct GasPriceStep {
|
||||
pub low: f64,
|
||||
pub average: f64,
|
||||
pub high: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Currency {
|
||||
pub coin_denom: String,
|
||||
pub coin_minimal_denom: String,
|
||||
pub coin_decimals: u8,
|
||||
pub coin_gecko_id: String,
|
||||
}
|
||||
|
||||
#[bon]
|
||||
impl Currency {
|
||||
#[builder]
|
||||
fn new(
|
||||
coin_denom: &'static str,
|
||||
coin_minimal_denom: &'static str,
|
||||
coin_decimals: u8,
|
||||
coin_gecko_id: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
coin_denom: coin_denom.to_string(),
|
||||
coin_minimal_denom: coin_minimal_denom.to_string(),
|
||||
coin_decimals,
|
||||
coin_gecko_id: coin_gecko_id.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Builder)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CurrencyWithGas {
|
||||
#[serde(flatten)]
|
||||
pub currency: Currency,
|
||||
|
||||
pub gas_price_step: GasPriceStep,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Blockchain {
|
||||
pub chain_name: String,
|
||||
pub chain_id: String,
|
||||
|
||||
pub rpc_url: String,
|
||||
pub rest_url: String,
|
||||
|
||||
pub explorer_url_format: String,
|
||||
|
||||
#[serde(rename = "bip44")]
|
||||
pub bip44_config: Bip44Config,
|
||||
|
||||
#[serde(rename = "bech32Config")]
|
||||
pub bech32_config: Bech32Config,
|
||||
|
||||
pub currencies: Vec<Currency>,
|
||||
pub fee_currencies: Vec<CurrencyWithGas>,
|
||||
|
||||
pub gas_price_step: GasPriceStep,
|
||||
|
||||
pub stake_currency: Currency,
|
||||
}
|
||||
|
||||
#[bon]
|
||||
impl Blockchain {
|
||||
#[builder]
|
||||
fn new(
|
||||
chain_id: &'static str,
|
||||
chain_name: &'static str,
|
||||
rpc_url: &'static str,
|
||||
rest_url: &'static str,
|
||||
explorer_url_format: &'static str,
|
||||
bip44_config: Bip44Config,
|
||||
bech32_config: Bech32Config,
|
||||
currencies: &[Currency],
|
||||
fee_currencies: &[CurrencyWithGas],
|
||||
gas_price_step: GasPriceStep,
|
||||
stake_currency: Currency,
|
||||
) -> Self {
|
||||
Self {
|
||||
chain_id: chain_id.to_string(),
|
||||
chain_name: chain_name.to_string(),
|
||||
rpc_url: rpc_url.to_string(),
|
||||
rest_url: rest_url.to_string(),
|
||||
explorer_url_format: explorer_url_format.to_string(),
|
||||
bip44_config,
|
||||
bech32_config,
|
||||
currencies: currencies.to_vec(),
|
||||
fee_currencies: fee_currencies.to_vec(),
|
||||
gas_price_step,
|
||||
stake_currency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_chains() -> Vec<Blockchain> {
|
||||
let mut chains = vec![];
|
||||
|
||||
let tkyve = Currency::builder()
|
||||
.coin_denom("KYVE")
|
||||
.coin_minimal_denom("tkyve")
|
||||
.coin_decimals(6)
|
||||
.coin_gecko_id("unknown")
|
||||
.build();
|
||||
let tkyve_gas = GasPriceStep::builder()
|
||||
.low(0.01)
|
||||
.average(0.025)
|
||||
.high(0.03)
|
||||
.build();
|
||||
|
||||
chains.push(
|
||||
Blockchain::builder()
|
||||
.chain_id("korellia-2")
|
||||
.chain_name("korellia")
|
||||
.rpc_url("https://rpc.korellia.kyve.network")
|
||||
.rest_url("https://api.korellia.kyve.network")
|
||||
.explorer_url_format("https://explorer.kyve.network/korellia/tx/%s")
|
||||
.bip44_config(Bip44Config::builder().coin_type(118).build())
|
||||
.bech32_config(
|
||||
Bech32Config::builder()
|
||||
.account_address_prefix("kyve")
|
||||
.account_address_public_prefix("kyvepub")
|
||||
.validator_operator_prefix("kyvevaloper")
|
||||
.validator_operator_public_prefix("kyvevaloperpub")
|
||||
.consensus_node_prefix("kyvevalcons")
|
||||
.consensus_node_public_prefix("kyvevalconspub")
|
||||
.build(),
|
||||
)
|
||||
.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
|
||||
}
|
|
@ -1,592 +0,0 @@
|
|||
use cosmrs::{
|
||||
proto::prost::Message,
|
||||
rpc::Client,
|
||||
tx::{self, BodyBuilder, Fee, Msg, SignDoc, SignerInfo},
|
||||
AccountId, Any,
|
||||
};
|
||||
use icepick_module::Module;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
use cosmrs::crypto::secp256k1;
|
||||
|
||||
mod coin_denoms;
|
||||
mod remote_serde;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetChainInfo {
|
||||
chain_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GenerateWallet {
|
||||
account: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetWalletAddress {
|
||||
address_prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetAccountData {
|
||||
account_id: String,
|
||||
|
||||
blockchain_config: coin_denoms::Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AwaitFunds {
|
||||
address: String,
|
||||
denom_name: String,
|
||||
amount: String,
|
||||
|
||||
blockchain_config: coin_denoms::Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Transfer {
|
||||
amount: String,
|
||||
denom: String,
|
||||
to_address: String,
|
||||
from_account: Option<String>,
|
||||
from_address: String,
|
||||
// TODO: find a way to simulate transaction and calculate gas necessary
|
||||
// for now, 0.01KYVE seems to be a reasonable mainnet number?
|
||||
// for testing purposes, i'm gonna go much lower. 0.0001.
|
||||
gas_factor: Option<String>,
|
||||
|
||||
blockchain_config: coin_denoms::Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Sign {
|
||||
fee: remote_serde::Fee,
|
||||
tx_messages: Vec<Any>,
|
||||
account_number: u64,
|
||||
sequence_number: u64,
|
||||
|
||||
blockchain_config: coin_denoms::Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Broadcast {
|
||||
transaction: Vec<u8>,
|
||||
|
||||
blockchain_config: coin_denoms::Blockchain,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Request {
|
||||
derived_keys: Option<Vec<[u8; 32]>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
operation: Operation,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "operation", content = "values", rename_all = "kebab-case")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Operation {
|
||||
GetChainInfo(GetChainInfo),
|
||||
GenerateWallet(GenerateWallet),
|
||||
GetWalletAddress(GetWalletAddress),
|
||||
GetAccountData(GetAccountData),
|
||||
AwaitFunds(AwaitFunds),
|
||||
Transfer(Transfer),
|
||||
Sign(Sign),
|
||||
Broadcast(Broadcast),
|
||||
}
|
||||
|
||||
pub fn run_async<F: std::future::Future>(f: F) -> F::Output {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(f)
|
||||
}
|
||||
|
||||
fn decode<T: cosmrs::proto::prost::Message + cosmrs::proto::prost::Name + std::default::Default>(
|
||||
_type: &str,
|
||||
value: &[u8],
|
||||
) -> Result<T, String> {
|
||||
// move past the first `/`.
|
||||
let _type = &_type[1..];
|
||||
let full_name = T::full_name();
|
||||
if _type != full_name {
|
||||
return Err(format!(
|
||||
"given type {_type} does not match expected type {full_name}"
|
||||
));
|
||||
}
|
||||
|
||||
T::decode(value).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn abci_query<
|
||||
Response: cosmrs::proto::prost::Message + cosmrs::proto::prost::Name + std::default::Default,
|
||||
>(
|
||||
client: &(impl Client + Sync),
|
||||
path: &'static str,
|
||||
request_data: Option<&impl cosmrs::proto::prost::Message>,
|
||||
height: Option<cosmrs::tendermint::block::Height>,
|
||||
prove: bool,
|
||||
) -> Result<Response, Box<dyn std::error::Error>> {
|
||||
let data = request_data.map(Message::encode_to_vec).unwrap_or_default();
|
||||
let response = client
|
||||
.abci_query(Some(path.to_string()), data, height, prove)
|
||||
.await?;
|
||||
let parsed = Response::decode(&*response.value)?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub struct Cosmos;
|
||||
|
||||
impl Module for Cosmos {
|
||||
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 address_prefix = Argument::builder()
|
||||
.name("address_prefix")
|
||||
.description("Prefix for the wallet (`cosmos`, `kyve`, etc.).")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build();
|
||||
|
||||
let get_chain_info = Operation::builder()
|
||||
.name("get-chain-info")
|
||||
.description("Get information for a given chain")
|
||||
.build()
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("chain_name")
|
||||
.description("Name of the blockchain")
|
||||
.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(&address_prefix);
|
||||
|
||||
let get_account_info = Operation::builder()
|
||||
.name("get-account-data")
|
||||
.description("Get the account number and sequence number for an account.")
|
||||
.build()
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("account_id")
|
||||
.description("The account ID to get account information for.")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let await_funds = icepick_module::help::Operation {
|
||||
name: "await-funds".to_string(),
|
||||
description: "Await a minimum amount of funds in an account".to_string(),
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "address".to_string(),
|
||||
description: "The address to monitor".to_string(),
|
||||
r#type: ArgumentType::Required,
|
||||
},
|
||||
Argument {
|
||||
name: "denom_name".to_string(),
|
||||
description: "The denomination of coin to monitor".to_string(),
|
||||
r#type: ArgumentType::Required,
|
||||
},
|
||||
Argument {
|
||||
name: "amount".to_string(),
|
||||
description: "The amount of the minimum denomination to await".to_string(),
|
||||
r#type: ArgumentType::Required,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let transfer = Operation::builder()
|
||||
.name("transfer")
|
||||
.description("Transfer coins from one address to another.")
|
||||
.build()
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("from_address")
|
||||
.description("The address to send coins from.")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
)
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("to_address")
|
||||
.description("The address to send coins to.")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
)
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("amount")
|
||||
.description("The amount of coins to send.")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
)
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("denom")
|
||||
.description("The denomination of coin to send.")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
)
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("gas_factor")
|
||||
.description("The factor to multiply the default gas amount by.")
|
||||
.r#type(ArgumentType::Optional)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let sign = Operation::builder()
|
||||
.name("sign")
|
||||
.description("Sign a previously-generated transaction.")
|
||||
.build()
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("account_number")
|
||||
.description("The sequence number for the given public key")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
)
|
||||
.argument(
|
||||
&Argument::builder()
|
||||
.name("sequence_number")
|
||||
.description("The account number for the given public key")
|
||||
.r#type(ArgumentType::Required)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let broadcast = Operation::builder()
|
||||
.name("broadcast")
|
||||
.description("Broadcast a transaction.")
|
||||
.build();
|
||||
|
||||
vec![
|
||||
get_chain_info,
|
||||
generate_wallet,
|
||||
get_wallet_address,
|
||||
get_account_info,
|
||||
await_funds,
|
||||
transfer,
|
||||
sign,
|
||||
broadcast,
|
||||
]
|
||||
}
|
||||
|
||||
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
|
||||
let Request {
|
||||
operation,
|
||||
derived_keys: _,
|
||||
} = request;
|
||||
|
||||
match operation {
|
||||
Operation::GetChainInfo(GetChainInfo { chain_name }) => {
|
||||
let chains = coin_denoms::default_chains();
|
||||
|
||||
let chain = chains
|
||||
.iter()
|
||||
.find(|chain| chain.chain_name == chain_name || chain.chain_id == chain_name);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"blockchain_config": chain,
|
||||
},
|
||||
}))
|
||||
}
|
||||
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 { address_prefix }) => {
|
||||
// NOTE: panics if doesn't exist
|
||||
let key = request.derived_keys.unwrap()[0];
|
||||
let privkey = secp256k1::SigningKey::from_slice(&key).unwrap();
|
||||
let pubkey = privkey.public_key();
|
||||
let sender_account_id = pubkey.account_id(&address_prefix).unwrap();
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"pubkey": sender_account_id,
|
||||
}
|
||||
}))
|
||||
}
|
||||
Operation::GetAccountData(GetAccountData {
|
||||
account_id,
|
||||
blockchain_config,
|
||||
}) => {
|
||||
use cosmrs::proto::cosmos::auth::v1beta1::*;
|
||||
|
||||
let response = run_async(async {
|
||||
let client =
|
||||
cosmrs::rpc::HttpClient::new(blockchain_config.rpc_url.as_str()).unwrap();
|
||||
let query = QueryAccountRequest {
|
||||
address: account_id,
|
||||
};
|
||||
let response: QueryAccountResponse = abci_query(
|
||||
&client,
|
||||
"/cosmos.auth.v1beta1.Query/Account",
|
||||
Some(&query),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
response
|
||||
});
|
||||
let Any { type_url, value } = response.account.unwrap();
|
||||
let account: BaseAccount = decode(&type_url, &value).unwrap();
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"account_number": account.account_number,
|
||||
"sequence_number": account.sequence,
|
||||
}
|
||||
}))
|
||||
}
|
||||
Operation::AwaitFunds(AwaitFunds {
|
||||
address,
|
||||
denom_name,
|
||||
amount,
|
||||
blockchain_config,
|
||||
}) => {
|
||||
use cosmrs::proto::cosmos::bank::v1beta1::*;
|
||||
|
||||
// Check if given denom is min denom or normal and adjust accordingly
|
||||
let Some(relevant_denom) = blockchain_config.currencies.iter().find(|c| {
|
||||
[&c.coin_denom, &c.coin_minimal_denom]
|
||||
.iter()
|
||||
.any(|name| **name == denom_name)
|
||||
}) else {
|
||||
panic!("{denom_name} not in {blockchain_config:?}");
|
||||
};
|
||||
|
||||
let amount = f64::from_str(&amount).unwrap();
|
||||
let adjusted_amount = if relevant_denom.coin_denom == denom_name {
|
||||
amount * 10f64.powi(i32::from(relevant_denom.coin_decimals))
|
||||
} else if relevant_denom.coin_minimal_denom == denom_name {
|
||||
amount
|
||||
} else {
|
||||
unreachable!("broke invariant: check denom checker");
|
||||
} as u128;
|
||||
|
||||
let coin = run_async(async {
|
||||
let client =
|
||||
cosmrs::rpc::HttpClient::new(blockchain_config.rpc_url.as_str()).unwrap();
|
||||
|
||||
loop {
|
||||
let response: QueryBalanceResponse = abci_query(
|
||||
&client,
|
||||
"/cosmos.bank.v1beta1.Query/Balance",
|
||||
Some(&QueryBalanceRequest {
|
||||
address: address.clone(),
|
||||
denom: relevant_denom.coin_minimal_denom.clone(),
|
||||
}),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(coin) = response.balance {
|
||||
let amount = u128::from_str(&coin.amount).unwrap();
|
||||
if amount >= adjusted_amount {
|
||||
break coin;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
}
|
||||
});
|
||||
let cosmrs::proto::cosmos::base::v1beta1::Coin { denom, amount } = coin;
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"balance": {
|
||||
"denom": denom,
|
||||
"amount": u128::from_str(&amount).unwrap(),
|
||||
},
|
||||
}
|
||||
}))
|
||||
}
|
||||
Operation::Transfer(Transfer {
|
||||
amount,
|
||||
denom,
|
||||
to_address,
|
||||
from_account: _,
|
||||
from_address,
|
||||
gas_factor,
|
||||
blockchain_config,
|
||||
}) => {
|
||||
// Check if given denom is min denom or normal and adjust accordingly
|
||||
let Some(relevant_denom) = blockchain_config.currencies.iter().find(|c| {
|
||||
[&c.coin_denom, &c.coin_minimal_denom]
|
||||
.iter()
|
||||
.any(|name| **name == denom)
|
||||
}) else {
|
||||
panic!("{denom} not in {blockchain_config:?}");
|
||||
};
|
||||
|
||||
let gas_factor = gas_factor
|
||||
.as_deref()
|
||||
.map(f64::from_str)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let amount = f64::from_str(&amount).unwrap();
|
||||
let adjusted_amount = if relevant_denom.coin_denom == denom {
|
||||
amount * 10f64.powi(i32::from(relevant_denom.coin_decimals))
|
||||
} else if relevant_denom.coin_minimal_denom == denom {
|
||||
amount
|
||||
} else {
|
||||
unreachable!("broke invariant: check denom checker");
|
||||
} as u128;
|
||||
|
||||
let from_id = AccountId::from_str(&from_address).unwrap();
|
||||
let to_id = AccountId::from_str(&to_address).unwrap();
|
||||
|
||||
let coin = cosmrs::Coin {
|
||||
denom: relevant_denom.coin_minimal_denom.parse().unwrap(),
|
||||
amount: adjusted_amount,
|
||||
};
|
||||
|
||||
let msg_send = cosmrs::bank::MsgSend {
|
||||
from_address: from_id,
|
||||
to_address: to_id,
|
||||
amount: vec![coin.clone()],
|
||||
}
|
||||
.to_any()
|
||||
.unwrap();
|
||||
|
||||
let expected_gas = 100_000u64;
|
||||
// convert gas "price" to minimum denom,
|
||||
// multiply by amount of gas required,
|
||||
// multiply by gas factor if necessary.
|
||||
let expected_fee = blockchain_config.gas_price_step.high
|
||||
// * dbg!(10f64.powi(relevant_denom.coin_decimals as i32))
|
||||
* expected_gas as f64
|
||||
* gas_factor;
|
||||
|
||||
let fee_coin = cosmrs::Coin {
|
||||
denom: relevant_denom.coin_minimal_denom.parse().unwrap(),
|
||||
amount: expected_fee as u128,
|
||||
};
|
||||
|
||||
let fee = Fee::from_amount_and_gas(
|
||||
fee_coin,
|
||||
expected_gas,
|
||||
);
|
||||
|
||||
#[allow(clippy::identity_op)]
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"fee": remote_serde::Fee::from(&fee),
|
||||
// TODO: Body does not implement Serialize and
|
||||
// needs to be constructed in Sign
|
||||
"tx_messages": [msg_send],
|
||||
// re-export, but in general this should be copied over
|
||||
// using workflows
|
||||
"blockchain_config": blockchain_config,
|
||||
},
|
||||
"derivation_accounts": [0u32 | 1 << 31],
|
||||
}))
|
||||
}
|
||||
Operation::Sign(Sign {
|
||||
fee,
|
||||
tx_messages,
|
||||
account_number,
|
||||
sequence_number,
|
||||
blockchain_config,
|
||||
}) => {
|
||||
let key = request.derived_keys.unwrap()[0];
|
||||
let privkey = secp256k1::SigningKey::from_slice(&key).unwrap();
|
||||
|
||||
let fee = cosmrs::tx::Fee::from(&fee);
|
||||
let tx_body = BodyBuilder::new().msgs(tx_messages).finish();
|
||||
|
||||
let auth_info =
|
||||
SignerInfo::single_direct(Some(privkey.public_key()), sequence_number)
|
||||
.auth_info(fee);
|
||||
let sign_doc = SignDoc::new(
|
||||
&tx_body,
|
||||
&auth_info,
|
||||
&blockchain_config.chain_id.parse().unwrap(),
|
||||
account_number,
|
||||
)
|
||||
.unwrap();
|
||||
let signed_tx = sign_doc.sign(&privkey).unwrap();
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"transaction": signed_tx.to_bytes().unwrap(),
|
||||
"blockchain_config": blockchain_config,
|
||||
}
|
||||
}))
|
||||
}
|
||||
Operation::Broadcast(Broadcast {
|
||||
transaction,
|
||||
blockchain_config,
|
||||
}) => {
|
||||
let tx = tx::Raw::from_bytes(&transaction).unwrap();
|
||||
|
||||
let response = run_async(async {
|
||||
let client =
|
||||
cosmrs::rpc::HttpClient::new(blockchain_config.rpc_url.as_str()).unwrap();
|
||||
|
||||
// broadcast_tx_sync awaits CheckTx, so we know that, at the bare minimum, the
|
||||
// transaction is valid.
|
||||
//
|
||||
// TODO: make this expect() into a keyfork_bug!(). An error in this area of
|
||||
// code may represent a bug in either our code or the blockchain's code.
|
||||
// We _should_ get an Err code if the error was with the transaction data, such
|
||||
// as us being out of gas, or the transaction being malformed.
|
||||
client
|
||||
.broadcast_tx_sync(tx.to_bytes().unwrap())
|
||||
.await
|
||||
.expect("The server encountered a fatal error while processing the request")
|
||||
});
|
||||
match response.code {
|
||||
cosmrs::tendermint::abci::Code::Ok => {
|
||||
// attempt to get transaction URL / blockchain explorer URL
|
||||
Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"status": "send_and_confirm",
|
||||
"success": response.hash.to_string(),
|
||||
"url": blockchain_config.explorer_url_format.replace("%s", response.hash.to_string().as_str()),
|
||||
}
|
||||
}))
|
||||
}
|
||||
cosmrs::tendermint::abci::Code::Err(non_zero) => Ok(serde_json::json!({
|
||||
"blob": {
|
||||
"status": "send_and_confirm",
|
||||
"error": response.log,
|
||||
"error_code": non_zero.get(),
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
use icepick_module::Module;
|
||||
use icepick_cosmos::Cosmos;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Cosmos::run_responder()
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Coin {
|
||||
amount: [u8; 16],
|
||||
denom: cosmrs::Denom,
|
||||
}
|
||||
|
||||
impl From<&cosmrs::Coin> for Coin {
|
||||
fn from(value: &cosmrs::Coin) -> Self {
|
||||
let cosmrs::Coin { denom, amount } = value;
|
||||
Coin {
|
||||
denom: denom.clone(),
|
||||
amount: amount.to_be_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Coin> for cosmrs::Coin {
|
||||
fn from(value: &Coin) -> Self {
|
||||
let Coin { amount, denom } = value;
|
||||
cosmrs::Coin {
|
||||
denom: denom.clone(),
|
||||
amount: u128::from_be_bytes(*amount),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Fee {
|
||||
amount: Vec<Coin>,
|
||||
gas_limit: u64,
|
||||
}
|
||||
|
||||
impl From<&cosmrs::tx::Fee> for Fee {
|
||||
fn from(value: &cosmrs::tx::Fee) -> Self {
|
||||
let cosmrs::tx::Fee {
|
||||
amount,
|
||||
gas_limit,
|
||||
payer,
|
||||
granter,
|
||||
} = value;
|
||||
|
||||
assert!(payer.is_none(), "unimplemented: payer");
|
||||
assert!(granter.is_none(), "unimplemented: granter");
|
||||
|
||||
let amounts = amount.iter().map(Coin::from).collect::<Vec<_>>();
|
||||
|
||||
Fee {
|
||||
amount: amounts,
|
||||
gas_limit: *gas_limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Fee> for cosmrs::tx::Fee {
|
||||
fn from(value: &Fee) -> Self {
|
||||
let Fee { amount, gas_limit } = value;
|
||||
let amounts = amount.iter().map(cosmrs::Coin::from).collect::<Vec<_>>();
|
||||
|
||||
cosmrs::tx::Fee {
|
||||
amount: amounts,
|
||||
gas_limit: *gas_limit,
|
||||
payer: None,
|
||||
granter: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,18 +5,12 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
bincode = "1.3.3"
|
||||
bs58 = "0.5.1"
|
||||
ed25519-dalek = "=1.0.1"
|
||||
icepick-module = { version = "0.1.0", path = "../../icepick-module" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
solana-rpc-client = { version = "2.1.1", default-features = false }
|
||||
solana-rpc-client-api = "2.1.7"
|
||||
solana-rpc-client-nonce-utils = "2.1.7"
|
||||
solana-sdk = { version = "2.1.1" }
|
||||
solana-transaction-status = "2.1.1"
|
||||
solana-transaction-status-client-types = "2.1.1"
|
||||
spl-associated-token-account = "6.0.0"
|
||||
spl-token = "7.0.0"
|
||||
spl-token-2022 = "6.0.0"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,5 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bon = "3.3.2"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
|
|
|
@ -16,26 +16,6 @@ pub mod help {
|
|||
pub arguments: Vec<Argument>,
|
||||
}
|
||||
|
||||
#[bon::bon]
|
||||
impl Operation {
|
||||
|
||||
#[builder]
|
||||
pub fn new(name: &'static str, description: &'static str) -> Self {
|
||||
Operation {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
arguments: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub fn argument(mut self, arg: &Argument) -> Self {
|
||||
self.arguments.push(arg.clone());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// The context of whether a signature is signed, needs to be signed, or has been signed.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
@ -70,19 +50,6 @@ pub mod help {
|
|||
/// The type of argument - this may affect how it displays in the frontend.
|
||||
pub r#type: ArgumentType,
|
||||
}
|
||||
|
||||
#[bon::bon]
|
||||
impl Argument {
|
||||
|
||||
#[builder]
|
||||
pub fn new(name: &'static str, description: &'static str, r#type: ArgumentType) -> Self {
|
||||
Argument {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
r#type,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation methods for Icepick Modules, performed over command I/O using JSON.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use clap::command;
|
||||
use icepick_module::help::*;
|
||||
use keyfork_derive_util::{request::DerivationAlgorithm, DerivationIndex, DerivationPath};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -18,57 +17,15 @@ pub fn get_command(bin_name: &str) -> (&str, Vec<&str>) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn derive_keys(
|
||||
algo: &DerivationAlgorithm,
|
||||
path_prefix: &DerivationPath,
|
||||
accounts: &[DerivationIndex],
|
||||
) -> Vec<Vec<u8>> {
|
||||
if accounts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let mut derived_keys = vec![];
|
||||
let mut client = keyforkd_client::Client::discover_socket().expect("keyforkd started");
|
||||
for account in accounts {
|
||||
let request = keyfork_derive_util::request::DerivationRequest::new(
|
||||
algo.clone(),
|
||||
&path_prefix.clone().chain_push(account.clone()),
|
||||
);
|
||||
let request = keyforkd_models::Request::Derivation(request);
|
||||
let response = client.request(&request).expect("valid derivation");
|
||||
match response {
|
||||
keyforkd_models::Response::Derivation(
|
||||
keyfork_derive_util::request::DerivationResponse { data, .. },
|
||||
) => {
|
||||
derived_keys.push(data.to_vec());
|
||||
}
|
||||
_ => panic!("Unexpected response"),
|
||||
}
|
||||
}
|
||||
derived_keys
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ModuleConfig {
|
||||
/// The name of the module.
|
||||
name: String,
|
||||
|
||||
/// The name of the command used to invoke the module. If not given, the default would be
|
||||
/// `format!("icepick-{name}")`, using the name of the module.
|
||||
command_name: Option<String>,
|
||||
algorithm: keyfork_derive_util::request::DerivationAlgorithm,
|
||||
|
||||
/// The bip32 derivation algorithm. This is currently used for deriving keys from Keyfork, but
|
||||
/// may be passed to modules within the workflow to provide additional context, such as the
|
||||
/// algorithm for a generic signer.
|
||||
#[serde(default)]
|
||||
algorithm: Option<DerivationAlgorithm>,
|
||||
#[serde(with = "serde_derivation")]
|
||||
derivation_prefix: keyfork_derive_util::DerivationPath,
|
||||
|
||||
/// The bip44 derivation prefix. This is currently used for deriving keys from Keyfork directly
|
||||
/// within Icepick, but may be passed to modules within the workflow to provide additional
|
||||
/// context, such as a module for deriving keys.
|
||||
#[serde(with = "serde_derivation", default)]
|
||||
derivation_prefix: Option<DerivationPath>,
|
||||
|
||||
/// All workflows for a module.
|
||||
#[serde(rename = "workflow", default)]
|
||||
workflows: Vec<workflow::Workflow>,
|
||||
}
|
||||
|
@ -78,28 +35,21 @@ mod serde_derivation {
|
|||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn serialize<S>(p: &Option<DerivationPath>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
pub fn serialize<S>(p: &DerivationPath, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Some(p) = p {
|
||||
let path = p.to_string();
|
||||
serializer.serialize_str(&path)
|
||||
} else {
|
||||
serializer.serialize_none()
|
||||
}
|
||||
let path = p.to_string();
|
||||
serializer.serialize_str(&path)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DerivationPath>, D::Error>
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<DerivationPath, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let opt_string = Option::<String>::deserialize(deserializer)?;
|
||||
opt_string
|
||||
.map(|string| DerivationPath::from_str(&string).map_err(Error::custom))
|
||||
.transpose()
|
||||
String::deserialize(deserializer)
|
||||
.and_then(|string| DerivationPath::from_str(&string).map_err(Error::custom))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,14 +72,7 @@ pub fn do_cli_thing() {
|
|||
});
|
||||
let config_path = config_file.unwrap_or_else(|| "icepick.toml".to_string());
|
||||
let config_content = std::fs::read_to_string(config_path).expect("can't read config file");
|
||||
let mut config: Config = toml::from_str(&config_content).expect("config file had invalid toml");
|
||||
config.modules.push(ModuleConfig {
|
||||
name: "internal".to_string(),
|
||||
command_name: Default::default(),
|
||||
algorithm: Default::default(),
|
||||
derivation_prefix: Default::default(),
|
||||
workflows: Default::default(),
|
||||
});
|
||||
let config: Config = toml::from_str(&config_content).expect("config file had invalid toml");
|
||||
|
||||
let mut commands = vec![];
|
||||
let mut icepick_command = command!();
|
||||
|
@ -216,7 +159,7 @@ pub fn do_cli_thing() {
|
|||
.find(|(module, _)| module == module_name)
|
||||
.and_then(|(_, workflows)| workflows.iter().find(|x| x.name == workflow_name))
|
||||
.expect("workflow from CLI should match config");
|
||||
workflow.handle(matches, commands, &config.modules);
|
||||
workflow.handle(matches, commands);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -276,11 +219,24 @@ pub fn do_cli_thing() {
|
|||
let accounts: Vec<keyfork_derive_util::DerivationIndex> =
|
||||
serde_json::from_value(accounts.clone())
|
||||
.expect("valid derivation_accounts");
|
||||
derived_keys.extend(derive_keys(
|
||||
&algo.expect("a module requested keys but didn't provide algorithm"),
|
||||
&path.expect("a module requested keys but didn't provide prefix"),
|
||||
&accounts,
|
||||
));
|
||||
let mut client =
|
||||
keyforkd_client::Client::discover_socket().expect("keyforkd started");
|
||||
for account in accounts {
|
||||
let request = keyfork_derive_util::request::DerivationRequest::new(
|
||||
algo.clone(),
|
||||
&path.clone().chain_push(account),
|
||||
);
|
||||
let request = keyforkd_models::Request::Derivation(request);
|
||||
let response = client.request(&request).expect("valid derivation");
|
||||
match response {
|
||||
keyforkd_models::Response::Derivation(
|
||||
keyfork_derive_util::request::DerivationResponse { data, .. },
|
||||
) => {
|
||||
derived_keys.push(data.to_vec());
|
||||
}
|
||||
_ => panic!("Unexpected response"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let json = serde_json::json!({
|
||||
|
@ -308,17 +264,10 @@ pub fn do_cli_thing() {
|
|||
let mut input = child.stdin.take().unwrap();
|
||||
serde_json::to_writer(&mut input, &json).unwrap();
|
||||
input.write_all(b"\n{\"operation\": \"exit\"}\n").unwrap();
|
||||
let output = child.wait_with_output().unwrap();
|
||||
let stdout = &output.stdout;
|
||||
if output.status.success() {
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(stdout).expect("valid json");
|
||||
let json_as_str = serde_json::to_string(&json).unwrap();
|
||||
println!("{json_as_str}");
|
||||
} else {
|
||||
eprintln!("Error while invoking operation, check logs");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let output = child.wait_with_output().unwrap().stdout;
|
||||
let json: serde_json::Value = serde_json::from_slice(&output).expect("valid json");
|
||||
let json_as_str = serde_json::to_string(&json).unwrap();
|
||||
println!("{json_as_str}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
use keyfork_derive_util::DerivationIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::{derive_keys, get_command, Commands, ModuleConfig, Operation};
|
||||
use super::{Commands, Operation};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Workflow {
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub inputs: Vec<String>,
|
||||
|
||||
#[serde(rename = "step")]
|
||||
|
@ -41,61 +34,11 @@ pub struct WorkflowStep {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
struct InvocableOperation {
|
||||
module: String,
|
||||
name: String,
|
||||
binary: String,
|
||||
operation: Operation,
|
||||
}
|
||||
|
||||
// TODO: This should probably be migrated to an actual Result type, instead of
|
||||
// currently just shoving everything in "blob". Probably done after derivation_accounts
|
||||
// gets hoisted out of here.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct OperationResult {
|
||||
// All values returned from an operation.
|
||||
blob: HashMap<String, Value>,
|
||||
|
||||
// Any requested accounts from an operation.
|
||||
//
|
||||
// TODO: Move this to its own step.
|
||||
#[serde(default)]
|
||||
derivation_accounts: Vec<DerivationIndex>,
|
||||
}
|
||||
|
||||
impl InvocableOperation {
|
||||
fn invoke(&self, input: &HashMap<String, Value>, derived_keys: &[Vec<u8>]) -> OperationResult {
|
||||
let (command, args) = get_command(&self.binary);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"operation": self.operation.name,
|
||||
"values": input,
|
||||
"derived_keys": derived_keys,
|
||||
});
|
||||
|
||||
let mut child = Command::new(command)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
let mut child_input = child.stdin.take().unwrap();
|
||||
serde_json::to_writer(&mut child_input, &json).unwrap();
|
||||
child_input
|
||||
.write_all(b"\n{\"operation\": \"exit\"}\n")
|
||||
.unwrap();
|
||||
|
||||
let result = child.wait_with_output().unwrap();
|
||||
if !result.status.success() {
|
||||
panic!("Bad exit: {}", String::from_utf8_lossy(&result.stderr));
|
||||
}
|
||||
|
||||
let output = result.stdout;
|
||||
let json: OperationResult = serde_json::from_slice(&output).expect("valid json");
|
||||
json
|
||||
}
|
||||
}
|
||||
|
||||
impl Workflow {
|
||||
/// Generate a [`clap::Command`] for a [`Workflow`], where the inputs can be defined either by
|
||||
/// command-line arguments or via a JSON input file.
|
||||
|
@ -155,7 +98,7 @@ impl Workflow {
|
|||
// Check if we have the keys we want to pass into the module.
|
||||
for in_memory_name in step.inputs.values() {
|
||||
if !data.contains(in_memory_name) && !step.values.contains_key(in_memory_name) {
|
||||
eprintln!("Failed simulation: step #{step_index} ({step_type}): missing value {in_memory_name}");
|
||||
panic!("Failed simulation: step #{step_index} ({step_type}): missing value {in_memory_name}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,87 +121,7 @@ impl Workflow {
|
|||
}
|
||||
}
|
||||
|
||||
fn run_workflow(
|
||||
&self,
|
||||
mut data: HashMap<String, Value>,
|
||||
operations: &[InvocableOperation],
|
||||
config: &[ModuleConfig],
|
||||
) {
|
||||
let mut derived_keys = vec![];
|
||||
let mut derivation_accounts = vec![];
|
||||
|
||||
for step in &self.steps {
|
||||
let operation = operations
|
||||
.iter()
|
||||
.find(|op| op.name == step.r#type)
|
||||
.expect("operation matched step type");
|
||||
|
||||
// Load keys from Keyfork, from previously requested workflow
|
||||
let config = config
|
||||
.iter()
|
||||
.find(|module| module.name == operation.module)
|
||||
.expect("could not find module config");
|
||||
let algo = &config.algorithm;
|
||||
let path_prefix = &config.derivation_prefix;
|
||||
if !derivation_accounts.is_empty() {
|
||||
derived_keys.extend(derive_keys(
|
||||
algo.as_ref()
|
||||
.expect("a module requested keys but didn't provide algorithm"),
|
||||
path_prefix
|
||||
.as_ref()
|
||||
.expect("a module requested keys but didn't provide prefix"),
|
||||
&derivation_accounts,
|
||||
));
|
||||
}
|
||||
derivation_accounts.clear();
|
||||
|
||||
// Prepare all inputs for the operation invocation
|
||||
//
|
||||
// NOTE: this could be .clone().into_iter() but it would create an extra allocation of
|
||||
// the HashMap, and an unnecessary alloc of the key.
|
||||
let inputs: HashMap<String, Value> = data
|
||||
.iter()
|
||||
.map(|(k, v)| (k, v.clone()))
|
||||
.filter_map(|(k, v)| {
|
||||
// We have our stored name, `k`, which matches with this inner loop's `v`. We
|
||||
// need to return our desired name, rather than our stored name, and the value
|
||||
// in our storage, our current `v`.
|
||||
let (desired, _stored) = step.inputs.iter().find(|(_, v)| k == *v)?;
|
||||
Some((desired.clone(), v))
|
||||
})
|
||||
.chain(
|
||||
step.values
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), Value::String(v.clone()))),
|
||||
)
|
||||
.collect();
|
||||
let OperationResult {
|
||||
blob,
|
||||
derivation_accounts: new_accounts,
|
||||
} = operation.invoke(&inputs, &derived_keys);
|
||||
derived_keys.clear();
|
||||
derivation_accounts.extend(new_accounts);
|
||||
data.extend(blob.into_iter().filter_map(|(k, v)| {
|
||||
// We have our stored name, `k`, which matches with this inner loop's `v`. We
|
||||
// need to return our desired name, rather than our stored name, and the value
|
||||
// in our storage, our current `v`.
|
||||
let (_given, stored) = step.outputs.iter().find(|(k1, _)| k == **k1)?;
|
||||
Some((stored.clone(), v))
|
||||
}));
|
||||
}
|
||||
|
||||
let last_outputs = &self.steps.last().unwrap().outputs;
|
||||
data.retain(|stored_name, _| {
|
||||
last_outputs
|
||||
.values()
|
||||
.any(|storage_name| stored_name == storage_name)
|
||||
});
|
||||
|
||||
let json_as_str = serde_json::to_string(&data).unwrap();
|
||||
println!("{json_as_str}");
|
||||
}
|
||||
|
||||
pub fn handle(&self, matches: &clap::ArgMatches, modules: Commands, config: &[ModuleConfig]) {
|
||||
pub fn handle(&self, matches: &clap::ArgMatches, modules: Commands) {
|
||||
let inputs = self.load_inputs(matches);
|
||||
let data: HashMap<String, Value> = inputs
|
||||
.into_iter()
|
||||
|
@ -271,7 +134,6 @@ impl Workflow {
|
|||
for operation in module_operations {
|
||||
let operation_name = &operation.name;
|
||||
let io = InvocableOperation {
|
||||
module: module_name.clone(),
|
||||
name: format!("{module_name}-{operation_name}"),
|
||||
binary: module_binary.clone(),
|
||||
operation: operation.clone(),
|
||||
|
@ -285,6 +147,6 @@ impl Workflow {
|
|||
return;
|
||||
}
|
||||
|
||||
self.run_workflow(data, &operations, config);
|
||||
todo!("Unsimulated transaction!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,12 +44,8 @@ RUN <<EOF
|
|||
cargo fetch --locked
|
||||
cargo build --frozen --release --target x86_64-unknown-linux-musl --bin icepick
|
||||
cargo build --frozen --release --target x86_64-unknown-linux-musl --bin icepick-sol
|
||||
cargo build --frozen --release --target x86_64-unknown-linux-musl --bin icepick-internal
|
||||
cp /app/target/x86_64-unknown-linux-musl/release/icepick /usr/bin
|
||||
cp /app/target/x86_64-unknown-linux-musl/release/icepick-internal /usr/bin
|
||||
cp /app/target/x86_64-unknown-linux-musl/release/icepick-sol /usr/bin
|
||||
EOF
|
||||
|
||||
ENV ICEPICK_DATA_DIRECTORY=/data
|
||||
|
||||
WORKDIR /
|
||||
|
|
|
@ -13,11 +13,15 @@ from_address="$(jq -r .from_address /data/input.json)"
|
|||
to_address="$(jq -r .to_address /data/input.json)"
|
||||
token_name="$(jq -r .token_name /data/input.json)"
|
||||
token_amount="$(jq -r .token_amount /data/input.json)"
|
||||
blockhash="$(jq -r .blockhash /data/input.json)"
|
||||
token_address="$(icepick sol get-token-info "$token_name" | jq -r .blob.token_address)"
|
||||
token_decimals="$(icepick sol get-token-info "$token_name" | jq -r .blob.token_decimals)"
|
||||
|
||||
jq . /data/input.json
|
||||
echo "Do these values look correct? If not, press ctrl-c. Otherwise, press Enter."
|
||||
read -r _
|
||||
read _
|
||||
|
||||
echo "Creating and signing transaction"
|
||||
|
||||
icepick workflow sol transfer-token --from-address "$from_address" --to-address "$to_address" --token-name "$token_name" --token-amount "$token_amount"
|
||||
icepick sol transfer-token "$token_amount" "$token_address" "$to_address" "$from_address" "$token_decimals" | icepick sol sign "$blockhash" > /data/output.json.tmp
|
||||
mv /data/output.json.tmp /data/output.json
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
printf "%s" "Public key of the sender address: "
|
||||
read -r from_address
|
||||
read from_address
|
||||
|
||||
printf "%s" "Public key of the recipient address: "
|
||||
read -r to_address
|
||||
|
||||
printf "%s" "Public ey of the nonce account: "
|
||||
read -r nonce_address
|
||||
read to_address
|
||||
|
||||
printf "%s" "Name of the token to transfer: "
|
||||
read -r token_name
|
||||
read token_name
|
||||
|
||||
printf "%s" "Amount of token to transfer: "
|
||||
read -r token_amount
|
||||
read token_amount
|
||||
|
||||
echo "Saving inputs to file"
|
||||
echo "Acquiring blockhash..."
|
||||
blockhash="$(icepick sol get-blockhash --cluster devnet | jq -r .blob.blockhash)"
|
||||
|
||||
echo "Saving information to file"
|
||||
|
||||
cat <<EOF > /data/input.json
|
||||
{
|
||||
"from_address": "$from_address",
|
||||
"to_address": "$to_address",
|
||||
"token_name": "$token_name",
|
||||
"token_amount": "$token_amount"
|
||||
"token_amount": "$token_amount",
|
||||
"blockhash": "$blockhash"
|
||||
}
|
||||
EOF
|
||||
|
||||
icepick workflow sol broadcast --cluster devnet --nonce-address "$nonce_address"
|
||||
echo "Waiting for signed transaction..."
|
||||
while test ! -f /data/output.json; do sleep 1; done
|
||||
|
||||
echo "Broadcasting transaction"
|
||||
icepick sol broadcast --cluster devnet < /data/output.json
|
||||
|
|
276
icepick.toml
276
icepick.toml
|
@ -15,6 +15,17 @@ name = "transfer-token"
|
|||
# of later-defined signature validation.
|
||||
inputs = ["from_address", "to_address", "token_name", "token_amount"]
|
||||
|
||||
## Load the Blockhash from the SD card
|
||||
#[[module.workflow.step]]
|
||||
#type = "internal-load-file"
|
||||
#
|
||||
## Pre-defined values to be passed to the module
|
||||
#values = { filename = "blockhash.json" }
|
||||
#
|
||||
## This value is marked to be saved in-memory, and can be used as an input for
|
||||
## later steps.
|
||||
#outputs = { blockhash = "blockhash" }
|
||||
|
||||
# Get the token address and token decimals for the given token
|
||||
[[module.workflow.step]]
|
||||
type = "sol-get-token-info"
|
||||
|
@ -22,29 +33,15 @@ type = "sol-get-token-info"
|
|||
# The key is the key that is passed to the program in the
|
||||
# `values` field. The value is the item in storage. In this case,
|
||||
# we read a `token-name` from our input, but the operation expects `token`.
|
||||
inputs = { token = "token_name" }
|
||||
inputs = { token= "token_name" }
|
||||
|
||||
# Because these two fields are currently unused in our storage, we can grab
|
||||
# them from the outputs of our module. The key is the key of the output value
|
||||
# we want to store, and the value is the name to be assigned in storage.
|
||||
outputs = { token_address = "token_address", token_decimals = "token_decimals" }
|
||||
|
||||
# Load the transaction nonce from the SD card
|
||||
[[module.workflow.step]]
|
||||
type = "internal-load-file"
|
||||
|
||||
# Pre-defined values to be passed to the module.
|
||||
# In this case, the `filename` field is reserved for marking which file to load.
|
||||
values = { filename = "nonce.json" }
|
||||
|
||||
# This value is marked to be saved in-memory, and can be used as an input for
|
||||
# later steps.
|
||||
outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
# Generate an unsigned Transaction
|
||||
# This step MUST run immediately before sol-sign, as in the current version of
|
||||
# Icepick, keys are only held in memory in-between a single module invocation.
|
||||
type = "sol-transfer-token"
|
||||
|
||||
# If using a lot of inputs, it may be best to use a non-inline table.
|
||||
|
@ -59,249 +56,34 @@ decimals = "token_decimals"
|
|||
to_address = "to_address"
|
||||
from_address = "from_address"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
instructions = "instructions"
|
||||
derivation_accounts = "derivation_accounts"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-compile"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
instructions = "instructions"
|
||||
derivation_accounts = "derivation_accounts"
|
||||
nonce_address = "nonce_address"
|
||||
nonce_authority = "nonce_authority"
|
||||
nonce_data = "nonce_data"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "unsigned_transaction"
|
||||
|
||||
# Get a blockhash
|
||||
[[module.workflow.step]]
|
||||
type = "sol-get-blockhash"
|
||||
|
||||
outputs = { blockhash = "blockhash" }
|
||||
|
||||
# Sign the transaction
|
||||
[[module.workflow.step]]
|
||||
type = "sol-sign"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
transaction = "unsigned_transaction"
|
||||
blockhash = "nonce_data"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "signed_transaction"
|
||||
|
||||
# Write the signed transaction to a file
|
||||
[[module.workflow.step]]
|
||||
type = "internal-save-file"
|
||||
|
||||
# We are using a static filename here, so we use `values` instead of `inputs`.
|
||||
values = { filename = "transaction.json" }
|
||||
|
||||
# All fields in both `inputs` and `values`, other than `filename`, will be
|
||||
# persisted to the file. In this case, the `transaction` field of the file will
|
||||
# contain the signed transaction.
|
||||
inputs = { transaction = "signed_transaction" }
|
||||
|
||||
# NOTE: To get a nonce address, the `generate-nonce-account` workflow should be
|
||||
# run. It is the only workflow that uses a blockhash, which is why a
|
||||
# `broadcast-with-blockhash` or similar is not, and should not be, implemented.
|
||||
[[module.workflow]]
|
||||
name = "broadcast"
|
||||
inputs = ["nonce_address", "cluster"]
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-get-nonce-account-data"
|
||||
inputs = { nonce_address = "nonce_address", cluster = "cluster" }
|
||||
outputs = { authority = "nonce_authority", durable_nonce = "nonce" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-save-file"
|
||||
values = { filename = "nonce.json" }
|
||||
inputs = { nonce_authority = "nonce_authority", nonce_data = "nonce", nonce_address = "nonce_address" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-load-file"
|
||||
values = { filename = "transaction.json" }
|
||||
outputs = { transaction = "transaction" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-broadcast"
|
||||
inputs = { cluster = "cluster", transaction = "transaction" }
|
||||
outputs = { status = "status", url = "url", error = "error" }
|
||||
|
||||
[[module.workflow]]
|
||||
name = "generate-nonce-account"
|
||||
inputs = ["cluster", "authorization_address"]
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-generate-wallet"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-get-wallet-address"
|
||||
outputs = { pubkey = "wallet_pubkey" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-await-funds"
|
||||
inputs = { address = "wallet_pubkey", cluster = "cluster" }
|
||||
# enough to cover two signatures and the 1_500_000 approx. rent fee
|
||||
values = { lamports = "1510000" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-get-blockhash"
|
||||
inputs = { cluster = "cluster" }
|
||||
outputs = { blockhash = "blockhash" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-create-nonce-account-and-signing-key"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
from_address = "wallet_pubkey"
|
||||
authorization_address = "authorization_address"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "unsigned_transaction"
|
||||
nonce_pubkey = "nonce_pubkey"
|
||||
nonce_privkey = "private_keys"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-sign"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
blockhash = "blockhash"
|
||||
signing_keys = "private_keys"
|
||||
transaction = "unsigned_transaction"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "signed_transaction"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-broadcast"
|
||||
inputs = { cluster = "cluster", transaction = "signed_transaction" }
|
||||
outputs = { status = "status", url = "url" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-cat"
|
||||
inputs = { status = "status", url = "url", nonce_account = "nonce_pubkey" }
|
||||
outputs = { status = "status", url = "url", nonce_account = "nonce_account" }
|
||||
|
||||
[[module.workflow]]
|
||||
# Transfer SOL from one address to another.
|
||||
name = "transfer"
|
||||
inputs = ["to_address", "from_address", "amount"]
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-load-file"
|
||||
values = { filename = "nonce.json" }
|
||||
outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-transfer"
|
||||
inputs = { from_address = "from_address", to_address = "to_address", amount = "amount" }
|
||||
outputs = { instructions = "instructions", derivation_accounts = "derivation_accounts" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-compile"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
instructions = "instructions"
|
||||
derivation_accounts = "derivation_accounts"
|
||||
nonce_address = "nonce_address"
|
||||
nonce_authority = "nonce_authority"
|
||||
nonce_data = "nonce_data"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "unsigned_transaction"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "sol-sign"
|
||||
|
||||
inputs = { blockhash = "nonce_data", transaction = "unsigned_transaction" }
|
||||
outputs = { transaction = "signed_transaction" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-save-file"
|
||||
|
||||
values = { filename = "transaction.json" }
|
||||
inputs = { transaction = "signed_transaction" }
|
||||
|
||||
[[module]]
|
||||
name = "cosmos"
|
||||
derivation_prefix = "m/44'/118'/0'"
|
||||
algorithm = "Secp256k1"
|
||||
|
||||
[[module.workflow]]
|
||||
name = "transfer"
|
||||
inputs = ["from_address", "to_address", "asset_name", "chain_name", "asset_amount"]
|
||||
|
||||
[[module.workflow.step]]
|
||||
# NOTE: chain_name can't be discoverable by filtering from asset_name, since
|
||||
# some asset devnets reuse the name. There's no difference between KYVE on Kyve
|
||||
# or Korellia (devnet).
|
||||
type = "cosmos-get-chain-info"
|
||||
inputs = { chain_name = "chain_name" }
|
||||
outputs = { blockchain_config = "blockchain_config" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-load-file"
|
||||
values = { filename = "account_info.json" }
|
||||
outputs = { account_number = "account_number", sequence_number = "sequence_number" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "cosmos-transfer"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
from_address = "from_address"
|
||||
to_address = "to_address"
|
||||
amount = "asset_amount"
|
||||
denom = "asset_name"
|
||||
blockchain_config = "blockchain_config"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
fee = "fee"
|
||||
tx_messages = "tx_messages"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "cosmos-sign"
|
||||
|
||||
[module.workflow.step.inputs]
|
||||
fee = "fee"
|
||||
tx_messages = "tx_messages"
|
||||
account_number = "account_number"
|
||||
sequence_number = "sequence_number"
|
||||
blockchain_config = "blockchain_config"
|
||||
|
||||
[module.workflow.step.outputs]
|
||||
transaction = "signed_transaction"
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-save-file"
|
||||
values = { filename = "transaction.json" }
|
||||
inputs = { transaction = "signed_transaction" }
|
||||
|
||||
[[module.workflow]]
|
||||
name = "broadcast"
|
||||
# NOTE: For the purpose of Cosmos, the nonce is a direct part of the signer's
|
||||
# account.
|
||||
inputs = ["nonce_address", "chain_name"]
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "cosmos-get-chain-info"
|
||||
inputs = { chain_name = "chain_name" }
|
||||
outputs = { blockchain_config = "blockchain_config" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "cosmos-get-account-data"
|
||||
inputs = { account_id = "nonce_address", blockchain_config = "blockchain_config" }
|
||||
outputs = { account_number = "account_number", sequence_number = "sequence_number" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-save-file"
|
||||
values = { filename = "account_info.json" }
|
||||
inputs = { account_number = "account_number", sequence_number = "sequence_number" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "internal-load-file"
|
||||
values = { filename = "transaction.json" }
|
||||
outputs = { transaction = "transaction" }
|
||||
|
||||
[[module.workflow.step]]
|
||||
type = "cosmos-broadcast"
|
||||
inputs = { blockchain_config = "blockchain_config", transaction = "transaction" }
|
||||
outputs = { status = "status", url = "url", error = "error", error_code = "error_code" }
|
||||
## Write the signed transaction to a file
|
||||
#[[module.workflow.step]]
|
||||
#type = "internal-save-file"
|
||||
#
|
||||
## We are using a static filename here, so we use `values` instead of `inputs`.
|
||||
#values = { filename = "transaction.json" }
|
||||
#
|
||||
## All fields in both `inputs` and `values`, other than `filename`, will be
|
||||
## persisted to the file. In this case, the `transaction` field of the file will
|
||||
## contain the signed transaction.
|
||||
#inputs = { transaction = "signed_transaction" }
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
mnemonics:
|
||||
keyfork: ENC[AES256_GCM,data:kz2vAo1XMCylVY6WtDfZ9Z0xKvccLRrOvfP2x0IJtJkRu3HmShTEzPlrTfRXrKcuxLqqJlxOnGPR7/Y7bPhRvH/nRj59Lz1SLocVl8UVq9YXsIpgymLJ0Hp2I6XUBuItOhGonvc61iAe7cXFTAO+T2VUMK0Tf40xoJcT2eBC9qOjkC5xOrHTa+FBDFcvQdHcMobm+y7Nv1BzpzbODaA=,iv:m3p+sAgZjQReM3YAld6n1uKppkQSn51IgQGsxlYHnn4=,tag:xrG7WLr9w4zE45TiHX6a8w==,type:str]
|
||||
solana: ENC[AES256_GCM,data:5/OKpwkZT+Vf6AvTiVj7zafVoqiqkKwLRLwjIHA6MGbei0ssCWqxM8QAtka+BBNGGhe5SUTlr/nAqGfoiP0t6fwUyjxUnOgu,iv:8Ctui1cO/RCZAdtfjiCnqvYyINdOcMHZfIZD0nGj2Kg=,tag:5ASiLG+hehhCYwdJ+1MZFg==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age: []
|
||||
lastmodified: "2025-01-03T23:53:42Z"
|
||||
mac: ENC[AES256_GCM,data:/WYbQmisG9jvUKvcmMwQWop0X3EbLbCurUDnoMoOKJ7XxKRpGOKh/GkqqSFgMzpC8A6X9Cmjqo4gatiYBBGjDi5PIk+9fGvUE0ZSc4p5p5+0RLu7KyxYeRtsmhEjUYJllVi9aDLJT4x+GXta91uTWZFUWExcZ1wJHh42wSFsbo4=,iv:ZLSH09zdyeDom5koxrS5KBCv2xd3cCEkJO4/hAmzUPM=,tag:AGtJTuWUEslY+uD7OFCM/g==,type:str]
|
||||
pgp:
|
||||
- created_at: "2025-01-03T23:53:41Z"
|
||||
enc: |
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQIMAw95Vf08z8oUAQ/8CC594sGEYJLHzFZy9UsExxy7CQl2q1SKQA8frZCU1cBu
|
||||
CyIex99UgQzKTSUqttlz5hxqfyodvpoRfBiZUOcyfOgVgTPtDJ9UfByMbsMc0wy0
|
||||
q8hErtNYhBmzWRway4xoAThJUrfi6jXl/m1doFVH6Ug0Q9qi56Eo8DYaUtsE+NFU
|
||||
HjHslQpMLWm3uf/i2mQhftmwE00tWTVmBfBtuAycj5jLc3AJAveNvB5jK1O22c9N
|
||||
PHhWeHQB6K3dQfTLS1O549oSfGTfrXXxq4cHYT9BZNHDi0T4/tH1xHwmLHOwnUiZ
|
||||
i0tQ8CTYL8eALyKxj/BQQxbLXKpmor7Yli1QH1UWGw5AddvVqIz1zIyukHN/AGN7
|
||||
E475zcvkc2uLPBwnZ3JS3n7e1X9TCa/iZlW/msEqmkLeh6eW47t8/p13yj0WnkCD
|
||||
1SqA6qFEIcH8TaWqC03vLZG9ue2gSZ11db+3ZeGzqykUAG/4NR8ncD+qdhRbCZtp
|
||||
ZPASpfZnByweyGVrnfMgR/sL+i8/C7KgCqj8pUOOS5Z5Av8DNMpNushPndhdHJDU
|
||||
XAzNe2gu5StPvqqlH9wONvxiYJSmNy/dWnnvgwozvm9aPPCboYjmO9fwxsy0Zl+x
|
||||
20Bb8G5nl6C6ZvToztzxKPzToxaX1x2MFwovqnHT2GACtZ6/tAmMjg3oCFd+k/PS
|
||||
XgHFcFzyleUy9LF8Yb7DJcEDe3Tue2wvvY8XlNsIYeMnpfJ/TCq9Grzho1/w31uX
|
||||
swHv2T4SnwFnoBQoXk8cSOMqrWK3XyWi0RI9X16m+rTGXZ13I8hggi/ne8QbMsI=
|
||||
=szJ5
|
||||
-----END PGP MESSAGE-----
|
||||
fp: 8E401478A3FBEF72
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.7.3
|
Loading…
Reference in New Issue