Repository: dusk-network/rusk (language: rust)

Issue:
rusk-wallet: Manage transaction history for Moonlight
#### Summary

Currently, users cannot see their Moonlight transaction history.

#### Possible Solution

Retrieve Moonlight transaction events from Archival nodes, where the events are filtered by the address of the user.

#### Additional context

This has a dependency on the implementation of the storing of the events on the Archival node


Discussion / hints:
depends on #2269 

Fix patch:
diff --git a/rusk-wallet/src/bin/command.rs b/rusk-wallet/src/bin/command.rs
index 9919612ddc..750665c102 100644
--- a/rusk-wallet/src/bin/command.rs
+++ b/rusk-wallet/src/bin/command.rs
@@ -27,12 +27,10 @@ use rusk_wallet::{
 };
 use wallet_core::BalanceInfo;
 
-use crate::io::prompt;
+use crate::io::prompt::{self, create_password};
 use crate::settings::Settings;
 use crate::{WalletFile, WalletPath};
 
-use self::prompt::create_password;
-
 /// Commands that can be run against the Dusk wallet
 #[allow(clippy::large_enum_variant)]
 #[derive(PartialEq, Eq, Hash, Clone, Subcommand, Debug)]
@@ -514,13 +512,23 @@ impl Command {
             }
             Command::History { profile_idx } => {
                 let profile_idx = profile_idx.unwrap_or_default();
+
                 wallet.sync().await?;
                 let notes = wallet.get_all_notes(profile_idx).await?;
+                let address = wallet.public_address(profile_idx)?;
 
-                let transactions =
+                let mut phoenix_history =
                     history::transaction_from_notes(settings, notes).await?;
 
-                Ok(RunResult::PhoenixHistory(transactions))
+                if let Ok(mut moonlight_history) =
+                    history::moonlight_history(settings, address).await
+                {
+                    phoenix_history.append(&mut moonlight_history);
+                } else {
+                    tracing::error!("Cannot fetch archive history");
+                }
+
+                Ok(RunResult::History(phoenix_history))
             }
             Command::Unshield {
                 profile_idx,
@@ -692,7 +700,7 @@ pub enum RunResult<'a> {
     Create(),
     Restore(),
     Settings(),
-    PhoenixHistory(Vec<TransactionHistory>),
+    History(Vec<TransactionHistory>),
 }
 
 impl fmt::Display for RunResult<'_> {
@@ -774,9 +782,9 @@ impl fmt::Display for RunResult<'_> {
                      > Key pair exported to: {kp}",
                 )
             }
