Compare commits

..

No commits in common. "main" and "merge-blob-and-values" have entirely different histories.

21 changed files with 587 additions and 3460 deletions

1494
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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]

View File

@ -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"

View File

@ -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,
}))
}
}
}
}

View File

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

View File

@ -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"] }

View File

@ -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
}

View File

@ -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(),
}
})),
}
}
}
}
}

View File

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

View File

@ -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,
}
}
}

View File

@ -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

View File

@ -4,6 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
bon = "3.3.2"
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

View File

@ -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.

View File

@ -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}");
}
}
}

View File

@ -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!");
}
}

View File

@ -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 /

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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