Skip to main content

xmtp_content_types/
intent.rs

1use crate::{CodecError, ContentCodec};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
6
7const INTENT_METADATA_LIMIT: usize = 10 * 1024;
8
9pub struct IntentCodec;
10impl IntentCodec {
11    const AUTHORITY_ID: &str = "coinbase.com";
12    pub const TYPE_ID: &str = "intent";
13    pub const MAJOR_VERSION: u32 = 1;
14    pub const MINOR_VERSION: u32 = 0;
15}
16
17impl IntentCodec {
18    fn fallback(intent: &Intent) -> Option<String> {
19        Some(format!("User selected action: {}", intent.action_id))
20    }
21}
22
23impl ContentCodec<Intent> for IntentCodec {
24    fn content_type() -> ContentTypeId {
25        ContentTypeId {
26            authority_id: Self::AUTHORITY_ID.to_string(),
27            type_id: Self::TYPE_ID.to_string(),
28            version_major: Self::MAJOR_VERSION,
29            version_minor: Self::MINOR_VERSION,
30        }
31    }
32
33    // implements: CTYPE-019
34    fn encode(intent: Intent) -> Result<EncodedContent, CodecError> {
35        if let Some(metadata) = &intent.metadata {
36            let intent_json = serde_json::to_vec(metadata).map_err(|e| {
37                CodecError::Encode(format!("Unable to serialize intent metadata. {e:?}"))
38            })?;
39            if intent_json.len() > INTENT_METADATA_LIMIT {
40                return Err(CodecError::Encode(format!(
41                    "Intent metadata is too large. (limit: {}kb)",
42                    INTENT_METADATA_LIMIT / 1024
43                )));
44            }
45        }
46
47        let intent_json = serde_json::to_vec(&intent)
48            .map_err(|e| CodecError::Encode(format!("Unable to serialize intent. {e:?}")))?;
49
50        Ok(EncodedContent {
51            r#type: Some(Self::content_type()),
52            content: intent_json,
53            fallback: Self::fallback(&intent),
54            ..Default::default()
55        })
56    }
57
58    fn decode(intent: EncodedContent) -> Result<Intent, CodecError> {
59        let intent = serde_json::from_slice(&intent.content)
60            .map_err(|e| CodecError::Decode(format!("Unable to deserialize intent. {e:?}")))?;
61
62        Ok(intent)
63    }
64
65    fn should_push() -> bool {
66        true
67    }
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
71pub struct Intent {
72    pub id: String,
73    #[serde(rename = "actionId", alias = "action_id")]
74    pub action_id: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub metadata: Option<HashMap<String, Value>>,
77}
78
79#[cfg(test)]
80mod tests {
81    use super::{Intent, IntentCodec};
82    use crate::ContentCodec;
83    use serde_json::Value;
84
85    #[xmtp_common::test(unwrap_try = true)]
86    // verifies: CTYPE-007
87    fn encode_decode_intent() {
88        let intent = Intent {
89            id: "thanksgiving_selection".to_string(),
90            action_id: "the_turkey_of_course".to_string(),
91            metadata: Some(
92                [
93                    (
94                        "amount_for_yourself".to_string(),
95                        Value::String("7lbs".to_string()),
96                    ),
97                    ("hugs_from_grandma".to_string(), Value::Bool(true)),
98                ]
99                .into_iter()
100                .collect(),
101            ),
102        };
103
104        let encoded = IntentCodec::encode(intent.clone())?;
105        assert_eq!(
106            encoded.fallback(),
107            "User selected action: the_turkey_of_course"
108        );
109        let decoded = IntentCodec::decode(encoded)?;
110
111        assert_eq!(decoded, intent);
112    }
113}