wasi_sol/core/
wallet.rs

1#![allow(deprecated)]
2
3use std::str::FromStr;
4
5use anyhow::Result;
6use emitter_rs::EventEmitter;
7use js_sys::{Promise, Uint8Array};
8use log::info;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use wasm_bindgen::JsValue;
12use wasm_bindgen_futures::JsFuture;
13use web_sys::window;
14
15use solana_client_wasm::WasmClient as RpcClient;
16use solana_sdk::{bs58, pubkey::Pubkey, signature::Signature, transaction::Transaction};
17
18use crate::{
19    adapter::{
20        backpack::{BACKPACK, XNFT},
21        phantom::SOLANA,
22        solflare::SOLFLARE,
23    },
24    core::{
25        error::WalletError,
26        response::{JsSignatureObject, JsSignatureResponse, SignaturesObject},
27        traits::{WalletAdapter, WalletAdapterEvents},
28        transaction::TransactionOrVersionedTransaction,
29    },
30};
31
32#[derive(Debug, Clone, PartialEq, Default)]
33pub enum Wallet {
34    #[default]
35    Phantom,
36    Solflare,
37    Backpack,
38}
39
40impl From<Wallet> for BaseWalletAdapter {
41    fn from(val: Wallet) -> Self {
42        match val {
43            Wallet::Phantom => BaseWalletAdapter::new(
44                Wallet::Phantom,
45                "https://phantom.app",
46                "images/phantom_logo.png",
47            ),
48            Wallet::Solflare => BaseWalletAdapter::new(
49                Wallet::Solflare,
50                "https://solflare.com",
51                "images/solflare_logo.png",
52            ),
53            Wallet::Backpack => BaseWalletAdapter::new(
54                Wallet::Backpack,
55                "https://backpack.app",
56                "images/backpack_logo.png",
57            ),
58        }
59    }
60}
61
62#[derive(Default, Clone, PartialEq, Serialize, Deserialize)]
63pub enum WalletReadyState {
64    Installed,
65    #[default]
66    NotDetected,
67    Loadable,
68    Unsupported,
69}
70
71#[derive(Default, Clone, PartialEq)]
72pub struct BaseWalletAdapter {
73    name: Wallet,
74    url: String,
75    icon: String,
76    ready_state: WalletReadyState,
77    public_key: Option<Pubkey>,
78    connecting: bool,
79    pub emitter: EventEmitter,
80}
81
82impl BaseWalletAdapter {
83    pub fn new(name: Wallet, url: &str, icon: &str) -> Self {
84        let ready_state = if cfg!(target_arch = "wasm32") {
85            WalletReadyState::Unsupported
86        } else {
87            WalletReadyState::NotDetected
88        };
89
90        BaseWalletAdapter {
91            ready_state,
92            name,
93            url: url.to_string(),
94            icon: icon.to_string(),
95            public_key: None,
96            connecting: false,
97            emitter: EventEmitter::new(),
98        }
99    }
100}
101
102impl WalletAdapter for BaseWalletAdapter {
103    fn name(&self) -> String {
104        format!("{:?}", self.name).to_string()
105    }
106
107    fn url(&self) -> String {
108        self.url.to_string()
109    }
110
111    fn icon(&self) -> String {
112        self.icon.to_string()
113    }
114
115    fn ready_state(&self) -> WalletReadyState {
116        self.ready_state.clone()
117    }
118
119    fn public_key(&self) -> Option<Pubkey> {
120        self.public_key
121    }
122
123    fn connecting(&self) -> bool {
124        self.connecting
125    }
126
127    async fn auto_connect(&mut self) -> Result<bool, WalletError> {
128        self.connect().await
129    }
130
131    async fn connect(&mut self) -> Result<bool, WalletError> {
132        info!("Connecting to wallet...");
133
134        if self.connecting {
135            self.emit_error(WalletError::WalletConnectionError);
136            return Err(WalletError::WalletConnectionError);
137        }
138
139        self.connecting = true;
140
141        let options = js_sys::Object::new();
142        js_sys::Reflect::set(
143            &options,
144            &serde_wasm_bindgen::to_value("onlyIfTrusted").unwrap(),
145            &serde_wasm_bindgen::to_value(&true).unwrap(),
146        )
147        .unwrap();
148
149        let promise: Option<Promise> = match self.name {
150            Wallet::Phantom if !SOLANA.is_undefined() => Some(SOLANA.sign_in(&options)),
151            Wallet::Solflare if !SOLFLARE.is_undefined() => Some(SOLFLARE.connect(&options)),
152            Wallet::Backpack if !XNFT.is_undefined() => Some(BACKPACK.sign_in(&options)),
153            Wallet::Phantom | Wallet::Solflare | Wallet::Backpack => None,
154        };
155
156        if promise.is_some() {
157            let result = JsFuture::from(promise.unwrap()).await;
158
159            match result {
160                Ok(_response) => {
161                    // Todo use response to get pubkey
162                    // let response: MessageObject = serde_wasm_bindgen::from_value(response).unwrap();
163                    info!("Wallet connected");
164
165                    let key: JsValue = match self.name {
166                        Wallet::Phantom => SOLANA.publicKey(),
167                        Wallet::Solflare => SOLFLARE.publicKey(),
168                        Wallet::Backpack => BACKPACK.publicKey(),
169                    };
170
171                    if key.is_undefined() {
172                        info!("Public key is undefined");
173                    } else {
174                        let key_str: String = JsValue::into_serde(&key).unwrap();
175
176                        let public_key = Pubkey::from_str(&key_str).unwrap();
177                        info!("Connected to wallet with public key: {:?}", public_key);
178                        self.public_key = Some(public_key);
179                        self.ready_state = WalletReadyState::Installed;
180                        self.emit_connect(public_key);
181
182                        self.connecting = false;
183                    }
184                }
185                Err(err) => {
186                    log::error!("Failed to connect wallet: {:?}", err);
187                }
188            }
189        } else {
190            let window = window().expect("no global `window` exists");
191            window
192                .open_with_url(&self.url)
193                .expect("failed to open a new tab");
194        }
195
196        Ok(!self.connecting)
197    }
198
199    async fn disconnect(&mut self) -> Result<bool, WalletError> {
200        info!("Disconnecting from wallet...");
201        let mut confirmed = false;
202
203        if self.public_key.is_none() {
204            self.emit_error(WalletError::WalletDisconnectedError);
205            return Err(WalletError::WalletDisconnectedError);
206        }
207
208        self.public_key = None;
209        self.ready_state = WalletReadyState::NotDetected;
210        self.emit_disconnect();
211        let promise: Promise = match self.name {
212            Wallet::Phantom => SOLANA.disconnect(),
213            Wallet::Solflare => SOLFLARE.disconnect(),
214            Wallet::Backpack => BACKPACK.disconnect(),
215        };
216        let result = JsFuture::from(promise).await;
217
218        match result {
219            Ok(_) => {
220                confirmed = true;
221                info!("Disconnected from wallet");
222            }
223            Err(err) => {
224                log::error!("Failed to disconnect wallet: {:?}", err);
225            }
226        }
227
228        Ok(confirmed)
229    }
230
231    async fn send_transaction(
232        &mut self,
233        client: Arc<RpcClient>,
234        transaction: TransactionOrVersionedTransaction,
235    ) -> Result<Signature, WalletError> {
236        info!("Sending transaction...");
237
238        if self.public_key.is_none() {
239            self.emit_error(WalletError::WalletNotConnectedError);
240            return Err(WalletError::WalletNotConnectedError);
241        }
242        let signature = match transaction {
243            TransactionOrVersionedTransaction::Transaction(tx) => client
244                .send_and_confirm_transaction(&tx)
245                .await
246                .map_err(|_| WalletError::WalletSendTransactionError)?,
247            TransactionOrVersionedTransaction::VersionedTransaction(_vtx) => {
248                // TODO: Add support
249                // client
250                //     .async_send_versioned_transaction(&vtx)
251                //     .await
252                //     .map_err(|_| WalletError::WalletSendTransactionError)?
253                Signature::default()
254            }
255        };
256
257        self.emit_transaction_sent(signature);
258        info!("Transaction sent: {}", signature);
259        Ok(signature)
260    }
261
262    async fn sign_transaction(
263        &mut self,
264        transaction: Transaction,
265    ) -> Result<Signature, WalletError> {
266        info!("Signing transaction...");
267
268        if self.public_key.is_none() {
269            self.emit_error(WalletError::WalletNotConnectedError);
270            return Err(WalletError::WalletNotConnectedError);
271        }
272
273        let transaction_bytes = bincode::serialize(&transaction)
274            .map_err(|_| WalletError::WalletSignTransactionError)?;
275
276        let transaction_js_array = Uint8Array::from(&transaction_bytes[..]);
277
278        let bs58_tx = bs58::encode(transaction_bytes).into_string();
279
280        let options = js_sys::Object::new();
281        js_sys::Reflect::set(
282            &options,
283            &serde_wasm_bindgen::to_value("method").unwrap(),
284            &serde_wasm_bindgen::to_value("signTransaction").unwrap(),
285        )
286        .expect("Failed to set method in options");
287
288        let params = js_sys::Object::new();
289        js_sys::Reflect::set(
290            &params,
291            &serde_wasm_bindgen::to_value("message").unwrap(),
292            &serde_wasm_bindgen::to_value(&bs58_tx).unwrap(),
293        )
294        .expect("Failed to set message in params");
295
296        js_sys::Reflect::set(
297            &options,
298            &serde_wasm_bindgen::to_value("params").unwrap(),
299            &JsValue::from(&params),
300        )
301        .expect("Failed to set params in options");
302
303        let promise: Promise = match self.name {
304            Wallet::Phantom => SOLANA.request(&options),
305            Wallet::Solflare => SOLFLARE.request(&options),
306            Wallet::Backpack => BACKPACK.sign_transaction(
307                &transaction_js_array,
308                &JsValue::from(self.public_key.unwrap()),
309                &JsValue::from(""),
310                &JsValue::from("uuid"),
311            ),
312        };
313
314        let result = JsFuture::from(promise).await;
315
316        match result {
317            Ok(json_str) => {
318                let deserialized: SignaturesObject = JsValue::into_serde(&json_str).unwrap();
319
320                let signature_map = &deserialized.signatures[0].signature;
321                let mut signature_bytes = [0u8; 64];
322
323                for (key, value) in signature_map.iter() {
324                    let index: usize = key.parse().unwrap();
325                    signature_bytes[index] = value.as_u64().unwrap() as u8;
326                }
327
328                let signature = Signature::new(&signature_bytes);
329                info!("Got signature: {:?}", signature);
330                Ok(signature)
331            }
332            Err(err) => {
333                log::error!("Failed to sign transaction: {:?}", err);
334                Err(WalletError::WalletSignTransactionError)
335            }
336        }
337    }
338
339    async fn sign_send_transaction(
340        &mut self,
341        transaction: Transaction,
342    ) -> Result<Signature, WalletError> {
343        info!("Signing and sending transaction...");
344
345        if self.public_key.is_none() {
346            self.emit_error(WalletError::WalletNotConnectedError);
347            return Err(WalletError::WalletNotConnectedError);
348        }
349
350        let transaction_bytes = bincode::serialize(&transaction)
351            .map_err(|_| WalletError::WalletSignTransactionError)?;
352
353        let transaction_js_array = Uint8Array::from(&transaction_bytes[..]);
354
355        let bs58_tx = bs58::encode(transaction_bytes).into_string();
356
357        let options = js_sys::Object::new();
358        js_sys::Reflect::set(
359            &options,
360            &serde_wasm_bindgen::to_value("method").unwrap(),
361            &serde_wasm_bindgen::to_value("signAndSendTransaction").unwrap(),
362        )
363        .expect("Failed to set method in options");
364
365        let params = js_sys::Object::new();
366        js_sys::Reflect::set(
367            &params,
368            &serde_wasm_bindgen::to_value("message").unwrap(),
369            &serde_wasm_bindgen::to_value(&bs58_tx).unwrap(),
370        )
371        .expect("Failed to set message in params");
372
373        js_sys::Reflect::set(
374            &options,
375            &serde_wasm_bindgen::to_value("params").unwrap(),
376            &JsValue::from(&params),
377        )
378        .expect("Failed to set params in options");
379
380        let promise: Promise = match self.name {
381            Wallet::Phantom => SOLANA.request(&options),
382            Wallet::Solflare => SOLFLARE.request(&options),
383            Wallet::Backpack => BACKPACK.sign_and_send_transaction(&transaction_js_array, &options),
384        };
385
386        let result = JsFuture::from(promise).await;
387
388        match result {
389            Ok(json_str) => {
390                let deserialized: JsSignatureObject = JsValue::into_serde(&json_str).unwrap();
391
392                let signature = Signature::from_str(&deserialized.signature).unwrap();
393                info!("Got signature: {:?}", signature);
394
395                Ok(signature)
396            }
397            Err(err) => {
398                log::error!("Failed to sign transaction: {:?}", err);
399                Err(WalletError::WalletSignTransactionError)
400            }
401        }
402    }
403
404    async fn sign_message(&mut self, message: &str) -> Result<Signature, WalletError> {
405        info!("Signing transaction...");
406
407        if self.public_key.is_none() {
408            return Err(WalletError::WalletNotConnectedError);
409        }
410
411        let message_bytes =
412            bincode::serialize(&message).map_err(|_| WalletError::WalletSignTransactionError)?;
413
414        let message_js_array = Uint8Array::from(&message_bytes[..]);
415
416        let promise: Promise = match self.name {
417            Wallet::Phantom => SOLANA.sign_message(&message_js_array),
418            Wallet::Solflare => SOLFLARE.sign_message(&message_js_array),
419            Wallet::Backpack => BACKPACK.sign_message(
420                &message_js_array,
421                &JsValue::from(self.public_key.unwrap()),
422                &JsValue::from("uuid"),
423            ),
424        };
425
426        let result = JsFuture::from(promise).await;
427
428        match result {
429            Ok(json_str) => {
430                let sig_obj: JsSignatureResponse = JsValue::into_serde(&json_str).unwrap();
431                let data = sig_obj.signature.data;
432
433                let signature = Signature::new(&data);
434                info!("Message signed: {:?}", signature);
435
436                Ok(signature)
437            }
438            Err(err) => {
439                log::error!("Failed to sign transaction: {:?}", err);
440                Err(WalletError::WalletSignTransactionError)
441            }
442        }
443    }
444}
445
446impl WalletAdapterEvents for BaseWalletAdapter {
447    fn emit_connect(&mut self, public_key: Pubkey) {
448        self.emitter.emit("connect", public_key);
449    }
450
451    fn emit_disconnect(&mut self) {
452        self.emitter.emit("disconnect", ());
453    }
454
455    fn emit_error(&mut self, error: WalletError) {
456        self.emitter.emit("error", error);
457    }
458
459    fn ready_state_change(&mut self, ready_state: WalletReadyState) {
460        self.emitter.emit("ready_state_change", ready_state);
461    }
462
463    fn emit_transaction_sent(&mut self, signature: Signature) {
464        self.emitter.emit("transaction_sent", signature);
465    }
466}