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 tendermint::chain::Id,
11 tx::{
12 self,
13 Fee,
14 MessageExt,
15 SignDoc,
16 SignerInfo,
17 },
18 AccountId,
19 Coin,
20 Denom,
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}
54
55impl Client {
56 pub async fn new(endpoints: Endpoints, topic: [u8; 32]) -> anyhow::Result<Self> {
58 let coin_denom = http_api::coin_denom(&endpoints.blockchain_rest_api)
59 .await?
60 .parse()
61 .map_err(|e| anyhow::anyhow!("{e:?}"))?;
62 let account_prefix =
63 http_api::get_account_prefix(&endpoints.blockchain_rest_api).await?;
64 let ss_chain_id = http_api::chain_id(&endpoints.blockchain_rest_api)
65 .await?
66 .parse()
67 .map_err(|e| anyhow::anyhow!("{e:?}"))?;
68 let ss_config = http_api::config(&endpoints.blockchain_rest_api).await?;
69
70 let mut minimum_gas_price = ss_config.minimum_gas_price;
71
72 if let Some(index) = minimum_gas_price.find('.') {
73 minimum_gas_price.truncate(index);
74 }
75 let gas_price: u128 = minimum_gas_price.parse()?;
76 let gas_price = gas_price.saturating_add(1);
78
79 Ok(Self {
80 topic,
81 endpoints,
82 account_prefix,
83 coin_denom,
84 ss_chain_id,
85 gas_price,
86 })
87 }
88
89 pub fn sender_account_id<S: Signer>(&self, signer: &S) -> anyhow::Result<AccountId> {
91 let sender_public_key = signer.public_key();
92 let sender_account_id = sender_public_key
93 .account_id(&self.account_prefix)
94 .map_err(|err| anyhow!("{err:?}"))?;
95
96 Ok(sender_account_id)
97 }
98
99 fn tendermint(&self) -> anyhow::Result<tendermint_rpc::HttpClient> {
100 Ok(tendermint_rpc::HttpClient::new(
101 &*self.endpoints.tendermint_rpc_api,
102 )?)
103 }
104
105 pub async fn latest_block_height(&self) -> anyhow::Result<u32> {
107 Ok(self
108 .tendermint()?
109 .abci_info()
110 .await?
111 .last_block_height
112 .value()
113 .try_into()?)
114 }
115
116 pub async fn get_account_meta<S: Signer>(
118 &self,
119 signer: &S,
120 ) -> anyhow::Result<AccountMetadata> {
121 let sender_account_id = self.sender_account_id(signer)?;
122 http_api::get_account(&self.endpoints.blockchain_rest_api, sender_account_id)
123 .await
124 }
125
126 pub async fn get_topic(&self) -> anyhow::Result<Option<TopicInfo>> {
128 http_api::get_topic(&self.endpoints.blockchain_rest_api, self.topic).await
129 }
130
131 pub async fn send<S: Signer>(
135 &self,
136 signer: &S,
137 account: AccountMetadata,
138 order: u64,
139 blob: Vec<u8>,
140 ) -> anyhow::Result<()> {
141 let latest_height = self.latest_block_height().await?;
142
143 self.send_raw(
144 latest_height.saturating_add(64),
148 signer,
149 account,
150 order,
151 self.topic,
152 Bytes::from(blob),
153 )
154 .await
155 }
156
157 #[allow(clippy::too_many_arguments)]
159 pub async fn send_raw<S: Signer>(
160 &self,
161 timeout_height: u32,
162 signer: &S,
163 account: AccountMetadata,
164 order: u64,
165 topic: [u8; 32],
166 data: Bytes,
167 ) -> anyhow::Result<()> {
168 let dummy_amount = Coin {
172 amount: 0,
173 denom: self.coin_denom.clone(),
174 };
175
176 let dummy_fee = Fee::from_amount_and_gas(dummy_amount, 0u64);
177
178 let dummy_payload = self
179 .make_payload(
180 timeout_height,
181 dummy_fee,
182 signer,
183 account,
184 order,
185 topic,
186 data.clone(),
187 )
188 .await?;
189
190 let used_gas = http_api::estimate_transaction(
191 &self.endpoints.blockchain_rest_api,
192 dummy_payload,
193 )
194 .await?;
195
196 let used_gas = used_gas.saturating_mul(2); let amount = Coin {
199 amount: self.gas_price.saturating_mul(used_gas as u128),
200 denom: self.coin_denom.clone(),
201 };
202
203 let fee = Fee::from_amount_and_gas(amount, used_gas);
204 let payload = self
205 .make_payload(timeout_height, fee, signer, account, order, topic, data)
206 .await?;
207
208 let r = self.tendermint()?.broadcast_tx_sync(payload).await?;
209 if r.code.is_err() {
210 return Err(PostBlobError { message: r.log }.into());
211 }
212 Ok(())
213 }
214
215 #[allow(clippy::too_many_arguments)]
216 async fn make_payload<S: Signer>(
217 &self,
218 timeout_height: u32,
219 fee: Fee,
220 signer: &S,
221 account: AccountMetadata,
222 order: u64,
223 topic: [u8; 32],
224 data: Bytes,
225 ) -> anyhow::Result<Vec<u8>> {
226 let sender_account_id = self.sender_account_id(signer)?;
227
228 let msg = MsgPostBlob {
229 from: sender_account_id.to_string(),
230 order: order.to_string(),
231 topic: Bytes::from(topic.to_vec()),
232 data,
233 };
234 let any_msg = cosmrs::Any {
235 type_url: "/fuelsequencer.sequencing.v1.MsgPostBlob".to_owned(),
236 value: msg.encode_to_vec(),
237 };
238 let tx_body = tx::Body::new(vec![any_msg], "", timeout_height);
239
240 let sender_public_key = signer.public_key();
241 let signer_info =
242 SignerInfo::single_direct(Some(sender_public_key), account.sequence);
243 let auth_info = signer_info.auth_info(fee);
244 let sign_doc = SignDoc::new(
245 &tx_body,
246 &auth_info,
247 &self.ss_chain_id,
248 account.account_number,
249 )
250 .map_err(|err| anyhow!("{err:?}"))?;
251
252 let sign_doc_bytes = sign_doc
253 .clone()
254 .into_bytes()
255 .map_err(|err| anyhow!("{err:?}"))?;
256 let signature = signer.sign(&sign_doc_bytes).await?;
257 let signature = signature.remove_recovery_id();
258
259 Ok(cosmos_sdk_proto::cosmos::tx::v1beta1::TxRaw {
260 body_bytes: sign_doc.body_bytes,
261 auth_info_bytes: sign_doc.auth_info_bytes,
262 signatures: vec![signature.to_vec()],
263 }
264 .to_bytes()?)
265 }
266}