rusk_wallet/
gql.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7//! The graphql endpoint can be queried with this helper struct.
8//! The <node-url>/on/gaphql/query if queried with empty bytes returns the
9//! graphql schema
10
11use dusk_core::transfer::Transaction;
12use serde::Deserialize;
13use serde_json::Value;
14use tokio::time::{sleep, Duration};
15
16use crate::{Address, Error, RuesHttpClient};
17
18/// GraphQL is a helper struct that aggregates all queries done
19/// to the Dusk GraphQL database.
20/// This helps avoid having helper structs and boilerplate code
21/// mixed with the wallet logic.
22#[derive(Clone)]
23pub struct GraphQL {
24    client: RuesHttpClient,
25    status: fn(&str),
26}
27
28/// The tx_for_block returns a Vec<BlockTransaction> which contains
29/// the dusk-core transaction, its id hash and gas spent
30pub struct BlockTransaction {
31    /// The dusk-core transaction struct obtained from GraphQL endpoint
32    pub tx: Transaction,
33    /// The hash of the transaction or the id of the transaction in string utf8
34    pub id: String,
35    /// Gas amount spent for the transaction
36    pub gas_spent: u64,
37}
38
39#[derive(Deserialize)]
40struct SpentTx {
41    pub id: String,
42    #[serde(default)]
43    pub raw: String,
44    pub err: Option<String>,
45    #[serde(alias = "gasSpent", default)]
46    pub gas_spent: f64,
47}
48
49#[derive(Deserialize)]
50struct Block {
51    pub transactions: Vec<SpentTx>,
52}
53
54#[derive(Deserialize)]
55struct BlockResponse {
56    pub block: Option<Block>,
57}
58
59#[derive(Deserialize, Debug)]
60pub struct BlockData {
61    pub gas_spent: u64,
62    pub sender: String,
63    pub value: f64,
64}
65
66#[derive(Deserialize, Debug)]
67pub struct BlockEvents {
68    pub data: BlockData,
69}
70
71#[derive(Deserialize, Debug)]
72pub struct MoonlightHistory {
73    pub block_height: u64,
74    pub origin: String,
75    pub events: Vec<BlockEvents>,
76}
77
78#[derive(Deserialize, Debug)]
79pub struct MoonlightHistoryJson {
80    pub json: Vec<MoonlightHistory>,
81}
82
83#[derive(Deserialize, Debug)]
84pub struct FullMoonlightHistory {
85    #[serde(rename(deserialize = "fullMoonlightHistory"))]
86    pub full_moonlight_history: MoonlightHistoryJson,
87}
88
89#[derive(Deserialize)]
90struct SpentTxResponse {
91    pub tx: Option<SpentTx>,
92}
93
94/// Transaction status
95#[derive(Debug)]
96pub enum TxStatus {
97    Ok,
98    NotFound,
99    Error(String),
100}
101
102impl GraphQL {
103    /// Create a new GraphQL wallet client
104    pub fn new<S: Into<String>>(
105        url: S,
106        status: fn(&str),
107    ) -> Result<Self, Error> {
108        Ok(Self {
109            client: RuesHttpClient::new(url)?,
110            status,
111        })
112    }
113
114    /// Wait for a transaction to be confirmed (included in a block)
115    pub async fn wait_for(&self, tx_id: &str) -> anyhow::Result<()> {
116        loop {
117            let status = self.tx_status(tx_id).await?;
118
119            match status {
120                TxStatus::Ok => break,
121                TxStatus::Error(err) => return Err(Error::Transaction(err))?,
122                TxStatus::NotFound => {
123                    (self.status)(
124                        "Waiting for tx to be included into a block...",
125                    );
126                    sleep(Duration::from_millis(1000)).await;
127                }
128            }
129        }
130        Ok(())
131    }
132
133    /// Obtain transaction status
134    async fn tx_status(&self, tx_id: &str) -> Result<TxStatus, Error> {
135        let query =
136            "query { tx(hash: \"####\") { id, err }}".replace("####", tx_id);
137        let response = self.query(&query).await?;
138        let response = serde_json::from_slice::<SpentTxResponse>(&response)?.tx;
139
140        match response {
141            Some(SpentTx { err: Some(err), .. }) => Ok(TxStatus::Error(err)),
142            Some(_) => Ok(TxStatus::Ok),
143            None => Ok(TxStatus::NotFound),
144        }
145    }
146
147    /// Obtain transactions inside a block
148    pub async fn txs_for_block(
149        &self,
150        block_height: u64,
151    ) -> Result<Vec<BlockTransaction>, Error> {
152        let query = "query { block(height: ####) { transactions {id, raw, gasSpent, err}}}"
153            .replace("####", block_height.to_string().as_str());
154
155        let response = self.query(&query).await?;
156        let response =
157            serde_json::from_slice::<BlockResponse>(&response)?.block;
158        let block = response.ok_or(GraphQLError::BlockInfo)?;
159        let mut ret = vec![];
160
161        for spent_tx in block.transactions {
162            let tx_raw = hex::decode(&spent_tx.raw)
163                .map_err(|_| GraphQLError::TxStatus)?;
164            let ph_tx = Transaction::from_slice(&tx_raw)
165                .map_err(|_| GraphQLError::BytesError)?;
166            ret.push(BlockTransaction {
167                tx: ph_tx,
168                id: spent_tx.id,
169                gas_spent: spent_tx.gas_spent as u64,
170            });
171        }
172
173        Ok(ret)
174    }
175
176    /// Sends an empty body to url to check if its available
177    pub async fn check_connection(&self) -> Result<(), Error> {
178        self.query("").await.map(|_| ())
179    }
180
181    /// Query the archival node for moonlight transactions given the
182    /// BlsPublicKey
183    pub async fn moonlight_history(
184        &self,
185        address: Address,
186    ) -> Result<FullMoonlightHistory, Error> {
187        let query = format!(
188            r#"query {{ fullMoonlightHistory(address: "{address}") {{ json }} }}"#
189        );
190
191        let response = self
192            .query(&query)
193            .await
194            .map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
195
196        let response =
197            serde_json::from_slice::<FullMoonlightHistory>(&response)
198                .map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
199
200        Ok(response)
201    }
202
203    /// Fetch the spent transaction given moonlight tx hash
204    pub async fn moonlight_tx(
205        &self,
206        origin: &str,
207    ) -> Result<Transaction, Error> {
208        let query =
209            format!(r#"query {{ tx(hash: "{origin}") {{ tx {{ raw }} }} }}"#);
210
211        let response = self.query(&query).await?;
212        let json: Value = serde_json::from_slice(&response)?;
213
214        let tx = json
215            .get("tx")
216            .and_then(|val| val.get("tx").and_then(|val| val.get("raw")))
217            .and_then(|val| val.as_str());
218
219        if let Some(tx) = tx {
220            let hex = hex::decode(tx).map_err(|_| GraphQLError::TxStatus)?;
221            let tx: Transaction = Transaction::from_slice(&hex)?;
222            Ok(tx)
223        } else {
224            Err(Error::GraphQLError(GraphQLError::TxStatus))
225        }
226    }
227}
228
229/// Errors generated from GraphQL
230#[derive(Debug, thiserror::Error)]
231pub enum GraphQLError {
232    /// Generic errors
233    #[error("Error fetching data from the node: {0}")]
234    Generic(serde_json::Error),
235    /// Failed to fetch transaction status
236    #[error("Failed to obtain transaction status")]
237    TxStatus,
238    #[error("Failed to obtain block info")]
239    /// Failed to obtain block info
240    BlockInfo,
241    /// Bytes decoding errors
242    #[error("A deserialization error occurred")]
243    BytesError,
244}
245
246impl From<serde_json::Error> for GraphQLError {
247    fn from(e: serde_json::Error) -> Self {
248        Self::Generic(e)
249    }
250}
251
252impl GraphQL {
253    /// Call the graphql endpoint of a node
254    pub async fn query(&self, query: &str) -> Result<Vec<u8>, Error> {
255        self.client
256            .call("graphql", None, "query", query.as_bytes())
257            .await
258    }
259}
260
261#[ignore = "Leave it here just for manual tests"]
262#[tokio::test]
263async fn test() -> Result<(), Error> {
264    let gql = GraphQL {
265        status: |s| {
266            println!("{s}");
267        },
268        client: RuesHttpClient::new(
269            "http://testnet.nodes.dusk.network:9500/graphql",
270        )?,
271    };
272    let _ = gql
273        .tx_status(
274            "dbc5a2c949516ecfb418406909d195c3cc267b46bd966a3ca9d66d2e13c47003",
275        )
276        .await?;
277    let block_txs = gql.txs_for_block(90).await?;
278    block_txs.into_iter().for_each(|tx_block| {
279        let tx = tx_block.tx;
280        let chain_txid = tx_block.id;
281        let hash = tx.hash();
282        let tx_id = hex::encode(hash.to_bytes());
283        assert_eq!(chain_txid, tx_id);
284        println!("txid: {tx_id}");
285    });
286    Ok(())
287}
288
289#[tokio::test]
290async fn deser() -> Result<(), Box<dyn std::error::Error>> {
291    let block_not_found = r#"{"block":null}"#;
292    serde_json::from_str::<BlockResponse>(block_not_found).unwrap();
293
294    let block_without_tx = r#"{"block":{"transactions":[]}}"#;
295    serde_json::from_str::<BlockResponse>(block_without_tx).unwrap();
296
297    let block_with_tx = r#"{"block":{"transactions":[{"id":"88e6804989cc2f3fd5bf94dcd39a4e7b7da9a1114d9b8bf4e0515264bc81c50f"}]}}"#;
298    serde_json::from_str::<BlockResponse>(block_with_tx).unwrap();
299
300    Ok(())
301}