├── .github └── workflows │ └── rust.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── qr.rs └── web3.rs └── src ├── client.rs ├── client ├── core.rs ├── old.rs ├── options.rs ├── session.rs ├── socket.rs └── storage.rs ├── crypto.rs ├── crypto ├── aead.rs └── key.rs ├── errors.rs ├── hex.rs ├── lib.rs ├── protocol.rs ├── protocol ├── message.rs ├── rpc.rs └── topic.rs ├── qr.rs ├── qr ├── image.rs ├── output.rs └── print.rs ├── serialization.rs ├── transport.rs └── uri.rs /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | - name: Lint 24 | run: cargo fmt --check && cargo clippy -- -D warnings 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "walletconnect" 3 | version = "0.2.0" 4 | authors = ["Nicholas Rodrigues Lordello "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/nlordell/walletconnect-rs" 8 | homepage = "https://github.com/nlordell/walletconnect-rs" 9 | documentation = "https://docs.rs/walletconnect" 10 | keywords = ["web3", "ethereum", "wallet", "connect", "async"] 11 | description = """ 12 | WalletConnect client implementation in Rust. 13 | """ 14 | 15 | [features] 16 | default = [] 17 | qr = ["atty", "qrcode", "termcolor", "terminfo"] 18 | transport = ["web3"] 19 | 20 | [dependencies] 21 | data-encoding = "2" 22 | ethers-core = "0" 23 | futures = "0.3" 24 | jsonrpc-core = "18" 25 | lazy_static = "1" 26 | log = "0.4" 27 | openssl = "0.10" 28 | parity-ws = { version = "0.11", features = ["ssl"] } 29 | rand = "0.8" 30 | ring = "0.16" 31 | serde = { version = "1", features = ["derive"] } 32 | serde_json = "1" 33 | thiserror = "1" 34 | url = { version = "2", features = ["serde"] } 35 | uuid = { version = "0.8", features = ["serde", "v4"] } 36 | zeroize = "1" 37 | 38 | # qr 39 | atty = { version = "0.2", optional = true } 40 | qrcode = { version = "0.12", optional = true } 41 | termcolor = { version = "1", optional = true } 42 | terminfo = { version = "0.7", optional = true } 43 | 44 | # transport 45 | web3 = { version = "0.18", optional = true } 46 | 47 | [dev-dependencies] 48 | env_logger = "0.9" 49 | tokio = { version = "1", features = ["full"] } 50 | 51 | [[example]] 52 | name = "qr" 53 | required-features = ["qr"] 54 | 55 | [[example]] 56 | name = "web3" 57 | required-features = ["qr", "transport"] 58 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Nicholas Rodrigues Lordello 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WalletConnect Rust Client 2 | 3 | :warning: **This crate is currently still a work in progress** :warning: 4 | 5 | A WalletConnect client implementation in Rust. Currently only a small subset of 6 | the full API is implemented. In particular: 7 | - Creating a session and performing a handshake with a wallet 8 | - Displaying a QR code to a UTF-8 compatible terminal 9 | - Sending a transaction 10 | -------------------------------------------------------------------------------- /examples/qr.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::process; 3 | use walletconnect::{qr, Client, Metadata, Transaction}; 4 | 5 | fn main() { 6 | env_logger::init(); 7 | if let Err(err) = futures::executor::block_on(run()) { 8 | log::error!("{}", err); 9 | process::exit(1); 10 | } 11 | } 12 | 13 | async fn run() -> Result<(), Box> { 14 | let client = Client::new( 15 | "examples-qr", 16 | Metadata { 17 | description: "WalletConnect-rs terminal QR code example".into(), 18 | url: "https://github.com/nlordell/walletconnect-rs".parse()?, 19 | icons: vec!["https://avatars0.githubusercontent.com/u/4210206".parse()?], 20 | name: "WalletConnect-rs QR Example".into(), 21 | }, 22 | )?; 23 | 24 | let (accounts, _) = client.ensure_session(qr::print_with_url).await?; 25 | 26 | println!("Connected accounts:"); 27 | for account in &accounts { 28 | println!(" - {:?}", account); 29 | } 30 | 31 | let tx = client 32 | .send_transaction(Transaction { 33 | from: accounts[0], 34 | to: Some("000102030405060708090a0b0c0d0e0f10111213".parse()?), 35 | value: 1_000_000_000_000_000u128.into(), 36 | ..Transaction::default() 37 | }) 38 | .await?; 39 | 40 | println!("Transaction sent:\n https://etherscan.io/tx/{:?}", tx); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/web3.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use walletconnect::transport::WalletConnect; 4 | use walletconnect::{qr, Client, Metadata}; 5 | use web3::types::TransactionRequest; 6 | use web3::Web3; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), Box> { 10 | env_logger::init(); 11 | 12 | let client = Client::new( 13 | "examples-web3", 14 | Metadata { 15 | description: "WalletConnect-rs web3 transport example.".into(), 16 | url: "https://github.com/nlordell/walletconnect-rs".parse()?, 17 | icons: vec!["https://avatars0.githubusercontent.com/u/4210206".parse()?], 18 | name: "WalletConnect-rs Web3 Example".into(), 19 | }, 20 | )?; 21 | 22 | client.ensure_session(qr::print_with_url).await?; 23 | 24 | let wc = WalletConnect::new(client, env::var("INFURA_PROJECT_ID")?)?; 25 | let web3 = Web3::new(wc); 26 | 27 | let accounts = web3.eth().accounts().await?; 28 | println!("Connected accounts:"); 29 | for account in &accounts { 30 | println!(" - {:?}", account); 31 | } 32 | 33 | let tx = web3 34 | .eth() 35 | .send_transaction(TransactionRequest { 36 | from: accounts[0], 37 | to: Some("000102030405060708090a0b0c0d0e0f10111213".parse()?), 38 | value: Some(1_000_000_000_000_000u128.into()), 39 | ..TransactionRequest::default() 40 | }) 41 | .await?; 42 | 43 | println!("Transaction sent:\n https://etherscan.io/tx/{:?}", tx); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod options; 3 | mod session; 4 | mod socket; 5 | mod storage; 6 | 7 | use self::core::Connector; 8 | pub use self::core::{CallError, ConnectorError, NotConnectedError, SessionError}; 9 | pub use self::options::{Connection, Options, DEFAULT_BRIDGE_URL}; 10 | pub use self::socket::SocketError; 11 | use crate::protocol::{Metadata, Transaction}; 12 | use crate::uri::Uri; 13 | use ethers_core::types::{Address, Bytes, Signature, H256}; 14 | use std::path::PathBuf; 15 | 16 | #[derive(Debug)] 17 | pub struct Client { 18 | connection: Connector, 19 | } 20 | 21 | impl Client { 22 | pub fn new( 23 | profile: impl Into, 24 | meta: impl Into, 25 | ) -> Result { 26 | Client::with_options(Options::new(profile, meta.into())) 27 | } 28 | 29 | pub fn with_options(options: Options) -> Result { 30 | Ok(Client { 31 | connection: Connector::new(options)?, 32 | }) 33 | } 34 | 35 | pub fn accounts(&self) -> Result<(Vec
, u64), NotConnectedError> { 36 | self.connection.accounts() 37 | } 38 | 39 | pub async fn ensure_session(&self, f: F) -> Result<(Vec
, u64), SessionError> 40 | where 41 | F: FnOnce(Uri), 42 | { 43 | self.connection.ensure_session(f).await 44 | } 45 | 46 | pub async fn send_transaction(&self, transaction: Transaction) -> Result { 47 | self.connection.send_transaction(transaction).await 48 | } 49 | 50 | pub async fn sign_transaction(&self, transaction: Transaction) -> Result { 51 | self.connection.sign_transaction(transaction).await 52 | } 53 | 54 | pub async fn personal_sign(&self, data: &[&str]) -> Result { 55 | let sig = self.connection.personal_sign(data).await?; 56 | Ok(sig.as_ref().try_into().unwrap()) 57 | } 58 | 59 | pub fn close(self) -> Result<(), SocketError> { 60 | self.connection.close() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/client/core.rs: -------------------------------------------------------------------------------- 1 | use super::options::{Connection, Options}; 2 | use super::session::Session; 3 | use super::socket::{MessageHandler, Socket, SocketError, SocketHandle}; 4 | use super::storage::Storage; 5 | use crate::protocol::{Topic, Transaction}; 6 | use crate::uri::Uri; 7 | use ethers_core::types::{Address, Bytes, H256}; 8 | use futures::channel::oneshot; 9 | use jsonrpc_core::{Id, MethodCall, Output, Params, Version}; 10 | use serde::de::DeserializeOwned; 11 | use serde::Serialize; 12 | use serde_json::{json, Value}; 13 | use std::collections::HashMap; 14 | use std::sync::atomic::{AtomicU64, Ordering}; 15 | use std::sync::{Arc, Mutex, MutexGuard}; 16 | use thiserror::Error; 17 | 18 | #[derive(Debug)] 19 | pub struct Connector { 20 | current_request: AtomicU64, 21 | context: SharedContext, 22 | socket: Socket, 23 | } 24 | 25 | impl Connector { 26 | pub fn new(options: Options) -> Result { 27 | let handshake_topic = match &options.connection { 28 | Connection::Uri(uri) => Some(uri.handshake_topic().clone()), 29 | _ => None, 30 | }; 31 | let session = Storage::for_session(options); 32 | let client_id = session.client_id.clone(); 33 | 34 | // NOTE: WalletConnect bridge URLs are expected to be automatically 35 | // converted from a `http(s)` to `ws(s)` protocol for the WebSocket 36 | // connection. 37 | let mut url = session.bridge.clone(); 38 | match url.scheme() { 39 | "http" => url.set_scheme("ws").unwrap(), 40 | "https" => url.set_scheme("wss").unwrap(), 41 | "ws" | "wss" => {} 42 | scheme => return Err(ConnectorError::BadScheme(scheme.into())), 43 | } 44 | 45 | let key = session.key.clone(); 46 | let context = SharedContext::new(session); 47 | let handler = ConnectorHandler { 48 | context: context.clone(), 49 | }; 50 | 51 | let socket = Socket::connect(url, key, handler)?; 52 | socket.subscribe(client_id)?; 53 | if let Some(handshake_topic) = handshake_topic { 54 | socket.subscribe(handshake_topic)?; 55 | } 56 | 57 | Ok(Connector { 58 | current_request: AtomicU64::default(), 59 | context, 60 | socket, 61 | }) 62 | } 63 | 64 | pub fn accounts(&self) -> Result<(Vec
, u64), NotConnectedError> { 65 | let session = &self.context.lock().session; 66 | if !session.connected { 67 | return Err(NotConnectedError); 68 | } 69 | 70 | Ok(( 71 | session.accounts.clone(), 72 | session.chain_id.unwrap_or_default(), 73 | )) 74 | } 75 | 76 | async fn call(&self, method: &str, params: P) -> Result 77 | where 78 | P: Serialize, 79 | R: DeserializeOwned, 80 | { 81 | let id = self.current_request.fetch_add(1, Ordering::SeqCst); 82 | 83 | let topic = { 84 | let context = self.context.lock(); 85 | context 86 | .session 87 | .peer_id 88 | .clone() 89 | .unwrap_or_else(|| context.session.handshake_topic.clone()) 90 | //.ok_or(CallError::NotConnected)? 91 | }; 92 | let payload = { 93 | let params = match json!(params) { 94 | Value::Array(params) => Params::Array(params), 95 | param => Params::Array(vec![param]), 96 | }; 97 | let request = MethodCall { 98 | jsonrpc: Some(Version::V2), 99 | method: method.into(), 100 | params, 101 | id: Id::Num(id), 102 | }; 103 | serde_json::to_string(&request)? 104 | }; 105 | let silent = match method { 106 | "wc_sessionRequest" | "wc_sessionUpdate" => true, 107 | "eth_sendTransaction" 108 | | "eth_signTransaction" 109 | | "eth_sign" 110 | | "eth_signTypedData" 111 | | "eth_signTypedData_v1" 112 | | "eth_signTypedData_v3" 113 | | "personal_sign" => false, 114 | _ => true, 115 | }; 116 | 117 | let (tx, rx) = oneshot::channel(); 118 | let existing = { 119 | let mut context = self.context.lock(); 120 | context.pending_requests.insert(Id::Num(id), tx) 121 | }; 122 | 123 | // NOTE: Make sure panic is always outside the mutex guard's scope to 124 | // make sure we don't accidentially poison the mutex. 125 | debug_assert!(existing.is_none(), "request IDs should never collide",); 126 | 127 | if let Err(err) = self.socket.publish(topic, payload, silent) { 128 | // NOTE: Remove the request from the pending request map if we were 129 | // unable to send it as there will never be a response. 130 | let removed = { 131 | let mut context = self.context.lock(); 132 | context.pending_requests.remove(&Id::Num(id)) 133 | }; 134 | 135 | // NOTE: Make sure panic is always outside the mutex guard's 136 | // scope to make sure we don't accidentially poison the mutex. 137 | debug_assert!( 138 | removed.is_some(), 139 | "immediately removed request should never be missing" 140 | ); 141 | 142 | return Err(err.into()); 143 | } 144 | 145 | let response = rx.await?; 146 | match response { 147 | Output::Success(response) => { 148 | let result = R::deserialize(&response.result)?; 149 | Ok(result) 150 | } 151 | Output::Failure(response) => Err(response.error.into()), 152 | } 153 | } 154 | 155 | pub async fn ensure_session(&self, f: F) -> Result<(Vec
, u64), SessionError> 156 | where 157 | F: FnOnce(Uri), 158 | { 159 | let uri = { 160 | let context = self.context.lock(); 161 | if context.session.connected { 162 | return Ok(( 163 | context.session.accounts.clone(), 164 | context.session.chain_id.unwrap_or_default(), 165 | )); 166 | } 167 | context.session.uri() 168 | }; 169 | 170 | f(uri); 171 | let (accounts, chain_id) = self.create_session().await?; 172 | 173 | Ok((accounts, chain_id)) 174 | } 175 | 176 | pub async fn create_session(&self) -> Result<(Vec
, u64), SessionError> { 177 | let params = { 178 | let mut context = self.context.lock(); 179 | if context.session.connected { 180 | return Err(SessionError::Connected); 181 | } 182 | if context.session_pending { 183 | return Err(SessionError::Pending); 184 | } 185 | 186 | context.session_pending = true; 187 | context.session.request() 188 | }; 189 | 190 | let result = self.call("wc_sessionRequest", params).await; 191 | 192 | let (accounts, chain_id) = { 193 | let mut context = self.context.lock(); 194 | context.session_pending = false; 195 | 196 | // NOTE: Propagate the error only after updating signaling that the 197 | // session is no longer pending. 198 | let session_params = result?; 199 | context 200 | .session 201 | .update(move |session| session.apply(session_params)); 202 | 203 | ( 204 | context.session.accounts.clone(), 205 | context.session.chain_id.unwrap_or_default(), 206 | ) 207 | }; 208 | 209 | Ok((accounts, chain_id)) 210 | } 211 | 212 | // pub fn update_session() {} 213 | // pub fn kill_session() {} 214 | 215 | pub async fn send_transaction(&self, transaction: Transaction) -> Result { 216 | self.call("eth_sendTransaction", transaction).await 217 | } 218 | 219 | pub async fn sign_transaction(&self, transaction: Transaction) -> Result { 220 | self.call("eth_signTransaction", transaction).await 221 | } 222 | 223 | pub async fn personal_sign(&self, data: &[&str]) -> Result { 224 | self.call("personal_sign", data).await 225 | } 226 | 227 | // pub fn sign_message() {} 228 | // pub fn signed_typed_data() {} 229 | // pub fn send_custom_request() {} 230 | 231 | // pub fn approve_session() {} 232 | // pub fn reject_session() {} 233 | // pub fn approve_request() {} 234 | // pub fn reject_request() {} 235 | 236 | pub fn close(self) -> Result<(), SocketError> { 237 | self.socket.close() 238 | } 239 | } 240 | 241 | #[derive(Debug, Error)] 242 | #[error("not connected to pear")] 243 | pub struct NotConnectedError; 244 | 245 | #[derive(Debug, Error)] 246 | pub enum ConnectorError { 247 | #[error("invalid URL scheme '{0}', must be 'http(s)' or 'ws(s)'")] 248 | BadScheme(String), 249 | #[error("socket error: {0}")] 250 | SocketError(#[from] SocketError), 251 | } 252 | 253 | #[derive(Debug, Error)] 254 | pub enum CallError { 255 | #[error("not connected to peer")] 256 | NotConnected, 257 | #[error("socket error: {0}")] 258 | Socket(#[from] SocketError), 259 | #[error("request was canceled")] 260 | Canceled(#[from] oneshot::Canceled), 261 | #[error("JSON RPC error: {0}")] 262 | Rpc(#[from] jsonrpc_core::Error), 263 | #[error("JSON serialization error: {0}")] 264 | Json(#[from] serde_json::Error), 265 | } 266 | 267 | #[derive(Debug, Error)] 268 | pub enum SessionError { 269 | #[error("session already connected")] 270 | Connected, 271 | #[error("session already pending")] 272 | Pending, 273 | #[error("error performing JSON RPC request")] 274 | Call(#[from] CallError), 275 | #[error("JSON serialization error: {0}")] 276 | Json(#[from] serde_json::Error), 277 | } 278 | 279 | #[derive(Clone, Debug)] 280 | struct SharedContext(Arc>); 281 | 282 | #[derive(Debug)] 283 | struct Context { 284 | session: Storage, 285 | pending_requests: HashMap>, 286 | session_pending: bool, 287 | } 288 | 289 | impl SharedContext { 290 | fn new(session: Storage) -> Self { 291 | SharedContext(Arc::new(Mutex::new(Context { 292 | session, 293 | pending_requests: HashMap::new(), 294 | session_pending: false, 295 | }))) 296 | } 297 | 298 | fn lock(&self) -> MutexGuard { 299 | self.0.lock().expect("mutex guard should never be poisoned") 300 | } 301 | } 302 | 303 | struct ConnectorHandler { 304 | context: SharedContext, 305 | } 306 | 307 | impl MessageHandler for ConnectorHandler { 308 | type Err = MessageError; 309 | 310 | fn message(&mut self, _: SocketHandle, _: Topic, payload: String) -> Result<(), MessageError> { 311 | if let Ok(request) = serde_json::from_str::(&payload) { 312 | match request.method.as_str() { 313 | "wc_sessionUpdate" => { 314 | let session_update = request.params.parse()?; 315 | let mut context = self.context.lock(); 316 | context 317 | .session 318 | .update(|session| session.update(session_update)); 319 | } 320 | _ => return Err(MessageError::UnsupportedRequest(payload)), 321 | } 322 | } else { 323 | let response = serde_json::from_str::(&payload)?; 324 | 325 | let mut context = self.context.lock(); 326 | let sender = context 327 | .pending_requests 328 | .remove(response.id()) 329 | .ok_or_else(|| { 330 | let id = response.id().clone(); 331 | MessageError::UnregisteredId(id) 332 | })?; 333 | 334 | // NOTE: We ignore send errors as they are "normal" in the sense 335 | // that it is not considered an error to drop the future that is 336 | // waiting for the response before it arrives. 337 | let _ = sender.send(response); 338 | } 339 | 340 | Ok(()) 341 | } 342 | } 343 | 344 | #[derive(Debug, Error)] 345 | pub enum MessageError { 346 | #[error("received response for unregistered request ID '{0:?}'")] 347 | UnregisteredId(Id), 348 | #[error("received unknown notification '{0}'")] 349 | UnsupportedRequest(String), 350 | #[error("JSON deserialization error: {0}")] 351 | Json(#[from] serde_json::Error), 352 | #[error("JSON RPC error: {0}")] 353 | Rpc(#[from] jsonrpc_core::Error), 354 | } 355 | -------------------------------------------------------------------------------- /src/client/old.rs: -------------------------------------------------------------------------------- 1 | /* 2 | use super::rpc::{Request, Response}; 3 | use super::storage::Storage; 4 | use crate::session::{Metadata, Session}; 5 | use futures::channel::{mpsc, oneshot}; 6 | use futures::future::{FutureExt, Shared}; 7 | use std::collections::HashMap; 8 | use std::path::Path; 9 | use std::sync::{Arc, Mutex}; 10 | use std::thread::{self, JoinHandle}; 11 | use thiserror::Error; 12 | use url::{ParseError, Url}; 13 | use ws::{Factory, Handler, Handshake, Message, Sender, WebSocket}; 14 | 15 | #[derive(Debug)] 16 | pub struct Client { 17 | sender: Sender, 18 | event_loop: JoinHandle>, 19 | connected: Shared>, 20 | context: Arc>, 21 | } 22 | 23 | #[derive(Debug)] 24 | struct Context { 25 | session: Storage, 26 | requests: HashMap>, 27 | } 28 | 29 | pub const DEFAULT_BRIDGE_URL: &str = "https://bridge.walletconnect.org"; 30 | 31 | impl Client { 32 | pub fn new(profile: impl AsRef, meta: Metadata) -> Result { 33 | Client::with_bridge(profile, DEFAULT_BRIDGE_URL, meta) 34 | } 35 | 36 | pub fn with_bridge( 37 | profile: impl AsRef, 38 | bridge: impl AsRef, 39 | meta: Metadata, 40 | ) -> Result { 41 | let bridge = Url::parse(bridge.as_ref())?; 42 | 43 | // NOTE: WalletConnect bridge URLs are expected to be automatically 44 | // converted from a `http(s)` to `ws(s)` protocol for the WebSocket 45 | // connection. 46 | let rpc_url = { 47 | let mut rpc_url = bridge.clone(); 48 | match rpc_url.scheme() { 49 | "http" => rpc_url.set_scheme("ws").unwrap(), 50 | "https" => rpc_url.set_scheme("wss").unwrap(), 51 | "ws" | "wss" => {} 52 | scheme => return Err(CreationError::BadScheme(scheme.into())), 53 | } 54 | rpc_url 55 | }; 56 | 57 | let context = { 58 | let session = Storage::for_session(profile.as_ref(), bridge, meta); 59 | Arc::new(Mutex::new(Context { 60 | session, 61 | requests: HashMap::new(), 62 | })) 63 | }; 64 | let (connected_tx, connected_rx) = oneshot::channel(); 65 | let handler = ClientHandler { 66 | context: context.clone(), 67 | connected: Some(connected_tx), 68 | }; 69 | 70 | let factory = ClientFactory(Some(handler)); 71 | let mut socket = WebSocket::new(factory)?; 72 | socket.connect(rpc_url)?; 73 | let sender = socket.broadcaster(); 74 | let event_loop = thread::spawn(move || socket.run().err()); 75 | 76 | Ok(Client { 77 | sender, 78 | event_loop, 79 | context, 80 | connected: connected_rx.shared(), 81 | }) 82 | } 83 | 84 | pub fn session(&self) -> Session { 85 | self.context.lock().unwrap().session.clone() 86 | } 87 | 88 | pub fn close(self) -> Result<(), CloseError> { 89 | self.sender.shutdown()?; 90 | // NOTE: Intentionally propagate the event loop panic, as it is not 91 | // intended to do so. 92 | let err = self.event_loop.join().unwrap(); 93 | 94 | if let Some(err) = err { 95 | Err(err.into()) 96 | } else { 97 | Ok(()) 98 | } 99 | } 100 | } 101 | 102 | struct ClientFactory(Option); 103 | 104 | impl Factory for ClientFactory { 105 | type Handler = ClientHandler; 106 | 107 | fn connection_made(&mut self, _: Sender) -> Self::Handler { 108 | self.0 109 | .take() 110 | .expect("more than one connection made for a single client") 111 | } 112 | } 113 | 114 | struct ClientHandler { 115 | context: Arc>, 116 | connected: Option>, 117 | } 118 | 119 | impl Handler for ClientHandler { 120 | fn on_open(&mut self, _: Handshake) -> ws::Result<()> { 121 | self.connected 122 | .take() 123 | .expect("connection opened more than once") 124 | .send(()) 125 | .expect("client should not be dropped"); 126 | 127 | Ok(()) 128 | } 129 | 130 | fn on_message(&mut self, msg: Message) -> ws::Result<()> { 131 | let response = Response::parse(msg.as_text()?); 132 | let mut context = self.context.lock().unwrap(); 133 | if let Some(sender) = context.requests.remove(&response.id) { 134 | // NOTE: ignore errors where the receiver is dropped. 135 | let _ = sender.send(response); 136 | } 137 | 138 | Ok(()) 139 | } 140 | } 141 | 142 | #[derive(Debug, Error)] 143 | pub enum CreationError { 144 | #[error("failed to parse URL: {0}")] 145 | Parse(#[from] ParseError), 146 | #[error("invalid URL scheme '{0}', must be 'http(s)' or 'ws(s)'")] 147 | BadScheme(String), 148 | #[error("IO error when creating the WebSocket: {0}")] 149 | Io(#[from] ws::Error), 150 | } 151 | 152 | #[derive(Debug, Error)] 153 | pub enum CloseError { 154 | #[error("failed to shutdown WebSocket: {0}")] 155 | Shutdown(#[from] ws::Error), 156 | } 157 | */ 158 | -------------------------------------------------------------------------------- /src/client/options.rs: -------------------------------------------------------------------------------- 1 | use super::session::Session; 2 | use crate::crypto::Key; 3 | use crate::protocol::{Metadata, Topic}; 4 | use crate::uri::Uri; 5 | use lazy_static::lazy_static; 6 | use std::path::PathBuf; 7 | use url::Url; 8 | 9 | lazy_static! { 10 | pub static ref DEFAULT_BRIDGE_URL: Url = 11 | Url::parse("https://bridge.walletconnect.org").unwrap(); 12 | } 13 | 14 | #[derive(Clone, Debug)] 15 | pub enum Connection { 16 | Bridge(Url), 17 | Uri(Uri), 18 | } 19 | 20 | impl Default for Connection { 21 | fn default() -> Self { 22 | Connection::Bridge(DEFAULT_BRIDGE_URL.clone()) 23 | } 24 | } 25 | 26 | #[derive(Clone, Debug)] 27 | pub struct Options { 28 | pub profile: PathBuf, 29 | pub meta: Metadata, 30 | pub connection: Connection, 31 | pub chain_id: Option, 32 | } 33 | 34 | impl Options { 35 | pub fn new(profile: impl Into, meta: Metadata) -> Self { 36 | Options { 37 | profile: profile.into(), 38 | meta, 39 | connection: Connection::default(), 40 | chain_id: None, 41 | } 42 | } 43 | 44 | pub fn with_uri(profile: impl Into, meta: Metadata, uri: Uri) -> Self { 45 | Options { 46 | profile: profile.into(), 47 | meta, 48 | connection: Connection::Uri(uri), 49 | chain_id: None, 50 | } 51 | } 52 | 53 | pub fn create_session(self) -> Session { 54 | let client_meta = self.meta; 55 | let (handshake_topic, bridge, key) = match self.connection { 56 | Connection::Bridge(bridge) => (Topic::new(), bridge, Key::random()), 57 | Connection::Uri(uri) => uri.into_parts(), 58 | }; 59 | let chain_id = self.chain_id; 60 | 61 | Session { 62 | connected: false, 63 | accounts: Vec::new(), 64 | chain_id, 65 | bridge, 66 | key, 67 | client_id: Topic::new(), 68 | client_meta, 69 | peer_id: None, 70 | peer_meta: None, 71 | handshake_id: 0, 72 | handshake_topic, 73 | } 74 | } 75 | 76 | pub fn matches(&self, session: &Session) -> bool { 77 | self.meta == session.client_meta 78 | && match &self.connection { 79 | Connection::Bridge(bridge) => *bridge == session.bridge, 80 | Connection::Uri(uri) => *uri == session.uri(), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/client/session.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto::Key; 2 | use crate::protocol::{ 3 | Metadata, PeerMetadata, SessionParams, SessionRequest, SessionUpdate, Topic, 4 | }; 5 | use crate::uri::Uri; 6 | use ethers_core::types::Address; 7 | use serde::{Deserialize, Serialize}; 8 | use url::form_urlencoded::Serializer; 9 | use url::Url; 10 | 11 | #[derive(Clone, Debug, Deserialize, Serialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct Session { 14 | pub connected: bool, 15 | pub accounts: Vec
, 16 | pub chain_id: Option, 17 | pub bridge: Url, 18 | pub key: Key, 19 | pub client_id: Topic, 20 | pub client_meta: Metadata, 21 | pub peer_id: Option, 22 | pub peer_meta: Option, 23 | pub handshake_id: u64, 24 | pub handshake_topic: Topic, 25 | } 26 | 27 | impl Session { 28 | pub fn uri(&self) -> Uri { 29 | Uri::parse(&format!( 30 | "wc:{}@1?{}", 31 | self.handshake_topic, 32 | Serializer::new(String::new()) 33 | .append_pair("bridge", self.bridge.as_str()) 34 | .append_pair("key", self.key.display().as_str()) 35 | .finish() 36 | )) 37 | .expect("WalletConnect URIs from sessions are always valid") 38 | } 39 | 40 | pub fn request(&self) -> SessionRequest { 41 | SessionRequest { 42 | peer_id: self.client_id.clone(), 43 | peer_meta: self.client_meta.clone(), 44 | chain_id: self.chain_id, 45 | } 46 | } 47 | 48 | pub fn apply(&mut self, params: SessionParams) { 49 | self.connected = params.approved; 50 | self.accounts = params.accounts; 51 | self.chain_id = Some(params.chain_id); 52 | self.peer_id = Some(params.peer_id); 53 | self.peer_meta = Some(params.peer_meta); 54 | } 55 | 56 | pub fn update(&mut self, update: SessionUpdate) { 57 | self.connected = update.approved; 58 | self.accounts = update.accounts; 59 | self.chain_id = Some(update.chain_id); 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::*; 66 | use serde_json::json; 67 | 68 | #[test] 69 | fn new_topic_is_random() { 70 | assert_ne!(Topic::new(), Topic::new()); 71 | } 72 | 73 | #[test] 74 | fn zero_topic() { 75 | assert_eq!( 76 | json!(Topic::zero()), 77 | json!("00000000-0000-0000-0000-000000000000") 78 | ); 79 | } 80 | 81 | #[test] 82 | fn topic_serialization() { 83 | let topic = Topic::new(); 84 | let serialized = serde_json::to_string(&topic).unwrap(); 85 | let deserialized = serde_json::from_str(&serialized).unwrap(); 86 | assert_eq!(topic, deserialized); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/client/socket.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto::{Key, OpenError, SealError}; 2 | use crate::protocol::{SocketMessage, SocketMessageKind, Topic}; 3 | use log::{trace, warn}; 4 | use parity_ws::{Handler, Message, Sender, WebSocket}; 5 | use std::error::Error; 6 | use std::str::Utf8Error; 7 | use std::thread::{self, JoinHandle}; 8 | use thiserror::Error; 9 | use url::Url; 10 | 11 | #[derive(Debug)] 12 | pub struct Socket { 13 | key: Key, 14 | sender: Sender, 15 | event_loop: JoinHandle>, 16 | } 17 | 18 | impl Socket { 19 | pub fn connect( 20 | url: Url, 21 | key: Key, 22 | message_handler: impl MessageHandler + Send + 'static, 23 | ) -> Result { 24 | let mut socket = WebSocket::new({ 25 | let mut params = Some((key.clone(), message_handler)); 26 | move |sender| { 27 | let (key, message_handler) = params 28 | .take() 29 | .expect("more than one WebSocket connection established"); 30 | SocketHandler { 31 | key, 32 | sender, 33 | message_handler, 34 | } 35 | } 36 | })?; 37 | 38 | socket.connect(url)?; 39 | let sender = socket.broadcaster(); 40 | let event_loop = thread::spawn(move || match socket.run() { 41 | Ok(_) => Ok(()), 42 | Err(err) => { 43 | warn!("socket runloop unexpectedly quit with error: {:?}", err); 44 | Err(err) 45 | } 46 | }); 47 | 48 | Ok(Socket { 49 | key, 50 | sender, 51 | event_loop, 52 | }) 53 | } 54 | 55 | fn handle(&self) -> SocketHandle { 56 | SocketHandle { 57 | key: &self.key, 58 | sender: &self.sender, 59 | } 60 | } 61 | 62 | pub fn subscribe(&self, topic: Topic) -> Result<(), SocketError> { 63 | self.handle().subscribe(topic) 64 | } 65 | 66 | pub fn publish( 67 | &self, 68 | topic: Topic, 69 | payload: impl AsRef, 70 | silent: bool, 71 | ) -> Result<(), SocketError> { 72 | self.handle().publish(topic, payload, silent) 73 | } 74 | 75 | pub fn close(self) -> Result<(), SocketError> { 76 | self.sender.shutdown()?; 77 | self.event_loop 78 | .join() 79 | .expect("event loop should never panic")?; 80 | 81 | Ok(()) 82 | } 83 | } 84 | 85 | #[derive(Debug, Error)] 86 | pub enum SocketError { 87 | #[error("WebSocket error")] 88 | WebSocket(#[from] parity_ws::Error), 89 | #[error("JSON serialization error: {0}")] 90 | Json(#[from] serde_json::Error), 91 | #[error("failed to seal AEAD payload: {0}")] 92 | Seal(#[from] SealError), 93 | } 94 | 95 | #[derive(Debug)] 96 | pub struct SocketHandle<'a> { 97 | key: &'a Key, 98 | sender: &'a Sender, 99 | } 100 | 101 | impl SocketHandle<'_> { 102 | pub fn subscribe(&self, topic: Topic) -> Result<(), SocketError> { 103 | self.send(SocketMessage { 104 | topic, 105 | kind: SocketMessageKind::Sub, 106 | payload: None, 107 | silent: true, 108 | })?; 109 | 110 | Ok(()) 111 | } 112 | 113 | pub fn publish( 114 | &self, 115 | topic: Topic, 116 | payload: impl AsRef, 117 | silent: bool, 118 | ) -> Result<(), SocketError> { 119 | trace!("sending payload '{}'", payload.as_ref()); 120 | 121 | let payload = self.key.seal(payload.as_ref())?; 122 | self.send(SocketMessage { 123 | topic, 124 | kind: SocketMessageKind::Pub, 125 | payload: Some(payload), 126 | silent, 127 | })?; 128 | 129 | Ok(()) 130 | } 131 | 132 | fn send(&self, message: SocketMessage) -> Result<(), SocketError> { 133 | let json = serde_json::to_string(&message)?; 134 | trace!("sending message '{}'", json); 135 | 136 | self.sender.send(json)?; 137 | 138 | Ok(()) 139 | } 140 | } 141 | 142 | pub trait MessageHandler { 143 | type Err: Error + Send + Sync + 'static; 144 | 145 | fn message( 146 | &mut self, 147 | socket: SocketHandle, 148 | topic: Topic, 149 | payload: String, 150 | ) -> Result<(), Self::Err>; 151 | } 152 | 153 | struct SocketHandler { 154 | key: Key, 155 | sender: Sender, 156 | message_handler: M, 157 | } 158 | 159 | impl SocketHandler 160 | where 161 | M: MessageHandler, 162 | { 163 | fn decrypt_message(&self, message: &str) -> Result<(Topic, String), MessageError> { 164 | trace!("received message '{}'", message); 165 | 166 | let message: SocketMessage = serde_json::from_str(message)?; 167 | if let SocketMessageKind::Sub = message.kind { 168 | return Err(MessageError::Sub(message.topic)); 169 | } 170 | 171 | let topic = message.topic; 172 | let payload = match message.payload { 173 | Some(payload) => payload, 174 | None => return Err(MessageError::MissingPayload), 175 | }; 176 | 177 | let opened = self.key.open(&payload)?; 178 | let decrypted = String::from_utf8(opened).map_err(|err| err.utf8_error())?; 179 | 180 | trace!("received payload '{}'", decrypted); 181 | 182 | Ok((topic, decrypted)) 183 | } 184 | } 185 | 186 | impl Handler for SocketHandler 187 | where 188 | M: MessageHandler, 189 | { 190 | fn on_message(&mut self, message: Message) -> parity_ws::Result<()> { 191 | let (topic, payload) = self.decrypt_message(message.as_text()?).map_err(Box::new)?; 192 | let handle = SocketHandle { 193 | key: &self.key, 194 | sender: &self.sender, 195 | }; 196 | self.message_handler 197 | .message(handle, topic, payload) 198 | .map_err(Box::new)?; 199 | 200 | Ok(()) 201 | } 202 | } 203 | 204 | #[derive(Debug, Error)] 205 | pub enum MessageError { 206 | #[error("JSON serialization error: {0}")] 207 | Json(#[from] serde_json::Error), 208 | #[error("unexpected 'sub' message with topic '{0}'")] 209 | Sub(Topic), 210 | #[error("message payload missing")] 211 | MissingPayload, 212 | #[error("failed to open AEAD payload: {0}")] 213 | Aead(#[from] OpenError), 214 | #[error("invalid UTF-8 in decrypted payload: {0}")] 215 | Utf8(#[from] Utf8Error), 216 | } 217 | -------------------------------------------------------------------------------- /src/client/storage.rs: -------------------------------------------------------------------------------- 1 | use super::options::Options; 2 | use super::session::Session; 3 | use log::warn; 4 | use serde::de::DeserializeOwned; 5 | use serde::Serialize; 6 | use std::env; 7 | use std::fs::File; 8 | use std::io; 9 | use std::ops::Deref; 10 | use std::path::{Path, PathBuf}; 11 | 12 | #[derive(Debug)] 13 | pub struct Storage { 14 | path: PathBuf, 15 | value: T, 16 | } 17 | 18 | impl Storage { 19 | pub fn for_session(options: Options) -> Self { 20 | let path = session_profile_path(&options.profile); 21 | let (value, save) = match Storage::load(&path) { 22 | Ok(session) if options.matches(&session) => (session, false), 23 | _ => (options.create_session(), true), 24 | }; 25 | 26 | let resource = Storage { path, value }; 27 | if save { 28 | resource.save(); 29 | } 30 | 31 | resource 32 | } 33 | } 34 | 35 | impl Storage { 36 | fn load(path: &Path) -> io::Result { 37 | let file = File::open(path)?; 38 | let value = serde_json::from_reader(file)?; 39 | 40 | Ok(value) 41 | } 42 | 43 | fn save(&self) { 44 | if let Err(err) = self.try_save() { 45 | warn!("error saving to '{}': {:?}", self.path.display(), err); 46 | } 47 | } 48 | 49 | fn try_save(&self) -> io::Result<()> { 50 | let file = File::create(&self.path)?; 51 | serde_json::to_writer_pretty(file, &self.value)?; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub fn update(&mut self, f: F) 57 | where 58 | F: FnOnce(&mut T), 59 | { 60 | f(&mut self.value); 61 | self.save(); 62 | } 63 | } 64 | 65 | impl Deref for Storage { 66 | type Target = T; 67 | 68 | fn deref(&self) -> &Self::Target { 69 | &self.value 70 | } 71 | } 72 | 73 | fn default_wallectconnect_cache_dir() -> PathBuf { 74 | let mut cache = env::var_os("XDG_CACHE_HOME") 75 | .map(PathBuf::from) 76 | .or_else(|| { 77 | env::var_os("HOME").map(|home| { 78 | let mut home = PathBuf::from(home); 79 | home.push(".cache"); 80 | home 81 | }) 82 | }) 83 | .unwrap_or_else(|| PathBuf::from("/etc/cache")); 84 | cache.push("walletconnect-rs"); 85 | cache 86 | } 87 | 88 | fn session_profile_path(profile: &Path) -> PathBuf { 89 | let mut path = default_wallectconnect_cache_dir(); 90 | path.push("profiles"); 91 | path.push(profile); 92 | path.set_extension("json"); 93 | path 94 | } 95 | -------------------------------------------------------------------------------- /src/crypto.rs: -------------------------------------------------------------------------------- 1 | mod aead; 2 | mod key; 3 | 4 | pub use aead::{OpenError, SealError}; 5 | pub use key::{DisplayKey, Key}; 6 | -------------------------------------------------------------------------------- /src/crypto/aead.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::EncryptionPayload; 2 | use openssl::error::ErrorStack; 3 | use openssl::hash::MessageDigest; 4 | use openssl::pkey::PKey; 5 | use openssl::sign::Signer; 6 | use openssl::symm::{self, Cipher}; 7 | use rand::Rng; 8 | use thiserror::Error; 9 | 10 | fn hmac_sha256(key: &[u8], iv: &[u8], data: &[u8]) -> Result, ErrorStack> { 11 | let key = PKey::hmac(key)?; 12 | let mut signer = Signer::new(MessageDigest::sha256(), &key)?; 13 | signer.update(data)?; 14 | signer.update(iv)?; 15 | let hmac = signer.sign_to_vec()?; 16 | 17 | Ok(hmac) 18 | } 19 | 20 | fn generate_iv() -> Vec { 21 | let mut iv = vec![0; 16]; 22 | rand::thread_rng().fill(&mut iv[..]); 23 | iv 24 | } 25 | 26 | pub fn seal(key: &[u8], plaintext: &[u8]) -> Result { 27 | let cipher = Cipher::aes_256_cbc(); 28 | let iv = generate_iv(); 29 | let data = symm::encrypt(cipher, key, Some(&iv), plaintext)?; 30 | let hmac = hmac_sha256(key, &iv, &data)?; 31 | 32 | Ok(EncryptionPayload { data, iv, hmac }) 33 | } 34 | 35 | pub fn open(key: &[u8], payload: &EncryptionPayload) -> Result, OpenError> { 36 | let hmac = hmac_sha256(key, &payload.iv, &payload.data)?; 37 | if hmac != payload.hmac { 38 | return Err(OpenError::Verify); 39 | } 40 | 41 | let cipher = Cipher::aes_256_cbc(); 42 | let plaintext = symm::decrypt(cipher, key, Some(&payload.iv), &payload.data)?; 43 | 44 | Ok(plaintext) 45 | } 46 | 47 | #[derive(Debug, Error)] 48 | pub enum SealError { 49 | #[error("internal OpenSSL error: {0}")] 50 | OpenSsl(#[from] ErrorStack), 51 | } 52 | 53 | #[derive(Debug, Error)] 54 | pub enum OpenError { 55 | #[error("internal OpenSSL error: {0}")] 56 | OpenSsl(#[from] ErrorStack), 57 | #[error("unable to verify integrity of payload")] 58 | Verify, 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | use crate::crypto::Key; 65 | use crate::hex; 66 | use std::str; 67 | 68 | #[test] 69 | fn roundtrip() { 70 | let message = "walletconnect-rs"; 71 | let key = Key::random(); 72 | 73 | let payload = seal(&key, message.as_bytes()).unwrap(); 74 | let plaintext = open(&key, &payload).unwrap(); 75 | 76 | assert_eq!(str::from_utf8(&plaintext).unwrap(), message); 77 | } 78 | 79 | #[test] 80 | fn open_payload() { 81 | // Test vector retrieved by inspecting a WalletConnect session with 82 | // https://example.walletconnect.org 83 | 84 | let key = hex::decode("26075c07b19284e193101d7f27d7f96aa1802645663110a47c5c3bd3da580cae") 85 | .unwrap(); 86 | let payload = EncryptionPayload { 87 | data: hex::decode( 88 | "61e66ba15a7cd452fe14a47ab47a0b49b5deb8bffb9b24c736539600a808a107\ 89 | 98b573ca1c8353e585d95866cd1f2756fef5b0ea334fca5a8f877322712e0b97\ 90 | 33b75400c199212c741bf973c11d3b797f5fb0f413db8a939cfddc4bf8dc96dd\ 91 | 62c01237c8e7038c93f8dbd7d14d22ea82b568cc45fadb3face32350847985cb\ 92 | 57a3e70cb520fe987544084ae125d7913de81c3e7e6e88039ef40cc4b19be1a7\ 93 | 90b6c5509d0822acb7f2bc6d83de528c8f787e29906c5f7ec50d7a8f7b36796f\ 94 | a3b44edc3538ca6ac039cd17714c50f63b6b9788d3860195e094e571a2a5dba9\ 95 | b74c8065c04aad11bce2545eb19bd94ad0ee261195b8fa0a738442983d6415a8\ 96 | 81d5d8cd69c07088eb4d979082762c429a3a7ac7d84a4eec84a5144a8675a0e4\ 97 | 094dc1fbc243def3edb2fd15196aa19bce82bedd955126992ff7d952a735a889", 98 | ) 99 | .unwrap(), 100 | hmac: hex::decode("1ff024bb7234f3b514b0e0ee130d81f1a367ec09fc2cf191ab52ed07e1f8bbe9") 101 | .unwrap(), 102 | iv: hex::decode("019dc30e6463c2c1acd165310d686553").unwrap(), 103 | }; 104 | let message = r#"{"id":1580823313241457,"jsonrpc":"2.0","method":"wc_sessionRequest","params":[{"peerId":"e8526892-8e47-42e4-9ea3-20c0b164bb83","peerMeta":{"description":"","url":"https://example.walletconnect.org","icons":["https://example.walletconnect.org/favicon.ico"],"name":"WalletConnect Example"},"chainId":null}]}"#; 105 | 106 | let plaintext = open(&key, &payload).unwrap(); 107 | assert_eq!(str::from_utf8(&plaintext).unwrap(), message); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/crypto/key.rs: -------------------------------------------------------------------------------- 1 | use super::aead::{self, OpenError, SealError}; 2 | use crate::hex; 3 | use crate::protocol::EncryptionPayload; 4 | use data_encoding::DecodeError; 5 | use serde::de::{self, Deserialize, Deserializer}; 6 | use serde::ser::{Serialize, Serializer}; 7 | use std::borrow::Cow; 8 | use std::fmt::{self, Debug, Display, Formatter}; 9 | use std::hash::{Hash, Hasher}; 10 | use std::ops::Deref; 11 | use std::str::FromStr; 12 | use thiserror::Error; 13 | use zeroize::Zeroizing; 14 | 15 | #[derive(Clone, Eq, PartialEq)] 16 | pub struct Key(Zeroizing<[u8; 32]>); 17 | 18 | impl Key { 19 | pub fn random() -> Self { 20 | Key::from_raw(rand::random()) 21 | } 22 | 23 | pub fn from_raw(raw: [u8; 32]) -> Self { 24 | Key(raw.into()) 25 | } 26 | 27 | pub fn display(&self) -> DisplayKey { 28 | DisplayKey(hex::encode(self.0.as_slice())) 29 | } 30 | 31 | pub fn seal(&self, data: impl AsRef<[u8]>) -> Result { 32 | aead::seal(self, data.as_ref()) 33 | } 34 | 35 | pub fn open(&self, payload: &EncryptionPayload) -> Result, OpenError> { 36 | aead::open(self, payload) 37 | } 38 | } 39 | 40 | impl FromStr for Key { 41 | type Err = DecodeError; 42 | 43 | fn from_str(s: &str) -> Result { 44 | let mut bytes = [0u8; 32]; 45 | hex::decode_mut(s, &mut bytes)?; 46 | Ok(Key::from_raw(bytes)) 47 | } 48 | } 49 | 50 | impl Debug for Key { 51 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 52 | f.write_str("Key(********)") 53 | } 54 | } 55 | 56 | #[allow(clippy::derive_hash_xor_eq)] 57 | impl Hash for Key { 58 | fn hash(&self, state: &mut H) { 59 | self.0.hash(state); 60 | } 61 | } 62 | 63 | impl Deref for Key { 64 | type Target = [u8]; 65 | 66 | fn deref(&self) -> &Self::Target { 67 | &*self.0 68 | } 69 | } 70 | 71 | #[derive(Clone, Copy, Debug, Default, Error, Eq, PartialEq)] 72 | #[error("key must be exactly 32 bytes")] 73 | pub struct KeyLengthError; 74 | 75 | #[derive(Debug)] 76 | pub struct DisplayKey(String); 77 | 78 | impl DisplayKey { 79 | pub fn as_str(&self) -> &str { 80 | &self.0 81 | } 82 | } 83 | 84 | impl Display for DisplayKey { 85 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 86 | f.write_str(self.as_str()) 87 | } 88 | } 89 | 90 | impl Serialize for Key { 91 | fn serialize(&self, serializer: S) -> Result { 92 | serializer.serialize_str(self.display().as_str()) 93 | } 94 | } 95 | 96 | impl<'de> Deserialize<'de> for Key { 97 | fn deserialize>(deserializer: D) -> Result { 98 | let s = Cow::<'de, str>::deserialize(deserializer)?; 99 | Key::from_str(&s).map_err(de::Error::custom) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/hex.rs: -------------------------------------------------------------------------------- 1 | use data_encoding::{DecodeError, DecodeKind, HEXLOWER_PERMISSIVE as HEX}; 2 | 3 | pub fn encode(data: impl AsRef<[u8]>) -> String { 4 | HEX.encode(data.as_ref()) 5 | } 6 | 7 | pub fn decode_mut( 8 | bytes: impl AsRef<[u8]>, 9 | mut buffer: impl AsMut<[u8]>, 10 | ) -> Result<(), DecodeError> { 11 | let bytes = bytes.as_ref(); 12 | let buffer = buffer.as_mut(); 13 | 14 | let decode_len = HEX.decode_len(bytes.len())?; 15 | if buffer.len() != decode_len { 16 | return Err(DecodeError { 17 | position: 0, 18 | kind: DecodeKind::Length, 19 | }); 20 | } 21 | 22 | HEX.decode_mut(bytes, buffer).map_err(|err| err.error)?; 23 | Ok(()) 24 | } 25 | 26 | pub fn decode(bytes: impl AsRef<[u8]>) -> Result, DecodeError> { 27 | let bytes = bytes.as_ref(); 28 | 29 | let buffer_len = HEX.decode_len(bytes.len())?; 30 | let mut buffer = vec![0; buffer_len]; 31 | 32 | decode_mut(bytes, &mut buffer)?; 33 | Ok(buffer) 34 | } 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::result_large_err)] 2 | 3 | pub mod client; 4 | mod crypto; 5 | pub mod errors; 6 | mod hex; 7 | mod protocol; 8 | #[cfg(feature = "qr")] 9 | pub mod qr; 10 | mod serialization; 11 | #[cfg(feature = "transport")] 12 | pub mod transport; 13 | mod uri; 14 | 15 | pub use client::Client; 16 | pub use protocol::*; 17 | pub use uri::Uri; 18 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | mod message; 2 | mod rpc; 3 | mod topic; 4 | 5 | pub use self::message::*; 6 | pub use self::rpc::*; 7 | pub use self::topic::*; 8 | 9 | pub use ethers_core::types::{Address, H160, H256, U256}; 10 | -------------------------------------------------------------------------------- /src/protocol/message.rs: -------------------------------------------------------------------------------- 1 | use super::Topic; 2 | use crate::serialization; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 6 | pub struct SocketMessage { 7 | pub topic: Topic, 8 | #[serde(rename = "type")] 9 | pub kind: SocketMessageKind, 10 | #[serde(with = "serialization::jsonstring")] 11 | pub payload: Option, 12 | #[serde(default)] 13 | pub silent: bool, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 17 | #[serde(rename_all = "lowercase")] 18 | pub enum SocketMessageKind { 19 | Pub, 20 | Sub, 21 | } 22 | 23 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 24 | pub struct EncryptionPayload { 25 | #[serde(with = "serialization::hexstring")] 26 | pub data: Vec, 27 | #[serde(with = "serialization::hexstring")] 28 | pub hmac: Vec, 29 | #[serde(with = "serialization::hexstring")] 30 | pub iv: Vec, 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use serde_json::json; 37 | 38 | #[test] 39 | fn message_serialization() { 40 | let message = SocketMessage { 41 | topic: "de5682be-2a03-4b8e-866e-1e89dbca422b".parse().unwrap(), 42 | kind: SocketMessageKind::Pub, 43 | payload: Some(EncryptionPayload { 44 | data: vec![0x04, 0x2], 45 | hmac: vec![0x13, 0x37], 46 | iv: vec![0x00], 47 | }), 48 | silent: false, 49 | }; 50 | let json = json!({ 51 | "topic": "de5682be-2a03-4b8e-866e-1e89dbca422b", 52 | "type": "pub", 53 | "payload": "{\"data\":\"0402\",\"hmac\":\"1337\",\"iv\":\"00\"}", 54 | "silent": false, 55 | }); 56 | 57 | assert_eq!(serde_json::to_value(&message).unwrap(), json); 58 | assert_eq!( 59 | serde_json::from_value::(json).unwrap(), 60 | message 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/protocol/rpc.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::Topic; 2 | use crate::serialization; 3 | use ethers_core::types::{Address, U256}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use url::Url; 7 | 8 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Metadata { 11 | pub description: String, 12 | pub url: Url, 13 | #[serde(default)] 14 | pub icons: Vec, 15 | pub name: String, 16 | } 17 | 18 | #[derive(Clone, Debug, Deserialize, Serialize)] 19 | #[serde(untagged)] 20 | pub enum PeerMetadata { 21 | Strict(Metadata), 22 | Malformed(Value), 23 | } 24 | 25 | #[derive(Clone, Debug, Deserialize, Serialize)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct SessionRequest { 28 | pub chain_id: Option, 29 | pub peer_id: Topic, 30 | pub peer_meta: Metadata, 31 | } 32 | 33 | #[derive(Clone, Debug, Deserialize, Serialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct SessionParams { 36 | pub approved: bool, 37 | pub accounts: Vec
, 38 | pub chain_id: u64, 39 | pub peer_id: Topic, 40 | pub peer_meta: PeerMetadata, 41 | } 42 | 43 | #[derive(Clone, Debug, Deserialize, Serialize)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct SessionUpdate { 46 | pub approved: bool, 47 | pub accounts: Vec
, 48 | pub chain_id: u64, 49 | } 50 | 51 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 52 | #[serde(rename_all = "camelCase")] 53 | pub struct Transaction { 54 | pub from: Address, 55 | #[serde(default, with = "serialization::emptynoneaddress")] 56 | pub to: Option
, 57 | #[serde(default)] 58 | pub gas_limit: Option, 59 | #[serde(default)] 60 | pub gas_price: Option, 61 | #[serde(default)] 62 | pub value: U256, 63 | #[serde(default, with = "serialization::prefixedhexstring")] 64 | pub data: Vec, 65 | #[serde(default)] 66 | pub nonce: Option, 67 | } 68 | -------------------------------------------------------------------------------- /src/protocol/topic.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt::{self, Display, Formatter}; 3 | use std::str::FromStr; 4 | use uuid::{self, Uuid}; 5 | 6 | #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 7 | #[serde(transparent)] 8 | pub struct Topic(String); 9 | 10 | impl Topic { 11 | pub fn new() -> Self { 12 | Topic(Uuid::new_v4().to_string()) 13 | } 14 | 15 | pub fn zero() -> Self { 16 | Topic(Uuid::nil().to_string()) 17 | } 18 | } 19 | 20 | impl Default for Topic { 21 | fn default() -> Self { 22 | Topic::zero() 23 | } 24 | } 25 | 26 | impl Display for Topic { 27 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 28 | self.0.fmt(f) 29 | } 30 | } 31 | 32 | impl FromStr for Topic { 33 | type Err = uuid::Error; 34 | 35 | fn from_str(s: &str) -> Result { 36 | Uuid::from_str(s)?; 37 | Ok(Topic(s.into())) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use serde_json::json; 45 | 46 | #[test] 47 | fn new_topic_is_random() { 48 | assert_ne!(Topic::new(), Topic::new()); 49 | } 50 | 51 | #[test] 52 | fn zero_topic() { 53 | assert_eq!( 54 | json!(Topic::zero()), 55 | json!("00000000-0000-0000-0000-000000000000") 56 | ); 57 | } 58 | 59 | #[test] 60 | fn topic_serialization() { 61 | let topic = Topic::new(); 62 | let serialized = serde_json::to_string(&topic).unwrap(); 63 | let deserialized = serde_json::from_str(&serialized).unwrap(); 64 | assert_eq!(topic, deserialized); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/qr.rs: -------------------------------------------------------------------------------- 1 | mod image; 2 | mod print; 3 | 4 | pub use crate::qr::image::{Dot, Grid}; 5 | pub use crate::qr::print::{Colors, Output, Print}; 6 | use qrcode::types::QrError; 7 | pub use qrcode::QrCode; 8 | use std::io::Error as IoError; 9 | pub use termcolor::Color; 10 | use thiserror::Error; 11 | 12 | #[derive(Debug, Error)] 13 | pub enum PrintError { 14 | #[error("error generating QR code: {0}")] 15 | Qr(#[from] QrError), 16 | 17 | #[error("error printing QR code: {0}")] 18 | Io(#[from] IoError), 19 | } 20 | 21 | pub fn try_print(data: impl AsRef<[u8]>) -> Result<(), PrintError> { 22 | Ok(QrCode::new(data)? 23 | .render::() 24 | .build() 25 | .print(Output::default(), Colors::from_env())?) 26 | } 27 | 28 | pub fn print_with_url(url: impl AsRef) { 29 | let url = url.as_ref(); 30 | println!("{url}"); 31 | print(url); 32 | } 33 | 34 | pub fn print(data: impl AsRef<[u8]>) { 35 | try_print(data).expect("unhandled error printing QR code to terminal") 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use std::env; 42 | 43 | #[test] 44 | fn print_qr_without_panic() { 45 | // NOTE: Use `cargo test --features qr -- --nocapture` for a beautiful 46 | // terminal QR code graphic. The `cargo test` command line arguments 47 | // are manually checked as `termcolor` ignores `Stdout` capturing. 48 | if env::args().any(|arg| arg == "--nocapture") { 49 | print("wc:8a5e5bdc-a0e4-4702-ba63-8f1a5655744f@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=41791102999c339c844880b23950704cc43aa840f3739e365323cda4dfa89e7a"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/qr/image.rs: -------------------------------------------------------------------------------- 1 | use qrcode::render::{Canvas, Pixel}; 2 | use qrcode::types::Color; 3 | use std::iter::{Copied, Zip}; 4 | use std::slice::Iter; 5 | 6 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 7 | pub enum Dot { 8 | Black, 9 | White, 10 | } 11 | 12 | impl Pixel for Dot { 13 | type Image = Grid; 14 | type Canvas = Grid; 15 | 16 | fn default_color(color: Color) -> Self { 17 | match color { 18 | Color::Light => Dot::White, 19 | Color::Dark => Dot::Black, 20 | } 21 | } 22 | 23 | fn default_unit_size() -> (u32, u32) { 24 | (1, 1) 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct Grid { 30 | dots: Vec, 31 | width: usize, 32 | dark: Dot, 33 | } 34 | 35 | pub type Row<'a> = Copied>; 36 | pub type Line<'a> = Zip, Row<'a>>; 37 | 38 | impl Grid { 39 | pub fn row(&self, row: usize) -> Row<'_> { 40 | let pos = row * self.width; 41 | self.dots[pos..pos + self.width].iter().copied() 42 | } 43 | 44 | pub fn line(&self, line: usize) -> Line<'_> { 45 | let y = line * 2; 46 | self.row(y).zip(self.row(y + 1)) 47 | } 48 | 49 | pub fn lines(&self) -> (impl Iterator>, Option>) { 50 | let height = self.dots.len() / self.width; 51 | let count = height / 2; 52 | 53 | let lines = (0..count).map(move |line| self.line(line)); 54 | let last_row = if height % 2 == 1 { 55 | Some(self.row(height - 1)) 56 | } else { 57 | None 58 | }; 59 | 60 | (lines, last_row) 61 | } 62 | } 63 | 64 | impl Canvas for Grid { 65 | type Pixel = Dot; 66 | type Image = Self; 67 | 68 | fn new(width: u32, height: u32, dark_pixel: Self::Pixel, light_pixel: Self::Pixel) -> Self { 69 | let (w, h) = (width as usize, height as usize); 70 | Grid { 71 | dots: vec![light_pixel; w * h], 72 | width: w, 73 | dark: dark_pixel, 74 | } 75 | } 76 | 77 | fn draw_dark_pixel(&mut self, x: u32, y: u32) { 78 | let (x, y) = (x as usize, y as usize); 79 | 80 | let i = x + y * self.width; 81 | if x >= self.width || i >= self.dots.len() { 82 | panic!("pixel out of bounds!") 83 | } 84 | 85 | self.dots[i] = self.dark; 86 | } 87 | 88 | fn into_image(self) -> Self::Image { 89 | self 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/qr/output.rs: -------------------------------------------------------------------------------- 1 | use atty::Stream; 2 | use termcolor::Color; 3 | use termcolor::{ColorChoice, StandardStream}; 4 | use terminfo::capability::MaxColors; 5 | use terminfo::Database; 6 | 7 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 8 | pub enum Output { 9 | Stdout, 10 | Stderr, 11 | } 12 | 13 | impl Output { 14 | pub fn stream(self) -> StandardStream { 15 | match self { 16 | Output::Stdout => StandardStream::stdout(default_color_choice(Stream::Stdout)), 17 | Output::Stderr => StandardStream::stderr(default_color_choice(Stream::Stderr)), 18 | } 19 | } 20 | } 21 | 22 | fn default_color_choice(stream: Stream) -> ColorChoice { 23 | if atty::is(stream) { 24 | ColorChoice::Auto 25 | } else { 26 | ColorChoice::Never 27 | } 28 | } 29 | 30 | impl Default for Output { 31 | fn default() -> Self { 32 | Output::Stdout 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub struct Colors { 38 | pub black: Option, 39 | pub white: Option, 40 | } 41 | 42 | impl Colors { 43 | pub fn none() -> Self { 44 | Colors { 45 | black: None, 46 | white: None, 47 | } 48 | } 49 | 50 | fn standard() -> Self { 51 | Colors { 52 | black: Some(Color::Black), 53 | white: Some(Color::White), 54 | } 55 | } 56 | 57 | #[cfg(unix)] 58 | fn ansi256() -> Self { 59 | Colors { 60 | black: Some(Color::Ansi256(16)), 61 | white: Some(Color::Ansi256(231)), 62 | } 63 | } 64 | } 65 | 66 | impl Default for Colors { 67 | #[cfg(unix)] 68 | fn default() -> Self { 69 | if let Ok(db) = Database::from_env() { 70 | match db.get::() { 71 | Some(MaxColors(8)) => Colors::standard(), 72 | Some(MaxColors(256)) => Colors::ansi256(), 73 | _ => Colors::none(), 74 | } 75 | } else { 76 | Colors::none() 77 | } 78 | } 79 | 80 | #[cfg(windows)] 81 | fn default() -> Self { 82 | Colors::standard() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/qr/print.rs: -------------------------------------------------------------------------------- 1 | use crate::qr::image::{Dot, Grid}; 2 | use atty::Stream; 3 | use std::io::{Result as IoResult, Write}; 4 | use termcolor::Color; 5 | use termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}; 6 | use terminfo::capability::MaxColors; 7 | use terminfo::Database; 8 | 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub enum Output { 11 | Stdout, 12 | Stderr, 13 | } 14 | 15 | impl Output { 16 | pub fn stream(self) -> StandardStream { 17 | match self { 18 | Output::Stdout => StandardStream::stdout(default_color_choice(Stream::Stdout)), 19 | Output::Stderr => StandardStream::stderr(default_color_choice(Stream::Stderr)), 20 | } 21 | } 22 | } 23 | 24 | fn default_color_choice(stream: Stream) -> ColorChoice { 25 | if atty::is(stream) { 26 | ColorChoice::Auto 27 | } else { 28 | ColorChoice::Never 29 | } 30 | } 31 | 32 | impl Default for Output { 33 | fn default() -> Self { 34 | Output::Stdout 35 | } 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | pub struct Colors { 40 | pub black: Option, 41 | pub white: Option, 42 | } 43 | 44 | impl Colors { 45 | pub fn none() -> Self { 46 | Colors { 47 | black: None, 48 | white: None, 49 | } 50 | } 51 | 52 | #[cfg(unix)] 53 | pub fn from_env() -> Self { 54 | if let Ok(db) = Database::from_env() { 55 | match db.get::() { 56 | Some(MaxColors(8)) => Colors::standard(), 57 | Some(MaxColors(256)) => Colors::ansi256(), 58 | _ => Colors::none(), 59 | } 60 | } else { 61 | Colors::none() 62 | } 63 | } 64 | 65 | #[cfg(windows)] 66 | pub fn from_env() -> Self { 67 | Colors::standard() 68 | } 69 | 70 | fn standard() -> Self { 71 | Colors { 72 | black: Some(Color::Black), 73 | white: Some(Color::White), 74 | } 75 | } 76 | 77 | #[cfg(unix)] 78 | fn ansi256() -> Self { 79 | Colors { 80 | black: Some(Color::Ansi256(16)), 81 | white: Some(Color::Ansi256(231)), 82 | } 83 | } 84 | } 85 | 86 | pub trait Print { 87 | fn print(&self, output: Output, colors: Colors) -> IoResult<()>; 88 | } 89 | 90 | impl Print for Grid { 91 | fn print(&self, output: Output, colors: Colors) -> IoResult<()> { 92 | let stream = output.stream(); 93 | let mut w = stream.lock(); 94 | let Colors { black, white } = colors; 95 | 96 | let (lines, last_row) = self.lines(); 97 | 98 | w.reset()?; 99 | for line in lines { 100 | w.set_color( 101 | ColorSpec::new() 102 | .set_reset(false) 103 | .set_fg(white) 104 | .set_bg(black), 105 | )?; 106 | for point in line { 107 | write!( 108 | w, 109 | "{}", 110 | match point { 111 | (Dot::Black, Dot::Black) => ' ', 112 | (Dot::Black, Dot::White) => '▄', 113 | (Dot::White, Dot::Black) => '▀', 114 | (Dot::White, Dot::White) => '█', 115 | } 116 | )?; 117 | } 118 | w.reset()?; 119 | writeln!(w)?; 120 | } 121 | 122 | if let Some(row) = last_row { 123 | w.set_color(ColorSpec::new().set_reset(false).set_fg(white))?; 124 | let mut current_fg = white; 125 | 126 | for dot in row { 127 | if black.is_some() && w.supports_color() { 128 | let fg = match dot { 129 | Dot::Black => black, 130 | Dot::White => white, 131 | }; 132 | if fg != current_fg { 133 | w.set_color(ColorSpec::new().set_reset(false).set_fg(fg))?; 134 | current_fg = fg; 135 | } 136 | write!(w, "▀")?; 137 | } else { 138 | write!( 139 | w, 140 | "{}", 141 | match dot { 142 | Dot::White => '▀', 143 | Dot::Black => ' ', 144 | } 145 | )?; 146 | } 147 | } 148 | writeln!(w)?; 149 | } 150 | 151 | Ok(()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/serialization.rs: -------------------------------------------------------------------------------- 1 | use crate::hex; 2 | use ethers_core::types::H160; 3 | use serde::de::{DeserializeOwned, Error as _}; 4 | use serde::ser::Error as _; 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | use std::borrow::Cow; 7 | 8 | pub mod jsonstring { 9 | use super::*; 10 | 11 | pub fn serialize(value: &Option, serializer: S) -> Result 12 | where 13 | T: Serialize, 14 | S: Serializer, 15 | { 16 | let json = match value { 17 | None => Cow::from(""), 18 | Some(value) => serde_json::to_string(value) 19 | .map_err(S::Error::custom)? 20 | .into(), 21 | }; 22 | serializer.serialize_str(&json) 23 | } 24 | 25 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> 26 | where 27 | T: DeserializeOwned, 28 | D: Deserializer<'de>, 29 | { 30 | let json = Cow::<'de, str>::deserialize(deserializer)?; 31 | if !json.is_empty() { 32 | let value = serde_json::from_str(&json).map_err(D::Error::custom)?; 33 | Ok(Some(value)) 34 | } else { 35 | Ok(None) 36 | } 37 | } 38 | } 39 | 40 | pub mod prefixedhexstring { 41 | use super::*; 42 | 43 | pub fn serialize(bytes: &[u8], serializer: S) -> Result 44 | where 45 | S: Serializer, 46 | { 47 | serializer.serialize_str(&format!("0x{}", hex::encode(bytes))) 48 | } 49 | 50 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 51 | where 52 | D: Deserializer<'de>, 53 | { 54 | let string = Cow::<'de, str>::deserialize(deserializer)?; 55 | if !string.starts_with("0x") { 56 | return Err(D::Error::custom("hex string missing '0x' prefix")); 57 | } 58 | 59 | let bytes = hex::decode(&string[2..]).map_err(D::Error::custom)?; 60 | Ok(bytes) 61 | } 62 | } 63 | 64 | pub mod hexstring { 65 | use super::*; 66 | 67 | pub fn serialize(bytes: &[u8], serializer: S) -> Result 68 | where 69 | S: Serializer, 70 | { 71 | serializer.serialize_str(&hex::encode(bytes)) 72 | } 73 | 74 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 75 | where 76 | D: Deserializer<'de>, 77 | { 78 | let string = Cow::<'de, str>::deserialize(deserializer)?; 79 | let bytes = hex::decode(&*string).map_err(D::Error::custom)?; 80 | Ok(bytes) 81 | } 82 | } 83 | 84 | pub mod emptynoneaddress { 85 | use super::*; 86 | 87 | pub fn serialize(value: &Option, serializer: S) -> Result 88 | where 89 | S: Serializer, 90 | { 91 | match value { 92 | Some(value) => value.serialize(serializer), 93 | None => serializer.serialize_str(""), 94 | } 95 | } 96 | 97 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 98 | where 99 | D: Deserializer<'de>, 100 | { 101 | match Cow::<'de, str>::deserialize(deserializer)?.as_ref() { 102 | "" => Ok(None), 103 | value => value.parse().map(Some).map_err(D::Error::custom), 104 | } 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::*; 111 | use ethers_core::types::H160; 112 | use serde_json::json; 113 | 114 | #[test] 115 | fn deserializes_empty_none_address() { 116 | assert_eq!( 117 | emptynoneaddress::deserialize(json!("0x000102030405060708090a0b0c0d0e0f10111213")) 118 | .unwrap(), 119 | Some(H160([ 120 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 121 | ])), 122 | ); 123 | 124 | assert_eq!(emptynoneaddress::deserialize(json!("")).unwrap(), None,); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{Client, ConnectorError, NotConnectedError, SessionError}; 2 | use crate::protocol::Transaction; 3 | use ethers_core::types::Address; 4 | use futures::future::{BoxFuture, FutureExt}; 5 | use jsonrpc_core::{Call, MethodCall, Params}; 6 | use serde::Deserialize; 7 | use serde_json::{json, Value}; 8 | use std::error::Error; 9 | use std::sync::Arc; 10 | use thiserror::Error; 11 | use web3::transports::Http; 12 | use web3::types::U64; 13 | use web3::{helpers, RequestId, Transport}; 14 | 15 | pub trait TransportFactory { 16 | type Transport: Transport; 17 | type Error: Error; 18 | 19 | fn new(&mut self, chain_id: u64) -> Result; 20 | } 21 | 22 | struct InfuraTransportFactory(String); 23 | 24 | impl TransportFactory for InfuraTransportFactory { 25 | type Transport = Http; 26 | type Error = web3::Error; 27 | 28 | fn new(&mut self, chain_id: u64) -> Result { 29 | let network = match chain_id { 30 | 1 => "mainnet", 31 | 3 => "ropsten", 32 | 4 => "rinkeby", 33 | 5 => "goerli", 34 | 42 => "kovan", 35 | _ => { 36 | return Err(web3::Error::Transport( 37 | web3::error::TransportError::Message(format!( 38 | "unknown chain ID '{}'", 39 | chain_id 40 | )), 41 | )) 42 | } 43 | }; 44 | let url = format!("https://{}.infura.io/v3/{}", network, self.0); 45 | let http = Http::new(&url)?; 46 | 47 | Ok(http) 48 | } 49 | } 50 | 51 | #[derive(Clone, Debug)] 52 | pub struct WalletConnect(Arc>); 53 | 54 | #[derive(Debug)] 55 | struct Inner { 56 | client: Client, 57 | accounts: Vec
, 58 | chain_id: u64, 59 | transport: T, 60 | } 61 | 62 | impl WalletConnect { 63 | pub fn new(client: Client, infura_id: impl Into) -> Result { 64 | WalletConnect::with_factory(client, InfuraTransportFactory(infura_id.into())) 65 | } 66 | } 67 | 68 | impl WalletConnect 69 | where 70 | T: Transport, 71 | { 72 | pub fn with_factory(client: Client, mut factory: F) -> Result 73 | where 74 | F: TransportFactory, 75 | F::Error: 'static, 76 | { 77 | let (accounts, chain_id) = client.accounts()?; 78 | let transport = factory 79 | .new(chain_id) 80 | .map_err(|err| TransportError::Transport(Box::new(err)))?; 81 | 82 | Ok(WalletConnect(Arc::new(Inner { 83 | client, 84 | accounts, 85 | chain_id, 86 | transport, 87 | }))) 88 | } 89 | 90 | pub fn accounts(&self) -> (Vec
, u64) { 91 | (self.0.accounts.clone(), self.0.chain_id) 92 | } 93 | } 94 | 95 | #[derive(Debug, Error)] 96 | pub enum TransportError { 97 | #[error("client connector error: {0}")] 98 | Connector(#[from] ConnectorError), 99 | #[error("error establising a WalletConnect session: {0}")] 100 | Session(#[from] SessionError), 101 | #[error("connection unexpectedly dropped: {0}")] 102 | ConnectionDropped(#[from] NotConnectedError), 103 | #[error("error creating transport: {0}")] 104 | Transport(Box), 105 | } 106 | 107 | impl Transport for WalletConnect 108 | where 109 | T: Transport + Send + Sync + 'static, 110 | T::Out: Send, 111 | { 112 | type Out = BoxFuture<'static, Result>; 113 | 114 | fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { 115 | log::trace!("preparing call '{}' {:?}", method, params); 116 | match method { 117 | "eth_accounts" | "eth_chainId" | "eth_sendTransaction" => { 118 | (0, helpers::build_request(0, method, params)) 119 | } 120 | _ => self.0.transport.prepare(method, params), 121 | } 122 | } 123 | 124 | fn send(&self, id: RequestId, request: Call) -> Self::Out { 125 | let inner = self.0.clone(); 126 | async move { 127 | match request { 128 | Call::MethodCall(MethodCall { method, .. }) if method == "eth_accounts" => { 129 | Ok(json!(inner.accounts)) 130 | } 131 | Call::MethodCall(MethodCall { method, .. }) if method == "eth_chainId" => { 132 | Ok(json!(U64::from(inner.chain_id))) 133 | } 134 | Call::MethodCall(MethodCall { 135 | method, 136 | params: Params::Array(params), 137 | .. 138 | }) if method == "eth_sendTransaction" && !params.is_empty() => { 139 | log::trace!(">>{}", params[0]); 140 | let transaction = Transaction::deserialize(¶ms[0])?; 141 | let tx = inner 142 | .client 143 | .send_transaction(transaction) 144 | .await 145 | .map_err(|err| { 146 | web3::Error::Transport(web3::error::TransportError::Message( 147 | err.to_string(), 148 | )) 149 | })?; 150 | Ok(json!(tx)) 151 | } 152 | request => inner.transport.send(id, request).await, 153 | } 154 | } 155 | .boxed() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/uri.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto::Key; 2 | use crate::protocol::Topic; 3 | use std::ops::Deref; 4 | use std::str::FromStr; 5 | use thiserror::Error; 6 | use url::Url; 7 | 8 | #[derive(Clone, Debug, Eq, PartialEq)] 9 | pub struct Uri { 10 | handshake_topic: Topic, 11 | version: u64, 12 | bridge: Url, 13 | key: Key, 14 | url: Url, 15 | } 16 | 17 | const VERSION: u64 = 1; 18 | 19 | impl Uri { 20 | pub fn parse(uri: impl AsRef) -> Result { 21 | let url = Url::parse(uri.as_ref())?; 22 | if url.scheme() != "wc" { 23 | return Err(InvalidSessionUri); 24 | } 25 | 26 | let mut path = url.path().splitn(2, '@'); 27 | let handshake_topic = path.next().ok_or(InvalidSessionUri)?.parse()?; 28 | let version = path.next().ok_or(InvalidSessionUri)?.parse()?; 29 | if version != VERSION { 30 | return Err(InvalidSessionUri); 31 | } 32 | 33 | let mut bridge: Option = None; 34 | let mut key: Option = None; 35 | for (name, value) in url.query_pairs() { 36 | match &*name { 37 | "bridge" => bridge = Some(value.parse()?), 38 | "key" => key = Some(value.parse()?), 39 | _ => return Err(InvalidSessionUri), 40 | } 41 | } 42 | 43 | Ok(Uri { 44 | handshake_topic, 45 | version, 46 | bridge: bridge.ok_or(InvalidSessionUri)?, 47 | key: key.ok_or(InvalidSessionUri)?, 48 | url, 49 | }) 50 | } 51 | 52 | pub fn handshake_topic(&self) -> &Topic { 53 | &self.handshake_topic 54 | } 55 | 56 | pub fn version(&self) -> u64 { 57 | self.version 58 | } 59 | 60 | pub fn bridge(&self) -> &Url { 61 | &self.bridge 62 | } 63 | 64 | pub fn key(&self) -> &Key { 65 | &self.key 66 | } 67 | 68 | pub fn into_parts(self) -> (Topic, Url, Key) { 69 | (self.handshake_topic, self.bridge, self.key) 70 | } 71 | 72 | pub fn as_url(&self) -> &Url { 73 | &self.url 74 | } 75 | } 76 | 77 | impl Deref for Uri { 78 | type Target = Url; 79 | 80 | fn deref(&self) -> &Self::Target { 81 | self.as_url() 82 | } 83 | } 84 | 85 | impl FromStr for Uri { 86 | type Err = InvalidSessionUri; 87 | 88 | fn from_str(s: &str) -> Result { 89 | Uri::parse(s) 90 | } 91 | } 92 | 93 | impl AsRef for Uri { 94 | fn as_ref(&self) -> &str { 95 | self.as_url().as_str() 96 | } 97 | } 98 | 99 | impl AsRef<[u8]> for Uri { 100 | fn as_ref(&self) -> &[u8] { 101 | self.as_url().as_str().as_bytes() 102 | } 103 | } 104 | 105 | #[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] 106 | #[error("session URI is invalid")] 107 | pub struct InvalidSessionUri; 108 | 109 | macro_rules! impl_invalid_session_uri_from { 110 | ($err:ty) => { 111 | impl From<$err> for InvalidSessionUri { 112 | fn from(_: $err) -> Self { 113 | InvalidSessionUri 114 | } 115 | } 116 | }; 117 | } 118 | 119 | impl_invalid_session_uri_from!(data_encoding::DecodeError); 120 | impl_invalid_session_uri_from!(std::num::ParseIntError); 121 | impl_invalid_session_uri_from!(url::ParseError); 122 | impl_invalid_session_uri_from!(uuid::Error); 123 | --------------------------------------------------------------------------------