Skip to main content

xmtp_content_types/
transaction_reference.rs

1use std::collections::HashMap;
2
3use crate::{CodecError, ContentCodec};
4use serde::{Deserialize, Deserializer, Serialize};
5
6use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
7
8pub struct TransactionReferenceCodec {}
9
10/// Legacy content type id at <https://github.com/xmtp/xmtp-js/blob/main/content-types/content-type-transaction-reference/src/TransactionReference.ts>
11impl TransactionReferenceCodec {
12    const AUTHORITY_ID: &'static str = "xmtp.org";
13    pub const TYPE_ID: &'static str = "transactionReference";
14    pub const MAJOR_VERSION: u32 = 1;
15    pub const MINOR_VERSION: u32 = 0;
16}
17
18impl TransactionReferenceCodec {
19    fn fallback(content: &TransactionReference) -> Option<String> {
20        if !content.reference.is_empty() {
21            Some(format!(
22                "[Crypto transaction] Use a blockchain explorer to learn more using the transaction hash: {}",
23                content.reference
24            ))
25        } else {
26            Some("Crypto transaction".to_string())
27        }
28    }
29}
30
31impl ContentCodec<TransactionReference> for TransactionReferenceCodec {
32    fn content_type() -> ContentTypeId {
33        ContentTypeId {
34            authority_id: Self::AUTHORITY_ID.to_string(),
35            type_id: Self::TYPE_ID.to_string(),
36            version_major: Self::MAJOR_VERSION,
37            version_minor: Self::MINOR_VERSION,
38        }
39    }
40
41    fn encode(data: TransactionReference) -> Result<EncodedContent, CodecError> {
42        let json = serde_json::to_vec(&data)
43            .map_err(|e| CodecError::Encode(format!("JSON encode error: {e}")))?;
44
45        Ok(EncodedContent {
46            r#type: Some(Self::content_type()),
47            parameters: HashMap::new(),
48            fallback: Self::fallback(&data),
49            compression: None,
50            content: json,
51        })
52    }
53
54    fn decode(encoded: EncodedContent) -> Result<TransactionReference, CodecError> {
55        serde_json::from_slice(&encoded.content)
56            .map_err(|e| CodecError::Decode(format!("JSON decode error: {e}")))
57    }
58
59    fn should_push() -> bool {
60        true
61    }
62}
63
64/// Custom deserializer for network_id that can handle both string and number
65fn deserialize_network_id<'de, D>(deserializer: D) -> Result<String, D::Error>
66where
67    D: Deserializer<'de>,
68{
69    use serde::de::Error;
70
71    // Use serde_json::Value to handle flexible deserialization
72    let value = serde_json::Value::deserialize(deserializer)?;
73
74    match value {
75        serde_json::Value::String(s) => Ok(s),
76        serde_json::Value::Number(n) => Ok(n.to_string()),
77        _ => Err(Error::custom("networkId must be a string or number")),
78    }
79}
80
81/// The main content type for transaction references
82#[derive(Debug, Serialize, Deserialize, Clone)]
83pub struct TransactionReference {
84    /// Optional namespace for the network (e.g., "eip155")
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub namespace: Option<String>,
87
88    /// Network ID (can be string or number in JSON)
89    #[serde(rename = "networkId", deserialize_with = "deserialize_network_id")]
90    pub network_id: String,
91
92    /// Transaction hash
93    pub reference: String,
94
95    /// Optional metadata for the transaction
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub metadata: Option<TransactionMetadata>,
98}
99
100/// Metadata attached to the transaction reference
101#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct TransactionMetadata {
103    #[serde(rename = "transactionType")]
104    pub transaction_type: String,
105    pub currency: String,
106    pub amount: f64,
107    pub decimals: u32,
108    #[serde(rename = "fromAddress")]
109    pub from_address: String,
110    #[serde(rename = "toAddress")]
111    pub to_address: String,
112}
113
114#[cfg(test)]
115pub(crate) mod tests {
116    use super::*;
117
118    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
119    #[cfg_attr(not(target_arch = "wasm32"), test)]
120    fn test_encode_decode_transaction_reference() {
121        let tx = TransactionReference {
122            namespace: Some("eip155".to_string()),
123            network_id: "1".to_string(),
124            reference: "0xabc123".to_string(),
125            metadata: Some(TransactionMetadata {
126                transaction_type: "payment".to_string(),
127                currency: "ETH".to_string(),
128                amount: 1.2345,
129                decimals: 18,
130                from_address: "0xsender".to_string(),
131                to_address: "0xrecipient".to_string(),
132            }),
133        };
134
135        let encoded = TransactionReferenceCodec::encode(tx.clone()).unwrap();
136        let decoded = TransactionReferenceCodec::decode(encoded).unwrap();
137
138        assert_eq!(decoded.reference, tx.reference);
139        assert_eq!(
140            decoded.metadata.as_ref().unwrap().currency,
141            "ETH".to_string()
142        );
143    }
144}