Skip to main content

nostr_browser_signer/
lib.rs

1// Copyright (c) 2022-2023 Yuki Kishimoto
2// Copyright (c) 2023-2025 Rust Nostr Developers
3// Distributed under the MIT software license
4
5//! Nostr Browser signer implementation (NIP-07).
6//!
7//! `window.nostr` capability for web browsers.
8//!
9//! <https://github.com/nostr-protocol/nips/blob/master/07.md>
10
11#![cfg_attr(test, allow(missing_docs))]
12#![cfg_attr(not(test), warn(missing_docs))]
13#![warn(rustdoc::bare_urls)]
14#![doc = include_str!("../README.md")]
15// Crate available only for WASM
16#![cfg(target_family = "wasm")]
17
18use std::future::Future;
19use std::pin::Pin;
20use std::str::FromStr;
21use std::task::{Context, Poll};
22
23use js_sys::{Array, Function, JsString, Object, Promise, Reflect};
24use nostr::prelude::*;
25use wasm_bindgen::prelude::*;
26use wasm_bindgen_futures::JsFuture;
27use web_sys::Window;
28
29pub mod error;
30pub mod prelude;
31
32use self::error::Error;
33
34const GET_PUBLIC_KEY: &str = "getPublicKey";
35const SIGN_EVENT: &str = "signEvent";
36const NIP04: &str = "nip04";
37const NIP44: &str = "nip44";
38const ENCRYPT: &str = "encrypt";
39const DECRYPT: &str = "decrypt";
40
41enum CallFunc<'a> {
42    Call0,
43    Call1(&'a JsValue),
44    Call2(&'a JsValue, &'a JsValue),
45}
46
47struct SendWasmFuture<F> {
48    inner: F,
49}
50
51// The browser signer is only compiled for wasm. JS promises are driven by the
52// browser event loop, and the wrapped future must not be polled concurrently.
53unsafe impl<F> Send for SendWasmFuture<F> {}
54
55impl<F> Future for SendWasmFuture<F>
56where
57    F: Future,
58{
59    type Output = F::Output;
60
61    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
62        // We only project the pinned field; the inner future is never moved.
63        unsafe { self.map_unchecked_mut(|this| &mut this.inner) }.poll(cx)
64    }
65}
66
67fn boxed_send<'a, F>(future: F) -> Pin<Box<dyn Future<Output = F::Output> + Send + 'a>>
68where
69    F: Future + 'a,
70{
71    Box::pin(SendWasmFuture { inner: future })
72}
73
74/// Signer for interaction with browser extensions (ex. Alby)
75///
76/// Browser extensions: <https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions>
77///
78/// <https://github.com/nostr-protocol/nips/blob/master/07.md>
79#[derive(Debug, Clone)]
80pub struct BrowserSigner {
81    /// `window.nostr` object
82    nostr_obj: Object,
83}
84
85unsafe impl Send for BrowserSigner {}
86
87unsafe impl Sync for BrowserSigner {}
88
89impl BrowserSigner {
90    /// Compose new NIP07 Signer
91    pub fn new() -> Result<Self, Error> {
92        let window: Window = web_sys::window().ok_or_else(Error::no_global_window_object)?;
93        let namespace: JsValue = Reflect::get(&window, &JsValue::from_str("nostr"))
94            .map_err(|_| Error::namespace_not_found("nostr"))?;
95        let nostr_obj: Object = namespace
96            .dyn_into()
97            .map_err(|_| Error::namespace_not_found("nostr"))?;
98        Ok(Self { nostr_obj })
99    }
100
101    fn get_func(&self, obj: &Object, name: &str) -> Result<Function, Error> {
102        let val: JsValue = Reflect::get(obj, &JsValue::from_str(name))
103            .map_err(|_| Error::namespace_not_found(name))?;
104        val.dyn_into().map_err(|_| Error::namespace_not_found(name))
105    }
106
107    fn get_sub_obj(&self, super_obj: &Object, name: &str) -> Result<Object, Error> {
108        let namespace: JsValue = Reflect::get(super_obj, &JsValue::from_str(name))
109            .map_err(|_| Error::namespace_not_found(name))?;
110        namespace
111            .dyn_into()
112            .map_err(|_| Error::namespace_not_found(name))
113    }
114
115    /// Get value from object key
116    #[inline]
117    fn get_value_by_key(&self, obj: &Object, key: &str) -> Result<JsValue, Error> {
118        Reflect::get(obj, &JsValue::from_str(key)).map_err(|_| Error::object_key_not_found(key))
119    }
120
121    async fn call_func<T>(&self, obj: &Object, name: &str, args: CallFunc<'_>) -> Result<T, Error>
122    where
123        T: JsCast,
124    {
125        let func: Function = self.get_func(obj, name)?;
126        let temp: JsValue = match args {
127            CallFunc::Call0 => func.call0(obj)?,
128            CallFunc::Call1(arg) => func.call1(obj, arg)?,
129            CallFunc::Call2(arg1, arg2) => func.call2(obj, arg1, arg2)?,
130        };
131        let promise: Promise = Promise::resolve(&temp);
132        let result: JsValue = JsFuture::from(promise).await?;
133
134        result.dyn_into().map_err(|_| Error::type_mismatch())
135    }
136
137    /// Get Public Key
138    async fn _get_public_key(&self) -> Result<PublicKey, Error> {
139        let public_key: JsString = self
140            .call_func(&self.nostr_obj, GET_PUBLIC_KEY, CallFunc::Call0)
141            .await?;
142        let public_key: String = public_key.into();
143        Ok(PublicKey::from_hex(&public_key)?)
144    }
145
146    async fn _sign_event(&self, unsigned: UnsignedEvent) -> Result<Event, Error> {
147        let unsigned_obj = Object::new();
148
149        let tags: Array = unsigned
150            .tags
151            .iter()
152            .map(|t| {
153                t.as_slice()
154                    .iter()
155                    .map(|v| JsValue::from_str(v))
156                    .collect::<Array>()
157            })
158            .collect();
159
160        if let Some(id) = unsigned.id {
161            Reflect::set(&unsigned_obj, &JsValue::from_str("id"), &id.to_hex().into())?;
162        }
163
164        Reflect::set(
165            &unsigned_obj,
166            &JsValue::from_str("pubkey"),
167            &unsigned.pubkey.to_hex().into(),
168        )?;
169
170        Reflect::set(
171            &unsigned_obj,
172            &JsValue::from_str("created_at"),
173            &(unsigned.created_at.as_secs() as f64).into(),
174        )?;
175
176        Reflect::set(
177            &unsigned_obj,
178            &JsValue::from_str("kind"),
179            &(unsigned.kind.as_u16() as f64).into(),
180        )?;
181
182        Reflect::set(&unsigned_obj, &JsValue::from_str("tags"), &tags.into())?;
183
184        Reflect::set(
185            &unsigned_obj,
186            &JsValue::from_str("content"),
187            &unsigned.content.as_str().into(),
188        )?;
189
190        let event_obj: Object = self
191            .call_func(&self.nostr_obj, SIGN_EVENT, CallFunc::Call1(&unsigned_obj))
192            .await?;
193
194        // Extract signature from event object
195        let sig: String = self
196            .get_value_by_key(&event_obj, "sig")?
197            .as_string()
198            .ok_or_else(Error::type_mismatch)?;
199        let sig: Signature = Signature::from_str(&sig)?;
200
201        // Add signature
202        Ok(unsigned.add_signature(sig)?)
203    }
204
205    async fn encryption_decryption(
206        &self,
207        namespace: &str,
208        func_name: &str,
209        public_key: &PublicKey,
210        content: &str,
211    ) -> Result<String, Error> {
212        let sub_obj: Object = self.get_sub_obj(&self.nostr_obj, namespace)?;
213        let pk: JsValue = JsValue::from_str(&public_key.to_hex());
214        let content: JsValue = JsValue::from_str(content);
215        let result: JsString = self
216            .call_func(&sub_obj, func_name, CallFunc::Call2(&pk, &content))
217            .await?;
218        Ok(result.into())
219    }
220}
221
222impl AsyncGetPublicKey for BrowserSigner {
223    type Error = Error;
224
225    #[inline]
226    fn get_public_key_async(
227        &self,
228    ) -> Pin<Box<dyn Future<Output = Result<PublicKey, Self::Error>> + Send + '_>> {
229        boxed_send(async move { self._get_public_key().await })
230    }
231}
232
233impl AsyncSignEvent for BrowserSigner {
234    type Error = Error;
235
236    #[inline]
237    fn sign_event_async(
238        &self,
239        unsigned: UnsignedEvent,
240    ) -> Pin<Box<dyn Future<Output = Result<Event, Self::Error>> + Send + '_>> {
241        boxed_send(async move { self._sign_event(unsigned).await })
242    }
243}
244
245impl AsyncNip04 for BrowserSigner {
246    type Error = Error;
247
248    fn nip04_encrypt_async<'a>(
249        &'a self,
250        public_key: &'a PublicKey,
251        content: &'a str,
252    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
253        boxed_send(async move {
254            self.encryption_decryption(NIP04, ENCRYPT, public_key, content)
255                .await
256        })
257    }
258
259    fn nip04_decrypt_async<'a>(
260        &'a self,
261        public_key: &'a PublicKey,
262        encrypted_content: &'a str,
263    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
264        boxed_send(async move {
265            self.encryption_decryption(NIP04, DECRYPT, public_key, encrypted_content)
266                .await
267        })
268    }
269}
270
271impl AsyncNip44 for BrowserSigner {
272    type Error = Error;
273
274    fn nip44_encrypt_async<'a>(
275        &'a self,
276        public_key: &'a PublicKey,
277        content: &'a str,
278    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
279        boxed_send(async move {
280            self.encryption_decryption(NIP44, ENCRYPT, public_key, content)
281                .await
282        })
283    }
284
285    fn nip44_decrypt_async<'a>(
286        &'a self,
287        public_key: &'a PublicKey,
288        payload: &'a str,
289    ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
290        boxed_send(async move {
291            self.encryption_decryption(NIP44, DECRYPT, public_key, payload)
292                .await
293        })
294    }
295}