wallet_standard_browser/
browser_wallet_info.rs

1#![allow(unsafe_code)]
2
3use std::collections::hash_map::DefaultHasher;
4use std::hash::Hash;
5use std::hash::Hasher;
6
7use js_sys::Array;
8use js_sys::Function;
9use js_sys::Object;
10use wallet_standard::WalletAccountInfo;
11use wallet_standard::WalletError;
12use wallet_standard::WalletInfo;
13use wallet_standard::WalletResult;
14use wasm_bindgen::prelude::*;
15use wasm_bindgen::JsCast;
16use wasm_bindgen::JsValue;
17
18use crate::FeatureFromJs;
19use crate::StandardConnectFeature;
20use crate::StandardDisconnectFeature;
21use crate::StandardEventsFeature;
22
23#[wasm_bindgen(module = "/js/wallet.js")]
24extern "C" {
25	/// Register a {@link "@wallet-standard/base".Wallet} as a Standard Wallet
26	/// with the app.
27	///
28	/// This dispatches a {@link
29	/// "@wallet-standard/base".WindowRegisterWalletEvent} to notify the app
30	/// that the Wallet is ready to be registered.
31	///
32	/// This also adds a listener for {@link
33	/// "@wallet-standard/base".WindowAppReadyEvent} to listen for a
34	/// notification from the app that the app is ready to register the Wallet.
35	///
36	/// This combination of event dispatch and listener guarantees that the
37	/// Wallet will be registered synchronously as soon as the app is ready
38	/// whether the Wallet loads before or after the app.
39	///
40	/// @param wallet Wallet to register.
41	///
42	/// @group Wallet
43	#[allow(unsafe_code)]
44	#[wasm_bindgen(js_name = registerWallet, catch)]
45	pub fn register_wallet(wallet: &BrowserWalletInfo) -> Result<(), JsValue>;
46}
47
48#[wasm_bindgen(module = "/js/app.js")]
49extern "C" {
50	#[derive(Clone, Debug)]
51	pub type BrowserWalletInfo;
52	/// {@link `WalletVersion` | Version} of the Wallet Standard implemented by
53	/// the Wallet.
54	///
55	/// Must be read-only, static, and canonically defined by the Wallet
56	/// Standard.
57	#[wasm_bindgen(getter, method, js_name = version)]
58	pub fn _version(this: &BrowserWalletInfo) -> String;
59	/// Name of the Wallet. This may be displayed by the app.
60	///
61	/// Must be read-only, static, descriptive, unique, and canonically defined
62	/// by the wallet extension or application.
63	#[wasm_bindgen(getter, method, js_name = name)]
64	pub fn _name(this: &BrowserWalletInfo) -> String;
65	/// {@link `WalletIcon` | Icon} of the Wallet. This may be displayed by the
66	/// app.
67	///
68	/// Must be read-only, static, and canonically defined by the wallet
69	/// extension or application.
70	#[wasm_bindgen(getter, method, js_name = icon)]
71	pub fn _icon(this: &BrowserWalletInfo) -> String;
72	/// Chains supported by the Wallet.
73	///
74	/// A **chain** is an {@link `IdentifierString`} which identifies a
75	/// blockchain in a canonical, human-readable format. [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) chain IDs are compatible with this,
76	/// but are not required to be used.
77	///
78	/// Each blockchain should define its own **chains** by extension of the
79	/// Wallet Standard, using its own namespace. The `standard` and
80	/// `experimental` namespaces are reserved by the Wallet Standard.
81	///
82	/// The {@link "@wallet-standard/features".EventsFeature | `standard:events`
83	/// feature} should be used to notify the app if the value changes.
84	#[wasm_bindgen(getter, method, js_name = chains)]
85	pub fn _chains(this: &BrowserWalletInfo) -> Vec<String>;
86	/// Features supported by the Wallet.
87	///
88	/// A **feature name** is an {@link `IdentifierString`} which identifies a
89	/// **feature** in a canonical, human-readable format.
90	///
91	/// Each blockchain should define its own features by extension of the
92	/// Wallet Standard.
93	///
94	/// The `standard` and `experimental` namespaces are reserved by the Wallet
95	/// Standard.
96	///
97	/// A **feature** may have any type. It may be a single method or value, or
98	/// a collection of them.
99	///
100	/// A **conventional feature** has the following structure:
101	///
102	/// ```ts
103	///  export type ExperimentalEncryptFeature = {
104	///      // Name of the feature.
105	///      'experimental:encrypt': {
106	///          // Version of the feature.
107	///          version: '1.0.0';
108	///          // Properties of the feature.
109	///          ciphers: readonly 'x25519-xsalsa20-poly1305'[];
110	///          // Methods of the feature.
111	///          encrypt (data: Uint8Array): Promise<Uint8Array>;
112	///      };
113	///  };
114	/// ```
115	///
116	/// The {@link "@wallet-standard/features".EventsFeature | `standard:events`
117	/// feature} should be used to notify the app if the value changes.
118	#[wasm_bindgen(getter, method, js_name = features)]
119	pub fn features_object(this: &BrowserWalletInfo) -> Object;
120	/// {@link `WalletAccount` | Accounts} that the app is authorized to use.
121	///
122	/// This can be set by the Wallet so the app can use authorized accounts on
123	/// the initial page load.
124	///
125	/// The {@link "@wallet-standard/features".ConnectFeature |
126	/// `standard:connect` feature} should be used to obtain authorization to
127	/// the accounts.
128	///
129	/// The {@link "@wallet-standard/features".EventsFeature | `standard:events`
130	/// feature} should be used to notify the app if the value changes.
131	#[wasm_bindgen(getter, method, js_name = accounts)]
132	pub fn _accounts(this: &BrowserWalletInfo) -> Vec<BrowserWalletAccountInfo>;
133	/// Interface of a **`WalletAccount`**, also referred to as an **Account**.
134	///
135	/// An account is a _read-only data object_ that is provided from the Wallet
136	/// to the app, authorizing the app to use it.
137	///
138	/// The app can use an account to display and query information from a
139	/// chain.
140	///
141	/// The app can also act using an account by passing it to {@link
142	/// Wallet.features | features} of the Wallet.
143	///
144	/// Wallets may use or extend {@link
145	/// "@wallet-standard/wallet".ReadonlyWalletAccount} which implements this
146	/// interface.
147	#[derive(Clone, Debug)]
148	pub type BrowserWalletAccountInfo;
149	/// Address of the account, corresponding with a public key.
150	#[wasm_bindgen(getter, method, js_name = address)]
151	pub fn _address(this: &BrowserWalletAccountInfo) -> String;
152	/// Public key of the account, corresponding with a secret key to use.
153	#[wasm_bindgen(getter, method, js_name = publicKey)]
154	pub fn _public_key(this: &BrowserWalletAccountInfo) -> Vec<u8>;
155	/// Chains supported by the account.
156	///
157	/// This must be a subset of the {@link Wallet.chains | chains} of the
158	/// Wallet.
159	#[wasm_bindgen(getter, method, js_name = chains)]
160	pub fn _chains(this: &BrowserWalletAccountInfo) -> Vec<String>;
161	/// Feature names supported by the account.
162	///
163	/// This must be a subset of the names of {@link Wallet.features | features}
164	/// of the Wallet.
165	#[wasm_bindgen(getter, method, js_name = features)]
166	pub fn _features(this: &BrowserWalletAccountInfo) -> Vec<String>;
167	/// Optional user-friendly descriptive label or name for the account. This
168	/// may be displayed by the app.
169	#[wasm_bindgen(getter, method, js_name = label)]
170	pub fn _label(this: &BrowserWalletAccountInfo) -> Option<String>;
171	/// Optional user-friendly icon for the account. This may be displayed by
172	/// the app.
173	#[wasm_bindgen(getter, method, js_name = icon)]
174	pub fn _icon(this: &BrowserWalletAccountInfo) -> Option<String>;
175	#[derive(Clone, Debug)]
176	pub type Wallets;
177	/// Get all Wallets that have been registered.
178	///
179	/// @return Registered Wallets.
180	#[wasm_bindgen(method)]
181	pub fn get(this: &Wallets) -> Vec<BrowserWalletInfo>;
182	/// Add an event listener and subscribe to events for Wallets that are
183	/// {@link WalletsEventsListeners.register | registered} and
184	/// {@link WalletsEventsListeners.unregister | unregistered}.
185	///
186	/// @param event    Event type to listen for. {@link
187	/// WalletsEventsListeners.register | `register`} and
188	/// {@link WalletsEventsListeners.unregister | `unregister`} are the only
189	/// event types. @param listener Function that will be called when an event
190	/// of the type is emitted.
191	///
192	/// @return
193	/// `off` function which may be called to remove the event listener and
194	/// unsubscribe from events.
195	///
196	/// As with all event listeners, be careful to avoid memory leaks.
197	#[wasm_bindgen(method)]
198	pub fn on(
199		this: &Wallets,
200		event_name: &str,
201		callback: &Closure<dyn Fn(BrowserWalletInfo)>,
202	) -> Function;
203	/// Register Wallets. This can be used to programmatically wrap non-standard
204	/// wallets as Standard Wallets.
205	///
206	/// Apps generally do not need to, and should not, call this.
207	///
208	/// @param wallets Wallets to register.
209	///
210	/// @return
211	/// `unregister` function which may be called to programmatically unregister
212	/// the registered Wallets.
213	///
214	/// Apps generally do not need to, and should not, call this.
215	#[wasm_bindgen(method, js_name = register, getter)]
216	pub fn register_fn(this: &Wallets) -> Function;
217
218	#[wasm_bindgen(js_name = getWallets)]
219	pub fn get_wallets() -> Wallets;
220}
221
222impl PartialEq for BrowserWalletInfo {
223	fn eq(&self, other: &Self) -> bool {
224		self.name().eq(&other.name())
225			&& self.chains().eq(&other.chains())
226			&& self.version().eq(&other.version())
227			&& self.icon().eq(&other.icon())
228	}
229}
230
231impl Eq for BrowserWalletInfo {}
232
233impl Hash for BrowserWalletInfo {
234	fn hash<H: Hasher>(&self, state: &mut H) {
235		self.name().hash(state);
236		self.chains().hash(state);
237		self.version().hash(state);
238		self.icon().hash(state);
239	}
240}
241
242impl BrowserWalletInfo {
243	pub fn get_hash(&self) -> u64 {
244		let mut hasher = DefaultHasher::new();
245		self.name().hash(&mut hasher);
246		self.chains().hash(&mut hasher);
247
248		hasher.finish()
249	}
250
251	/// Get the feature from the provide type. Must implement `FeatureFromJs`.
252	pub fn get_feature_option<T: FeatureFromJs>(&self) -> Option<T> {
253		T::feature_from_js_object(&self.features_object())
254	}
255
256	/// Get the required feature and throw an error if it isn't supported.
257	pub fn get_feature<T: FeatureFromJs>(&self) -> WalletResult<T> {
258		self.get_feature_option::<T>()
259			.ok_or(WalletError::UnsupportedFeature {
260				feature: T::NAME.to_string(),
261				wallet: self.name(),
262			})
263	}
264
265	/// Check whether a feature is supported by the given wallet.
266	pub fn is_feature_supported<T: FeatureFromJs>(&self) -> bool {
267		self.get_feature_option::<T>().is_some()
268	}
269
270	pub fn is_standard_compatible(&self) -> bool {
271		self.is_feature_supported::<StandardConnectFeature>()
272			&& self.is_feature_supported::<StandardEventsFeature>()
273			&& self.is_feature_supported::<StandardDisconnectFeature>()
274	}
275}
276
277impl WalletInfo for BrowserWalletInfo {
278	type Account = BrowserWalletAccountInfo;
279
280	fn version(&self) -> String {
281		self._version()
282	}
283
284	fn name(&self) -> String {
285		self._name()
286	}
287
288	fn icon(&self) -> String {
289		self._icon()
290	}
291
292	fn chains(&self) -> Vec<String> {
293		self._chains()
294	}
295
296	fn features(&self) -> Vec<String> {
297		Object::keys(&self.features_object())
298			.into_iter()
299			.map(|value| value.as_string().unwrap_throw())
300			.collect::<Vec<_>>()
301	}
302
303	fn accounts(&self) -> Vec<Self::Account> {
304		self._accounts()
305	}
306}
307
308impl PartialEq for BrowserWalletAccountInfo {
309	fn eq(&self, other: &Self) -> bool {
310		self.address().eq(&other.address())
311			&& self.public_key().eq(&other.public_key())
312			&& self.chains().eq(&other.chains())
313			&& self.features().eq(&other.features())
314			&& self.label().eq(&other.label())
315			&& self.icon().eq(&other.icon())
316	}
317}
318
319impl Eq for BrowserWalletAccountInfo {}
320
321impl Hash for BrowserWalletAccountInfo {
322	fn hash<H: Hasher>(&self, state: &mut H) {
323		self.address().hash(state);
324		self.public_key().hash(state);
325		self.chains().hash(state);
326		self.features().hash(state);
327		self.label().hash(state);
328		self.icon().hash(state);
329	}
330}
331
332impl WalletAccountInfo for BrowserWalletAccountInfo {
333	fn address(&self) -> String {
334		self._address()
335	}
336
337	fn public_key(&self) -> Vec<u8> {
338		self._public_key()
339	}
340
341	fn chains(&self) -> Vec<String> {
342		self._chains()
343	}
344
345	fn features(&self) -> Vec<String> {
346		self._features()
347	}
348
349	fn label(&self) -> Option<String> {
350		self._label()
351	}
352
353	fn icon(&self) -> Option<String> {
354		self._icon()
355	}
356}
357
358impl Wallets {
359	/// Currently only supports one wallet at a time.
360	/// <https://github.com/rustwasm/wasm-bindgen/issues/3715>
361	pub fn on_register(&self, callback: &Closure<dyn Fn(BrowserWalletInfo)>) -> Box<dyn Fn()> {
362		let dispose = self.on("register", callback);
363
364		Box::new(move || {
365			let _ = dispose.call0(&JsValue::NULL);
366		})
367	}
368
369	/// Currently only supports one wallet at a time.
370	/// <https://github.com/rustwasm/wasm-bindgen/issues/3715>
371	pub fn on_unregister(&self, callback: &Closure<dyn Fn(BrowserWalletInfo)>) -> Box<dyn Fn()> {
372		let dispose = self.on("unregister", callback);
373
374		Box::new(move || {
375			let _ = dispose.call0(&JsValue::NULL);
376		})
377	}
378
379	pub fn register(&self, wallets: &[BrowserWalletInfo]) -> Box<dyn Fn()> {
380		let args = Array::new();
381
382		for wallet in wallets {
383			args.push(wallet.unchecked_ref());
384		}
385
386		let dispose: Function = self
387			.register_fn()
388			.apply(self.unchecked_ref(), &args)
389			.unwrap()
390			.dyn_into()
391			.unwrap();
392
393		Box::new(move || {
394			let _ = dispose.call0(&JsValue::NULL);
395		})
396	}
397}