Skip to main content

fuel_core_shared_sequencer/
lib.rs

1//! Shared sequencer client
2
3#![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
32// Re-exports
33pub 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
45/// Shared sequencer client
46pub 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    /// Create a new shared sequencer client from config.
59    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        // Ceil the gas price to the next integer.
89        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    /// Returns the Cosmos account ID of the sender.
104    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    /// Retrieve latest block height
120    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    /// Retrieve account metadata by its ID
131    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    /// Retrieve the topic info, if it exists
145    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    /// Post a sealed block to the sequencer chain using some
151    /// reasonable defaults and the config.
152    /// This is a convenience wrapper for `send_raw`.
153    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            // We don't want our transactions to be stay in the mempool for a long time,
164            // so we set the timeout height to be 64 blocks ahead of the latest block height.
165            // The 64 is an arbitrary number.
166            latest_height.saturating_add(64),
167            signer,
168            account,
169            order,
170            self.topic,
171            Bytes::from(blob),
172        )
173        .await
174    }
175
176    /// Post a blob of raw data to the sequencer chain
177    #[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        // We want to estimate the transaction to know what amount and fee to use.
188        // We use a dummy amount and fee to estimate the gas, and based on the result
189        // we calculate the actual amount and fee to use in real transaction.
190        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); // Add some buffer
217
218        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}