xmtp_content_types/
transaction_reference.rs1use 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
10impl 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
64fn deserialize_network_id<'de, D>(deserializer: D) -> Result<String, D::Error>
66where
67 D: Deserializer<'de>,
68{
69 use serde::de::Error;
70
71 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#[derive(Debug, Serialize, Deserialize, Clone)]
83pub struct TransactionReference {
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub namespace: Option<String>,
87
88 #[serde(rename = "networkId", deserialize_with = "deserialize_network_id")]
90 pub network_id: String,
91
92 pub reference: String,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub metadata: Option<TransactionMetadata>,
98}
99
100#[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}