diff --git a/mm2src/crypto/src/walletconnect.rs b/mm2src/crypto/src/walletconnect.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mm2src/kdf_walletconnect/Cargo.toml b/mm2src/kdf_walletconnect/Cargo.toml index f8198bbb3c..8292020592 100644 --- a/mm2src/kdf_walletconnect/Cargo.toml +++ b/mm2src/kdf_walletconnect/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] # chacha20poly1305 = "0.10" +chrono = { version = "0.4.23", "features" = ["serde"] } common = { path = "../common" } derive_more = "0.99" futures = { version = "0.3", package = "futures", features = [ diff --git a/mm2src/kdf_walletconnect/src/error.rs b/mm2src/kdf_walletconnect/src/error.rs index 5c369ab7d8..6968182711 100644 --- a/mm2src/kdf_walletconnect/src/error.rs +++ b/mm2src/kdf_walletconnect/src/error.rs @@ -1,26 +1,38 @@ use derive_more::Display; use pairing_api::PairingClientError; use relay_client::error::{ClientError, Error}; -use relay_rpc::rpc::PublishError; +use relay_rpc::rpc::{PublishError, SubscriptionError}; use serde::{Deserialize, Serialize}; #[derive(Debug, Display, Serialize, Deserialize)] -pub enum WalletConnectClientError { +pub enum WalletConnectCtxError { PairingError(String), EncodeError(String), PublishError(String), ClientError(String), PairingNotFound(String), + SubscriptionError(String), + InternalError(String), + SerdeError(String), + UnsuccessfulResponse(String) } -impl From for WalletConnectClientError { - fn from(error: PairingClientError) -> Self { WalletConnectClientError::PairingError(error.to_string()) } +impl From for WalletConnectCtxError { + fn from(error: PairingClientError) -> Self { WalletConnectCtxError::PairingError(error.to_string()) } } -impl From for WalletConnectClientError { - fn from(error: ClientError) -> Self { WalletConnectClientError::ClientError(error.to_string()) } +impl From for WalletConnectCtxError { + fn from(error: ClientError) -> Self { WalletConnectCtxError::ClientError(error.to_string()) } } -impl From> for WalletConnectClientError { - fn from(error: Error) -> Self { WalletConnectClientError::PublishError(format!("{error:?}")) } +impl From> for WalletConnectCtxError { + fn from(error: Error) -> Self { WalletConnectCtxError::PublishError(format!("{error:?}")) } +} + +impl From> for WalletConnectCtxError { + fn from(error: Error) -> Self { WalletConnectCtxError::SubscriptionError(format!("{error:?}")) } +} + +impl From for WalletConnectCtxError { + fn from(value: serde_json::Error) -> Self { WalletConnectCtxError::SerdeError(value.to_string()) } } diff --git a/mm2src/kdf_walletconnect/src/handler.rs b/mm2src/kdf_walletconnect/src/handler.rs index 3288bed03c..5c92d42998 100644 --- a/mm2src/kdf_walletconnect/src/handler.rs +++ b/mm2src/kdf_walletconnect/src/handler.rs @@ -1,11 +1,7 @@ -use std::time::Duration; - use common::log::info; use futures::channel::mpsc::UnboundedSender; use relay_client::{error::ClientError, - websocket::{CloseFrame, ConnectionHandler, PublishedMessage}, - ConnectionOptions}; -use relay_rpc::auth::{ed25519_dalek::SigningKey, AuthToken}; + websocket::{CloseFrame, ConnectionHandler, PublishedMessage}}; pub struct Handler { name: &'static str, @@ -44,15 +40,3 @@ impl ConnectionHandler for Handler { info!("\n[{}] outbound error: {error}", self.name); } } - -fn create_conn_opts(relay_address: &str, project_id: &str) -> ConnectionOptions { - let key = SigningKey::generate(&mut rand::thread_rng()); - - let auth = AuthToken::new("https://komodefi.com") - .aud(relay_address) - .ttl(Duration::from_secs(60 * 60)) - .as_jwt(&key) - .unwrap(); - - ConnectionOptions::new(project_id, auth).with_address(relay_address) -} diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs index f83811824e..fd9c164149 100644 --- a/mm2src/kdf_walletconnect/src/lib.rs +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -1,27 +1,33 @@ mod error; mod handler; +mod pairing; +mod session; mod session_key; use common::log::info; -use error::WalletConnectClientError; -use futures::{channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, +use error::WalletConnectCtxError; +use futures::{channel::mpsc::{unbounded, UnboundedReceiver}, lock::Mutex, StreamExt}; use handler::Handler; use mm2_err_handle::prelude::MmResult; use mm2_err_handle::prelude::*; +use pairing::{process_pairing_delete_response, process_pairing_extend_response, process_pairing_ping_response}; use pairing_api::{Methods, PairingClient}; use relay_client::{websocket::{Client, PublishedMessage}, ConnectionOptions, MessageIdGenerator}; -use relay_rpc::{domain::{MessageId, SubscriptionId, Topic}, - rpc::{params::{IrnMetadata, Metadata}, +use relay_rpc::{auth::{ed25519_dalek::SigningKey, AuthToken}, + domain::{MessageId, Topic}, + rpc::{params::{IrnMetadata, Metadata, ResponseParamsSuccess}, Params, Payload, Request, Response, SuccessfulResponse, JSON_RPC_VERSION_STR}}; -use session_key::SessionKey; -use std::{collections::HashMap, sync::Arc, time::Duration}; -use wc_common::{encrypt_and_encode, EnvelopeType}; +use session::{Session, APP_DESCRIPTION, APP_NAME}; +use std::{sync::Arc, time::Duration}; +use wc_common::{decode_and_decrypt_type0, encrypt_and_encode, EnvelopeType}; const RELAY_ADDRESS: &str = "wss://relay.walletconnect.com"; const PROJECT_ID: &str = "86e916bcbacee7f98225dde86b697f5b"; +const AUTH_TOKEN_SUB: &str = "http://127.0.0.1:8000"; + const SUPPORTED_PROTOCOL: &str = "irn"; const SUPPORTED_METHODS: &[&str] = &[ "eth_sendTransaction", @@ -35,31 +41,20 @@ const SUPPORTED_CHAINS: &[&str] = &["eip155:1", "eip155:5"]; const SUPPORTED_EVENTS: &[&str] = &["chainChanged", "accountsChanged"]; const SUPPORTED_ACCOUNTS: &[&str] = &["eip155:5:0xBA5BA3955463ADcc7aa3E33bbdfb8A68e0933dD8"]; -#[derive(Debug)] -pub struct Session { - /// Pairing subscription id. - pub subscription_id: SubscriptionId, - /// Session symmetric key. - pub session_key: SessionKey, -} - -pub struct WalletConnectClient { +pub struct WalletConnectCtx { pub client: Client, pub pairing: PairingClient, - pub sessions: Arc>>, + pub session: Session, pub handler: Arc>>, - pub rpc_handler: Arc>>, - pub rpc_sender: UnboundedSender, } -impl Default for WalletConnectClient { +impl Default for WalletConnectCtx { fn default() -> Self { Self::new() } } -impl WalletConnectClient { +impl WalletConnectCtx { pub fn new() -> Self { let (msg_sender, msg_receiver) = unbounded(); - let (rpc_sender, rpc_receiver) = unbounded::(); let pairing = PairingClient::new(); let client = Client::new(Handler::new("Komodefi", msg_sender)); @@ -67,39 +62,78 @@ impl WalletConnectClient { Self { client, pairing, - rpc_sender, - sessions: Arc::new(Mutex::new(HashMap::new())), + session: Session::new(), handler: Arc::new(Mutex::new(msg_receiver)), - rpc_handler: Arc::new(Mutex::new(rpc_receiver)), } } - pub async fn create_pairing( - &self, - metadata: Metadata, - methods: Option, - ) -> MmResult<(Topic, String), WalletConnectClientError> { - Ok(self.pairing.create(metadata, methods, &self.client).await?) + pub async fn connect_client(&self) -> MmResult<(), WalletConnectCtxError> { + let auth = { + let key = SigningKey::generate(&mut rand::thread_rng()); + AuthToken::new(AUTH_TOKEN_SUB) + .aud(RELAY_ADDRESS) + .ttl(Duration::from_secs(60 * 60)) + .as_jwt(&key) + .unwrap() + }; + let opts = ConnectionOptions::new(PROJECT_ID, auth).with_address(RELAY_ADDRESS); + self.client.connect(&opts).await?; + + info!("WC connected"); + + Ok(()) } - pub async fn connect_to_pairing(&self, url: &str, activate: bool) -> MmResult { - Ok(self.pairing.pair(url, activate, &self.client).await?) + // todo: return slice + async fn sym_key(&self, topic: &Topic) -> MmResult, WalletConnectCtxError> { + println!("sym topic: {topic:?}"); + { + let sessions = self.session.lock().await; + if let Some(sesssion) = sessions.get(topic) { + return Ok(sesssion.session_key.symmetric_key().to_vec()); + } + } + + { + let pairings = self.pairing.pairings.lock().await; + if let Some(pairing) = pairings.get(topic.as_ref()) { + let key = hex::decode(pairing.sym_key.clone()) + .map_to_mm(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + return Ok(key); + } + } + + MmError::err(WalletConnectCtxError::PairingNotFound("Topic not found".to_owned())) } - pub async fn connect(&self, opts: &ConnectionOptions) -> MmResult<(), WalletConnectClientError> { - Ok(self.client.connect(opts).await?) + pub async fn create_pairing(&self) -> MmResult<(Topic, String), WalletConnectCtxError> { + let metadata = Metadata { + description: APP_DESCRIPTION.to_owned(), + url: "127.0.0.1:3000".to_owned(), + icons: vec![], + name: APP_NAME.to_owned(), + }; + let methods = Methods(vec![SUPPORTED_METHODS + .iter() + .map(|m| m.to_string()) + .collect::>()]); + + Ok(self.pairing.create(metadata, Some(methods), &self.client).await?) + } + + pub async fn connect_to_pairing(&self, url: &str, activate: bool) -> MmResult { + Ok(self.pairing.pair(url, activate, &self.client).await?) } /// Private function to publish a request. async fn publish_request( &self, - topic: &str, + topic: &Topic, params: Params, irn_metadata: IrnMetadata, - ) -> MmResult<(), WalletConnectClientError> { + ) -> MmResult<(), WalletConnectCtxError> { let message_id = MessageIdGenerator::new().next(); let request = Request::new(message_id, params); - // Publish the encrypted message self.publish_payload(topic, irn_metadata, Payload::Request(request)) .await?; @@ -111,55 +145,42 @@ impl WalletConnectClient { /// Private function to publish a request response. async fn publish_response( &self, - topic: &str, - params: Params, + topic: &Topic, + params: serde_json::Value, irn_metadata: IrnMetadata, message_id: MessageId, - ) -> MmResult<(), WalletConnectClientError> { + ) -> MmResult<(), WalletConnectCtxError> { let response = Response::Success(SuccessfulResponse { id: message_id, jsonrpc: JSON_RPC_VERSION_STR.into(), - result: serde_json::to_value(params) - .map_to_mm(|err| WalletConnectClientError::EncodeError(err.to_string()))?, + result: params, }); - // Publish the encrypted message self.publish_payload(topic, irn_metadata, Payload::Response(response)) .await?; - println!("\nOutbound request sent!"); - Ok(()) } /// Private function to publish a payload. async fn publish_payload( &self, - topic: &str, + topic: &Topic, irn_metadata: IrnMetadata, payload: Payload, - ) -> MmResult<(), WalletConnectClientError> { - // try to extend session before updating local store. - let sym_key = { - let pairings = self.pairing.pairings.lock().await; - let pairing = pairings.get(topic).ok_or_else(|| { - WalletConnectClientError::PairingNotFound(format!("Pariring not found for topic:{topic}")) - })?; - hex::decode(pairing.sym_key.clone()).map_to_mm(|err| { - WalletConnectClientError::EncodeError(format!("Failed to decode sym_key: {:?}", err)) - })? - }; + ) -> MmResult<(), WalletConnectCtxError> { + let sym_key = self.sym_key(topic).await?; let payload = - serde_json::to_string(&payload).map_to_mm(|err| WalletConnectClientError::EncodeError(err.to_string()))?; - let message = encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key) - .map_to_mm(|err| WalletConnectClientError::EncodeError(err.to_string()))?; + serde_json::to_string(&payload).map_to_mm(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + info!("\n Sending Outbound request: {payload}!"); - // Publish the encrypted message + let message = encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key) + .map_to_mm(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; { self.client .publish( - topic.into(), + topic.clone(), message, None, irn_metadata.tag, @@ -171,20 +192,92 @@ impl WalletConnectClient { Ok(()) } -} -pub async fn published_message_event_loop(client: Arc) { - let mut recv = client.handler.lock().await; - while let Some(msg) = recv.next().await { - info!("Pulished Message: {msg:?}"); - todo!() + pub async fn published_message_event_loop(self: Arc) { + let mut recv = self.handler.lock().await; + while let Some(msg) = recv.next().await { + let message = { + let key = self.sym_key(&msg.topic).await.unwrap(); + decode_and_decrypt_type0(msg.message.as_bytes(), &key).unwrap() + }; + + println!("\nInbound message payload={message}"); + + let response = serde_json::from_str::(&message).unwrap(); + let result = match response { + Payload::Request(request) => process_inbound_request(self.clone(), request, &msg.topic).await, + Payload::Response(response) => process_inbound_response(self.clone(), response).await, + }; + + match result { + Ok(()) => info!("Inbound message was handled succesfully"), + Err(err) => info!("Error while handling inbound message: {err:?}"), + }; + } } } -pub async fn wc_rpc_event_loop(receiver: Arc>>) { - let mut recv = receiver.lock().await; - while let Some(param) = recv.next().await { - info!("Params: {param:?}"); - todo!() +async fn process_inbound_request( + ctx: Arc, + request: Request, + topic: &Topic, +) -> MmResult<(), WalletConnectCtxError> { + let response = match request.params { + Params::SessionPropose(proposal) => ctx.session.process_proposal_request(&ctx, proposal).await, + Params::SessionExtend(param) => ctx.session.process_session_extend_request(topic, param).await, + Params::SessionDelete(param) => ctx.session.process_session_delete_request(param), + Params::SessionPing(()) => ctx.session.process_session_ping_request(), + Params::SessionSettle(param) => ctx.session.process_session_settle_request(topic, param).await, + Params::SessionUpdate(param) => ctx.session.process_session_update_request(topic, param).await, + Params::SessionRequest(_) => todo!(), + Params::SessionEvent(_) => todo!(), + + Params::PairingPing(_param) => process_pairing_ping_response().await, + Params::PairingDelete(param) => process_pairing_delete_response(&ctx, topic, param).await, + Params::PairingExtend(param) => process_pairing_extend_response(&ctx, topic, param).await, + _ => todo!(), + }?; + + info!("Publishing reponse"); + ctx.publish_response(topic, response.0, response.1, request.id).await?; + + // todo + // ctx.session.session_delete_cleanup(ctx.clone(), topic).await? + + Ok(()) +} + +async fn process_inbound_response( + _ctx: Arc, + response: Response, +) -> MmResult<(), WalletConnectCtxError> { + match response { + Response::Success(value) => { + let params = serde_json::from_value::(value.result)?; + match params { + ResponseParamsSuccess::SessionPropose(param) => { + info!("Session Propose Response: {param:?}"); + todo!() + }, + ResponseParamsSuccess::SessionSettle(success) | ResponseParamsSuccess::SessionUpdate(success) |ResponseParamsSuccess::SessionExtend(success) + | ResponseParamsSuccess::SessionRequest(success) + | ResponseParamsSuccess::SessionEvent(success) + | ResponseParamsSuccess::SessionDelete(success) + | ResponseParamsSuccess::SessionPing(success) + | ResponseParamsSuccess::PairingExtend(success) + | ResponseParamsSuccess::PairingDelete(success) + | ResponseParamsSuccess::PairingPing(success) => { + if !success { + return MmError::err(WalletConnectCtxError::UnsuccessfulResponse(format!("Unsuccessful response={params:?}"))); + } + + Ok(()) + }, + } + }, + Response::Error(err) => { + println!("Error: {err:?}"); + todo!() + }, } } diff --git a/mm2src/kdf_walletconnect/src/pairing.rs b/mm2src/kdf_walletconnect/src/pairing.rs new file mode 100644 index 0000000000..a781722e90 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/pairing.rs @@ -0,0 +1,50 @@ +use crate::{error::WalletConnectCtxError, session::WcRequestResult, WalletConnectCtx}; + +use relay_rpc::rpc::params::RelayProtocolMetadata; +use relay_rpc::{domain::Topic, + rpc::params::{pairing_delete::PairingDeleteRequest, pairing_extend::PairingExtendRequest, + ResponseParamsSuccess}}; + +pub(crate) async fn process_pairing_ping_response() -> WcRequestResult { + let response = ResponseParamsSuccess::PairingPing(true); + let irn_metadata = response.irn_metadata(); + let value = serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) +} + +pub(crate) async fn process_pairing_extend_response( + ctx: &WalletConnectCtx, + topic: &Topic, + extend: PairingExtendRequest, +) -> WcRequestResult { + { + let mut pairings = ctx.pairing.pairings.lock().await; + if let Some(pairing) = pairings.get_mut(topic.as_ref()) { + pairing.pairing.expiry = extend.expiry; + pairing.pairing.active = true; + }; + } + + let response = ResponseParamsSuccess::PairingPing(true); + let irn_metadata = response.irn_metadata(); + let value = serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) +} + +pub(crate) async fn process_pairing_delete_response( + ctx: &WalletConnectCtx, + topic: &Topic, + _delete: PairingDeleteRequest, +) -> WcRequestResult { + { + ctx.pairing.disconnect(topic.as_ref(), &ctx.client).await?; + } + + let response = ResponseParamsSuccess::PairingDelete(true); + let irn_metadata = response.irn_metadata(); + let value = serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) +} diff --git a/mm2src/kdf_walletconnect/src/session.rs b/mm2src/kdf_walletconnect/src/session.rs new file mode 100644 index 0000000000..67f2ece7ab --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session.rs @@ -0,0 +1,281 @@ +use crate::{error::WalletConnectCtxError, session_key::SessionKey, WalletConnectCtx, SUPPORTED_ACCOUNTS, + SUPPORTED_CHAINS, SUPPORTED_EVENTS, SUPPORTED_METHODS, SUPPORTED_PROTOCOL}; +use chrono::Utc; +use common::log::info; +use futures::lock::Mutex; +use mm2_err_handle::prelude::{MapToMmResult, MmResult}; +use relay_rpc::rpc::params::session_delete::SessionDeleteRequest; +use relay_rpc::rpc::params::session_extend::SessionExtendRequest; +use relay_rpc::rpc::params::session_update::SessionUpdateRequest; +use relay_rpc::rpc::params::{IrnMetadata, RelayProtocolMetadata}; +use relay_rpc::{domain::{SubscriptionId, Topic}, + rpc::params::{session::{ProposeNamespace, ProposeNamespaces, SettleNamespace, SettleNamespaces}, + session_propose::{SessionProposeRequest, SessionProposeResponse}, + session_settle::{Controller, SessionSettleRequest}, + Metadata, Relay, RequestParams, ResponseParamsSuccess}}; +use serde_json::Value; +use std::collections::HashMap; +use std::ops::Deref; +use std::{collections::BTreeMap, sync::Arc}; + +pub(crate) const APP_NAME: &str = "Komodefi Framework"; +pub(crate) const APP_DESCRIPTION: &str = "WallectConnect Komodefi Framework Playground"; + +pub(crate) type WcRequestResult = MmResult<(Value, IrnMetadata), WalletConnectCtxError>; + +#[derive(Debug, Clone)] +pub struct SessionInfo { + /// Pairing subscription id. + pub subscription_id: SubscriptionId, + /// Session symmetric key + pub session_key: SessionKey, + pub controller: Controller, + pub relay: Relay, + pub namespaces: ProposeNamespaces, + pub settled_namespaces: SettleNamespaces, + pub expiry: u64, +} + +impl SessionInfo { + fn new(subscription_id: SubscriptionId, session_key: SessionKey, responder_public_key: String) -> Self { + let mut namespaces = BTreeMap::::new(); + namespaces.insert("eip155".to_string(), ProposeNamespace { + chains: SUPPORTED_CHAINS.iter().map(|c| c.to_string()).collect(), + methods: SUPPORTED_METHODS.iter().map(|m| m.to_string()).collect(), + events: SUPPORTED_EVENTS.iter().map(|e| e.to_string()).collect(), + }); + let mut settled_namespaces = BTreeMap::::new(); + settled_namespaces.insert("eip155".to_string(), SettleNamespace { + accounts: SUPPORTED_ACCOUNTS.iter().map(|a| a.to_string()).collect(), + methods: SUPPORTED_METHODS.iter().map(|m| m.to_string()).collect(), + events: SUPPORTED_EVENTS.iter().map(|e| e.to_string()).collect(), + }); + let relay = Relay { + protocol: SUPPORTED_PROTOCOL.to_string(), + data: None, + }; + let controller = Controller { + public_key: responder_public_key, + metadata: Metadata { + name: APP_NAME.to_owned(), + description: APP_DESCRIPTION.to_owned(), + icons: vec!["https://www.rust-lang.org/static/images/rust-logo-blk.svg".to_string()], + ..Default::default() + }, + }; + + Self { + subscription_id, + session_key, + controller, + namespaces: ProposeNamespaces(namespaces), + settled_namespaces: SettleNamespaces(settled_namespaces), + relay, + expiry: Utc::now().timestamp() as u64 + 300, + } + } + + fn supported_propose_namespaces(&self) -> &ProposeNamespaces { &self.namespaces } + fn supported_settle_namespaces(&self) -> &SettleNamespaces { &self.settled_namespaces } + fn create_settle_request(&self) -> RequestParams { + RequestParams::SessionSettle(SessionSettleRequest { + relay: self.relay.clone(), + controller: self.controller.clone(), + namespaces: self.supported_settle_namespaces().clone(), + expiry: Utc::now().timestamp() as u64 + 300, // 5 min TTL + }) + } + fn create_proposal_response(&self) -> Result<(Value, IrnMetadata), WalletConnectCtxError> { + let response = ResponseParamsSuccess::SessionPropose(SessionProposeResponse { + relay: self.relay.clone(), + responder_public_key: self.controller.public_key.clone(), + }); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } +} + +#[derive(Debug, Clone)] +pub struct Session { + session: Arc>>, +} + +impl Deref for Session { + type Target = Arc>>; + fn deref(&self) -> &Self::Target { &self.session } +} + +impl Default for Session { + fn default() -> Self { Self::new() } +} + +impl Session { + pub fn new() -> Self { + Self { + session: Default::default(), + } + } + + pub fn from_session_info(topic: Topic, session_info: SessionInfo) -> Self { + Self { + session: Arc::new(Mutex::new(HashMap::from([(topic, session_info)]))), + } + } + + pub(crate) async fn process_session_extend_request( + &self, + topic: &Topic, + extend: SessionExtendRequest, + ) -> WcRequestResult { + let mut sessions = self.session.lock().await; + if let Some(session) = sessions.get_mut(topic) { + session.expiry = extend.expiry; + info!("Updated extended, info: {:?}", session); + } + + let response = ResponseParamsSuccess::SessionExtend(true); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } + + /// https://specs.walletconnect.com/2.0/specs/clients/sign/session-proposal + pub async fn process_proposal_request( + &self, + ctx: &WalletConnectCtx, + proposal: SessionProposeRequest, + ) -> WcRequestResult { + let sender_public_key = hex::decode(&proposal.proposer.public_key) + .map_to_mm(|err| WalletConnectCtxError::EncodeError(err.to_string()))? + .as_slice() + .try_into() + .unwrap(); + + let session_key = SessionKey::from_osrng(&sender_public_key) + .map_to_mm(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + let responder_public_key = hex::encode(session_key.diffie_public_key()); + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectCtxError::SubscriptionError(err.to_string()))?; + + let session = SessionInfo::new(subscription_id, session_key, responder_public_key); + session + .supported_propose_namespaces() + .supported(&proposal.required_namespaces) + .map_to_mm(|err| WalletConnectCtxError::InternalError(err.to_string()))?; + + { + let mut sessions = ctx.session.deref().lock().await; + _ = sessions.insert(session_topic.clone(), session.clone()); + } + + let settle_params = session.create_settle_request(); + let irn_metadata = settle_params.irn_metadata(); + ctx.publish_request(&session_topic, settle_params.into(), irn_metadata) + .await?; + + Ok(session.create_proposal_response()?) + } + + pub(crate) async fn process_session_settle_request( + &self, + topic: &Topic, + settle: SessionSettleRequest, + ) -> WcRequestResult { + { + let mut sessions = self.session.lock().await; + if let Some(session) = sessions.get_mut(topic) { + session.settled_namespaces = settle.namespaces.clone(); + session.controller = settle.controller.clone(); + session.relay = settle.relay.clone(); + session.expiry = settle.expiry; + + info!("Session successfully settled for topic: {:?}", topic); + info!("Updated session info: {:?}", session); + } + } + + let response = ResponseParamsSuccess::SessionSettle(true); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } + + pub(crate) fn process_session_ping_request(&self) -> WcRequestResult { + let response = ResponseParamsSuccess::SessionPing(true); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } + + pub(crate) fn process_session_delete_request(&self, delete_params: SessionDeleteRequest) -> WcRequestResult { + info!( + "\nSession is being terminated reason={}, code={}", + delete_params.message, delete_params.code, + ); + + let response = ResponseParamsSuccess::SessionDelete(true); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } + + pub(crate) async fn session_delete_cleanup( + &self, + ctx: Arc, + topic: &Topic, + ) -> MmResult<(), WalletConnectCtxError> { + let mut sessions = ctx.session.lock().await; + sessions.remove(topic).ok_or_else(|| { + WalletConnectCtxError::InternalError("Attempt to remove non-existing session".to_string()) + })?; + + ctx.client.unsubscribe(topic.clone()).await?; + + // Check if there are no active sessions remaining + if sessions.is_empty() { + info!("\nNo active sessions left, disconnecting the pairing"); + + // Attempt to disconnect and remove the pairing associated with the topic + ctx.pairing + .disconnect(topic.as_ref(), &ctx.client) + .await + .map_err(|e| WalletConnectCtxError::InternalError(format!("Failed to disconnect pairing: {}", e)))?; + } + + Ok(()) + } + + pub(crate) async fn process_session_update_request( + &self, + topic: &Topic, + update: SessionUpdateRequest, + ) -> WcRequestResult { + let mut sessions = self.session.lock().await; + if let Some(session) = sessions.get_mut(topic) { + session.settled_namespaces = update.namespaces.clone(); + info!("Updated extended, info: {:?}", session); + } + + let response = ResponseParamsSuccess::SessionUpdate(true); + let irn_metadata = response.irn_metadata(); + let value = + serde_json::to_value(response).map_err(|err| WalletConnectCtxError::EncodeError(err.to_string()))?; + + Ok((value, irn_metadata)) + } +} diff --git a/mm2src/kdf_walletconnect/src/session_key.rs b/mm2src/kdf_walletconnect/src/session_key.rs index ecab541f6f..b1b1f7a89f 100644 --- a/mm2src/kdf_walletconnect/src/session_key.rs +++ b/mm2src/kdf_walletconnect/src/session_key.rs @@ -5,12 +5,13 @@ use {hkdf::Hkdf, x25519_dalek::{EphemeralSecret, PublicKey}}; /// Session key and topic derivation errors. -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Clone, thiserror::Error)] pub enum SessionError { #[error("Failed to generate symmetric session key: {0}")] SymKeyGeneration(String), } +#[derive(Clone)] pub struct SessionKey { sym_key: [u8; 32], public_key: PublicKey, diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8b95f8bf0d..f6ee573e6d 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -5,7 +5,7 @@ use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, use common::log::{self, LogLevel, LogOnError, LogState}; use common::{cfg_native, cfg_wasm32, small_rng}; use gstuff::{try_s, Constructible, ERR, ERRL}; -use kdf_walletconnect::WalletConnectClient; +use kdf_walletconnect::WalletConnectCtx; use lazy_static::lazy_static; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; use mm2_metrics::{MetricsArc, MetricsOps}; @@ -143,7 +143,7 @@ pub struct MmCtx { /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: Constructible>>, - pub wallect_connect: Arc, + pub wallect_connect: Arc, } impl MmCtx { @@ -193,7 +193,7 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: Constructible::default(), - wallect_connect: Arc::new(WalletConnectClient::default()), + wallect_connect: Arc::new(WalletConnectCtx::default()), } } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 3b73947ffb..286d0d0e02 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -472,6 +472,14 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { init_message_service(&ctx).await?; + // connect walletconnect + ctx.wallect_connect + .connect_client() + .await + .map_err(|err| MmInitError::WalletInitError(err.to_string()))?; + ctx.spawner() + .spawn(ctx.wallect_connect.clone().published_message_event_loop()); + let balance_update_ordermatch_handler = BalanceUpdateOrdermatchHandler::new(ctx.clone()); register_balance_update_handler(ctx.clone(), Box::new(balance_update_ordermatch_handler)).await; diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 6003f9f6c2..bb07a3ef88 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -236,7 +236,7 @@ async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Re #[cfg(not(target_arch = "wasm32"))] async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Response { - const NON_ALLOWED_CHARS: &[char] = &['<', '>', '&']; + const NON_ALLOWED_CHARS: &[char] = &['Ò']; /// Unwraps a result or propagates its error 500 response with the specified headers (if they are present). macro_rules! try_sf { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index b9066bf540..f9fb65d111 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,3 +1,4 @@ +use super::lp_commands::connect_to_peer; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; #[cfg(target_arch = "wasm32")] @@ -167,6 +168,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, add_node_to_version_stat).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, + "wc_connect_to_peer" => handle_mmrpc(ctx, request, connect_to_peer).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_slp" => handle_mmrpc(ctx, request, enable_token::).await, "enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs index ae992c6d3e..f206fe6c41 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs @@ -114,3 +114,31 @@ pub async fn trezor_connection_status( status: hw_ctx.trezor_connection_status().await, }) } + +use serde::Serialize; + +#[derive(Deserialize)] +pub struct CreatePairingRequest { + url: String, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct CreatePairingResponse { + pub topic: String, +} + +/// `connect_to_peer` RPC command implementation. +pub async fn connect_to_peer( + ctx: MmArc, + req: CreatePairingRequest, +) -> MmResult { + let topic = ctx + .wallect_connect + .connect_to_pairing(&req.url, true) + .await + .map_err(|err| TrezorConnectionError::Internal(err.to_string()))?; + + Ok(CreatePairingResponse { + topic: topic.to_string(), + }) +}