fuel_core_shared_sequencer/
lib.rs1#![deny(clippy::arithmetic_side_effects)]
4#![deny(clippy::cast_possible_truncation)]
5#![deny(unused_crate_dependencies)]
6#![deny(missing_docs)]
7
8use anyhow::anyhow;
9use cosmrs::{
10 AccountId,
11 Coin,
12 Denom,
13 tendermint::chain::Id,
14 tx::{
15 self,
16 Fee,
17 MessageExt,
18 SignDoc,
19 SignerInfo,
20 },
21};
22use error::PostBlobError;
23use fuel_sequencer_proto::protos::fuelsequencer::sequencing::v1::MsgPostBlob;
24use http_api::{
25 AccountMetadata,
26 TopicInfo,
27};
28use ports::Signer;
29use prost::Message;
30use tendermint_rpc::Client as _;
31
32pub use config::{
34 Config,
35 Endpoints,
36};
37pub use prost::bytes::Bytes;
38
39mod config;
40mod error;
41mod http_api;
42pub mod ports;
43pub mod service;
44
45pub struct Client {
47 endpoints: Endpoints,
48 topic: [u8; 32],
49 ss_chain_id: Id,
50 gas_price: u128,
51 coin_denom: Denom,
52 account_prefix: String,
53 http: reqwest::Client,
54 request_timeout: core::time::Duration,
55}
56
57impl Client {
58 pub async fn new(
60 endpoints: Endpoints,
61 topic: [u8; 32],
62 request_timeout: core::time::Duration,
63 connect_timeout: core::time::Duration,
64 ) -> anyhow::Result<Self> {
65 let http = reqwest::Client::builder()
66 .timeout(request_timeout)
67 .connect_timeout(connect_timeout)
68 .build()?;
69
70 let coin_denom = http_api::coin_denom(&http, &endpoints.blockchain_rest_api)
71 .await?
72 .parse()
73 .map_err(|e| anyhow::anyhow!("{e:?}"))?;
74 let account_prefix =
75 http_api::get_account_prefix(&http, &endpoints.blockchain_rest_api).await?;
76 let ss_chain_id = http_api::chain_id(&http, &endpoints.blockchain_rest_api)
77 .await?
78 .parse()
79 .map_err(|e| anyhow::anyhow!("{e:?}"))?;
80 let ss_config = http_api::config(&http, &endpoints.blockchain_rest_api).await?;
81
82 let mut minimum_gas_price = ss_config.minimum_gas_price;
83
84 if let Some(index) = minimum_gas_price.find('.') {
85 minimum_gas_price.truncate(index);
86 }
87 let gas_price: u128 = minimum_gas_price.parse()?;
88 let gas_price = gas_price.saturating_add(1);
90
91 Ok(Self {
92 topic,
93 endpoints,
94 account_prefix,
95 coin_denom,
96 ss_chain_id,
97 gas_price,
98 http,
99 request_timeout,
100 })
101 }
102
103 pub fn sender_account_id<S: Signer>(&self, signer: &S) -> anyhow::Result<AccountId> {
105 let sender_public_key = signer.public_key();
106 let sender_account_id = sender_public_key
107 .account_id(&self.account_prefix)
108 .map_err(|err| anyhow!("{err:?}"))?;
109
110 Ok(sender_account_id)
111 }
112
113 fn tendermint(&self) -> anyhow::Result<tendermint_rpc::HttpClient> {
114 Ok(tendermint_rpc::HttpClient::new(
115 &*self.endpoints.tendermint_rpc_api,
116 )?)
117 }
118
119 pub async fn latest_block_height(&self) -> anyhow::Result<u32> {
121 let info =
122 tokio::time::timeout(self.request_timeout, self.tendermint()?.abci_info())
123 .await
124 .map_err(|_| {
125 anyhow!("Timeout fetching latest block height from tendermint")
126 })??;
127 Ok(info.last_block_height.value().try_into()?)
128 }
129
130 pub async fn get_account_meta<S: Signer>(
132 &self,
133 signer: &S,
134 ) -> anyhow::Result<AccountMetadata> {
135 let sender_account_id = self.sender_account_id(signer)?;
136 http_api::get_account(
137 &self.http,
138 &self.endpoints.blockchain_rest_api,
139 sender_account_id,
140 )
141 .await
142 }
143
144 pub async fn get_topic(&self) -> anyhow::Result<Option<TopicInfo>> {
146 http_api::get_topic(&self.http, &self.endpoints.blockchain_rest_api, self.topic)
147 .await
148 }
149
150 pub async fn send<S: Signer>(
154 &self,
155 signer: &S,
156 account: AccountMetadata,
157 order: u64,
158 blob: Vec<u8>,
159 ) -> anyhow::Result<()> {
160 let latest_height = self.latest_block_height().await?;
161
162 self.send_raw(
163 latest_height.saturating_add(64),
167 signer,
168 account,
169 order,
170 self.topic,
171 Bytes::from(blob),
172 )
173 .await
174 }
175
176 #[allow(clippy::too_many_arguments)]
178 pub async fn send_raw<S: Signer>(
179 &self,
180 timeout_height: u32,
181 signer: &S,
182 account: AccountMetadata,
183 order: u64,
184 topic: [u8; 32],
185 data: Bytes,
186 ) -> anyhow::Result<()> {
187 let dummy_amount = Coin {
191 amount: 0,
192 denom: self.coin_denom.clone(),
193 };
194
195 let dummy_fee = Fee::from_amount_and_gas(dummy_amount, 0u64);
196
197 let dummy_payload = self
198 .make_payload(
199 timeout_height,
200 dummy_fee,
201 signer,
202 account,
203 order,
204 topic,
205 data.clone(),
206 )
207 .await?;
208
209 let used_gas = http_api::estimate_transaction(
210 &self.http,
211 &self.endpoints.blockchain_rest_api,
212 dummy_payload,
213 )
214 .await?;
215
216 let used_gas = used_gas.saturating_mul(2); let amount = Coin {
219 amount: self.gas_price.saturating_mul(used_gas as u128),
220 denom: self.coin_denom.clone(),
221 };
222
223 let fee = Fee::from_amount_and_gas(amount, used_gas);
224 let payload = self
225 .make_payload(timeout_height, fee, signer, account, order, topic, data)
226 .await?;
227
228 let r = tokio::time::timeout(
229 self.request_timeout,
230 self.tendermint()?.broadcast_tx_sync(payload),
231 )
232 .await
233 .map_err(|_| anyhow!("Timeout broadcasting tx to tendermint"))??;
234 if r.code.is_err() {
235 return Err(PostBlobError { message: r.log }.into());
236 }
237 Ok(())
238 }
239
240 #[allow(clippy::too_many_arguments)]
241 async fn make_payload<S: Signer>(
242 &self,
243 timeout_height: u32,
244 fee: Fee,
245 signer: &S,
246 account: AccountMetadata,
247 order: u64,
248 topic: [u8; 32],
249 data: Bytes,
250 ) -> anyhow::Result<Vec<u8>> {
251 let sender_account_id = self.sender_account_id(signer)?;
252
253 let msg = MsgPostBlob {
254 from: sender_account_id.to_string(),
255 order: order.to_string(),
256 topic: Bytes::from(topic.to_vec()),
257 data,
258 };
259 let any_msg = cosmrs::Any {
260 type_url: "/fuelsequencer.sequencing.v1.MsgPostBlob".to_owned(),
261 value: msg.encode_to_vec(),
262 };
263 let tx_body = tx::Body::new(vec![any_msg], "", timeout_height);
264
265 let sender_public_key = signer.public_key();
266 let signer_info =
267 SignerInfo::single_direct(Some(sender_public_key), account.sequence);
268 let auth_info = signer_info.auth_info(fee);
269 let sign_doc = SignDoc::new(
270 &tx_body,
271 &auth_info,
272 &self.ss_chain_id,
273 account.account_number,
274 )
275 .map_err(|err| anyhow!("{err:?}"))?;
276
277 let sign_doc_bytes = sign_doc
278 .clone()
279 .into_bytes()
280 .map_err(|err| anyhow!("{err:?}"))?;
281 let signature = signer.sign(&sign_doc_bytes).await?;
282 let signature = signature.remove_recovery_id();
283
284 Ok(cosmos_sdk_proto::cosmos::tx::v1beta1::TxRaw {
285 body_bytes: sign_doc.body_bytes,
286 auth_info_bytes: sign_doc.auth_info_bytes,
287 signatures: vec![signature.to_vec()],
288 }
289 .to_bytes()?)
290 }
291}