wallet_adapter/wallet_ser_der/
signin_standard.rs

1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3use crate::{Reflection, WalletError, WalletResult};
4
5use wallet_adapter_common::{clusters::Cluster, signin_standard::SigninInput as SigninInputLib};
6use web_sys::{
7    js_sys::{self, Array},
8    wasm_bindgen::JsValue,
9    Window,
10};
11
12/// The Sign In input used as parameters when performing
13/// `SignInWithSolana (SIWS)` requests as defined by the
14/// [SIWS](https://github.com/phantom/sign-in-with-solana) standard.
15/// A backup fork can be found at [https://github.com/JamiiDao/sign-in-with-solana](https://github.com/JamiiDao/sign-in-with-solana)
16#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct SigninInput(pub(crate) SigninInputLib);
18
19impl SigninInput {
20    /// Same as `Self::default()` as it initializes [Self] with default values
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// An EIP-4361 domain requesting the sign-in.
26    /// If not provided, the wallet must determine the domain to include in the message.
27    /// Sets the domain name by fetching the details from [window.location().host()](web_sys::Location) .
28    pub fn set_domain(&mut self, window: &Window) -> WalletResult<&mut Self> {
29        let host = window.location().host()?;
30
31        self.0.set_domain(&host);
32
33        Ok(self)
34    }
35
36    /// An EIP-4361 domain requesting the sign-in.
37    /// If not provided, the wallet must determine the domain to include in the message.
38    /// Sets a custom domain name instead of fetching from
39    /// [window.location().host()](web_sys::Location)
40    pub fn set_custom_domain(&mut self, domain: &str) -> &mut Self {
41        self.0.set_domain(domain);
42
43        self
44    }
45
46    /// The Base58 public key address
47    /// NOTE: Some wallets require this field or
48    /// an error `MessageResponseMismatch` which is as
49    /// a result of the sent message not corresponding with the signed message
50    pub fn set_address(&mut self, address: &str) -> WalletResult<&mut Self> {
51        self.0.set_address(address)?;
52
53        Ok(self)
54    }
55
56    ///  An EIP-4361 Statement which is a human readable string and should not have new-line characters (\n).
57    /// Sets the message that is shown to the user during Sign In With Solana
58    pub fn set_statement(&mut self, statement: &str) -> &mut Self {
59        self.0.set_statement(statement);
60
61        self
62    }
63
64    /// An EIP-4361 URI is automatically set to the `window.location.href`
65    /// since if it is not the same, the wallet will ignore it and
66    /// show the user an error.
67    /// This is the URL that is requesting the sign-in.
68    pub fn set_uri(&mut self, window: &Window) -> WalletResult<&mut Self> {
69        self.0.set_uri(&window.location().href()?);
70
71        Ok(self)
72    }
73
74    /// An EIP-4361 version.
75    /// Sets the version
76    pub fn set_version(&mut self, version: &str) -> &mut Self {
77        self.0.set_version(version);
78
79        self
80    }
81
82    /// An EIP-4361 Chain ID.
83    /// The chainId can be one of the following:
84    /// mainnet, testnet, devnet, localnet, solana:mainnet, solana:testnet, solana:devnet.
85    pub fn set_chain_id(&mut self, cluster: Cluster) -> &mut Self {
86        self.0.set_chain_id(cluster);
87
88        self
89    }
90
91    /// An EIP-4361 Nonce which is an alphanumeric string containing a minimum of 8 characters.
92    /// This is generated from the Cryptographically Secure Random Number Generator
93    /// and the bytes converted to hex formatted string.
94    pub fn set_nonce(&mut self) -> &mut Self {
95        self.0.set_nonce();
96        self
97    }
98
99    ///  An EIP-4361 Nonce which is an alphanumeric string containing a minimum of 8 characters.
100    /// This is generated from the Cryptographically Secure Random Number Generator
101    /// and the bytes converted to hex formatted string.
102    pub fn custom_nonce(&mut self, nonce: &str) -> WalletResult<&mut Self> {
103        self.0.set_custom_nonce(nonce)?;
104
105        Ok(self)
106    }
107
108    /// Fetches the time from [JavaScript Date Now](js_sys::Date::now()) .
109    /// This is converted to [SystemTime]
110    pub fn time_now() -> WalletResult<SystemTime> {
111        let date_now = js_sys::Date::now() as u64;
112
113        UNIX_EPOCH
114            .checked_add(Duration::from_millis(date_now))
115            .ok_or(WalletError::JsError {
116                name: "UNIX_EPOCH.checked_add(js_sys::Date::now()".to_string(),
117                message: "Unable to get the current time".to_string(),
118                stack: "INTERNAL ERROR".to_string(),
119            })
120    }
121
122    ///  This represents the time at which the sign-in request was issued to the wallet.
123    /// Note: For Phantom, issuedAt has a threshold and it should be within +- 10 minutes
124    /// from the timestamp at which verification is taking place.
125    /// If not provided, the wallet does not include Issued At in the message.
126    /// This also follows the ISO 8601 datetime.
127    pub fn set_issued_at(&mut self) -> WalletResult<&mut Self> {
128        self.0.set_issued_at(Self::time_now()?);
129
130        Ok(self)
131    }
132
133    /// An ergonomic method for [Self::set_expiration_time()]
134    /// where you can add milliseconds and [SystemTime] is automatically calculated for you
135    pub fn set_expiration_time_millis(
136        &mut self,
137        expiration_time_milliseconds: u64,
138    ) -> WalletResult<&mut Self> {
139        self.0
140            .set_expiration_time_millis(Self::time_now()?, expiration_time_milliseconds)?;
141
142        Ok(self)
143    }
144
145    /// An ergonomic method for [Self::set_expiration_time()]
146    /// where you can add seconds and [SystemTime] is automatically calculated for you
147    pub fn set_expiration_time_seconds(
148        &mut self,
149        expiration_time_seconds: u64,
150    ) -> WalletResult<&mut Self> {
151        self.0
152            .set_expiration_time_seconds(Self::time_now()?, expiration_time_seconds)?;
153
154        Ok(self)
155    }
156
157    /// An ISO 8601 datetime string. This represents the time at which the sign-in request should expire.
158    /// If not provided, the wallet does not include Expiration Time in the message.
159    /// Expiration time should be in future or an error will be thrown even before a request to the wallet is sent
160    pub fn set_expiration_time(&mut self, expiration_time: SystemTime) -> WalletResult<&mut Self> {
161        if let Some(issued_at) = self.0.issued_at() {
162            if issued_at > &expiration_time {
163                return Err(WalletError::ExpiryTimeEarlierThanIssuedTime);
164            }
165        }
166
167        let now = Self::time_now()?;
168
169        if now > expiration_time {
170            return Err(WalletError::ExpirationTimeIsInThePast);
171        }
172
173        self.0.set_expiration_time(now, expiration_time)?;
174
175        Ok(self)
176    }
177
178    /// An ergonomic method for [Self::set_not_before_time()]
179    /// where you can add milliseconds and [SystemTime] is automatically calculated for you
180    pub fn set_not_before_time_millis(
181        &mut self,
182        expiration_time_milliseconds: u64,
183    ) -> WalletResult<&mut Self> {
184        self.0
185            .set_not_before_time_millis(Self::time_now()?, expiration_time_milliseconds)?;
186
187        Ok(self)
188    }
189
190    /// An ergonomic method for [Self::set_not_before_time()]
191    /// where you can add seconds and [SystemTime] is automatically calculated for you
192    pub fn set_not_before_time_seconds(
193        &mut self,
194        expiration_time_seconds: u64,
195    ) -> WalletResult<&mut Self> {
196        self.0
197            .set_not_before_time_seconds(Self::time_now()?, expiration_time_seconds)?;
198
199        Ok(self)
200    }
201
202    /// An ISO 8601 datetime string.
203    /// This represents the time at which the sign-in request becomes valid.
204    /// If not provided, the wallet does not include Not Before in the message.
205    /// Time must be after `IssuedTime`
206    pub fn set_not_before_time(&mut self, not_before: SystemTime) -> WalletResult<&mut Self> {
207        self.0.set_not_before_time(Self::time_now()?, not_before)?;
208
209        Ok(self)
210    }
211
212    /// Converts [Self] to a [JsValue] to pass to the wallet where it's internal representation
213    /// is a [js_sys::Object]
214    pub fn get_object(&self) -> WalletResult<JsValue> {
215        let mut signin_input_object = Reflection::new_object();
216
217        signin_input_object.set_object_string_optional("domain", self.0.domain())?;
218        signin_input_object.set_object_string_optional("address", self.0.address())?;
219        signin_input_object.set_object_string_optional("statement", self.0.statement())?;
220        signin_input_object.set_object_string_optional("uri", self.0.uri())?;
221        signin_input_object.set_object_string_optional("version", self.0.version())?;
222        signin_input_object.set_object_string_optional("address", self.0.address())?;
223        signin_input_object.set_object_string_optional(
224            "chainId",
225            self.0
226                .chain_id()
227                .map(|cluster| cluster.chain().to_string())
228                .as_ref(),
229        )?;
230        signin_input_object.set_object_string_optional("nonce", self.0.nonce())?;
231        signin_input_object
232            .set_object_string_optional("issuedAt", self.issued_at_iso8601().as_ref())?;
233        signin_input_object.set_object_string_optional(
234            "expirationTime",
235            self.expiration_time_iso8601().as_ref(),
236        )?;
237        signin_input_object
238            .set_object_string_optional("notBefore", self.not_before_iso8601().as_ref())?;
239        signin_input_object.set_object_string_optional("requestId", self.0.request_id())?;
240
241        if !self.0.resources().is_empty() {
242            let stringify_resources = Array::new();
243            self.0.resources().iter().for_each(|resource| {
244                stringify_resources.push(&resource.into());
245            });
246            signin_input_object.set_object(&"resources".into(), &stringify_resources)?;
247        }
248
249        Ok(signin_input_object.take())
250    }
251
252    /// An EIP-4361 Request ID.
253    /// In addition to using nonce to avoid replay attacks,
254    /// dapps can also choose to include a unique signature in the requestId .
255    /// Once the wallet returns the signed message,
256    /// dapps can then verify this signature against the state to add an additional,
257    /// strong layer of security. If not provided, the wallet must not include Request ID in the message.
258    pub fn set_request_id(&mut self, id: &str) -> &mut Self {
259        self.0.set_request_id(id);
260
261        self
262    }
263
264    /// An EIP-4361 Resources.
265    /// Usually a list of references in the form of URIs that the dapp wants the user to be aware of.
266    /// These URIs should be separated by \n-, ie, URIs in new lines starting with the character -.
267    /// If not provided, the wallet must not include Resources in the message.
268    pub fn add_resource(&mut self, resource: &str) -> &mut Self {
269        self.0.add_resource(resource);
270
271        self
272    }
273
274    /// Helper for [Self::add_resource()] when you want to add multiple resources at the same time
275    pub fn add_resources(&mut self, resources: &[&str]) -> &mut Self {
276        self.0.add_resources(resources);
277
278        self
279    }
280
281    /// Get the `domain` field
282    pub fn domain(&self) -> Option<&String> {
283        self.0.domain()
284    }
285
286    /// Get the `address` field
287    pub fn address(&self) -> Option<&String> {
288        self.0.address()
289    }
290
291    /// Get the `statement` field
292    pub fn statement(&self) -> Option<&String> {
293        self.0.statement()
294    }
295
296    /// Get the `uri` field
297    pub fn uri(&self) -> Option<&String> {
298        self.0.uri()
299    }
300
301    /// Get the `version` field
302    pub fn version(&self) -> Option<&String> {
303        self.0.version()
304    }
305
306    /// Get the `chain_id` field
307    pub fn chain_id(&self) -> Option<&Cluster> {
308        self.0.chain_id()
309    }
310
311    /// Get the `nonce` field
312    pub fn nonce(&self) -> Option<&String> {
313        self.0.nonce()
314    }
315
316    /// Get the `issued_at` field
317    pub fn issued_at(&self) -> Option<&SystemTime> {
318        self.0.issued_at()
319    }
320
321    /// Get the `expiration_time` field
322    pub fn expiration_time(&self) -> Option<&SystemTime> {
323        self.0.expiration_time()
324    }
325
326    /// Get the `not_before` field
327    pub fn not_before(&self) -> Option<&SystemTime> {
328        self.0.not_before()
329    }
330
331    /// Get the `issued_at` field as ISO8601 date time string
332    pub fn issued_at_iso8601(&self) -> Option<String> {
333        self.0.issued_at_iso8601()
334    }
335
336    /// Get the `expiration_time` field as ISO8601 date time string
337    pub fn expiration_time_iso8601(&self) -> Option<String> {
338        self.0.expiration_time_iso8601()
339    }
340
341    /// Get the `not_before` field as ISO8601 date time string
342    pub fn not_before_iso8601(&self) -> Option<String> {
343        self.0.not_before_iso8601()
344    }
345
346    /// Get the `request_id` field
347    pub fn request_id(&self) -> Option<&String> {
348        self.0.request_id()
349    }
350
351    /// Get the `resources` field
352    pub fn resources(&self) -> &[String] {
353        self.0.resources()
354    }
355}
356
357#[cfg(test)]
358#[cfg(target_arch = "wasm32")]
359mod signin_input_sanity_checks {
360    use super::*;
361
362    #[test]
363    fn set_issued_at() {
364        let mut signin_input = SigninInput::default();
365
366        assert!(signin_input.issued_at().is_none());
367
368        signin_input.set_issued_at().unwrap();
369
370        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH)
371    }
372
373    #[test]
374    fn set_expiration_time() {
375        let mut signin_input = SigninInput::default();
376
377        let now = SigninInput::time_now().unwrap();
378
379        let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
380        assert_eq!(
381            Some(WalletError::ExpirationTimeIsInThePast),
382            signin_input.set_expiration_time(past_time).err()
383        );
384
385        signin_input.set_issued_at().unwrap();
386        assert_eq!(
387            Some(WalletError::ExpiryTimeEarlierThanIssuedTime),
388            signin_input.set_expiration_time(past_time).err()
389        );
390
391        let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
392        assert!(signin_input.set_expiration_time(valid_expiry).is_ok());
393
394        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
395
396        assert!(signin_input.set_expiration_time_millis(4000).is_ok());
397        assert!(signin_input.set_expiration_time_seconds(4).is_ok());
398    }
399
400    #[test]
401    fn set_not_before_time() {
402        let mut signin_input = SigninInput::default();
403
404        let now = SigninInput::time_now().unwrap();
405
406        let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
407        assert_eq!(
408            Some(WalletError::NotBeforeTimeIsInThePast),
409            signin_input.set_not_before_time(past_time).err()
410        );
411
412        signin_input.set_issued_at().unwrap();
413        let future_time = now.checked_sub(Duration::from_secs(3000000)).unwrap();
414        assert_eq!(
415            Some(WalletError::NotBeforeTimeEarlierThanIssuedTime),
416            signin_input.set_not_before_time(future_time).err()
417        );
418
419        signin_input.set_issued_at().unwrap();
420        let future_time = SigninInput::time_now()
421            .unwrap()
422            .checked_add(Duration::from_secs(30000))
423            .unwrap();
424        signin_input.set_expiration_time(future_time).unwrap();
425        let future_time = now.checked_add(Duration::from_secs(3000000)).unwrap();
426        assert_eq!(
427            Some(WalletError::NotBeforeTimeLaterThanExpirationTime),
428            signin_input.set_not_before_time(future_time).err()
429        );
430
431        let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
432        assert!(signin_input.set_not_before_time(valid_expiry).is_ok());
433
434        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
435
436        assert!(signin_input.set_not_before_time_millis(4000).is_ok());
437        assert!(signin_input.set_not_before_time_seconds(4).is_ok());
438    }
439}