diff --git a/crates/by-chain/icepick-cosmos/src/lib.rs b/crates/by-chain/icepick-cosmos/src/lib.rs index 22cbb6c..7e461dc 100644 --- a/crates/by-chain/icepick-cosmos/src/lib.rs +++ b/crates/by-chain/icepick-cosmos/src/lib.rs @@ -6,7 +6,7 @@ use cosmrs::{ }; use icepick_module::Module; use serde::{Deserialize, Serialize}; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use cosmrs::crypto::secp256k1; @@ -21,6 +21,18 @@ pub struct GetChainInfo { chain_name: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetValidatorNames { + blockchain_config: coin_denoms::Blockchain, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetDelegation { + delegator_address: String, + validator_address: String, + blockchain_config: coin_denoms::Blockchain, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GenerateWallet { account: Option, @@ -54,11 +66,30 @@ pub struct Transfer { to_address: String, from_account: Option, 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, + gas_factor: Option, + blockchain_config: coin_denoms::Blockchain, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Stake { + amount: String, + denom: String, + delegate_address: String, + validator_address: String, + + gas_factor: Option, + blockchain_config: coin_denoms::Blockchain, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Withdraw { + amount: String, + denom: String, + delegate_address: String, + validator_address: String, + + gas_factor: Option, blockchain_config: coin_denoms::Blockchain, } @@ -92,11 +123,15 @@ pub struct Request { #[allow(clippy::large_enum_variant)] pub enum Operation { GetChainInfo(GetChainInfo), + GetValidatorNames(GetValidatorNames), + GetDelegation(GetDelegation), GenerateWallet(GenerateWallet), GetWalletAddress(GetWalletAddress), GetAccountData(GetAccountData), AwaitFunds(AwaitFunds), Transfer(Transfer), + Stake(Stake), + Withdraw(Withdraw), Sign(Sign), Broadcast(Broadcast), } @@ -175,6 +210,30 @@ impl Module for Cosmos { .build(), ); + let get_validators = Operation::builder() + .name("get-validator-names") + .description("Get a list of all validators, by name (if the validator provides one)") + .build(); + + let get_delegation = Operation::builder() + .name("get-delegation") + .description("Get the delegate information for a delegator-validator pair.") + .build() + .argument( + &Argument::builder() + .name("delegator_address") + .description("The address of the delegator.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("validator_address") + .description("The address of the validator.") + .r#type(ArgumentType::Required) + .build(), + ); + let generate_wallet = Operation::builder() .name("generate-wallet") .description("Generate a wallet for the given account.") @@ -261,6 +320,86 @@ impl Module for Cosmos { .build(), ); + let stake = Operation::builder() + .name("stake") + .description("Delegate coins to a specified validator.") + .build() + .argument( + &Argument::builder() + .name("amount") + .description("The amount of coins to stake.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("denom") + .description("The denomination of coin to stake.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("delegate_address") + .description("The address holding funds to be staked.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("validator_address") + .description("The address of the validator operator to stake upon.") + .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 withdraw = Operation::builder() + .name("withdraw") + .description("Delegate coins to a specified validator.") + .build() + .argument( + &Argument::builder() + .name("amount") + .description("The amount of coins to withdraw.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("denom") + .description("The denomination of coin to withdraw.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("delegate_address") + .description("The address holding funds to be withdrawn.") + .r#type(ArgumentType::Required) + .build(), + ) + .argument( + &Argument::builder() + .name("validator_address") + .description("The address of the validator operator to withdraw from.") + .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.") @@ -287,12 +426,16 @@ impl Module for Cosmos { vec![ get_chain_info, + get_validators, + get_delegation, generate_wallet, get_wallet_address, get_account_info, await_funds, transfer, sign, + stake, + withdraw, broadcast, ] } @@ -317,6 +460,84 @@ impl Module for Cosmos { }, })) } + Operation::GetValidatorNames(GetValidatorNames { blockchain_config }) => { + use cosmrs::proto::cosmos::staking::v1beta1::*; + let validators = run_async(async { + let client = + cosmrs::rpc::HttpClient::new(blockchain_config.rpc_url.as_str()).unwrap(); + + // TODO: Pagination + let validator: QueryValidatorsResponse = abci_query( + &client, + "/cosmos.staking.v1beta1.Query/Validators", + Some(&QueryValidatorsRequest { + status: BondStatus::Bonded.as_str_name().to_string(), + pagination: None, + }), + None, + false, + ) + .await + .unwrap(); + validator.validators + }); + let id_to_name = validators + .iter() + .map(|val| { + let name = val + .description + .as_ref() + .map(|desc| &desc.moniker) + .filter(|moniker| !moniker.is_empty()) + .unwrap_or(&val.operator_address); + (val.operator_address.clone(), name.clone()) + }) + .collect::>(); + Ok(serde_json::json!({ + "blob": { + "validators": id_to_name, + } + })) + } + Operation::GetDelegation(GetDelegation { + delegator_address, + validator_address, + blockchain_config, + }) => { + use cosmrs::proto::cosmos::staking::v1beta1::*; + let delegation = run_async(async { + let client = + cosmrs::rpc::HttpClient::new(blockchain_config.rpc_url.as_str()).unwrap(); + + let delegation: QueryDelegationResponse = abci_query( + &client, + "/cosmos.staking.v1beta1.Query/Delegation", + Some(&QueryDelegationRequest { + delegator_addr: delegator_address, + validator_addr: validator_address, + }), + None, + false, + ) + .await + .unwrap(); + delegation.delegation_response.unwrap() + }); + let DelegationResponse { + delegation: Some(delegation), + balance: Some(balance), + } = delegation + else { + panic!("Either delegation or balance were not accessible"); + }; + // NOTE: The return value here is an i128. Do not parse it. serde becomes unhappy. + Ok(serde_json::json!({ + "blob": { + "shares": delegation.shares, + "balance": balance.amount, + } + })) + } Operation::GenerateWallet(GenerateWallet { account }) => { let account = u32::from_str(account.as_deref().unwrap_or("0")).unwrap(); Ok(serde_json::json!({ @@ -433,6 +654,158 @@ impl Module for Cosmos { } })) } + Operation::Stake(Stake { + amount, + denom, + delegate_address, + validator_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 delegate_id = AccountId::from_str(&delegate_address).unwrap(); + let validator_id = AccountId::from_str(&validator_address).unwrap(); + + let coin = cosmrs::Coin { + denom: relevant_denom.coin_minimal_denom.parse().unwrap(), + amount: adjusted_amount, + }; + + let msg_delegate = cosmrs::staking::MsgDelegate { + delegator_address: delegate_id, + validator_address: validator_id, + amount: coin, + } + .to_any() + .unwrap(); + + let expected_gas = 200_000u64; + // convert gas "price" to minimum denom, + // multiply by amount of gas required, + // multiply by gas factor + let expected_fee = + blockchain_config.gas_price_step.high * 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_delegate], + // re-export, but in general this should be copied over + // using workflows + }, + "derivation_accounts": [0u32 | 1 << 31], + })) + } + Operation::Withdraw(Withdraw { + amount, + denom, + delegate_address, + validator_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 delegate_id = AccountId::from_str(&delegate_address).unwrap(); + let validator_id = AccountId::from_str(&validator_address).unwrap(); + + let coin = cosmrs::Coin { + denom: relevant_denom.coin_minimal_denom.parse().unwrap(), + amount: adjusted_amount, + }; + + let msg_undelegate = cosmrs::staking::MsgUndelegate { + delegator_address: delegate_id, + validator_address: validator_id, + amount: coin, + } + .to_any() + .unwrap(); + + let expected_gas = 250_000u64; + // convert gas "price" to minimum denom, + // multiply by amount of gas required, + // multiply by gas factor + let expected_fee = + blockchain_config.gas_price_step.high * 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_undelegate], + // re-export, but in general this should be copied over + // using workflows + }, + "derivation_accounts": [0u32 | 1 << 31], + })) + } Operation::Transfer(Transfer { amount, denom, @@ -486,11 +859,9 @@ impl Module for Cosmos { 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; + // multiply by gas factor + let expected_fee = + blockchain_config.gas_price_step.high * expected_gas as f64 * gas_factor; let fee_coin = cosmrs::Coin { denom: relevant_denom.coin_minimal_denom.parse().unwrap(), @@ -508,7 +879,6 @@ impl Module for Cosmos { "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], })) @@ -540,7 +910,6 @@ impl Module for Cosmos { Ok(serde_json::json!({ "blob": { "transaction": signed_tx.to_bytes().unwrap(), - "blockchain_config": blockchain_config, } })) } diff --git a/crates/icepick/workflows/cosmos/stake.yaml b/crates/icepick/workflows/cosmos/stake.yaml new file mode 100644 index 0000000..4bb8edb --- /dev/null +++ b/crates/icepick/workflows/cosmos/stake.yaml @@ -0,0 +1,46 @@ +name: stake +inputs: +- delegate_address +- validator_address +- chain_name +- asset_name +- asset_amount +optional_inputs: +- gas_factor +step: +- type: cosmos-get-chain-info + inputs: + chain_name: chain_name + outputs: + blockchain_config: blockchain_config +- type: internal-load-file + values: + filename: "account_info.json" + outputs: + account_number: account_number + sequence_number: sequence_number +- type: cosmos-stake + inputs: + delegate_address: delegate_address + validator_address: validator_address + amount: asset_amount + denom: asset_name + blockchain_config: blockchain_config + gas_factor: gas_factor + outputs: + fee: fee + tx_messages: tx_messages +- type: cosmos-sign + inputs: + fee: fee + tx_messages: tx_messages + account_number: account_number + sequence_number: sequence_number + blockchain_config: blockchain_config + outputs: + transaction: signed_transaction +- type: internal-save-file + values: + filename: "transaction.json" + inputs: + transaction: signed_transaction diff --git a/crates/icepick/workflows/cosmos/withdraw.yaml b/crates/icepick/workflows/cosmos/withdraw.yaml new file mode 100644 index 0000000..9f9c6cc --- /dev/null +++ b/crates/icepick/workflows/cosmos/withdraw.yaml @@ -0,0 +1,46 @@ +name: withdraw +inputs: +- delegate_address +- validator_address +- chain_name +- asset_name +- asset_amount +optional_inputs: +- gas_factor +step: +- type: cosmos-get-chain-info + inputs: + chain_name: chain_name + outputs: + blockchain_config: blockchain_config +- type: internal-load-file + values: + filename: "account_info.json" + outputs: + account_number: account_number + sequence_number: sequence_number +- type: cosmos-withdraw + inputs: + delegate_address: delegate_address + validator_address: validator_address + amount: asset_amount + denom: asset_name + blockchain_config: blockchain_config + gas_factor: gas_factor + outputs: + fee: fee + tx_messages: tx_messages +- type: cosmos-sign + inputs: + fee: fee + tx_messages: tx_messages + account_number: account_number + sequence_number: sequence_number + blockchain_config: blockchain_config + outputs: + transaction: signed_transaction +- type: internal-save-file + values: + filename: "transaction.json" + inputs: + transaction: signed_transaction