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    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
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}
54
55impl Client {
56    /// Create a new shared sequencer client from config.
57    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        // Ceil the gas price to the next integer.
77        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    /// Returns the Cosmos account ID of the sender.
90    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    /// Retrieve latest block height
106    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    /// Retrieve account metadata by its ID
117    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    /// Retrieve the topic info, if it exists
127    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    /// Post a sealed block to the sequencer chain using some
132    /// reasonable defaults and the config.
133    /// This is a convenience wrapper for `send_raw`.
134    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            // We don't want our transactions to be stay in the mempool for a long time,
145            // so we set the timeout height to be 64 blocks ahead of the latest block height.
146            // The 64 is an arbitrary number.
147            latest_height.saturating_add(64),
148            signer,
149            account,
150            order,
151            self.topic,
152            Bytes::from(blob),
153        )
154        .await
155    }
156
157    /// Post a blob of raw data to the sequencer chain
158    #[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        // We want to estimate the transaction to know what amount and fee to use.
169        // We use a dummy amount and fee to estimate the gas, and based on the result
170        // we calculate the actual amount and fee to use in real transaction.
171        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); // Add some buffer
197
198        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}