Skip to main content

nostro2_web_signer/
lib.rs

1#![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    /// The `nostr` object is a global object that is injected by the Nostr client.
45    /// It provides access to the Nostr protocol and its features.
46    /// must be accessed through window.nostr.
47    /// This object is available in the browser environment.
48
49    #[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    /// Returns the public key from the web extension.
98    ///
99    /// # Errors
100    ///
101    /// If the Nostr client is not available or the public key is not available.
102    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    /// Returns the public key from the web extension.
109    ///
110    /// # Errors
111    ///
112    /// If the Nostr client is not available or the public key is not available.
113    #[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    /// Signs a Nostr event using the Nostr `Nip 07` extension.
127    ///
128    /// # Errors
129    ///
130    /// If the Nostr client is not available or the event is not available.
131    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    /// Decrypts a Nostr event using the Nostr `Nip 07` extension.
145    ///
146    /// # Errors
147    ///
148    /// If the Nostr client is not available or the event is not available.
149    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}