1use std::time::Duration;
6use std::{str, thread};
7
8use alloy::hex;
9use alloy::primitives::Address;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use serde_json::Value;
13
14use crate::cacao::Cacao;
15use crate::connection::Connection;
16use crate::error::{Error, Result};
17use crate::message::Message;
18use crate::types::{
19 EncryptedMessage, IrnTag, Namespace, Participant, Relay,
20 SessionAuthenticateResponse, SessionProposeResponse, SessionSettleParams,
21};
22use crate::utils::{
23 DAYS, UriParameters, derive_sym_key, random_bytes32, sha256, unix_timestamp,
24};
25use crate::wc_message::{WcData, WcMessage, WcMethod};
26
27#[derive(Debug, Clone, Copy)]
28pub enum Topic {
29 Initial,
31 Response,
33 Derived,
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct Pairing {
39 private_key: [u8; 32],
40 params: UriParameters,
41 connection: Connection,
42 proposal_request: Option<WcMessage>,
43 authenticate_request: Option<WcMessage>,
44 approve_done: bool,
45}
46
47impl Pairing {
48 pub fn new(uri: &str, connection: Connection) -> crate::Result<Self> {
49 let params = UriParameters::try_from(uri.to_string())?;
50 Ok(Self {
51 private_key: random_bytes32(),
53 params,
54 connection,
55 proposal_request: None,
56 authenticate_request: None,
57 approve_done: false,
58 })
59 }
60
61 pub async fn init_pairing(&mut self) -> Result<WcMessage> {
70 self.subscribe(Topic::Initial).await?;
71
72 let messages = self.fetch_messages(Topic::Initial).await?;
73
74 if messages.is_empty() {
75 return Err(
76 "Please generate a fresh WalletConnect URI from the dApp"
77 .into(),
78 );
79 }
80
81 if messages.len() == 1 {
82 if messages[0].method() != Some(WcMethod::SessionPropose) {
84 return Err("Message is not SessionPropose".into());
85 }
86
87 self.proposal_request = Some(messages[0].clone());
88 Ok(messages[0].clone())
89 } else if messages.len() == 2 {
90 let proposal_request = messages
92 .iter()
93 .find(|m| m.method() == Some(WcMethod::SessionPropose))
94 .ok_or("SessionPropose message not found")?;
95
96 let authenticate_request = messages
97 .iter()
98 .find(|m| m.method() == Some(WcMethod::SessionAuthenticate))
99 .ok_or("SessionAuthenticate message not found")?;
100
101 self.proposal_request = Some(proposal_request.clone());
102 self.authenticate_request = Some(authenticate_request.clone());
103
104 {
105 let proposal_request =
106 proposal_request.data.as_session_propose().ok_or(
107 crate::Error::InternalError2("not session propose"),
108 )?;
109 let authenticate_request = authenticate_request
110 .data
111 .as_session_authenticate()
112 .ok_or(crate::Error::InternalError2(
113 "not session authenticate",
114 ))?;
115 assert_eq!(
116 proposal_request.proposer.public_key,
117 authenticate_request.requester.public_key,
118 "proposer and requester public keys are not equal - {messages:?}"
119 );
120 }
121 Ok(proposal_request.clone())
122 } else {
123 Err(format!(
124 "Expected 1 or 2 messages, got {}: {messages:?}",
125 messages.len()
126 )
127 .into())
128 }
129 }
130
131 pub async fn approve_with_session_settle(
137 &mut self,
138 account_address: Address,
139 ) -> Result<Vec<WcMessage>> {
140 let proposal = self.get_proposal()?;
141 let response = proposal.create_response(
142 WcData::SessionProposeResponse(SessionProposeResponse {
143 relay: Relay {
144 protocol: "irn".to_string(),
145 },
146 responder_public_key: self.public_key(),
147 }),
148 None,
149 );
150
151 self.send_message(
152 Topic::Initial,
153 &response.into_raw()?,
154 Some(0),
155 IrnTag::SessionProposeApproveResponse,
156 3600,
157 )
158 .await?;
159
160 self.subscribe(Topic::Derived).await?;
161
162 let proposal =
163 proposal.data.as_session_propose().ok_or("not proposal")?;
164
165 let session_settle =
166 self.new_message(WcData::SessionSettle(SessionSettleParams {
167 controller: self.participant(),
168 expiry: unix_timestamp()? + 10 * DAYS,
169 namespaces: proposal
170 .required_namespaces
171 .clone()
172 .into_iter()
173 .chain(proposal.optional_namespaces.clone())
174 .map(|(name, n)| {
175 (
176 name,
177 Namespace {
178 accounts: Some(
179 n.chains
180 .iter()
181 .map(|c| {
182 format!("{c}:{account_address}")
183 })
184 .collect(),
185 ),
186 chains: n.chains,
187 events: n.events,
188 methods: vec![
189 "personal_sign".to_string(),
190 "eth_sendTransaction".to_string(),
191 "eth_signTypedData_v4".to_string(),
192 ],
193 },
194 )
195 })
196 .collect(),
197 relay: Relay {
198 protocol: "irn".to_string(),
199 },
200 session_properties: None,
201 }))?;
202
203 self.send_message(
204 Topic::Derived,
205 &session_settle,
206 Some(0),
207 IrnTag::SessionSettle,
208 3600,
209 )
210 .await?;
211
212 let mut excess_messages = vec![];
213 let mut success = false;
214
215 loop {
216 let mut messages = self.fetch_messages(Topic::Derived).await?;
217 let mut rm_idx = None;
218 for (i, msg) in messages.iter().enumerate() {
219 if msg.id == session_settle.id {
220 let result = msg.data.as_result::<bool>();
221
222 if let Some(result) = result {
223 success = result;
224 }
225
226 rm_idx = Some(i);
227 }
228 }
229 if let Some(i) = rm_idx {
230 messages.remove(i);
231 }
232 excess_messages.extend(messages);
233 if rm_idx.is_some() {
234 break;
235 }
236 }
237
238 if success {
239 self.approve_done = true;
240 Ok(excess_messages)
241 } else {
242 Err(crate::Error::PairingNotApproved)
243 }
244 }
245
246 pub async fn approve_with_cacao(&self, cacao: Cacao) -> Result<()> {
252 if cacao.signature.is_none() {
253 return Err("Cacao signature is None".into());
254 }
255
256 cacao.verify()?;
257
258 let message =
259 self.authenticate_request.as_ref().unwrap().create_response(
260 WcData::SessionAuthenticateResponse(
261 SessionAuthenticateResponse {
262 cacaos: vec![cacao],
263 responder: self.participant(),
264 },
265 ),
266 None,
267 );
268
269 self.send_message(
270 Topic::Derived,
271 &message.into_raw()?,
272 Some(1),
273 IrnTag::SessionAuthenticateApproveResponse,
274 3600,
275 )
276 .await?;
277
278 Ok(())
279 }
280
281 pub async fn watch_messages(
282 &self,
283 topic: Topic,
284 dur: Option<Duration>,
285 ) -> Result<Vec<WcMessage>> {
286 loop {
287 let result = self.fetch_messages(topic).await?;
288 if !result.is_empty() {
289 return Ok(result);
290 }
291 thread::sleep(dur.unwrap_or(Duration::from_secs(1)));
292 }
293 }
294
295 fn new_message(&self, data: WcData) -> crate::Result<Message> {
296 if data.result()?.is_some() {
297 return Err("Cannot create new message with result".into());
298 }
299 Ok(Message {
300 jsonrpc: "2.0".to_string(),
301 method: data.method().map(|s| s.to_string()),
302 params: data.params()?,
303 result: None,
304 error: None,
305 id: self.connection.get_id(),
306 })
307 }
308
309 async fn subscribe(&self, topic: Topic) -> Result<String> {
313 self.connection.irn_subscribe(&self.topic(topic)?).await
314 }
315
316 async fn fetch_messages(&self, topic: Topic) -> Result<Vec<WcMessage>> {
317 self.connection
318 .irn_fetch_messages(&self.topic(topic)?)
319 .await?
320 .iter()
321 .map(|m| {
322 Message::decrypt(&m.message, self.sym_key(topic)?, None)
323 .and_then(|m| m.decode())
324 })
325 .collect()
326 }
327
328 pub async fn send_message<T>(
329 &self,
330 topic: Topic,
331 message: &Message<String, T>,
332 type_byte: Option<u8>,
333 tag: IrnTag,
334 ttl: u64,
335 ) -> Result<Value>
336 where
337 T: Serialize + DeserializeOwned,
338 {
339 let cipher_text = message.encrypt(
340 self.sym_key(topic)?,
341 type_byte,
342 Some(self.public_key()),
343 None,
344 )?;
345
346 let result = self
347 .connection
348 .irn_publish(EncryptedMessage::new(
349 self.topic(topic)?,
350 cipher_text,
351 tag,
352 ttl,
353 ))
354 .await?;
355
356 Ok(result)
357 }
358
359 fn public_key(&self) -> String {
360 let secret = x25519_dalek::StaticSecret::from(self.private_key);
361 let public_key = x25519_dalek::PublicKey::from(&secret);
362 hex::encode(public_key.to_bytes())
363 }
364
365 fn other_public_key(&self) -> Result<[u8; 32]> {
366 let proposer_public_key = self
367 .proposal_request
368 .as_ref()
369 .and_then(|p| p.data.as_session_propose())
370 .map(|p| &p.proposer.public_key);
371 let auth_public_key = self
372 .authenticate_request
373 .as_ref()
374 .and_then(|p| p.data.as_session_authenticate())
375 .map(|p| &p.requester.public_key);
376 proposer_public_key
377 .or(auth_public_key)
378 .map(|k| {
379 hex::decode_to_array::<String, 32>(k.clone())
380 .map_err(Error::from)
381 })
382 .ok_or::<Error>(
383 "other_public_key not found because pairing is not init".into(),
384 )?
385 }
386
387 pub(crate) fn participant(&self) -> Participant {
388 Participant {
389 public_key: self.public_key(),
390 metadata: self.connection.metadata().clone(),
391 }
392 }
393
394 fn sym_key(&self, topic: Topic) -> Result<[u8; 32]> {
395 Ok(match topic {
396 Topic::Initial => self.params.sym_key,
397 Topic::Response | Topic::Derived => {
398 derive_sym_key(self.private_key, self.other_public_key()?)
399 }
400 })
401 }
402
403 fn topic(&self, topic: Topic) -> Result<String> {
404 Ok(match topic {
405 Topic::Initial | Topic::Derived => {
407 hex::encode(sha256(self.sym_key(topic)?))
408 }
409 Topic::Response => hex::encode(sha256(self.other_public_key()?)),
411 })
412 }
413
414 pub fn get_proposal(&self) -> Result<&WcMessage> {
415 self.proposal_request
416 .as_ref()
417 .ok_or("error: proposal_request is None".into())
418 }
419
420 #[allow(dead_code)]
421 pub fn get_proposal_old(
422 &self,
423 account_address: Address,
424 chain_id: u64,
425 ) -> Result<(Cacao, WcMessage, WcMessage)> {
426 if self.proposal_request.is_none()
427 || self.authenticate_request.is_none()
428 {
429 return Err("Pairing not initialised".into());
430 }
431
432 let cacao = Cacao::from_auth_request(
433 &self
434 .authenticate_request
435 .as_ref()
436 .unwrap()
437 .data
438 .as_session_authenticate()
439 .unwrap()
440 .auth_payload,
441 account_address,
442 chain_id,
443 )?;
444
445 Ok((
446 cacao,
447 self.proposal_request.clone().unwrap(),
449 self.authenticate_request.clone().unwrap(),
450 ))
451 }
452}