1#![doc = include_str!("../README.md")]
4
5pub mod explorer;
6
7mod eip1193;
8mod event;
9
10#[cfg(feature = "leptos")]
11pub mod leptos;
12
13mod walletconnect;
14#[cfg(feature = "yew")]
15pub mod yew;
16
17use async_trait::async_trait;
18use eip1193::{error::Eip1193Error, Eip1193};
19use ethers::{
20 providers::{Http, HttpClientError, JsonRpcClient, JsonRpcError, ProviderError, RpcError},
21 types::{Address, Signature, SignatureError, U256},
22 utils::ConversionError,
23};
24use gloo_storage::{LocalStorage, Storage};
25use gloo_utils::format::JsValueSerdeExt;
26use hex::FromHexError;
27use log::{debug, error};
28use serde::{de::DeserializeOwned, Deserialize, Serialize};
29use std::{
30 fmt::{Debug, Formatter, Result as FmtResult},
31 str::FromStr,
32 sync::Arc,
33};
34use thiserror::Error;
35use tokio::sync::{
36 mpsc::{channel, Receiver, Sender},
37 Mutex,
38};
39use url::Url;
40use walletconnect_client::{
41 prelude::{Metadata, WalletConnectError},
42 WalletConnect, WalletConnectState,
43};
44use wasm_bindgen_futures::spawn_local;
45
46const STATUS_KEY: &str = "ETHERS_WEB_STATE";
47
48use crate::event::WalletEvent;
49use walletconnect::WalletConnectProvider;
50use walletconnect_client::prelude::Event as WalletConnectEvent;
51
52pub struct EthereumBuilder {
54 pub chain_id: u64,
55 pub name: String,
56 pub description: String,
57 pub url: Url,
58 pub wc_project_id: Option<String>,
59 pub icons: Vec<String>,
60 pub rpc_node: Option<String>,
61}
62
63impl Default for EthereumBuilder {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl EthereumBuilder {
70 pub fn new() -> Self {
72 Self {
73 chain_id: 1,
74 name: "Example dApp".to_string(),
75 description: "An example dApp written in Rust".to_string(),
76 url: Url::parse("https://github.com/quay-rs/ethers-web").unwrap(),
77 wc_project_id: None,
78 icons: Vec::new(),
79 rpc_node: None,
80 }
81 }
82
83 pub fn chain_id(&mut self, chain_id: u64) -> &Self {
85 self.chain_id = chain_id;
86 self
87 }
88
89 pub fn name(&mut self, name: &str) -> &Self {
91 self.name = name.to_string();
92 self
93 }
94
95 pub fn description(&mut self, description: &str) -> &Self {
97 self.description = description.to_string();
98 self
99 }
100
101 pub fn url(&mut self, url: Url) -> &Self {
103 self.url = url;
104 self
105 }
106
107 pub fn walletconnect_id(&mut self, wc_project_id: &str) -> &Self {
109 self.wc_project_id = Some(wc_project_id.to_string());
110 self
111 }
112
113 pub fn rpc_node(&mut self, rpc_node: &str) -> &Self {
115 self.rpc_node = Some(rpc_node.to_string());
116 self
117 }
118
119 pub fn add_icon(&mut self, icon_url: &str) -> &Self {
121 self.icons.push(icon_url.to_string());
122 self
123 }
124
125 pub fn build(&self) -> Ethereum {
127 Ethereum::new(
128 self.chain_id,
129 self.name.clone(),
130 self.description.clone(),
131 self.url.clone(),
132 self.wc_project_id.clone(),
133 self.icons.clone(),
134 self.rpc_node.clone(),
135 )
136 }
137}
138
139#[derive(Clone, Debug, Copy)]
141pub enum WalletType {
142 Injected,
143 WalletConnect,
144}
145
146#[derive(Error, Debug)]
148pub enum EthereumError {
149 #[error("Wallet unavaibale")]
150 Unavailable,
151
152 #[error("Not connected")]
153 NotConnected,
154
155 #[error("Already connected")]
156 AlreadyConnected,
157
158 #[error(transparent)]
159 ConversionError(#[from] ConversionError),
160
161 #[error(transparent)]
162 ProviderError(#[from] ProviderError),
163
164 #[error(transparent)]
165 HttpClientError(#[from] HttpClientError),
166
167 #[error(transparent)]
168 SignatureError(#[from] SignatureError),
169
170 #[error(transparent)]
171 HexError(#[from] FromHexError),
172
173 #[error(transparent)]
174 Eip1193Error(#[from] Eip1193Error),
175
176 #[error(transparent)]
177 WalletConnectError(#[from] crate::walletconnect::error::Error),
178
179 #[error(transparent)]
180 WalletConnectClientError(#[from] WalletConnectError),
181
182 #[error(transparent)]
183 ReqwestError(#[from] reqwest::Error),
184}
185
186impl From<EthereumError> for ProviderError {
187 fn from(src: EthereumError) -> Self {
188 ProviderError::JsonRpcClientError(Box::new(src))
189 }
190}
191
192impl RpcError for EthereumError {
193 fn as_error_response(&self) -> Option<&JsonRpcError> {
194 match self {
195 EthereumError::Eip1193Error(e) => e.as_error_response(),
196 EthereumError::WalletConnectError(e) => e.as_error_response(),
197 EthereumError::WalletConnectClientError(e) => e.as_error_response(),
198 _ => None,
199 }
200 }
201
202 fn is_error_response(&self) -> bool {
203 self.as_error_response().is_some()
204 }
205
206 fn as_serde_error(&self) -> Option<&serde_json::Error> {
207 match self {
208 EthereumError::Eip1193Error(e) => e.as_serde_error(),
209 EthereumError::WalletConnectError(e) => e.as_serde_error(),
211 EthereumError::WalletConnectClientError(e) => e.as_serde_error(),
212 _ => None,
213 }
214 }
215
216 fn is_serde_error(&self) -> bool {
217 self.as_error_response().is_some()
218 }
219}
220
221#[derive(Clone)]
223pub(crate) enum WebProvider {
224 None,
225 Injected(Eip1193),
226 WalletConnect(WalletConnectProvider),
227}
228
229impl WebProvider {
230 fn is_some(&self) -> bool {
231 !matches!(self, Self::None)
232 }
233}
234
235impl PartialEq for WebProvider {
236 fn eq(&self, other: &Self) -> bool {
237 matches!(
238 (self, other),
239 (Self::None, Self::None)
240 | (Self::Injected(_), Self::Injected(_))
241 | (Self::WalletConnect(_), Self::WalletConnect(_))
242 )
243 }
244}
245
246#[derive(Clone, Serialize, Deserialize)]
248pub struct EthereumState {
249 pub chain_id: Option<u64>,
250 pub wc_state: Option<WalletConnectState>,
251}
252
253#[derive(Debug, Clone, PartialEq)]
255pub enum Event {
256 ConnectionWaiting(String),
257 Connected,
258 Disconnected,
259 Broken,
260 ChainIdChanged(Option<u64>),
261 AccountsChanged(Option<Vec<Address>>),
262}
263
264impl Event {
265 fn is_connection_established(&self) -> bool {
266 !matches!(
267 self,
268 Self::ConnectionWaiting(_)
269 | Self::Disconnected
270 | Self::ChainIdChanged(None)
271 | Self::AccountsChanged(None)
272 )
273 }
274}
275
276impl From<WalletConnectEvent> for Event {
277 fn from(value: WalletConnectEvent) -> Self {
278 match value {
279 WalletConnectEvent::Disconnected => Self::Disconnected,
280 WalletConnectEvent::Connected => Self::Connected,
281 WalletConnectEvent::AccountsChanged(acc) => Self::AccountsChanged(acc),
282 WalletConnectEvent::ChainIdChanged(id) => Self::ChainIdChanged(Some(id)),
283 WalletConnectEvent::Broken => Self::Broken,
284 }
285 }
286}
287
288#[derive(Clone)]
290pub struct Ethereum {
291 pub metadata: Metadata,
292 pub wc_project_id: Option<String>,
293 pub rpc_node: Option<String>,
294 pub http_provider: Option<Http>,
295
296 accounts: Option<Vec<Address>>,
297 chain_id: Option<u64>,
298
299 sender: Sender<Event>,
300 receiver: Arc<Mutex<Receiver<Event>>>,
301
302 wallet: WebProvider,
303}
304
305impl PartialEq for Ethereum {
306 fn eq(&self, other: &Self) -> bool {
307 self.metadata == other.metadata
308 && self.wc_project_id == other.wc_project_id
309 && self.rpc_node == other.rpc_node
310 && self.accounts == other.accounts
311 && self.chain_id == other.chain_id
312 && self.wallet == other.wallet
313 }
314}
315
316impl Debug for Ethereum {
317 fn fmt(&self, f: &mut Formatter) -> FmtResult {
318 write!(f, "Ethereum with accounts: {:?}, chain_id: {:?} ", self.accounts, self.chain_id)
319 }
320}
321
322impl Ethereum {
323 fn new(
325 chain_id: u64,
326 name: String,
327 description: String,
328 url: Url,
329 wc_project_id: Option<String>,
330 icons: Vec<String>,
331 rpc_node: Option<String>,
332 ) -> Self {
333 let (sender, receiver) = channel::<Event>(10);
334
335 let http_provider = match rpc_node {
336 Some(ref url) => Some(Http::from_str(&url).unwrap()),
337 None => None,
338 };
339 Ethereum {
340 metadata: Metadata::from(&name, &description, url, icons),
341 wc_project_id,
342 rpc_node,
343 http_provider,
344 accounts: None,
345 chain_id: Some(chain_id),
346 sender,
347 receiver: Arc::new(Mutex::new(receiver)),
348 wallet: WebProvider::None,
349 }
350 }
351
352 pub fn is_available(&self, wallet_type: WalletType) -> bool {
354 match wallet_type {
355 WalletType::Injected => self.injected_available(),
356 WalletType::WalletConnect => self.walletconnect_available(),
357 }
358 }
359
360 pub fn has_provider(&self) -> bool {
362 self.wallet.is_some()
363 }
364
365 pub fn connected_wallet_type(&self) -> Option<WalletType> {
367 match &self.wallet {
368 WebProvider::None => None,
369 WebProvider::Injected(_) => Some(WalletType::Injected),
370 WebProvider::WalletConnect(_) => Some(WalletType::WalletConnect),
371 }
372 }
373
374 pub fn available_wallets(&self) -> Vec<WalletType> {
376 let mut types = Vec::new();
377
378 if Eip1193::is_available() {
379 types.push(WalletType::Injected);
380 }
381
382 if self.wc_project_id.is_some() {
383 types.push(WalletType::WalletConnect);
384 }
385
386 types
387 }
388
389 pub fn injected_available(&self) -> bool {
391 Eip1193::is_available()
392 }
393
394 pub fn walletconnect_available(&self) -> bool {
396 self.wc_project_id.is_some()
397 }
398
399 pub async fn fetch_available_wallets(
401 &self,
402 ) -> Result<Vec<explorer::WalletDescription>, EthereumError> {
403 match &self.wc_project_id {
404 None => Err(EthereumError::Unavailable),
405 Some(p_id) => {
406 let client = reqwest::Client::new();
407 let resp: explorer::ExplorerResponse = client
408 .get(format!(
409 "https://explorer-api.walletconnect.com/v3/wallets?projectId={p_id}",
410 ))
411 .send()
412 .await?
413 .json()
414 .await?;
415 Ok(resp.parse_wallets(p_id))
416 }
417 }
418 }
419
420 pub async fn connect(&mut self, wallet: WalletType) -> Result<(), EthereumError> {
422 if self.wallet != WebProvider::None {
423 return Err(EthereumError::AlreadyConnected);
424 }
425
426 match wallet {
427 WalletType::Injected => self.connect_injected().await,
428 WalletType::WalletConnect => self.connect_wc(None).await,
429 }
430 }
431
432 pub async fn disconnect(&mut self) {
434 if let WebProvider::WalletConnect(wc) = &self.wallet {
435 wc.disconnect().await;
436 }
437
438 self.wallet = WebProvider::None;
439 self.accounts = None;
440
441 _ = self.sender.send(Event::Disconnected).await;
442 }
443
444 async fn connect_injected(&mut self) -> Result<(), EthereumError> {
445 if !self.injected_available() {
446 return Err(EthereumError::Unavailable);
447 }
448
449 let injected = Eip1193::new();
450
451 {
452 let s = self.sender.clone();
453 _ = injected.clone().on(
454 WalletEvent::Disconnect,
455 Box::new(move |_| {
456 let sender = s.clone();
457 spawn_local(async move {
458 _ = sender.send(Event::Disconnected).await;
459 })
460 }),
461 );
462 }
463 {
464 let s = self.sender.clone();
465 _ = injected.clone().on(
466 WalletEvent::ChainChanged,
467 Box::new(move |chain_id| {
468 let sender = s.clone();
469 spawn_local(async move {
470 _ = sender
471 .send(Event::ChainIdChanged(
472 chain_id.into_serde::<U256>().ok().map(|c| c.low_u64()),
473 ))
474 .await;
475 });
476 }),
477 );
478 }
479 {
480 let s = self.sender.clone();
481 _ = injected.clone().on(
482 WalletEvent::AccountsChanged,
483 Box::new(move |accounts| {
484 let sender = s.clone();
485 spawn_local(async move {
486 let accounts = accounts.into_serde::<Vec<Address>>().ok();
487 _ = sender.send(Event::AccountsChanged(accounts.clone())).await;
488 match &accounts {
489 Some(acc) => {
490 _ = sender
491 .send(if acc.is_empty() {
492 Event::Disconnected
493 } else {
494 Event::Connected
495 })
496 .await;
497 }
498 None => _ = sender.send(Event::Disconnected).await,
499 }
500 });
501 }),
502 );
503 }
504 self.wallet = WebProvider::Injected(injected);
505 self.accounts = Some(self.request_accounts().await?);
506 self.chain_id = Some(self.request_chain_id().await?.low_u64());
507
508 _ = self.sender.send(Event::Connected).await;
509 if self.chain_id.is_some() {
510 _ = self.sender.send(Event::ChainIdChanged(self.chain_id)).await;
511 }
512 if self.accounts.is_some() {
513 _ = self.sender.send(Event::AccountsChanged(self.accounts.clone())).await;
514 }
515
516 Ok(())
517 }
518
519 pub async fn next(&self) -> Result<Option<Event>, EthereumError> {
521 let event = match &self.wallet {
522 WebProvider::WalletConnect(provider) => {
523 let mut recvr = self.receiver.lock().await;
524 tokio::select! {
525 e = recvr.recv() => Ok(e),
526 e = provider.next() => Ok(e?.map(|e| e.into()))
527 }
528 }
529 _ => Ok(self.receiver.lock().await.recv().await),
530 };
531
532 debug!("NEW EVENT {:?}", event);
533 if let Ok(Some(e)) = &event {
534 if e == &Event::Connected {
535 if let WebProvider::WalletConnect(provider) = &self.wallet {
536 _ = self.sender.send(Event::ChainIdChanged(Some(provider.chain_id()))).await;
537 _ = self.sender.send(Event::AccountsChanged(provider.accounts())).await;
538 }
539 }
540
541 if !e.is_connection_established() {
542 LocalStorage::delete(STATUS_KEY);
543 } else {
544 _ = LocalStorage::set(STATUS_KEY, self.collect_state());
545 }
546 }
547
548 event
549 }
550
551 pub async fn sign_typed_data<T: Send + Sync + Serialize>(
553 &self,
554 data: T,
555 from: &Address,
556 ) -> Result<Signature, EthereumError> {
557 match &self.wallet {
558 WebProvider::None => Err(EthereumError::NotConnected),
559 WebProvider::Injected(provider) => Ok(provider.sign_typed_data(data, from).await?),
560 WebProvider::WalletConnect(provider) => {
561 Ok(provider.sign_typed_data(data, from).await?)
562 }
563 }
564 }
565
566 pub async fn switch_network(&mut self, chain_id: u64) -> Result<(), EthereumError> {
568 match self.wallet {
569 WebProvider::WalletConnect(ref mut provider) => {
570 if let Some(accounts) = provider.accounts_for_chain(chain_id) {
572 if !accounts.is_empty() {
573 self.accounts = Some(accounts.clone());
574 self.chain_id = Some(chain_id);
575 provider.set_chain_id(chain_id);
576 _ = self.sender.send(Event::ChainIdChanged(Some(chain_id))).await;
577 _ = self.sender.send(Event::AccountsChanged(Some(accounts))).await;
578 return Ok(());
579 }
580 }
581 Err(EthereumError::Unavailable)
582 }
583 _ => Err(EthereumError::Unavailable),
584 }
585 }
586
587 async fn connect_wc(&mut self, state: Option<WalletConnectState>) -> Result<(), EthereumError> {
588 if !self.walletconnect_available() {
589 return Err(EthereumError::Unavailable);
590 }
591
592 let wc = WalletConnect::connect(
593 self.wc_project_id.clone().unwrap().into(),
594 self.chain_id.unwrap_or(1),
595 self.metadata.clone(),
596 state.clone(),
597 )?;
598
599 let url = wc
600 .initiate_session(
601 state
602 .as_ref()
603 .map(|s| s.keys.clone().into_iter().map(|(t, _)| t).collect::<Vec<_>>()),
604 )
605 .await?;
606
607 self.wallet =
608 WebProvider::WalletConnect(WalletConnectProvider::new(wc, self.rpc_node.clone()));
609
610 if !url.is_empty() {
611 _ = self.sender.send(Event::ConnectionWaiting(url)).await;
612 } else {
613 _ = self.sender.send(Event::Connected).await;
614 _ = self.sender.send(Event::ChainIdChanged(self.chain_id)).await;
615 _ = self.sender.send(Event::AccountsChanged(self.accounts.clone())).await;
616 }
617
618 Ok(())
619 }
620
621 async fn request_accounts(&self) -> Result<Vec<Address>, EthereumError> {
622 match &self.wallet {
623 WebProvider::None => Err(EthereumError::NotConnected),
624 WebProvider::Injected(_) => Ok(self.request("eth_requestAccounts", ()).await?),
625 WebProvider::WalletConnect(wc) => match wc.accounts() {
626 Some(a) => Ok(a),
627 None => Err(EthereumError::Unavailable),
628 },
629 }
630 }
631
632 async fn request_chain_id(&self) -> Result<U256, EthereumError> {
633 match &self.wallet {
634 WebProvider::None => Err(EthereumError::NotConnected),
635 WebProvider::Injected(_) => Ok(self.request("eth_chainId", ()).await?),
636 WebProvider::WalletConnect(wc) => Ok(wc.chain_id().into()),
637 }
638 }
639
640 pub async fn restore(&mut self) -> bool {
642 match LocalStorage::get::<EthereumState>(STATUS_KEY) {
643 Ok(state) => {
644 match state.wc_state {
645 None => _ = self.connect_injected().await,
646 Some(wc_settings) => _ = self.connect_wc(Some(wc_settings)).await,
647 }
648 true
649 }
650 Err(err) => {
651 error!("Status not loaded {err:?}!");
652 false
653 }
654 }
655 }
656
657 fn collect_state(&self) -> EthereumState {
658 match &self.wallet {
659 WebProvider::WalletConnect(p) => {
660 EthereumState { chain_id: Some(p.chain_id()), wc_state: Some(p.get_state()) }
661 }
662 _ => EthereumState { chain_id: self.chain_id, wc_state: None },
663 }
664 }
665}
666
667#[cfg_attr(target_arch = "wasm32", async_trait(? Send))]
668#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
669impl JsonRpcClient for Ethereum {
670 type Error = EthereumError;
671
672 async fn request<T: Serialize + Send + Sync + std::fmt::Debug, R: DeserializeOwned + Send>(
673 &self,
674 method: &str,
675 params: T,
676 ) -> Result<R, Self::Error> {
677 match &self.wallet {
678 WebProvider::None => match &self.http_provider {
679 Some(provider) => Ok(provider.request(method, params).await?),
680 None => Err(EthereumError::NotConnected),
681 },
682 WebProvider::Injected(provider) => Ok(provider.request(method, params).await?),
683 WebProvider::WalletConnect(provider) => Ok(provider.request(method, params).await?),
684 }
685 }
686}