diff --git a/crates/by-chain/icepick-cosmos/src/lib.rs b/crates/by-chain/icepick-cosmos/src/lib.rs index 7e461dc..482f54a 100644 --- a/crates/by-chain/icepick-cosmos/src/lib.rs +++ b/crates/by-chain/icepick-cosmos/src/lib.rs @@ -93,6 +93,15 @@ pub struct Withdraw { blockchain_config: coin_denoms::Blockchain, } +#[derive(Serialize, Deserialize, Debug)] +pub struct WithdrawRewards { + delegate_address: String, + validator_address: String, + + gas_factor: Option, + blockchain_config: coin_denoms::Blockchain, +} + #[derive(Deserialize, Serialize, Debug)] pub struct Sign { fee: remote_serde::Fee, @@ -132,6 +141,7 @@ pub enum Operation { Transfer(Transfer), Stake(Stake), Withdraw(Withdraw), + WithdrawRewards(WithdrawRewards), Sign(Sign), Broadcast(Broadcast), } @@ -362,7 +372,7 @@ impl Module for Cosmos { let withdraw = Operation::builder() .name("withdraw") - .description("Delegate coins to a specified validator.") + .description("Withdraw coins delegated to a specified validator.") .build() .argument( &Argument::builder() @@ -400,6 +410,32 @@ impl Module for Cosmos { .build(), ); + let withdraw_rewards = Operation::builder() + .name("withdraw-rewards") + .description("Withdraw staking rewards from a validator.") + .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.") @@ -436,6 +472,7 @@ impl Module for Cosmos { sign, stake, withdraw, + withdraw_rewards, broadcast, ] } @@ -806,6 +843,58 @@ impl Module for Cosmos { "derivation_accounts": [0u32 | 1 << 31], })) } + Operation::WithdrawRewards(WithdrawRewards { + delegate_address, + validator_address, + gas_factor, + blockchain_config, + }) => { + let gas_denom = blockchain_config.fee_currencies[0].currency.clone(); + + let gas_factor = gas_factor + .as_deref() + .map(f64::from_str) + .transpose() + .unwrap() + .unwrap_or(1.0); + + let delegate_id = AccountId::from_str(&delegate_address).unwrap(); + let validator_id = AccountId::from_str(&validator_address).unwrap(); + + let msg_withdraw_rewards = cosmrs::distribution::MsgWithdrawDelegatorReward { + delegator_address: delegate_id, + validator_address: validator_id, + } + .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: gas_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_withdraw_rewards], + // re-export, but in general this should be copied over + // using workflows + }, + "derivation_accounts": [0u32 | 1 << 31], + })) + } Operation::Transfer(Transfer { amount, denom,