ethers_web/
lib.rs

1//! A simple dApp client library for Eip1193 and WalletConnect wallet connections with ethers
2//! library.
3#![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
52/// Ethereum builder for Ethereum object
53pub 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    /// Simple builder constructor
71    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    /// Setting defaults chain id
84    pub fn chain_id(&mut self, chain_id: u64) -> &Self {
85        self.chain_id = chain_id;
86        self
87    }
88
89    /// Setting dApp name
90    pub fn name(&mut self, name: &str) -> &Self {
91        self.name = name.to_string();
92        self
93    }
94
95    /// Setting dApp description
96    pub fn description(&mut self, description: &str) -> &Self {
97        self.description = description.to_string();
98        self
99    }
100
101    /// Setting dApp url
102    pub fn url(&mut self, url: Url) -> &Self {
103        self.url = url;
104        self
105    }
106
107    /// Setting WalletConnects ProjectId
108    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    /// Setting RPC node working together with WalletConnect connection for non-signer interactions
114    pub fn rpc_node(&mut self, rpc_node: &str) -> &Self {
115        self.rpc_node = Some(rpc_node.to_string());
116        self
117    }
118
119    /// Setting dApp icon url
120    pub fn add_icon(&mut self, icon_url: &str) -> &Self {
121        self.icons.push(icon_url.to_string());
122        self
123    }
124
125    /// Building final Ethereum object
126    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/// Available wallet types
140#[derive(Clone, Debug, Copy)]
141pub enum WalletType {
142    Injected,
143    WalletConnect,
144}
145
146/// Error struct
147#[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::ProviderError(e) => e.as_serde_error(),
210            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/// Currently connected provider
222#[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/// Ethereum's state to store or to be restored from
247#[derive(Clone, Serialize, Deserialize)]
248pub struct EthereumState {
249    pub chain_id: Option<u64>,
250    pub wc_state: Option<WalletConnectState>,
251}
252
253/// Ethereum's connection event
254#[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/// Main wallet connectivity object to maintain connections
289#[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    /// Ethereum constructor
324    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    /// Checks if given wallet type is currently available to connect
353    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    /// Checks if we have a provider connection
361    pub fn has_provider(&self) -> bool {
362        self.wallet.is_some()
363    }
364
365    /// Checks what type of the wallet is currently connected
366    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    /// Returns available wallets types
375    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    /// Checks if injected wallet is available in current context
390    pub fn injected_available(&self) -> bool {
391        Eip1193::is_available()
392    }
393
394    /// Checks if WalletConnect connection is available in current context (configuration)
395    pub fn walletconnect_available(&self) -> bool {
396        self.wc_project_id.is_some()
397    }
398
399    /// Fetching available wallets from WalletConnect explorer
400    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    /// Performing connection to selected wallet
421    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    /// Disconnects from wallet
433    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    /// Getting next available event from the event queue
520    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    /// Signs typed data using connected wallet
552    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    /// Performs network switch to other chain id
567    pub async fn switch_network(&mut self, chain_id: u64) -> Result<(), EthereumError> {
568        match self.wallet {
569            WebProvider::WalletConnect(ref mut provider) => {
570                // We need to check if we've got any accounts under that id
571                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    /// Restores connection state from local storage
641    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}