xmtp_content_types/
wallet_send_calls.rs1use std::collections::HashMap;
2
3use crate::{CodecError, ContentCodec};
4use serde::{Deserialize, Serialize};
5use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent};
6
7pub struct WalletSendCallsCodec {}
8
9impl WalletSendCallsCodec {
10 const AUTHORITY_ID: &'static str = "xmtp.org";
11 pub const TYPE_ID: &'static str = "walletSendCalls";
12 pub const MAJOR_VERSION: u32 = 1;
13 pub const MINOR_VERSION: u32 = 0;
14}
15
16impl WalletSendCallsCodec {
17 fn fallback(content: &WalletSendCalls) -> Option<String> {
18 let json = serde_json::to_string(content).unwrap_or_else(|_| "{}".to_string());
19 Some(format!("[Transaction request generated]: {}", json))
20 }
21}
22
23impl ContentCodec<WalletSendCalls> for WalletSendCallsCodec {
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 fn encode(content: WalletSendCalls) -> Result<EncodedContent, CodecError> {
34 let json = serde_json::to_vec(&content)
35 .map_err(|e| CodecError::Encode(format!("JSON encode error: {e}")))?;
36
37 Ok(EncodedContent {
38 r#type: Some(Self::content_type()),
39 parameters: HashMap::new(),
40 fallback: Self::fallback(&content),
41 compression: None,
42 content: json,
43 })
44 }
45
46 fn decode(encoded: EncodedContent) -> Result<WalletSendCalls, CodecError> {
47 serde_json::from_slice(&encoded.content)
48 .map_err(|e| CodecError::Decode(format!("JSON decode error: {e}")))
49 }
50
51 fn should_push() -> bool {
52 true
53 }
54}
55
56#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct WalletSendCalls {
58 pub version: String,
59 #[serde(rename = "chainId")]
60 pub chain_id: String,
61 pub from: String,
62 pub calls: Vec<WalletCall>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub capabilities: Option<HashMap<String, String>>,
65}
66
67#[derive(Debug, Serialize, Deserialize, Clone)]
68pub struct WalletCall {
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub to: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub data: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub value: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub gas: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub metadata: Option<WalletCallMetadata>,
79}
80
81#[derive(Debug, Serialize, Deserialize, Clone)]
82pub struct WalletCallMetadata {
83 pub description: String,
84 #[serde(rename = "transactionType")]
85 pub transaction_type: String,
86 #[serde(flatten)]
87 pub extra: HashMap<String, String>,
88}
89
90#[cfg(test)]
91pub(crate) mod tests {
92 use super::*;
93 use crate::ContentCodec;
94
95 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
96 #[cfg_attr(not(target_arch = "wasm32"), test)]
97 fn test_encode_decode_wallet_send_calls() {
98 let params = WalletSendCalls {
99 version: "1".to_string(),
100 chain_id: "0x1".to_string(),
101 from: "0xsender".to_string(),
102 calls: vec![WalletCall {
103 to: Some("0xrecipient".to_string()),
104 data: Some("0xdeadbeef".to_string()),
105 value: Some("0x0".to_string()),
106 gas: Some("0x5208".to_string()),
107 metadata: Some(WalletCallMetadata {
108 description: "Send funds".to_string(),
109 transaction_type: "transfer".to_string(),
110 extra: HashMap::from([("note".to_string(), "test".to_string())]),
111 }),
112 }],
113 capabilities: Some(HashMap::from([("foo".to_string(), "bar".to_string())])),
114 };
115
116 let encoded = WalletSendCallsCodec::encode(params.clone()).unwrap();
117 let decoded = WalletSendCallsCodec::decode(encoded).unwrap();
118
119 assert_eq!(decoded.version, params.version);
120 assert_eq!(decoded.chain_id, params.chain_id);
121 assert_eq!(decoded.from, params.from);
122 assert_eq!(decoded.calls.len(), 1);
123 assert_eq!(
124 decoded.calls[0].metadata.as_ref().unwrap().transaction_type,
125 "transfer"
126 );
127 }
128}