|
| 1 | +use std::collections::VecDeque; |
| 2 | + |
| 3 | +use attested_light_client::msg::QueryMsg; |
| 4 | +use attested_light_client_types::Header; |
| 5 | +use ibc_union_spec::Timestamp; |
| 6 | +use jsonrpsee::{ |
| 7 | + Extensions, |
| 8 | + core::{RpcResult, async_trait}, |
| 9 | + types::ErrorObject, |
| 10 | +}; |
| 11 | +use protos::cosmwasm::wasm::v1::{QuerySmartContractStateRequest, QuerySmartContractStateResponse}; |
| 12 | +use serde::{Deserialize, Serialize}; |
| 13 | +use tracing::instrument; |
| 14 | +use unionlabs::{ |
| 15 | + ErrorReporter, |
| 16 | + ibc::core::client::height::Height, |
| 17 | + never::Never, |
| 18 | + primitives::{Bech32, H256}, |
| 19 | +}; |
| 20 | +use voyager_sdk::{ |
| 21 | + DefaultCmd, anyhow, |
| 22 | + hook::UpdateHook, |
| 23 | + into_value, |
| 24 | + message::{ |
| 25 | + PluginMessage, VoyagerMessage, |
| 26 | + call::Call, |
| 27 | + data::{Data, DecodedHeaderMeta, OrderedHeaders}, |
| 28 | + }, |
| 29 | + plugin::Plugin, |
| 30 | + primitives::{ChainId, ClientType}, |
| 31 | + rpc::{FATAL_JSONRPC_ERROR_CODE, PluginServer, types::PluginInfo}, |
| 32 | + vm::{Op, Visit, data, pass::PassResult}, |
| 33 | +}; |
| 34 | + |
| 35 | +use crate::call::{FetchUpdate, ModuleCall}; |
| 36 | + |
| 37 | +pub mod call { |
| 38 | + use serde::{Deserialize, Serialize}; |
| 39 | + |
| 40 | + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 41 | + pub enum ModuleCall { |
| 42 | + FetchUpdate(FetchUpdate), |
| 43 | + } |
| 44 | + |
| 45 | + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 46 | + pub struct FetchUpdate { |
| 47 | + pub to: u64, |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +#[tokio::main(flavor = "multi_thread")] |
| 52 | +async fn main() { |
| 53 | + Module::run().await; |
| 54 | +} |
| 55 | + |
| 56 | +#[derive(Debug, Clone)] |
| 57 | +pub struct Module { |
| 58 | + pub chain_id: ChainId, |
| 59 | + pub attestation_client_address: Bech32<H256>, |
| 60 | + pub cometbft_client: cometbft_rpc::Client, |
| 61 | +} |
| 62 | + |
| 63 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 64 | +#[serde(deny_unknown_fields)] |
| 65 | +pub struct Config { |
| 66 | + pub chain_id: ChainId, |
| 67 | + pub attestation_client_address: Bech32<H256>, |
| 68 | + pub rpc_url: String, |
| 69 | +} |
| 70 | + |
| 71 | +impl Plugin for Module { |
| 72 | + type Call = ModuleCall; |
| 73 | + type Callback = Never; |
| 74 | + |
| 75 | + type Config = Config; |
| 76 | + type Cmd = DefaultCmd; |
| 77 | + |
| 78 | + async fn new(config: Self::Config) -> anyhow::Result<Self> { |
| 79 | + Ok(Self { |
| 80 | + chain_id: config.chain_id, |
| 81 | + attestation_client_address: config.attestation_client_address, |
| 82 | + cometbft_client: cometbft_rpc::Client::new(config.rpc_url).await?, |
| 83 | + }) |
| 84 | + } |
| 85 | + |
| 86 | + fn info(config: Self::Config) -> PluginInfo { |
| 87 | + PluginInfo { |
| 88 | + name: plugin_name(&config.chain_id), |
| 89 | + interest_filter: UpdateHook::filter( |
| 90 | + &config.chain_id, |
| 91 | + &ClientType::new(ClientType::ATTESTED), |
| 92 | + ), |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + async fn cmd(_: Self::Config, cmd: Self::Cmd) { |
| 97 | + match cmd {} |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +#[async_trait] |
| 102 | +impl PluginServer<ModuleCall, Never> for Module { |
| 103 | + #[instrument(skip_all, fields(chain_id = %self.chain_id))] |
| 104 | + async fn run_pass( |
| 105 | + &self, |
| 106 | + _: &Extensions, |
| 107 | + msgs: Vec<Op<VoyagerMessage>>, |
| 108 | + ) -> RpcResult<PassResult<VoyagerMessage>> { |
| 109 | + Ok(PassResult { |
| 110 | + optimize_further: vec![], |
| 111 | + ready: msgs |
| 112 | + .into_iter() |
| 113 | + .map(|mut op| { |
| 114 | + UpdateHook::new( |
| 115 | + &self.chain_id, |
| 116 | + &ClientType::new(ClientType::ATTESTED), |
| 117 | + |fetch| { |
| 118 | + Call::Plugin(PluginMessage::new( |
| 119 | + self.plugin_name(), |
| 120 | + ModuleCall::FetchUpdate(FetchUpdate { |
| 121 | + to: fetch.update_to.height(), |
| 122 | + }), |
| 123 | + )) |
| 124 | + }, |
| 125 | + ) |
| 126 | + .visit_op(&mut op); |
| 127 | + |
| 128 | + op |
| 129 | + }) |
| 130 | + .enumerate() |
| 131 | + .map(|(i, op)| (vec![i], op)) |
| 132 | + .collect(), |
| 133 | + }) |
| 134 | + } |
| 135 | + |
| 136 | + #[instrument(skip_all, fields(chain_id = %self.chain_id))] |
| 137 | + async fn call(&self, _: &Extensions, msg: ModuleCall) -> RpcResult<Op<VoyagerMessage>> { |
| 138 | + match msg { |
| 139 | + ModuleCall::FetchUpdate(FetchUpdate { to }) => { |
| 140 | + let timestamp = self.query_attested_timestamp_at_height(to).await?; |
| 141 | + |
| 142 | + Ok(data(OrderedHeaders { |
| 143 | + headers: vec![( |
| 144 | + DecodedHeaderMeta { |
| 145 | + height: Height::new(to), |
| 146 | + }, |
| 147 | + into_value(Header { |
| 148 | + height: to, |
| 149 | + timestamp, |
| 150 | + }), |
| 151 | + )], |
| 152 | + })) |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + #[instrument(skip_all, fields(chain_id = %self.chain_id))] |
| 158 | + async fn callback( |
| 159 | + &self, |
| 160 | + _: &Extensions, |
| 161 | + cb: Never, |
| 162 | + _: VecDeque<Data>, |
| 163 | + ) -> RpcResult<Op<VoyagerMessage>> { |
| 164 | + match cb {} |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +fn plugin_name(chain_id: &ChainId) -> String { |
| 169 | + pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME"); |
| 170 | + |
| 171 | + format!("{PLUGIN_NAME}/{}", chain_id) |
| 172 | +} |
| 173 | + |
| 174 | +impl Module { |
| 175 | + fn plugin_name(&self) -> String { |
| 176 | + plugin_name(&self.chain_id) |
| 177 | + } |
| 178 | + |
| 179 | + async fn query_attested_timestamp_at_height(&self, height: u64) -> RpcResult<Timestamp> { |
| 180 | + let req = QuerySmartContractStateRequest { |
| 181 | + address: self.attestation_client_address.to_string(), |
| 182 | + query_data: serde_json::to_vec(&QueryMsg::TimestampAtHeight { |
| 183 | + chain_id: self.chain_id.to_string(), |
| 184 | + height, |
| 185 | + }) |
| 186 | + .unwrap(), |
| 187 | + }; |
| 188 | + |
| 189 | + let raw = self |
| 190 | + .cometbft_client |
| 191 | + .grpc_abci_query::<_, QuerySmartContractStateResponse>( |
| 192 | + "/cosmwasm.wasm.v1.Query/SmartContractState", |
| 193 | + &req, |
| 194 | + None, |
| 195 | + false, |
| 196 | + ) |
| 197 | + .await |
| 198 | + .map_err(|e| { |
| 199 | + ErrorObject::owned( |
| 200 | + -1, |
| 201 | + ErrorReporter(e).with_message("error fetching attested timestamp at height"), |
| 202 | + None::<()>, |
| 203 | + ) |
| 204 | + })? |
| 205 | + .into_result() |
| 206 | + .map_err(|e| { |
| 207 | + ErrorObject::owned( |
| 208 | + -1, |
| 209 | + ErrorReporter(e).with_message("error fetching attested timestamp at height"), |
| 210 | + None::<()>, |
| 211 | + ) |
| 212 | + })? |
| 213 | + .unwrap() |
| 214 | + .data; |
| 215 | + |
| 216 | + Ok(serde_json::from_slice::<Option<Timestamp>>(&raw) |
| 217 | + .map_err(|e| { |
| 218 | + ErrorObject::owned( |
| 219 | + FATAL_JSONRPC_ERROR_CODE, |
| 220 | + ErrorReporter(e).with_message("height {height} has not been attested to"), |
| 221 | + None::<()>, |
| 222 | + ) |
| 223 | + })? |
| 224 | + .unwrap()) |
| 225 | + } |
| 226 | +} |
0 commit comments