wallet_adapter/
adapter.rs

1use std::{borrow::Borrow, sync::Arc};
2
3use async_channel::{bounded, Receiver};
4use async_lock::RwLock;
5use ed25519_dalek::Signature;
6use wallet_adapter_common::{clusters::Cluster, signin_standard::SignInOutput};
7use web_sys::{js_sys::Object, Document, Window};
8
9use crate::{
10    events::InitEvents, send_wallet_event, SendOptions, SignedMessageOutput, SigninInput, Wallet,
11    WalletAccount, WalletError, WalletEvent, WalletEventReceiver, WalletEventSender, WalletResult,
12    WalletStorage,
13};
14
15/// Contains the connected wallet and account.
16/// Containing them in the same struct allows passing of this type
17/// by containing it in types like [Arc] and [RwLock] when moving the type
18/// out of it's scope like in background tasks or async functions (`async move`).
19#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
20pub struct ConnectionInfo {
21    wallet: Option<Wallet>,
22    account: Option<WalletAccount>,
23    previous_accounts: Vec<WalletAccount>,
24}
25
26impl ConnectionInfo {
27    /// Create a default [ConnectionInfo]
28    pub fn new() -> Self {
29        ConnectionInfo::default()
30    }
31
32    /// Set the connected wallet
33    pub fn set_wallet(&mut self, wallet: Wallet) -> &mut Self {
34        self.wallet.replace(wallet);
35
36        self
37    }
38
39    /// Set the connected account
40    pub fn set_account(&mut self, account: WalletAccount) -> &mut Self {
41        self.account.replace(account);
42
43        self
44    }
45
46    /// Send a connect request to the browser wallet
47    pub async fn connect(&mut self, sender: WalletEventSender) -> WalletResult<WalletAccount> {
48        let wallet = self.connected_wallet()?;
49
50        let connected_account = wallet.features.connect.call_connect().await?;
51
52        self.set_account(connected_account.clone());
53
54        send_wallet_event(WalletEvent::Connected(connected_account.clone()), sender).await;
55
56        Ok(connected_account)
57    }
58
59    /// Set the disconnected account
60    pub async fn set_disconnected(&mut self, sender: WalletEventSender) -> &mut Self {
61        self.wallet.take();
62        self.account.take();
63        self.previous_accounts.clear();
64
65        send_wallet_event(WalletEvent::Disconnected, sender).await;
66
67        self
68    }
69
70    /// Get the connected [wallet](Wallet)
71    pub fn connected_wallet(&self) -> WalletResult<&Wallet> {
72        self.wallet.as_ref().ok_or(WalletError::WalletNotFound)
73    }
74
75    /// Get the connected [account](WalletAccount)
76    pub fn connected_account(&self) -> WalletResult<&WalletAccount> {
77        self.account.as_ref().ok_or(WalletError::AccountNotFound)
78    }
79
80    /// Get the connected [wallet](Wallet) but return an [Option]
81    /// to show the wallet exists instead of a [WalletResult]
82    pub fn connected_wallet_raw(&self) -> Option<&Wallet> {
83        self.wallet.as_ref()
84    }
85
86    /// Get the connected [account](WalletAccount)
87    /// but return an [Option] to show the account exists instead of a [WalletResult]
88    pub fn connected_account_raw(&self) -> Option<&WalletAccount> {
89        self.account.as_ref()
90    }
91
92    /// Emit an [event](WalletEvent) after processing the `[standard:events].on` result
93    pub async fn emit_wallet_event(
94        &mut self,
95        wallet_name: &str,
96        account_processing: Option<WalletAccount>,
97        sender: WalletEventSender,
98    ) {
99        match self.connected_wallet() {
100            Ok(wallet) => {
101                let event_outcome = match account_processing {
102                    Some(connected_account) => {
103                        if self.account.is_none()
104                            && self.wallet.is_none()
105                            && self.previous_accounts.is_empty()
106                        {
107                            self.set_account(connected_account.clone());
108
109                            WalletEvent::Connected(connected_account)
110                        } else if self.account.is_none()
111                            && self.wallet.is_some()
112                            && self.previous_accounts.iter().any(|wallet_account| {
113                                wallet_account.account.public_key
114                                    == connected_account.account.public_key
115                            })
116                        {
117                            self.push_previous_account();
118                            self.set_account(connected_account.clone());
119
120                            WalletEvent::Connected(connected_account)
121                        } else if wallet.name().as_bytes() == wallet_name.as_bytes()
122                            && self.account.is_none()
123                            && self.previous_accounts.iter().any(|wallet_account| {
124                                wallet_account.account.public_key
125                                    == connected_account.account.public_key
126                            })
127                        {
128                            self.push_previous_account();
129                            self.set_account(connected_account.clone());
130
131                            WalletEvent::Reconnected(connected_account)
132                        } else if wallet.name().as_bytes() == wallet_name.as_bytes()
133                            && self.account.is_some()
134                        {
135                            self.push_previous_account();
136                            self.set_account(connected_account.clone());
137
138                            WalletEvent::AccountChanged(connected_account)
139                        } else {
140                            WalletEvent::Skip
141                        }
142                    }
143                    None => {
144                        if wallet.name().as_bytes() == wallet_name.as_bytes() {
145                            self.push_previous_account();
146                            WalletEvent::Disconnected
147                        } else {
148                            WalletEvent::Skip
149                        }
150                    }
151                };
152
153                send_wallet_event(event_outcome, sender).await
154            }
155            Err(error) => {
156                web_sys::console::log_2(
157                    &"ON EVENT EMITTED BUT NO CONNECTED WALLET FOUND: ".into(),
158                    &format!("{error:?}").into(),
159                );
160            }
161        }
162    }
163
164    fn push_previous_account(&mut self) {
165        let take_connected_account = self.account.take();
166
167        if let Some(connected_account_inner) = take_connected_account {
168            self.previous_accounts.push(connected_account_inner);
169        }
170
171        self.previous_accounts.dedup();
172    }
173}
174
175/// The [ConnectionInfo] wrapped in an `Arc<RwLock<T>>`
176pub type ConnectionInfoInner = Arc<RwLock<ConnectionInfo>>;
177
178/// Operations on a browser window.
179/// `Window` and `Document` object must be present otherwise
180/// an error is thrown.
181#[derive(Debug, Clone)]
182pub struct WalletAdapter {
183    window: Window,
184    document: Document,
185    storage: WalletStorage,
186    connection_info: ConnectionInfoInner,
187    wallet_events: WalletEventReceiver,
188    wallet_events_sender: WalletEventSender,
189    signal_receiver: Receiver<()>,
190}
191
192impl WalletAdapter {
193    /// Get the `Window` and `Document` object in the current browser window,
194    /// initialize the `AppReady` and `Register` events of the wallet standard
195    /// and creates a bounded channel with capacity default of 5 messages before capacity is filled.
196    /// Use [WalletAdapter::init_with_channel_capacity] to initialize with a desired channel capacity.
197    pub fn init() -> WalletResult<Self> {
198        let window = if let Some(window) = web_sys::window() {
199            window
200        } else {
201            return Err(WalletError::MissingAccessToBrowserWindow);
202        };
203
204        let document = if let Some(document) = window.document() {
205            document
206        } else {
207            return Err(WalletError::MissingAccessToBrowserDocument);
208        };
209
210        Self::init_with_channel_capacity_window_and_document(5, window, document)
211    }
212
213    /// Get the `Window` and `Document` object in the current browser window,
214    /// initialize the `AppReady` and `Register` events of the wallet standard
215    /// and creates a bounded channel with user-specified capacity.
216    pub fn init_with_channel_capacity(capacity: usize) -> WalletResult<Self> {
217        let window = if let Some(window) = web_sys::window() {
218            window
219        } else {
220            return Err(WalletError::MissingAccessToBrowserWindow);
221        };
222
223        let document = if let Some(document) = window.document() {
224            document
225        } else {
226            return Err(WalletError::MissingAccessToBrowserDocument);
227        };
228
229        Self::init_with_channel_capacity_window_and_document(capacity, window, document)
230    }
231
232    /// Same as [WalletAdapter::init] but a `capacity` value
233    /// can be passed to create an channel with a desired capacity
234    #[allow(clippy::arc_with_non_send_sync)]
235    pub fn init_with_channel_capacity_window_and_document(
236        capacity: usize,
237        window: Window,
238        document: Document,
239    ) -> WalletResult<Self> {
240        let storage = WalletStorage::default();
241
242        let (sender, receiver) = bounded::<WalletEvent>(capacity);
243        let (_, signal_receiver) = bounded::<()>(capacity);
244
245        let mut new_self = Self {
246            window: window.clone(),
247            document,
248            storage,
249            connection_info: Arc::new(RwLock::new(ConnectionInfo::default())),
250            wallet_events: receiver,
251            wallet_events_sender: sender,
252            signal_receiver,
253        };
254
255        InitEvents::new(&window).init(&mut new_self)?;
256
257        Ok(new_self)
258    }
259
260    /// Initializes with a [web_sys::Window] and [web_sys::Document] that have been
261    /// initialized elsewhere. For example some Rust frontend frameworks already
262    /// expose the window and document objects, you could pass them here.
263    pub fn init_custom(window: Window, document: Document) -> WalletResult<Self> {
264        Self::init_with_channel_capacity_window_and_document(5, window, document)
265    }
266
267    /// Listen for [WalletEvent] to be notified when a wallet
268    /// receives `connected`, `disconnected` and `accountChanged` events triggered
269    /// when the `change` event is dispatched by a connected browser extension
270    pub fn events(&self) -> WalletEventReceiver {
271        self.wallet_events.clone()
272    }
273
274    /// Send a connect request to the browser wallet
275    pub async fn connect(&mut self, wallet: Wallet) -> WalletResult<WalletAccount> {
276        let wallet_name = wallet.name().to_string();
277        let sender = self.wallet_events_sender.clone();
278        let signal_receiver = self.signal_receiver.clone();
279
280        if self.connection_info().await.connected_wallet().is_ok() {
281            let capacity = signal_receiver.capacity().unwrap_or(5);
282            let (_, signal_receiver) = bounded::<()>(capacity);
283            self.signal_receiver = signal_receiver;
284        }
285
286        let wallet_account = self
287            .connection_info
288            .write()
289            .await
290            .set_wallet(wallet)
291            .connect(sender.clone())
292            .await?;
293
294        self.connection_info()
295            .await
296            .connected_wallet()?
297            .call_on_event(
298                self.connection_info.clone(),
299                wallet_name,
300                sender,
301                signal_receiver,
302            )
303            .await?;
304
305        Ok(wallet_account)
306    }
307
308    /// Lookup a wallet entry by name from the registered wallets
309    /// and then send a connect request to the browser extension wallet
310    pub async fn connect_by_name(&mut self, wallet_name: &str) -> WalletResult<WalletAccount> {
311        let wallet = self.get_wallet(wallet_name)?;
312
313        self.connect(wallet).await
314    }
315
316    /// Send a disconnect request to the browser wallet
317    pub async fn disconnect(&mut self) {
318        let sender = self.wallet_events_sender.clone();
319
320        self.connection_info
321            .write()
322            .await
323            .set_disconnected(sender)
324            .await;
325        self.signal_receiver.close();
326    }
327
328    /// Send a sign in request to the browser wallet to Sign In With Solana
329    pub async fn sign_in(
330        &self,
331        signin_input: &SigninInput,
332        public_key: [u8; 32],
333    ) -> WalletResult<SignInOutput> {
334        self.connection_info()
335            .await
336            .connected_wallet()?
337            .sign_in(signin_input, public_key)
338            .await
339    }
340
341    /// Send a sign and send transaction request to the browser wallet
342    pub async fn sign_and_send_transaction(
343        &self,
344        transaction_bytes: &[u8],
345        cluster: Cluster,
346        options: SendOptions,
347    ) -> WalletResult<Signature> {
348        let connection_info = self.connection_info();
349
350        connection_info
351            .await
352            .connected_wallet()?
353            .sign_and_send_transaction(
354                transaction_bytes,
355                cluster,
356                options,
357                self.connection_info().await.connected_account()?,
358            )
359            .await
360    }
361
362    /// Send a connect request to the browser wallet
363    pub async fn sign_transaction(
364        &self,
365        transaction_bytes: &[u8],
366        cluster: Option<Cluster>,
367    ) -> WalletResult<Vec<Vec<u8>>> {
368        let connection_info = self.connection_info();
369
370        connection_info
371            .await
372            .connected_wallet()?
373            .sign_transaction(
374                transaction_bytes,
375                cluster,
376                self.connection_info().await.connected_account()?,
377            )
378            .await
379    }
380
381    /// Send a sign message request to the browser wallet
382    pub async fn sign_message<'a>(
383        &self,
384        message: &'a [u8],
385    ) -> WalletResult<SignedMessageOutput<'a>> {
386        let connection_info = self.connection_info();
387
388        self.connection_info()
389            .await
390            .connected_wallet()?
391            .sign_message(message, connection_info.await.connected_account()?)
392            .await
393    }
394
395    /// Check if an [account](WalletAccount) is connected
396    pub async fn is_connected(&self) -> bool {
397        self.connection_info
398            .as_ref()
399            .write()
400            .await
401            .account
402            .is_some()
403    }
404
405    /// Get the connected [ConnectionInfo] containing the
406    /// [account](WalletAccount) and [wallet](Wallet)
407    pub async fn connection_info(&self) -> async_lock::RwLockReadGuard<'_, ConnectionInfo> {
408        self.connection_info.as_ref().read().await
409    }
410
411    /// Get an entry in the `Window` object
412    pub fn get_entry(&self, property: &str) -> Option<Object> {
413        self.window.get(property)
414    }
415
416    /// Get the browser window
417    pub fn window(&self) -> &Window {
418        &self.window
419    }
420
421    /// Get the browser document
422    pub fn document(&self) -> &Document {
423        &self.document
424    }
425
426    /// Get the storage where the adapter stores the registered wallets
427    pub fn storage(&self) -> &WalletStorage {
428        self.storage.borrow()
429    }
430
431    /// Get the clusters supported by the connected wallet
432    pub async fn clusters(&self) -> WalletResult<Vec<Cluster>> {
433        let mut clusters = Vec::<Cluster>::default();
434
435        if self.mainnet().await? {
436            clusters.push(Cluster::MainNet);
437        }
438        if self.devnet().await? {
439            clusters.push(Cluster::DevNet);
440        }
441        if self.localnet().await? {
442            clusters.push(Cluster::LocalNet);
443        }
444        if self.testnet().await? {
445            clusters.push(Cluster::TestNet);
446        }
447
448        Ok(clusters)
449    }
450
451    /// Get the registered wallets
452    pub fn wallets(&self) -> Vec<Wallet> {
453        self.storage.borrow().get_wallets()
454    }
455
456    /// Get a certain wallet by its name
457    pub fn get_wallet(&self, wallet_name: &str) -> WalletResult<Wallet> {
458        self.storage
459            .get_wallet(wallet_name)
460            .ok_or(WalletError::WalletNotFound)
461    }
462
463    /// Check if the connected wallet supports mainnet cluster
464    pub async fn mainnet(&self) -> WalletResult<bool> {
465        Ok(self.connection_info().await.connected_wallet()?.mainnet())
466    }
467
468    /// Check if the connected wallet supports devnet cluster
469    pub async fn devnet(&self) -> WalletResult<bool> {
470        Ok(self.connection_info().await.connected_wallet()?.devnet())
471    }
472
473    /// Check if the connected wallet supports testnet cluster
474    pub async fn testnet(&self) -> WalletResult<bool> {
475        Ok(self.connection_info().await.connected_wallet()?.testnet())
476    }
477
478    /// Check if the connected wallet supports localnet cluster
479    pub async fn localnet(&self) -> WalletResult<bool> {
480        Ok(self.connection_info().await.connected_wallet()?.localnet())
481    }
482
483    /// Check if the connected wallet supports `standard:connect` feature
484    pub async fn standard_connect(&self) -> WalletResult<bool> {
485        Ok(self
486            .connection_info()
487            .await
488            .connected_wallet()?
489            .standard_connect())
490    }
491
492    /// Check if the connected wallet supports `standard:disconnect` feature
493    pub async fn standard_disconnect(&self) -> WalletResult<bool> {
494        Ok(self
495            .connection_info()
496            .await
497            .connected_wallet()?
498            .standard_disconnect())
499    }
500
501    /// Check if the connected wallet supports `standard:events` feature
502    pub async fn standard_events(&self) -> WalletResult<bool> {
503        Ok(self
504            .connection_info()
505            .await
506            .connected_wallet()?
507            .standard_events())
508    }
509
510    /// Check if the connected wallet supports `solana:signIn` feature
511    pub async fn solana_signin(&self) -> WalletResult<bool> {
512        Ok(self
513            .connection_info()
514            .await
515            .connected_wallet()?
516            .solana_signin())
517    }
518
519    /// Check if the connected wallet supports `solana:signMessage` feature
520    pub async fn solana_sign_message(&self) -> WalletResult<bool> {
521        Ok(self
522            .connection_info()
523            .await
524            .connected_wallet()?
525            .solana_sign_message())
526    }
527
528    /// Check if the connected wallet supports `solana:signAndSendTransaction` feature
529    pub async fn solana_sign_and_send_transaction(&self) -> WalletResult<bool> {
530        Ok(self
531            .connection_info()
532            .await
533            .connected_wallet()?
534            .solana_sign_and_send_transaction())
535    }
536
537    /// Check if the connected wallet supports `solana:signTransaction` feature
538    pub async fn solana_sign_transaction(&self) -> WalletResult<bool> {
539        Ok(self
540            .connection_info()
541            .await
542            .connected_wallet()?
543            .solana_sign_transaction())
544    }
545}
546
547impl PartialEq for WalletAdapter {
548    fn eq(&self, other: &Self) -> bool {
549        self.window.eq(&other.window)
550            && self.document.eq(&other.document)
551            && self.storage.eq(&other.storage)
552    }
553}
554impl Eq for WalletAdapter {}