nostro2_web_signer/
lib.rs1#![warn(
2 clippy::all,
3 clippy::style,
4 clippy::unseparated_literal_suffix,
5 clippy::pedantic,
6 clippy::nursery
7)]
8#![allow(clippy::future_not_send)]
9
10use wasm_bindgen::prelude::*;
11
12#[derive(Debug)]
13pub enum NostrWindowObjectError {
14 NotAvailable,
15 NotReady,
16 NotNostr,
17}
18impl std::fmt::Display for NostrWindowObjectError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Self::NotAvailable => write!(f, "Nostr is not available"),
22 Self::NotReady => write!(f, "Nostr is not ready"),
23 Self::NotNostr => write!(f, "Nostr is not a Nostr object"),
24 }
25 }
26}
27impl From<wasm_bindgen::JsValue> for NostrWindowObjectError {
28 fn from(value: wasm_bindgen::JsValue) -> Self {
29 if value.is_null() {
30 Self::NotAvailable
31 } else if value.is_undefined() {
32 Self::NotReady
33 } else if value.is_object() {
34 Self::NotNostr
35 } else {
36 Self::NotAvailable
37 }
38 }
39}
40impl std::error::Error for NostrWindowObjectError {}
41
42#[wasm_bindgen]
43extern "C" {
44 #[derive(Debug, Clone)]
50 pub type NostrWindowObject;
51
52 #[wasm_bindgen(method, js_name = getPublicKey)]
53 pub async fn get_public_key(this: &NostrWindowObject) -> JsValue;
54
55 #[wasm_bindgen(method, js_name = signEvent)]
56 #[wasm_bindgen(catch)]
57 pub async fn sign_event(this: &NostrWindowObject, event: JsValue) -> Result<JsValue, JsValue>;
58
59 pub type NostrWindowObjectEncryption;
60
61 #[wasm_bindgen(method, js_name = encrypt)]
62 #[wasm_bindgen(catch)]
63 pub async fn encrypt(
64 this: &NostrWindowObjectEncryption,
65 pubkey: JsValue,
66 plaintext: JsValue,
67 ) -> Result<JsValue, JsValue>;
68
69 #[wasm_bindgen(method, js_name = decrypt)]
70 #[wasm_bindgen(catch)]
71 pub async fn decrypt(
72 this: &NostrWindowObjectEncryption,
73 pubkey: JsValue,
74 ciphertext: JsValue,
75 ) -> Result<JsValue, JsValue>;
76}
77impl NostrWindowObject {
78 pub async fn new() -> Option<Self> {
79 let window = web_sys::window()?;
80 let document = window.document()?;
81 if document.ready_state() != "completed" {
82 let (sender, receiver) = futures::channel::oneshot::channel();
83 let closure = wasm_bindgen::prelude::Closure::once_into_js(
84 move |nostr: wasm_bindgen::JsValue| {
85 let _ = sender.send(nostr);
86 },
87 );
88 if window
89 .add_event_listener_with_callback("load", closure.as_ref().unchecked_ref())
90 .is_ok()
91 {
92 let _ = receiver.await;
93 }
94 }
95 window.get("nostr").map(JsCast::unchecked_into::<Self>)
96 }
97 pub async fn public_key(&self) -> Result<String, NostrWindowObjectError> {
103 self.get_public_key()
104 .await
105 .as_string()
106 .ok_or(NostrWindowObjectError::NotAvailable)
107 }
108 #[cfg(target_arch = "wasm32")]
114 pub async fn sign_note(
115 &self,
116 event: nostro2::note::NostrNote,
117 ) -> Result<nostro2::note::NostrNote, NostrWindowObjectError> {
118 let event: JsValue = event.into();
119 let signed_event = self
120 .sign_event(event)
121 .await
122 .map_err(|_| NostrWindowObjectError::NotAvailable)?;
123 Ok(TryInto::<nostro2::note::NostrNote>::try_into(signed_event)
124 .map_err(|_| NostrWindowObjectError::NotAvailable)?)
125 }
126 pub async fn encrypt(
132 &self,
133 pubkey: &str,
134 plaintext: &str,
135 ) -> Result<String, NostrWindowObjectError> {
136 let nip_44 = web_sys::js_sys::Reflect::get(self, &"nip44".into())?
137 .unchecked_into::<crate::NostrWindowObjectEncryption>();
138 nip_44
139 .encrypt(pubkey.into(), plaintext.into())
140 .await?
141 .as_string()
142 .ok_or(NostrWindowObjectError::NotAvailable)
143 }
144 pub async fn decrypt(
150 &self,
151 pubkey: &str,
152 ciphertext: &str,
153 ) -> Result<String, NostrWindowObjectError> {
154 let nip_44 = web_sys::js_sys::Reflect::get(self, &"nip44".into())?
155 .unchecked_into::<crate::NostrWindowObjectEncryption>();
156 nip_44
157 .decrypt(pubkey.into(), ciphertext.into())
158 .await?
159 .as_string()
160 .ok_or(NostrWindowObjectError::NotAvailable)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
167
168 #[wasm_bindgen_test::wasm_bindgen_test]
169 async fn _window_nostr_decrypt() {
170 let nostr = crate::NostrWindowObject::new()
171 .await
172 .expect("nostr is not available");
173 let public_key = nostr
174 .public_key()
175 .await
176 .expect("public key is not available");
177 let plaintext = "Hello, world!";
178 let ciphertext = nostr
179 .encrypt(&public_key, &plaintext)
180 .await
181 .expect("encryption failed");
182 let decrypted = nostr
183 .decrypt(&public_key, &ciphertext)
184 .await
185 .expect("decryption failed");
186 assert!(
187 decrypted == plaintext,
188 "decrypted is not the same as plaintext"
189 );
190 }
191}