use guardian_client::{HistoryEntry as ProtoHistoryEntry, HistoryNote as ProtoHistoryNote};
use crate::client::MultisigClient;
use crate::error::{MultisigError, Result};
#[derive(Debug, Clone)]
pub struct HistoryPage {
pub entries: Vec<HistoryEntry>,
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub nonce: u64,
pub status: HistoryEntryStatus,
pub timestamp: String,
pub new_commitment: Option<String>,
pub input_notes: Vec<HistoryNote>,
pub output_notes: Vec<HistoryNote>,
pub decode_warnings: Vec<HistoryDecodeWarning>,
}
#[derive(Debug, Clone)]
pub struct HistoryNote {
pub note_id: String,
pub tag: HistoryNoteTag,
pub note_type: HistoryNoteVisibility,
pub assets: Vec<HistoryNoteAsset>,
pub sender: Option<String>,
pub recipient: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HistoryNoteAsset {
pub asset_id: String,
pub kind: HistoryAssetKind,
pub amount: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HistoryDecodeWarning {
pub section: HistoryDecodeSection,
pub reason: String,
}
macro_rules! wire_enum {
($(#[$doc:meta])* $name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum $name {
$($variant,)+
Other(String),
}
impl $name {
fn from_wire(label: String) -> Self {
match label.as_str() {
$($label => Self::$variant,)+
_ => Self::Other(label),
}
}
pub fn as_str(&self) -> &str {
match self {
$(Self::$variant => $label,)+
Self::Other(label) => label,
}
}
}
};
}
wire_enum! {
HistoryNoteTag {
P2id => "p2id",
P2ide => "p2ide",
Pswap => "pswap",
Mint => "mint",
Burn => "burn",
Custom => "custom",
}
}
wire_enum! {
HistoryNoteVisibility {
Public => "public",
Private => "private",
}
}
wire_enum! {
HistoryAssetKind {
Fungible => "fungible",
NonFungible => "non_fungible",
}
}
wire_enum! {
HistoryDecodeSection {
TxSummary => "tx_summary",
Metadata => "metadata",
InputNotes => "input_notes",
OutputNotes => "output_notes",
Vault => "vault",
Storage => "storage",
}
}
wire_enum! {
HistoryEntryStatus {
Canonical => "canonical",
}
}
impl HistoryNote {
fn from_proto(note: ProtoHistoryNote) -> Self {
Self {
note_id: note.note_id,
tag: HistoryNoteTag::from_wire(note.tag),
note_type: HistoryNoteVisibility::from_wire(note.note_type),
assets: note
.assets
.into_iter()
.map(|asset| HistoryNoteAsset {
asset_id: asset.asset_id,
kind: HistoryAssetKind::from_wire(asset.kind),
amount: asset.amount,
})
.collect(),
sender: note.sender,
recipient: note.recipient,
}
}
}
impl HistoryEntry {
fn from_proto(entry: ProtoHistoryEntry) -> Self {
Self {
nonce: entry.nonce,
status: HistoryEntryStatus::from_wire(entry.status),
timestamp: entry.timestamp,
new_commitment: entry.new_commitment,
input_notes: entry
.input_notes
.into_iter()
.map(HistoryNote::from_proto)
.collect(),
output_notes: entry
.output_notes
.into_iter()
.map(HistoryNote::from_proto)
.collect(),
decode_warnings: entry
.decode_warnings
.into_iter()
.map(|warning| HistoryDecodeWarning {
section: HistoryDecodeSection::from_wire(warning.section),
reason: warning.reason,
})
.collect(),
}
}
}
impl MultisigClient {
pub async fn delta_history(
&mut self,
limit: Option<u32>,
cursor: Option<String>,
) -> Result<HistoryPage> {
let account_id = self.require_account()?.id();
let mut guardian_client = self.create_authenticated_guardian_client().await?;
let response = guardian_client
.get_delta_history(&account_id, limit, cursor)
.await
.map_err(|e| MultisigError::GuardianServer(format!("failed to get history: {}", e)))?;
Ok(HistoryPage {
entries: response
.entries
.into_iter()
.map(HistoryEntry::from_proto)
.collect(),
next_cursor: response.next_cursor,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use guardian_client::{HistoryDecodeWarning as ProtoWarning, HistoryNoteAsset as ProtoAsset};
#[test]
fn entry_from_proto_maps_every_field() {
let entry = HistoryEntry::from_proto(ProtoHistoryEntry {
nonce: 7,
status: "canonical".to_string(),
timestamp: "2026-08-19T12:00:07Z".to_string(),
new_commitment: Some("0xnew".to_string()),
input_notes: vec![ProtoHistoryNote {
note_id: "0xin".to_string(),
tag: "custom".to_string(),
note_type: "private".to_string(),
assets: vec![],
sender: None,
recipient: None,
}],
output_notes: vec![ProtoHistoryNote {
note_id: "0xout".to_string(),
tag: "p2id".to_string(),
note_type: "public".to_string(),
assets: vec![ProtoAsset {
asset_id: "0xfaucet".to_string(),
kind: "fungible".to_string(),
amount: Some("100".to_string()),
}],
sender: Some("0xsender".to_string()),
recipient: Some("0xrecipient".to_string()),
}],
decode_warnings: vec![ProtoWarning {
section: "tx_summary".to_string(),
reason: "malformed_tx_summary".to_string(),
}],
});
assert_eq!(entry.nonce, 7);
assert_eq!(entry.status, HistoryEntryStatus::Canonical);
assert_eq!(entry.timestamp, "2026-08-19T12:00:07Z");
assert_eq!(entry.new_commitment.as_deref(), Some("0xnew"));
assert_eq!(entry.input_notes.len(), 1);
assert_eq!(entry.input_notes[0].note_id, "0xin");
assert_eq!(entry.input_notes[0].tag, HistoryNoteTag::Custom);
assert_eq!(
entry.input_notes[0].note_type,
HistoryNoteVisibility::Private
);
assert!(entry.input_notes[0].sender.is_none());
let out = &entry.output_notes[0];
assert_eq!(out.note_id, "0xout");
assert_eq!(out.tag, HistoryNoteTag::P2id);
assert_eq!(out.note_type, HistoryNoteVisibility::Public);
assert_eq!(out.assets[0].asset_id, "0xfaucet");
assert_eq!(out.assets[0].kind, HistoryAssetKind::Fungible);
assert_eq!(out.assets[0].amount.as_deref(), Some("100"));
assert_eq!(out.sender.as_deref(), Some("0xsender"));
assert_eq!(out.recipient.as_deref(), Some("0xrecipient"));
assert_eq!(entry.decode_warnings.len(), 1);
assert_eq!(
entry.decode_warnings[0].section,
HistoryDecodeSection::TxSummary
);
assert_eq!(entry.decode_warnings[0].reason, "malformed_tx_summary");
assert_eq!(
HistoryNoteTag::from_wire("brand_new_tag".to_string()),
HistoryNoteTag::Other("brand_new_tag".to_string())
);
}
}