walletconnect_sdk/
types.rs

1/// Types
2///
3/// All the RPC types and logic to encode and decode. There are some tests with
4/// actual payloads to ensure that the logic works.
5///
6use std::collections::HashMap;
7use std::fmt::{self, Display};
8
9use alloy::primitives::Address;
10use alloy::rpc::types::TransactionRequest;
11use serde::de::{MapAccess, Visitor};
12use serde::ser::SerializeSeq;
13use serde::{Deserialize, Deserializer, Serialize};
14use serde_json::Number;
15use serde_json::Value;
16
17use crate::cacao::Cacao;
18use crate::error::Result;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(untagged)]
22pub enum Id {
23    String(String),
24    Number(Number),
25    U128(u128),
26}
27
28impl From<String> for Id {
29    fn from(value: String) -> Self {
30        Id::String(value)
31    }
32}
33
34impl From<u128> for Id {
35    fn from(value: u128) -> Self {
36        Id::String(value.to_string())
37    }
38}
39
40impl Id {
41    #[allow(dead_code)]
42    pub fn to_u128(&self) -> Result<u128> {
43        match self {
44            Id::String(s) => Ok(s.parse::<u128>()?),
45            Id::Number(n) => n.as_u128().ok_or("number too big".into()),
46            Id::U128(n) => Ok(*n),
47        }
48    }
49}
50
51impl PartialEq for Id {
52    fn eq(&self, other: &Self) -> bool {
53        self.to_u128().unwrap().eq(&other.to_u128().unwrap())
54    }
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize)]
58pub struct JsonRpcRequest<ParamType = Value> {
59    pub jsonrpc: String,
60    pub method: JsonRpcMethod,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub params: Option<ParamType>,
63    pub id: Id,
64}
65
66impl Display for JsonRpcRequest {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "{}", serde_json::to_string(self).unwrap())
69    }
70}
71
72#[derive(Serialize, Deserialize, Debug)]
73pub struct JsonRpcResponse<ResultType = Value> {
74    #[allow(dead_code)]
75    pub jsonrpc: String,
76    #[serde(default)]
77    pub result: Option<ResultType>,
78    #[serde(default)]
79    pub error: Option<JsonRpcError>,
80    #[serde(default)]
81    #[allow(dead_code)]
82    pub id: Option<Id>,
83}
84
85#[derive(Serialize, Deserialize, Debug)]
86#[allow(dead_code)]
87pub struct JsonRpcError {
88    code: i64,
89    message: String,
90    #[serde(default)]
91    data: Option<Value>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[repr(u16)]
96#[serde(try_from = "u16", into = "u16")]
97pub enum IrnTag {
98    // https://specs.walletconnect.com/2.0/specs/clients/core/pairing/rpc-methods#methods
99    UnsupportedMethod = 0,
100
101    PairingDelete = 1000,
102    PairingDeleteResponse = 1001,
103
104    PairingPing = 1002,
105    PairingPingResponse = 1003,
106
107    PairingExtend = 1004,
108    PairingExtendResponse = 1005,
109
110    // https://specs.walletconnect.com/2.0/specs/clients/sign/rpc-methods#methods
111    SessionPropose = 1100,
112    SessionProposeApproveResponse = 1101,
113    SessionProposeRejectResponse = 1120,
114    SessionProposeAutoRejectResponse = 1121,
115
116    SessionSettle = 1102,
117    SessionSettleResponse = 1103,
118
119    SessionUpdate = 1104,
120    SessionUpdateResponse = 1105,
121
122    SessionExtend = 1106,
123    SessionExtendResponse = 1107,
124
125    SessionRequest = 1108,
126    SessionRequestResponse = 1109,
127
128    SessionEvent = 1110,
129    SessionEventResponse = 1111,
130
131    SessionDelete = 1112,
132    SessionDeleteResponse = 1113,
133
134    SessionPing = 1114,
135    SessionPingResponse = 1115,
136
137    SessionAuthenticate = 1116,
138    SessionAuthenticateApproveResponse = 1117,
139    SessionAuthenticateRejectResponse = 1118,
140    SessionAuthenticateAutoRejectResponse = 1119,
141}
142
143impl From<IrnTag> for u16 {
144    fn from(tag: IrnTag) -> Self {
145        tag as u16
146    }
147}
148
149impl TryFrom<u16> for IrnTag {
150    type Error = String;
151
152    fn try_from(value: u16) -> Result<Self, Self::Error> {
153        match value {
154            0 => Ok(IrnTag::UnsupportedMethod),
155
156            1000 => Ok(IrnTag::PairingDelete),
157            1001 => Ok(IrnTag::PairingDeleteResponse),
158
159            1002 => Ok(IrnTag::PairingPing),
160            1003 => Ok(IrnTag::PairingPingResponse),
161
162            1004 => Ok(IrnTag::PairingExtend),
163            1005 => Ok(IrnTag::PairingExtendResponse),
164
165            1100 => Ok(IrnTag::SessionPropose),
166            1101 => Ok(IrnTag::SessionProposeApproveResponse),
167            1120 => Ok(IrnTag::SessionProposeRejectResponse),
168            1121 => Ok(IrnTag::SessionProposeAutoRejectResponse),
169
170            1102 => Ok(IrnTag::SessionSettle),
171            1103 => Ok(IrnTag::SessionSettleResponse),
172
173            1104 => Ok(IrnTag::SessionUpdate),
174            1105 => Ok(IrnTag::SessionUpdateResponse),
175
176            1106 => Ok(IrnTag::SessionExtend),
177            1107 => Ok(IrnTag::SessionExtendResponse),
178
179            1108 => Ok(IrnTag::SessionRequest),
180            1109 => Ok(IrnTag::SessionRequestResponse),
181
182            1110 => Ok(IrnTag::SessionEvent),
183            1111 => Ok(IrnTag::SessionEventResponse),
184
185            1112 => Ok(IrnTag::SessionDelete),
186            1113 => Ok(IrnTag::SessionDeleteResponse),
187
188            1114 => Ok(IrnTag::SessionPing),
189            1115 => Ok(IrnTag::SessionPingResponse),
190
191            1116 => Ok(IrnTag::SessionAuthenticate),
192            1117 => Ok(IrnTag::SessionAuthenticateApproveResponse),
193
194            1118 => Ok(IrnTag::SessionAuthenticateRejectResponse),
195            1119 => Ok(IrnTag::SessionAuthenticateAutoRejectResponse),
196
197            _ => Err(format!("{:?}", crate::Error::InvalidIrnTag(value))),
198        }
199    }
200}
201
202#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
203pub enum JsonRpcMethod {
204    #[serde(rename = "irn_publish")]
205    IrnPublish,
206
207    #[serde(rename = "irn_subscribe")]
208    IrnSubscribe,
209
210    #[serde(rename = "irn_subscription")]
211    IrnSubscription,
212
213    #[serde(rename = "irn_fetchMessages")]
214    IrnFetchMessages,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
218pub struct IrnPublishParams {
219    pub topic: String,
220    pub message: String,
221    pub ttl: u64,
222    pub prompt: bool,
223    pub tag: IrnTag,
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
227pub struct IrnSubscriptionParams {
228    pub id: Id,
229    pub data: IrnSubscriptionData,
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
233pub struct IrnSubscriptionData {
234    pub topic: String,
235    pub message: String,
236    #[serde(rename = "publishedAt")]
237    pub published_at: Id,
238    pub tag: IrnTag,
239}
240
241#[derive(Debug, Default, Serialize, Deserialize)]
242pub struct IrnFetchMessageResult {
243    #[serde(rename = "hasMore")]
244    pub has_more: bool,
245    pub messages: Vec<EncryptedMessage>,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
249pub struct EncryptedMessage {
250    pub topic: String,
251    pub message: String,
252    pub tag: u16,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub ttl: Option<u64>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub prompt: Option<bool>,
257    #[serde(rename = "publishedAt")]
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub published_at: Option<u64>,
260    // TODO verify this thing on every response
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub attestation: Option<String>,
263}
264
265impl EncryptedMessage {
266    pub fn new(topic: String, message: String, tag: IrnTag, ttl: u64) -> Self {
267        Self {
268            topic,
269            message,
270            tag: tag as u16,
271            ttl: Some(ttl),
272            prompt: Some(false),
273            published_at: None,
274            attestation: None,
275        }
276    }
277}
278
279#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
280pub struct SessionProposeParams {
281    #[serde(rename = "requiredNamespaces")]
282    pub required_namespaces: HashMap<String, Namespace>,
283    #[serde(rename = "optionalNamespaces")]
284    pub optional_namespaces: HashMap<String, Namespace>,
285    pub relays: Vec<Relay>,
286    #[serde(rename = "pairingTopic")]
287    pub pairing_topic: String,
288    pub proposer: Participant,
289    #[serde(rename = "expiryTimestamp")]
290    pub expiry_timestamp: u64,
291}
292
293#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
294pub struct SessionProposeResponse {
295    pub relay: Relay,
296    #[serde(rename = "responderPublicKey")]
297    pub responder_public_key: String,
298}
299
300#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
301pub struct Namespace {
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub accounts: Option<Vec<String>>,
304    pub chains: Vec<String>,
305    pub events: Vec<String>,
306    pub methods: Vec<String>,
307}
308
309#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
310pub struct Relay {
311    pub protocol: String,
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
315pub struct SessionAuthenticateParams {
316    #[serde(rename = "authPayload")]
317    pub auth_payload: AuthPayload,
318    pub requester: Participant,
319    #[serde(rename = "expiryTimestamp")]
320    pub expiry_timestamp: u64,
321}
322
323#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
324pub struct AuthPayload {
325    #[serde(rename = "type")]
326    pub payload_type: String,
327    pub chains: Vec<String>,
328    pub statement: String,
329    pub aud: String,
330    pub domain: String,
331    pub version: String,
332    pub nonce: String,
333    pub iat: String,
334    pub resources: Vec<String>,
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
338pub struct Participant {
339    #[serde(rename = "publicKey")]
340    pub public_key: String,
341    pub metadata: Metadata,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
345pub struct Metadata {
346    pub name: String,
347    pub description: String,
348    pub url: String,
349    pub icons: Vec<String>,
350}
351
352#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
353pub struct SessionAuthenticateResponse {
354    pub cacaos: Vec<Cacao>,
355    pub responder: Participant,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
359pub struct SessionSettleParams {
360    pub controller: Participant,
361    pub expiry: u64,
362    pub namespaces: HashMap<String, Namespace>,
363    pub relay: Relay,
364    #[serde(rename = "sessionProperties")]
365    pub session_properties: Option<SessionSettleProperties>,
366}
367
368#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
369pub struct SessionSettleProperties {
370    pub capabilities: String,
371}
372
373#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
374pub struct SessionRequestParams {
375    #[serde(rename = "sessionId")]
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub session_id: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub scope: Option<String>,
380    pub request: SessionRequestObject,
381    #[serde(rename = "chainId")]
382    pub chain_id: String,
383}
384
385#[derive(Clone, Debug, Serialize, PartialEq)]
386pub struct SessionRequestObject {
387    pub method: SessionRequestMethod,
388    pub params: SessionRequestData,
389    #[serde(rename = "expiryTimestamp")]
390    pub expiry_timestamp: u64,
391}
392
393#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
394pub enum SessionRequestMethod {
395    #[serde(rename = "personal_sign")]
396    PersonalSign,
397    #[serde(rename = "eth_sendTransaction")]
398    EthSendTransaction,
399    #[serde(rename = "eth_signTypedData_v4")]
400    EthSignTypedDataV4,
401}
402
403#[derive(Clone, Debug, PartialEq)]
404pub enum SessionRequestData {
405    EthSendTransaction(Box<TransactionRequest>),
406    PersonalSign { message: String, account: Address },
407    EthSignTypedDataV4 { account: Address, typed_data: Value },
408}
409
410impl Serialize for SessionRequestData {
411    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
412    where
413        S: serde::Serializer,
414    {
415        match self {
416            SessionRequestData::EthSendTransaction(tx) => {
417                let mut seq = serializer.serialize_seq(Some(1))?;
418                seq.serialize_element(tx)?;
419                seq.end()
420            }
421            SessionRequestData::PersonalSign { message, account } => {
422                let mut seq = serializer.serialize_seq(Some(2))?;
423                seq.serialize_element(&message)?;
424                seq.serialize_element(&account)?;
425                seq.end()
426            }
427            SessionRequestData::EthSignTypedDataV4 {
428                account,
429                typed_data,
430            } => {
431                let mut seq = serializer.serialize_seq(Some(2))?;
432                seq.serialize_element(account)?;
433                seq.serialize_element(typed_data)?;
434                seq.end()
435            }
436        }
437    }
438}
439
440impl<'de> Deserialize<'de> for SessionRequestObject {
441    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442    where
443        D: Deserializer<'de>,
444    {
445        #[derive(Deserialize)]
446        #[serde(field_identifier, rename_all = "camelCase")]
447        enum Field {
448            Method,
449            Params,
450            ExpiryTimestamp,
451        }
452
453        struct SessionRequestVisitor;
454
455        impl<'de> Visitor<'de> for SessionRequestVisitor {
456            type Value = SessionRequestObject;
457
458            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
459                formatter.write_str("SessionRequestObject with method, params, and expiryTimestamp")
460            }
461
462            fn visit_map<V>(
463                self,
464                mut map: V,
465            ) -> Result<SessionRequestObject, V::Error>
466            where
467                V: MapAccess<'de>,
468            {
469                use serde_json::Value;
470
471                let mut method: Option<SessionRequestMethod> = None;
472                let mut raw_params: Option<Value> = None;
473                let mut expiry: Option<u64> = None;
474
475                while let Some(key) = map.next_key()? {
476                    match key {
477                        Field::Method => method = Some(map.next_value()?),
478                        Field::Params => raw_params = Some(map.next_value()?),
479                        Field::ExpiryTimestamp => {
480                            expiry = Some(map.next_value()?)
481                        }
482                    }
483                }
484
485                let method = method
486                    .ok_or_else(|| serde::de::Error::missing_field("method"))?;
487                let raw_params = raw_params
488                    .ok_or_else(|| serde::de::Error::missing_field("params"))?;
489                let expiry = expiry.ok_or_else(|| {
490                    serde::de::Error::missing_field("expiryTimestamp")
491                })?;
492
493                // Deserialize the params based on the method
494                let params = match method {
495                    SessionRequestMethod::EthSendTransaction => {
496                        let tx: [Box<TransactionRequest>; 1] =
497                            serde_json::from_value(raw_params)
498                                .map_err(serde::de::Error::custom)?;
499                        SessionRequestData::EthSendTransaction(tx[0].clone())
500                    }
501                    SessionRequestMethod::PersonalSign => {
502                        let (message, account) =
503                            serde_json::from_value(raw_params)
504                                .map_err(serde::de::Error::custom)?;
505                        SessionRequestData::PersonalSign { message, account }
506                    }
507                    SessionRequestMethod::EthSignTypedDataV4 => {
508                        if let Ok((account, typed_data)) =
509                            serde_json::from_value(raw_params.clone())
510                        {
511                            SessionRequestData::EthSignTypedDataV4 {
512                                account,
513                                typed_data,
514                            }
515                        } else {
516                            // Fallback: second item is a string that itself is JSON
517                            let (account, typed_data_str): (Address, String) =
518                                serde_json::from_value(raw_params)
519                                    .map_err(serde::de::Error::custom)?;
520                            let typed_data: Value =
521                                serde_json::from_str(&typed_data_str)
522                                    .map_err(serde::de::Error::custom)?;
523                            SessionRequestData::EthSignTypedDataV4 {
524                                account,
525                                typed_data,
526                            }
527                        }
528                    }
529                };
530
531                Ok(SessionRequestObject {
532                    method,
533                    params,
534                    expiry_timestamp: expiry,
535                })
536            }
537        }
538
539        deserializer.deserialize_map(SessionRequestVisitor)
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn test_decode_id() {
549        let result = serde_json::from_str::<Id>("\"1234\"").unwrap();
550        println!("result {result:?}");
551
552        let result = serde_json::from_str::<Id>("1234").unwrap();
553        println!("result {result:?}");
554    }
555
556    #[test]
557    fn test_decode_fetch_messages() {
558        let resp = "{\"id\":1744205603590064025,\"jsonrpc\":\"2.0\",\"result\":{\"hasMore\":false,\"messages\":[{\"attestation\":\"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDQyMDkxOTAsImlkIjoiZWRhYjVmMjQyNTNmZGUyYTVkYzI4NTcyZmRkNGE4NTViYTJhZjRkOGM2ZmQxMjQxN2NlMWUwNDMzODY5MjcwMCIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwa2l0LWxhYi5yZW93bi5jb20iLCJpc1NjYW0iOmZhbHNlLCJpc1ZlcmlmaWVkIjp0cnVlfQ.zeNQehxpiQ0JqVBHTlkxgDwValU2NhGEsUMT5irJgiM92d1M361ifoxRIJfh1_EF2dVBqyr6zLV4cbp3g4jLSQ\",\"message\":\"ALW+zRyGiw5kj8hlp/Xv1jQ3vGFNcFNsfp4PyyVPbkcbMohD0sGhUejguhlNRA0fXnCfzQ9/hdI5xcmy1SubXts/Db6Kt1cStgOgIXSGReS3jP2AgldAv5WR77dfXDytJzYmg6af9lbKMYCuI+4qWyuaswKvsO4cyeRaT1jVsITT6uy+z2A50H0uBuNcnE4YGKia8yxlZYfLj0dbJ1Q/zX8d9DF/YLBQzUhLOD6P9ajCDUTNs7S24lDtB1xglkKHyxG4/cSktAGmvu7QWcYzkzeLmGcqxK7TxO3+N8baOMf4bq0n2tJjFCQPrs31d/mrQc1e7d7GwmndZjvbWRL+ab+B2INkfs6cEkLTtwu10TXGAScQCrrVtBR/apIiGFpg27YD+KbbM7hCmFoOIxJc1DXH8psf+MjDhwLqQRVxMe26DYbL50jwqQmDu71qvi4DHZgRyAq/wWSIHe+BGqfZKzK45zK7cz72WsZhuEjOqmDK0gZePbHzWaVvI9uApzpVXGVPOwp4eeZOBYbED+Oucq6CovHRjVIw0CZitjQH14yv0XNIFZ1U1/byB/jWgvQwW42O3v9M3cmljeXTeuEaao+bngEv6zN7CDpGMbiQ4gDTxihtpTv7Mgl+9LpyhrtY79QJ6XMx9wkWuxkbsIrQalFgJuhcJEMfULjLSe49gzIC91gSJ5rVKd1ej82yIsDHUn6roKFb0McQXJNa5vb+Wh1cfn46LeX+m6XBRFXGKqr6xXs4dujZg2TeDA9XyNT4B99hXGQl4BkNmAv6BYjH6pB63Fr7f+10iyf9SrfXlYVR9mLTqsVVd53sVuKWW6esKhgPHc2+Fd30+LRMTmhG/IebxRaRBpBvjjh8lkVFowQn37TruR4sVX0NB/UsqX2U9Ns9A1LLMVI3+QycUmU/aDPR3fxmn1OF2abskgjvWLr3lqVVZGPf3nFgxMfU4u/+xsztwuw2Pc5lFos06z+dY0sZaRpB4b59Qb79KT/1CFodRjfGDcM6eRbaj2XcMfn46HkczxKl494aiu6KwLIzx5ChpCt9EMyO6SV/dhEhvL/cwcWAZsUmft5yPSa24J5bCtTetcL7fGMrjyyJYsQBFMgdQJC7YtlUcrPm5NIhJu8DYjef86sr8m38t2vixFGifC55RQhlKdG12cUrtY9TVQMJrEBdLsmuBbSFVa9oSzbh4yNvPRciLkaUAo5A5QfBlfZmbLZutE9v9IsjmIqZ78xys6Q93Hx5keDk3xdM1/1and9v5kjVMvmpQX1vqtlebIJY5sqM3BZVt9Tyxg6eSl+fKI+Jug+K19T9b1dGsfqByIf/js9pEAATxbV+e8I2CAtxNlipfHJleamZY1mVe74sokGMbM/Tz7MYYXp8MePKPXH+Te6yFMtvHfHdPi1Qmte+w9VLEin2PuLWt/DNX277YrOvhBYuRx30L9DWAXSIcN3qnxI6GpL45dX2rf8pyMdO1vdsPQG/Sr1EnU8jTi+ul182Zyen04kOhtqtuoYV2Sh73k2/6AVRWPtFqhGUI0cUBrdVnE1PAFgRaYMDmTTMYFnmz/PXptuP/T14x5F0wdoFNOUw88opmRrIsqQgtRYIwMa5303aHhB7H+aY7Hg3/y2RNOxklXUMikwI/8bHw0l2jVS69egUe1JMVk51sG8IxG45lf/q7zUEm5BcJ6QJ21OTqZqMJHPDEO0r0okc1C17/rylIh9c0+2zUdqJa7N0sV9Qd9oGNciKswoHLeNhHdQQrEyEFjmQr5TV9MM8HaWVIrRf5SGgsAmSd+GHUyz1sNt8DzlOQja2qKEZIgNZYBE6f667dX5WTmNcQ9zZje45uVKRkO8YGkBT0m1b4iD+hgHuYQdkLQMHvfsDiZo=\",\"publishedAt\":1744205591202,\"tag\":1100,\"topic\":\"74818ca920a949b9adad9ed0c5bdfa1f5e6ea5e5b1a18e71a7ba1ac9923295e5\"},{\"attestation\":\"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDQyMDkxOTAsImlkIjoiZGU0MjhhYjU0NzhiZWJkOGU5M2M3OGFhNTVjYTNkYTEwZWViYzE0MTY4MTgwYWJkNTYxYmRhZmVmYjMxMmZkOSIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwa2l0LWxhYi5yZW93bi5jb20iLCJpc1NjYW0iOmZhbHNlLCJpc1ZlcmlmaWVkIjp0cnVlfQ.EJMNd8iO0Uvr1owRbomGuC3One2K6pfKZa3nvIl4MpI00EkHL86Tdo63MJXKtGK65ixFZqhx-L__M8kXbhFoJA\",\"message\":\"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\",\"publishedAt\":1744205591323,\"tag\":1116,\"topic\":\"74818ca920a949b9adad9ed0c5bdfa1f5e6ea5e5b1a18e71a7ba1ac9923295e5\"}]}}";
559        let result = serde_json::from_str::<
560            JsonRpcResponse<IrnFetchMessageResult>,
561        >(resp)
562        .unwrap();
563        println!("result {result:?}");
564    }
565}