use core::time::Duration;
use std::ops::Add;
use bytes::BufMut;
use flex_error::define_error;
use tendermint::Hash as TxHash;
use ibc_proto::cosmos::gov::v1beta1::MsgSubmitProposal;
use ibc_proto::cosmos::upgrade::v1beta1::Plan;
use ibc_proto::google::protobuf::Any;
use ibc_proto::ibc::core::client::v1::UpgradeProposal;
use ibc_relayer_types::clients::ics07_tendermint::client_state::UpgradeOptions;
use ibc_relayer_types::core::ics02_client::client_state::ClientState;
use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ClientId};
use ibc_relayer_types::{downcast, Height};
use crate::chain::handle::ChainHandle;
use crate::chain::requests::{IncludeProof, QueryClientStateRequest, QueryHeight};
use crate::chain::tracking::TrackedMsgs;
use crate::client_state::AnyClientState;
use crate::error::Error;
define_error! {
UpgradeChainError {
Query
[ Error ]
|_| { "error during a query" },
Key
[ Error ]
|_| { "key error" },
Submit
{ chain_id: ChainId }
[ Error ]
|e| {
format!("failed while submitting the Transfer message to chain {0}", e.chain_id)
},
TxResponse
{ event: String }
|e| {
format!("tx response event consists of an error: {}", e.event)
},
TendermintOnly
|_| { "only Tendermint clients can be upgraded" },
UpgradeHeightRevision
{ revision: u64 }
|r| {
format!("invalid upgrade height revision: {r}")
}
}
}
#[derive(Clone, Debug)]
pub struct UpgradePlanOptions {
pub src_client_id: ClientId,
pub amount: u64,
pub denom: String,
pub height_offset: u64,
pub upgraded_chain_id: ChainId,
pub upgraded_unbonding_period: Option<Duration>,
pub upgrade_plan_name: String,
}
pub fn build_and_send_ibc_upgrade_proposal(
dst_chain: impl ChainHandle, src_chain: impl ChainHandle, opts: &UpgradePlanOptions,
) -> Result<TxHash, UpgradeChainError> {
let plan_height = dst_chain
.query_latest_height() .map_err(UpgradeChainError::query)?
.add(opts.height_offset);
let upgraded_client_latest_height =
if dst_chain.id().version() == opts.upgraded_chain_id.version() {
plan_height.increment()
} else {
Height::new(opts.upgraded_chain_id.version(), 1).map_err(|_| {
UpgradeChainError::upgrade_height_revision(opts.upgraded_chain_id.version())
})?
};
let (client_state, _) = src_chain
.query_client_state(
QueryClientStateRequest {
client_id: opts.src_client_id.clone(),
height: QueryHeight::Latest,
},
IncludeProof::No,
)
.map_err(UpgradeChainError::query)?;
let mut client_state = downcast!(client_state => AnyClientState::Tendermint)
.ok_or_else(UpgradeChainError::tendermint_only)?;
let upgrade_options = UpgradeOptions {
unbonding_period: opts
.upgraded_unbonding_period
.unwrap_or(client_state.unbonding_period),
};
client_state.upgrade(
upgraded_client_latest_height,
upgrade_options,
opts.upgraded_chain_id.clone(),
);
let proposal = UpgradeProposal {
title: "proposal 0".to_string(),
description: "upgrade the chain software and unbonding period".to_string(),
upgraded_client_state: Some(Any::from(AnyClientState::from(client_state))),
plan: Some(Plan {
name: opts.upgrade_plan_name.clone(),
height: plan_height.revision_height() as i64,
info: "".to_string(),
..Default::default() }),
};
let proposal = Proposal::Default(proposal);
let mut buf_proposal = Vec::new();
proposal.encode(&mut buf_proposal);
let any_proposal = Any {
type_url: proposal.type_url(),
value: buf_proposal,
};
let proposer = dst_chain.get_signer().map_err(UpgradeChainError::key)?;
let coins = ibc_proto::cosmos::base::v1beta1::Coin {
denom: opts.denom.clone(),
amount: opts.amount.to_string(),
};
let msg = MsgSubmitProposal {
content: Some(any_proposal),
initial_deposit: vec![coins],
proposer: proposer.to_string(),
};
let mut buf_msg = Vec::new();
prost::Message::encode(&msg, &mut buf_msg).unwrap();
let any_msg = Any {
type_url: "/cosmos.gov.v1beta1.MsgSubmitProposal".to_string(),
value: buf_msg,
};
let responses = dst_chain
.send_messages_and_wait_check_tx(TrackedMsgs::new_single(any_msg, "upgrade"))
.map_err(|e| UpgradeChainError::submit(dst_chain.id(), e))?;
Ok(responses[0].hash)
}
enum Proposal {
Default(UpgradeProposal),
}
impl Proposal {
fn encode(&self, buf: &mut impl BufMut) {
match self {
Proposal::Default(p) => prost::Message::encode(p, buf),
}
.unwrap()
}
fn type_url(&self) -> String {
match self {
Proposal::Default(_) => "/ibc.core.client.v1.UpgradeProposal",
}
.to_owned()
}
}