use dusk_core::abi::ContractId;
use dusk_core::stake::StakeEvent;
use dusk_core::transfer::phoenix::StealthAddress;
use dusk_core::transfer::{
ConvertEvent, DepositEvent, MoonlightTransactionEvent, Transaction,
WithdrawEvent,
};
use serde::Deserialize;
use serde_json::Value;
use serde_with::hex::Hex;
use serde_with::{As, DisplayFromStr};
use tokio::time::{Duration, sleep};
use crate::rues::HttpClient as RuesHttpClient;
use crate::{Address, Error};
#[derive(Clone)]
pub struct GraphQL {
state_client: RuesHttpClient,
archiver_client: RuesHttpClient,
status: fn(&str),
}
pub struct BlockTransaction {
pub tx: Transaction,
pub id: String,
pub gas_spent: u64,
}
#[derive(Deserialize)]
struct SpentTx {
pub id: String,
#[serde(default)]
pub raw: String,
pub err: Option<String>,
#[serde(alias = "gasSpent", default)]
pub gas_spent: u64,
}
#[derive(Deserialize)]
struct Block {
pub transactions: Vec<SpentTx>,
}
#[derive(Deserialize)]
struct BlockResponse {
pub block: Option<Block>,
}
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)]
struct NoteAddress {
stealth_address: StealthAddress,
}
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
pub struct PhoenixTransactionEventSubset {
#[serde(rename(deserialize = "notes"))]
note_addresses: Vec<NoteAddress>,
#[serde(with = "As::<Hex>")]
memo: Vec<u8>,
#[serde(with = "As::<DisplayFromStr>")]
gas_spent: u64,
#[serde(rename(deserialize = "refund_note"))]
refund_note_address: Option<NoteAddress>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum BlockData {
MoonlightTransactionEvent(MoonlightTransactionEvent),
PhoenixTransactionEvent(PhoenixTransactionEventSubset),
ConvertEvent(ConvertEvent),
StakeEvent(StakeEvent),
DepositEvent(DepositEvent),
WithdrawEvent(WithdrawEvent),
}
#[derive(Deserialize, Debug)]
pub struct BlockEvents {
pub data: BlockData,
pub target: ContractId,
}
#[derive(Deserialize, Debug)]
pub struct MoonlightHistory {
pub block_height: u64,
pub origin: String,
pub events: Vec<BlockEvents>,
}
#[derive(Deserialize, Debug)]
pub struct MoonlightHistoryJson {
pub json: Vec<MoonlightHistory>,
}
#[derive(Deserialize, Debug)]
pub struct FullMoonlightHistory {
#[serde(rename(deserialize = "fullMoonlightHistory"))]
pub full_moonlight_history: Option<MoonlightHistoryJson>,
}
#[derive(Deserialize)]
struct SpentTxResponse {
pub tx: Option<SpentTx>,
}
#[derive(Debug)]
pub enum TxStatus {
Ok,
NotFound,
Error(String),
}
impl GraphQL {
pub fn new<S: Into<String>>(
state_url: S,
archiver_url: S,
status: fn(&str),
) -> Result<Self, Error> {
Ok(Self {
state_client: RuesHttpClient::new(state_url)?,
archiver_client: RuesHttpClient::new(archiver_url)?,
status,
})
}
pub async fn wait_for(&self, tx_id: &str) -> anyhow::Result<()> {
loop {
let status = self.tx_status(tx_id).await?;
match status {
TxStatus::Ok => break,
TxStatus::Error(err) => return Err(Error::Transaction(err))?,
TxStatus::NotFound => {
(self.status)(
"Waiting for tx to be included into a block...",
);
sleep(Duration::from_millis(1000)).await;
}
}
}
Ok(())
}
async fn tx_status(&self, tx_id: &str) -> Result<TxStatus, Error> {
let query =
"query { tx(hash: \"####\") { id, err }}".replace("####", tx_id);
let response = self.query_state(&query).await?;
let response = serde_json::from_slice::<SpentTxResponse>(&response)?.tx;
match response {
Some(SpentTx { err: Some(err), .. }) => Ok(TxStatus::Error(err)),
Some(_) => Ok(TxStatus::Ok),
None => Ok(TxStatus::NotFound),
}
}
pub async fn txs_for_block(
&self,
block_height: u64,
) -> Result<Vec<BlockTransaction>, Error> {
let query = "query { block(height: ####) { transactions {id, raw, gasSpent, err}}}"
.replace("####", block_height.to_string().as_str());
let response = self.query_state(&query).await?;
let response =
serde_json::from_slice::<BlockResponse>(&response)?.block;
let block = response.ok_or(GraphQLError::BlockInfo)?;
let mut ret = vec![];
for spent_tx in block.transactions {
let tx_raw = hex::decode(&spent_tx.raw)
.map_err(|_| GraphQLError::TxStatus)?;
let ph_tx = Transaction::from_slice(&tx_raw)
.map_err(|_| GraphQLError::BytesError)?;
ret.push(BlockTransaction {
tx: ph_tx,
id: spent_tx.id,
gas_spent: spent_tx.gas_spent,
});
}
Ok(ret)
}
pub async fn check_connection(&self) -> Result<(), Error> {
match (self.query_state("").await, self.query_archiver("").await) {
(Ok(_), Ok(_)) => Ok(()),
(Err(e), _) | (_, Err(e)) => Err(e),
}
}
pub async fn moonlight_history(
&self,
public_address: Address,
) -> Result<FullMoonlightHistory, Error> {
let query = format!(
r#"query {{ fullMoonlightHistory(address: "{public_address}") {{ json }} }}"#
);
let response = self
.query_archiver(&query)
.await
.map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
let response =
serde_json::from_slice::<FullMoonlightHistory>(&response)
.map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
Ok(response)
}
pub async fn moonlight_history_at_block(
&self,
public_address: &Address,
block: u64,
) -> Result<FullMoonlightHistory, Error> {
let query = format!(
r#"query {{ fullMoonlightHistory(address: "{public_address}", fromBlock: {block}, toBlock: {block}) {{ json }} }}"#
);
let response = self
.query_archiver(&query)
.await
.map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
let response =
serde_json::from_slice::<FullMoonlightHistory>(&response)
.map_err(|err| Error::ArchiveJsonError(err.to_string()))?;
Ok(response)
}
pub async fn moonlight_tx(
&self,
origin: &str,
) -> Result<Transaction, Error> {
let query =
format!(r#"query {{ tx(hash: "{origin}") {{ tx {{ raw }} }} }}"#);
let response = self.query_state(&query).await?;
let json: Value = serde_json::from_slice(&response)?;
let tx = json
.get("tx")
.and_then(|val| val.get("tx").and_then(|val| val.get("raw")))
.and_then(|val| val.as_str());
if let Some(tx) = tx {
let hex = hex::decode(tx).map_err(|_| GraphQLError::TxStatus)?;
let tx: Transaction = Transaction::from_slice(&hex)?;
Ok(tx)
} else {
Err(Error::GraphQLError(GraphQLError::TxStatus))
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum GraphQLError {
#[error("Error fetching data from the node: {0}")]
Generic(serde_json::Error),
#[error("Failed to obtain transaction status")]
TxStatus,
#[error("Failed to obtain block info")]
BlockInfo,
#[error("A deserialization error occurred")]
BytesError,
}
impl From<serde_json::Error> for GraphQLError {
fn from(e: serde_json::Error) -> Self {
Self::Generic(e)
}
}
impl GraphQL {
pub async fn query_state(&self, query: &str) -> Result<Vec<u8>, Error> {
self.query(&self.state_client, query).await
}
pub async fn query_archiver(&self, query: &str) -> Result<Vec<u8>, Error> {
self.query(&self.archiver_client, query).await
}
async fn query(
&self,
client: &RuesHttpClient,
query: &str,
) -> Result<Vec<u8>, Error> {
client
.call("graphql", None, "query", query.as_bytes())
.await
}
}
#[ignore = "Leave it here just for manual tests"]
#[tokio::test]
async fn test() -> Result<(), Error> {
let gql = GraphQL {
status: |s| {
println!("{s}");
},
state_client: RuesHttpClient::new(
"http://testnet.nodes.dusk.network:9500/graphql",
)?,
archiver_client: RuesHttpClient::new(
"http://testnet.nodes.dusk.network:9500/graphql",
)?,
};
let _ = gql
.tx_status(
"dbc5a2c949516ecfb418406909d195c3cc267b46bd966a3ca9d66d2e13c47003",
)
.await?;
let block_txs = gql.txs_for_block(90).await?;
block_txs.into_iter().for_each(|tx_block| {
let tx = tx_block.tx;
let chain_txid = tx_block.id;
let hash = tx.hash();
let tx_id = hex::encode(hash.to_bytes());
assert_eq!(chain_txid, tx_id);
println!("txid: {tx_id}");
});
Ok(())
}
#[tokio::test]
async fn deser() -> Result<(), Box<dyn std::error::Error>> {
let block_not_found = r#"{"block":null}"#;
serde_json::from_str::<BlockResponse>(block_not_found).unwrap();
let block_without_tx = r#"{"block":{"transactions":[]}}"#;
serde_json::from_str::<BlockResponse>(block_without_tx).unwrap();
let block_with_tx = r#"{"block":{"transactions":[{"id":"88e6804989cc2f3fd5bf94dcd39a4e7b7da9a1114d9b8bf4e0515264bc81c50f"}]}}"#;
serde_json::from_str::<BlockResponse>(block_with_tx).unwrap();
let empty_full_moonlight_history = r#"{"fullMoonlightHistory":null}"#;
serde_json::from_str::<FullMoonlightHistory>(empty_full_moonlight_history)
.unwrap();
let full_moonlight_history =
include_str!("./gql/tests/assets/full_moonlight_history.json");
serde_json::from_str::<FullMoonlightHistory>(full_moonlight_history)
.unwrap();
Ok(())
}