-            PhoenixHistory(transactions) => {
+            History(txns) => {
                 writeln!(f, "{}", TransactionHistory::header())?;
-                for th in transactions {
+                for th in txns {
                     writeln!(f, "{th}")?;
                 }
                 Ok(())
diff --git a/rusk-wallet/src/bin/command/history.rs b/rusk-wallet/src/bin/command/history.rs
index c3b3e04577..950ff88b64 100644
--- a/rusk-wallet/src/bin/command/history.rs
+++ b/rusk-wallet/src/bin/command/history.rs
@@ -27,8 +27,8 @@ pub struct TransactionHistory {
 impl TransactionHistory {
     pub fn header() -> String {
         format!(
-            "{: ^9} | {: ^64} | {: ^8} | {: ^17} | {: ^12}",
-            "BLOCK", "TX_ID", "METHOD", "AMOUNT", "FEE"
+            "{: ^9} | {: ^64} | {: ^8} | {: ^17} | {: ^12} | {: ^8}",
+            "BLOCK", "TX_ID", "METHOD", "AMOUNT", "FEE", "TRANSACTION_TYPE"
         )
     }
 }
@@ -42,20 +42,25 @@ impl Display for TransactionHistory {
         };
 
         let fee = match self.direction {
-            TransactionDirection::In => "".into(),
+            TransactionDirection::In => format!("{: >12.9}", ""),
             TransactionDirection::Out => {
-                let fee = self.fee;
+                let fee: u64 = self.fee;
                 let fee = from_dusk(fee);
                 format!("{: >12.9}", fee)
             }
         };
 
         let tx_id = &self.id;
-        let heigth = self.height;
+        let height = self.height;
+
+        let tx_type = match self.tx {
+            Transaction::Moonlight(_) => dusk_core::transfer::MOONLIGHT_TOPIC,
+            Transaction::Phoenix(_) => dusk_core::transfer::PHOENIX_TOPIC,
+        };
 
         write!(
             f,
-            "{heigth: >9} | {tx_id} | {contract: ^8} | {dusk: >+17.9} | {fee}",
+            "{height: >9} | {tx_id} | {contract: ^8} | {dusk: >+17.9} | {fee} | {tx_type}",
         )
     }
 }
@@ -152,7 +157,57 @@ pub(crate) async fn transaction_from_notes(
     Ok(ret)
 }
 
-#[derive(PartialEq)]
+pub(crate) async fn moonlight_history(
+    settings: &Settings,
+    address: rusk_wallet::Address,
+) -> anyhow::Result<Vec<TransactionHistory>> {
+    let gql =
+        GraphQL::new(settings.state.to_string(), io::status::interactive)?;
+
+    let history = gql
+        .moonlight_history(address.clone())
+        .await?
+        .full_moonlight_history;
+
+    let mut collected_history = Vec::new();
+
+    for history_item in history.json {
+        let id = history_item.origin;
+        let events = history_item.events;
+        let height = history_item.block_height;
+        let tx = gql.moonlight_tx(&id).await?;
+
+        for event in events {
+            let data = event.data;
+            let gas_spent = data.gas_spent;
+            let mut amount = data.value;
+            let sender = data.sender;
+
+            let direction: TransactionDirection =
+                match sender == address.to_string() {
+                    true => {
+                        amount = -amount;
+
+                        TransactionDirection::Out
+                    }
+                    false => TransactionDirection::In,
+                };
+
+            collected_history.push(TransactionHistory {
+                direction,
+                height,
+                amount,
+                fee: gas_spent * tx.gas_price(),
+                tx: tx.clone(),
+                id: id.clone(),
+            })
+        }
+    }
+
+    Ok(collected_history)
+}
+
+#[derive(PartialEq, Debug)]
 enum TransactionDirection {
     In,
     Out,
diff --git a/rusk-wallet/src/bin/interactive/command_menu.rs b/rusk-wallet/src/bin/interactive/command_menu.rs
index d5e5b8b945..907fdc31f5 100644
--- a/rusk-wallet/src/bin/interactive/command_menu.rs
+++ b/rusk-wallet/src/bin/interactive/command_menu.rs
@@ -334,6 +334,7 @@ pub(crate) async fn online(
         }
         MenuItem::History => {
             let profile_idx = Some(profile_idx);
+
             ProfileOp::Run(Box::new(Command::History { profile_idx }))
         }
         MenuItem::StakeInfo => ProfileOp::Run(Box::new(Command::StakeInfo {
diff --git a/rusk-wallet/src/bin/main.rs b/rusk-wallet/src/bin/main.rs
index 1e9c4b9ee6..56e339e81f 100644
--- a/rusk-wallet/src/bin/main.rs
+++ b/rusk-wallet/src/bin/main.rs
@@ -392,9 +392,9 @@ async fn exec() -> anyhow::Result<()> {
                 RunResult::ExportedKeys(pub_key, key_pair) => {
                     println!("{},{}", pub_key.display(), key_pair.display())
                 }
-                RunResult::PhoenixHistory(transactions) => {
+                RunResult::History(txns) => {
                     println!("{}", TransactionHistory::header());
-                    for th in transactions {
+                    for th in txns {
                         println!("{th}");
                     }
                 }
diff --git a/rusk-wallet/src/error.rs b/rusk-wallet/src/error.rs
index 8d900e67f0..807421fef0 100644
--- a/rusk-wallet/src/error.rs
+++ b/rusk-wallet/src/error.rs
@@ -162,6 +162,9 @@ pub enum Error {
     /// Inquire error
     #[error("Inquire error: {0}")]
     InquireError(String),
+    /// Error while querying archival node
+    #[error("Archive node query error: {0}")]
+    ArchiveJsonError(String),
 }
 
 impl From<dusk_bytes::Error> for Error {
diff --git a/rusk-wallet/src/gql.rs b/rusk-wallet/src/gql.rs
index c361db99fc..731198b1f2 100644
--- a/rusk-wallet/src/gql.rs
+++ b/rusk-wallet/src/gql.rs
@@ -10,9 +10,10 @@
 
 use dusk_core::transfer::Transaction;
 use serde::Deserialize;
+use serde_json::Value;
 use tokio::time::{sleep, Duration};
 
-use crate::{Error, RuesHttpClient};
+use crate::{Address, Error, RuesHttpClient};
 
 /// GraphQL is a helper struct that aggregates all queries done
 /// to the Dusk GraphQL database.
@@ -55,6 +56,36 @@ struct BlockResponse {
     pub block: Option<Block>,
 }
 
+#[derive(Deserialize, Debug)]
+pub struct BlockData {
+    pub gas_spent: u64,
+    pub sender: String,
+    pub value: f64,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct BlockEvents {
+    pub data: BlockData,
+}
+
+#[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: MoonlightHistoryJson,
+}
+
 #[derive(Deserialize)]
 struct SpentTxResponse {
     pub tx: Option<SpentTx>,
@@ -146,6 +177,53 @@ impl GraphQL {
     pub async fn check_connection(&self) -> Result<(), Error> {
         self.query("").await.map(|_| ())
     }
+
+    /// Query the archival node for moonlight transactions given the
+    /// BlsPublicKey
+    pub async fn moonlight_history(
+        &self,
+        address: Address,
+    ) -> Result<FullMoonlightHistory, Error> {
+        let query = format!(
+            r#"query {{ fullMoonlightHistory(address: "{address}") {{ json }} }}"#
+        );
+
+        let response = self
+            .query(&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)
+    }
+
+    /// Fetch the spent transaction given moonlight tx hash
+    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(&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))
+        }
+    }
 }
 
 /// Errors generated from GraphQL


Test patch: