icepick workflow cosmos {stake,withdraw}; icepick cosmos {get-delegation,get-validators}

This commit is contained in:
Ryan Heywood 2025-01-25 03:24:20 -05:00
parent 611aad6665
commit 2ed1e64db8
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
3 changed files with 473 additions and 12 deletions

View File

@ -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<String>,
@ -54,11 +66,30 @@ pub struct Transfer {
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>,
gas_factor: Option<String>,
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<String>,
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<String>,
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::<HashMap<String, String>>();
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,
}
}))
}

View File

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

View File

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