Skip to main content

wallet_standard_base/
sign_in.rs

1use core::{fmt::Debug, hash::Hash};
2use std::{
3    borrow::Cow,
4    time::{Duration, SystemTime},
5};
6
7use crate::{BaseUtils, Cluster, RandomBytes, WalletAccount, WalletBaseError, WalletBaseResult};
8
9/// The Sign In input used as parameters when performing
10/// `SignInWithSolana (SIWS)` requests as defined by the
11/// [SIWS](https://github.com/phantom/sign-in-with-solana) standard.
12/// A backup fork can be found at [https://github.com/JamiiDao/sign-in-with-solana](https://github.com/JamiiDao/sign-in-with-solana)
13#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct SignInInput<'wa> {
15    /// Optional EIP-4361 domain requesting the sign-in.
16    /// If not provided, the wallet must determine the domain to include in the message.
17    domain: Option<Cow<'wa, str>>,
18    /// Optional Solana Base58 address performing the sign-in.
19    /// The address is case-sensitive.
20    /// If not provided, the wallet must determine the Address to include in the message.
21    address: Option<Cow<'wa, str>>,
22    /// Optional EIP-4361 Statement.
23    /// The statement is a human readable string and should not have new-line characters (\n).
24    /// If not provided, the wallet does not include Statement in the message.
25    statement: Option<Cow<'wa, str>>,
26    /// Optional EIP-4361 URI.
27    /// The URL that is requesting the sign-in.
28    /// If not provided, the wallet does not include URI in the message.
29    uri: Option<Cow<'wa, str>>,
30    /// Optional EIP-4361 version.
31    /// If not provided, the wallet does not include Version in the message.
32    version: Option<Cow<'wa, str>>,
33    /// Optional EIP-4361 Chain ID.
34    /// The chainId can be one of the following:
35    /// mainnet, testnet, devnet, localnet, solana:mainnet, solana:testnet, solana:devnet.
36    /// If not provided, the wallet does not include Chain ID in the message.
37    chain_id: Option<Cow<'wa, str>>,
38    /// Optional EIP-4361 Nonce.
39    /// It should be an alphanumeric string containing a minimum of 8 characters.
40    /// If not provided, the wallet does not include Nonce in the message.
41    nonce: Option<Cow<'wa, str>>,
42    /// Optional ISO 8601 datetime string.
43    /// This represents the time at which the sign-in request was issued to the wallet.
44    /// Note: For Phantom, issuedAt has a threshold and it should be
45    /// within +- 10 minutes from the timestamp at which verification is taking place.
46    /// If not provided, the wallet does not include Issued At in the message.
47    issued_at: Option<Cow<'wa, str>>,
48    /// Optional ISO 8601 datetime string.
49    /// This represents the time at which the sign-in request should expire.
50    /// If not provided, the wallet does not include Expiration Time in the message.
51    expiration_time: Option<Cow<'wa, str>>,
52    /// Optional ISO 8601 datetime string.
53    /// This represents the time at which the sign-in request becomes valid.
54    /// If not provided, the wallet does not include Not Before in the message.
55    not_before: Option<Cow<'wa, str>>,
56    /// Optional EIP-4361 Request ID.
57    /// In addition to using nonce to avoid replay attacks,
58    /// dapps can also choose to include a unique signature in the requestId .
59    /// Once the wallet returns the signed message,
60    /// dapps can then verify this signature against the state to add an additional,
61    /// strong layer of security. If not provided, the wallet does not include Request ID in the message.
62    request_id: Option<Cow<'wa, str>>,
63    /// Optional EIP-4361 Resources.
64    /// Usually a list of references in the form of URIs that the
65    /// dapp wants the user to be aware of.
66    /// These URIs should be separated by \n-, ie,
67    /// URIs in new lines starting with the character -.
68    /// If not provided, the wallet does not include Resources in the message.
69    resources: Cow<'wa, [Cow<'wa, str>]>,
70}
71
72impl<'wa> SignInInput<'_> {
73    /// Same as `Self::default()` as it initializes [Self] with default values
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// An EIP-4361 domain requesting the sign-in.
79    /// If not provided, the wallet must determine the domain to include in the message.
80    pub fn set_domain(&mut self, domain: &str) -> &mut Self {
81        self.domain.replace(Cow::Owned(domain.to_string()));
82
83        self
84    }
85
86    /// The Base58 public key address
87    /// NOTE: Some wallets require this field or
88    /// an error `MessageResponseMismatch` is returned which is as
89    /// a result of the sent message not corresponding with the signed message
90    pub fn set_address(&'_ mut self, address: &str) -> WalletBaseResult<'_, &'_ mut Self> {
91        let mut buffer = [0u8; 32];
92        let buffer_written_len = bs58::decode(address).onto(&mut buffer).or(Err(
93            WalletBaseError::InvalidBase58Address(Cow::Owned(address.to_string())),
94        ))?;
95
96        if buffer_written_len != 32 {
97            return Err(WalletBaseError::InvalidEd25519PublicKeyLen(
98                buffer_written_len as u8,
99            ));
100        }
101
102        self.address.replace(Cow::Owned(address.to_string()));
103
104        Ok(self)
105    }
106    ///  An EIP-4361 Statement which is a human readable string and should not have new-line characters (\n).
107    /// Sets the message that is shown to the user during Sign In With Solana
108    pub fn set_statement(&mut self, statement: &str) -> &mut Self {
109        self.statement.replace(Cow::Owned(statement.to_string()));
110
111        self
112    }
113
114    /// An EIP-4361 URI is automatically set to the `window.location.href`
115    /// since if it is not the same, the wallet will ignore it and
116    /// show the user an error.
117    /// This is the URL that is requesting the sign-in.
118    pub fn set_uri(&mut self, uri: &str) -> &mut Self {
119        self.uri.replace(Cow::Owned(uri.to_string()));
120
121        self
122    }
123
124    /// An EIP-4361 version.
125    /// Sets the version
126    pub fn set_version(&mut self, version: &str) -> &mut Self {
127        self.version.replace(Cow::Owned(version.to_string()));
128
129        self
130    }
131
132    /// An EIP-4361 Chain ID.
133    /// The chainId can be one of the following:
134    /// mainnet, testnet, devnet, localnet, solana:mainnet, solana:testnet, solana:devnet.
135    pub fn set_chain_id(&mut self, cluster: impl Cluster) -> &mut Self {
136        self.chain_id
137            .replace(Cow::Owned(cluster.chain().to_string()));
138
139        self
140    }
141
142    /// An EIP-4361 Nonce which is an alphanumeric string containing a minimum of 8 characters.
143    /// This is generated from the Cryptographically Secure Random Number Generator
144    /// and the bytes converted to hex formatted string.
145    pub fn set_nonce(&mut self) -> &mut Self {
146        let random_bytes = RandomBytes::<32>::generate();
147
148        self.nonce
149            .replace(Cow::Owned(blake3::hash(random_bytes.expose()).to_string()));
150
151        self
152    }
153
154    /// An EIP-4361 Nonce which is an alphanumeric string containing a minimum of 8 characters.
155    /// This is generated from the Cryptographically Secure Random Number Generator
156    /// and the bytes converted to hex formatted string.
157    pub fn set_custom_nonce(&'_ mut self, nonce: &str) -> WalletBaseResult<'_, &'_ mut Self> {
158        let nonce_length = nonce.len();
159        if nonce_length < 8 {
160            return Err(WalletBaseError::NonceMustBeAtLeast8Characters(
161                nonce_length as u8,
162            ));
163        }
164
165        self.nonce.replace(Cow::Owned(nonce.to_string()));
166
167        Ok(self)
168    }
169
170    ///  This represents the time at which the sign-in request was issued to the wallet.
171    /// Note: For Phantom, issuedAt has a threshold and it should be within +- 10 minutes
172    /// from the timestamp at which verification is taking place.
173    /// If not provided, the wallet does not include Issued At in the message.
174    /// This also follows the ISO 8601 datetime.
175    pub fn set_issued_at(&mut self, time: SystemTime) -> &mut Self {
176        self.issued_at.replace(Cow::Owned(
177            humantime::format_rfc3339_millis(time).to_string(),
178        ));
179
180        self
181    }
182
183    /// An ergonomic method for [Self::set_expiration_time()]
184    /// where you can add milliseconds and [SystemTime] is automatically calculated for you
185    pub fn set_expiration_time_millis(
186        &'_ mut self,
187        now: SystemTime,
188        expiration_time_milliseconds: u64,
189    ) -> WalletBaseResult<'_, &'_ mut Self> {
190        let duration = Duration::from_millis(expiration_time_milliseconds);
191
192        self.set_expiry_internal(now, duration)
193    }
194
195    /// An ergonomic method for [Self::set_expiration_time()]
196    /// where you can add seconds and [SystemTime] is automatically calculated for you
197    pub fn set_expiration_time_seconds(
198        &'_ mut self,
199        now: SystemTime,
200        expiration_time_seconds: u64,
201    ) -> WalletBaseResult<'_, &'_ mut Self> {
202        let duration = Duration::from_secs(expiration_time_seconds);
203
204        self.set_expiry_internal(now, duration)
205    }
206
207    fn set_expiry_internal(
208        &'_ mut self,
209        now: SystemTime,
210        duration: Duration,
211    ) -> WalletBaseResult<'_, &'_ mut Self> {
212        let expiry_time = if let Some(issued_time) = self.issued_at.as_ref() {
213            let issued_time = humantime::parse_rfc3339(issued_time).or(Err(
214                WalletBaseError::InvalidISO8601Timestamp(issued_time.clone()),
215            ))?;
216            issued_time
217                .checked_add(duration)
218                .ok_or(WalletBaseError::SystemTimeCheckedAddOverflow)?
219        } else {
220            now
221        };
222
223        self.set_expiration_time(now, expiry_time)
224    }
225
226    /// An ISO 8601 datetime string. This represents the time at which the sign-in request should expire.
227    /// If not provided, the wallet does not include Expiration Time in the message.
228    /// Expiration time should be in future or an error will be thrown even before a request to the wallet is sent
229    pub fn set_expiration_time(
230        &'_ mut self,
231        now: SystemTime,
232        expiration_time: SystemTime,
233    ) -> WalletBaseResult<'_, &'_ mut Self> {
234        if let Some(issued_at) = self.issued_at.as_ref() {
235            let issued_at = humantime::parse_rfc3339(issued_at).or(Err(
236                WalletBaseError::InvalidISO8601Timestamp(issued_at.clone()),
237            ))?;
238
239            if issued_at > expiration_time {
240                let issued = BaseUtils::to_iso860(issued_at).to_string();
241                let expiry = BaseUtils::to_iso860(expiration_time).to_string();
242
243                return Err(WalletBaseError::ExpiryTimeEarlierThanIssuedTime {
244                    issued: issued.into(),
245                    expiry: expiry.into(),
246                });
247            }
248        }
249
250        if now > expiration_time {
251            let now = BaseUtils::to_iso860(now).to_string();
252            let expiry = BaseUtils::to_iso860(expiration_time).to_string();
253            return Err(WalletBaseError::ExpirationTimeIsInThePast {
254                now: now.into(),
255                expiry: expiry.into(),
256            });
257        }
258
259        self.expiration_time.replace(Cow::Owned(
260            humantime::format_rfc3339_millis(expiration_time).to_string(),
261        ));
262
263        Ok(self)
264    }
265
266    fn set_not_before_internal(
267        &'_ mut self,
268        now: SystemTime,
269        duration: Duration,
270    ) -> WalletBaseResult<'_, &'_ mut Self> {
271        let not_before = if let Some(issued_time) = self.issued_at.as_ref() {
272            let issued_time = humantime::parse_rfc3339(issued_time).or(Err(
273                WalletBaseError::InvalidISO8601Timestamp(issued_time.clone()),
274            ))?;
275
276            issued_time
277                .checked_add(duration)
278                .ok_or(WalletBaseError::SystemTimeCheckedAddOverflow)?
279        } else {
280            now
281        };
282
283        self.set_not_before_time(now, not_before)
284    }
285
286    /// An ergonomic method for [Self::set_not_before_time()]
287    /// where you can add milliseconds and [SystemTime] is automatically calculated for you
288    pub fn set_not_before_time_millis(
289        &'_ mut self,
290        now: SystemTime,
291        expiration_time_milliseconds: u64,
292    ) -> WalletBaseResult<'_, &'_ mut Self> {
293        let duration = Duration::from_millis(expiration_time_milliseconds);
294
295        self.set_not_before_internal(now, duration)
296    }
297
298    /// An ergonomic method for [Self::set_not_before_time()]
299    /// where you can add seconds and [SystemTime] is automatically calculated for you
300    pub fn set_not_before_time_seconds(
301        &'_ mut self,
302        now: SystemTime,
303        expiration_time_seconds: u64,
304    ) -> WalletBaseResult<'_, &'_ mut Self> {
305        let duration = Duration::from_secs(expiration_time_seconds);
306
307        self.set_not_before_internal(now, duration)
308    }
309
310    /// An ISO 8601 datetime string.
311    /// This represents the time at which the sign-in request becomes valid.
312    /// If not provided, the wallet does not include Not Before in the message.
313    /// Time must be after `IssuedTime`
314    pub fn set_not_before_time(
315        &'_ mut self,
316        now: SystemTime,
317        not_before: SystemTime,
318    ) -> WalletBaseResult<'_, &'_ mut Self> {
319        if let Some(issued_at) = self.issued_at.as_ref() {
320            let issued_at = humantime::parse_rfc3339(issued_at).or(Err(
321                WalletBaseError::InvalidISO8601Timestamp(issued_at.clone()),
322            ))?;
323
324            if issued_at > not_before {
325                let issued = BaseUtils::to_iso860(issued_at).to_string();
326                let not_before = BaseUtils::to_iso860(not_before).to_string();
327                return Err(WalletBaseError::NotBeforeTimeEarlierThanIssuedTime {
328                    issued_at: issued.into(),
329                    not_before: not_before.into(),
330                });
331            }
332        }
333
334        if now > not_before {
335            let now = BaseUtils::to_iso860(now).to_string();
336            let not_before = BaseUtils::to_iso860(not_before).to_string();
337
338            return Err(WalletBaseError::NotBeforeTimeIsInThePast {
339                now: now.into(),
340                not_before: not_before.into(),
341            });
342        }
343
344        if let Some(expiration_time) = self.expiration_time.as_ref() {
345            let expiration_time = humantime::parse_rfc3339(expiration_time).or(Err(
346                WalletBaseError::InvalidISO8601Timestamp(expiration_time.clone()),
347            ))?;
348
349            if not_before > expiration_time {
350                let expiry = BaseUtils::to_iso860(expiration_time).to_string();
351                let not_before = BaseUtils::to_iso860(not_before).to_string();
352                return Err(WalletBaseError::NotBeforeTimeLaterThanExpirationTime {
353                    not_before: not_before.into(),
354                    expiry: expiry.into(),
355                });
356            }
357        }
358
359        self.not_before.replace(Cow::Owned(
360            humantime::format_rfc3339_millis(not_before).to_string(),
361        ));
362
363        Ok(self)
364    }
365
366    /// Parses the Sign In With Solana (SIWS) result of the Response from a wallet
367    pub fn parser(input: &'wa str) -> WalletBaseResult<'wa, SignInInput<'wa>> {
368        let mut signin_input = SignInInput::default();
369
370        input
371            .split_once(" ")
372            .map(|(left, _right)| signin_input.domain.replace(left.trim().into()));
373
374        let split_colon = |value: &str| -> Option<Cow<'_, str>> {
375            value
376                .split_once(":")
377                .map(|(_left, right)| Cow::Owned(right.trim().to_string()))
378        };
379
380        let split_colon_system_time = |value: &str| -> WalletBaseResult<Option<Cow<'_, str>>> {
381            value
382                .split_once(":")
383                .map(|(_left, right)| {
384                    humantime::parse_rfc3339(right.trim()).or(Err(
385                        WalletBaseError::InvalidISO8601Timestamp(right.to_string().into()),
386                    ))?;
387                    Ok(Cow::Owned(right.to_string()))
388                })
389                .transpose()
390        };
391
392        input
393            .split("\n")
394            .enumerate()
395            .try_for_each(|(index, input)| {
396                if index == 1 {
397                    signin_input.address.replace(input.trim().into());
398                }
399
400                if index == 3 {
401                    signin_input.statement.replace(input.trim().into());
402                }
403
404                if input.contains("URI") {
405                    signin_input.uri = split_colon(input);
406                }
407
408                if input.contains("Version") {
409                    signin_input.version = split_colon(input);
410                }
411
412                if input.contains("Chain ID") {
413                    if let Some((_left, right)) = input.split_once(":") {
414                        let cluster = right.trim().into();
415
416                        signin_input.chain_id.replace(cluster);
417                    }
418                }
419                if input.contains("Nonce") {
420                    signin_input.nonce = split_colon(input);
421                }
422
423                if input.contains("Issued At") {
424                    signin_input.issued_at = split_colon_system_time(input)?;
425                }
426
427                if input.contains("Expiration") {
428                    signin_input.expiration_time = split_colon_system_time(input)?;
429                }
430
431                if input.contains("Not Before") {
432                    signin_input.not_before = split_colon_system_time(input)?;
433                }
434
435                if input.contains("Request ID") {
436                    signin_input.request_id = split_colon(input);
437                }
438
439                if input.starts_with("-") {
440                    if let Some(value) = input.split("-").nth(1) {
441                        signin_input
442                            .resources
443                            .to_mut()
444                            .push(Cow::Owned(value.trim().to_string()));
445                    }
446                }
447
448                Ok::<(), WalletBaseError>(())
449            })?;
450
451        Ok(signin_input)
452    }
453
454    /// Checks if the response of a Sign In With Solana (SIWS) from the Wallet is the same as the
455    /// request data sent to the wallet to be signed
456    pub fn check_eq(&'_ self, other: &'_ Self) -> WalletBaseResult<'_, ()> {
457        if self.eq(other) {
458            Ok(())
459        } else {
460            Err(WalletBaseError::MessageResponseMismatch)
461        }
462    }
463
464    /// An EIP-4361 Request ID.
465    /// In addition to using nonce to avoid replay attacks,
466    /// dapps can also choose to include a unique signature in the requestId .
467    /// Once the wallet returns the signed message,
468    /// dapps can then verify this signature against the state to add an additional,
469    /// strong layer of security. If not provided, the wallet must not include Request ID in the message.
470    pub fn set_request_id(&mut self, id: &str) -> &mut Self {
471        self.request_id.replace(Cow::Owned(id.into()));
472
473        self
474    }
475
476    /// An EIP-4361 Resources.
477    /// Usually a list of references in the form of URIs that the dapp wants the user to be aware of.
478    /// These URIs should be separated by \n-, ie, URIs in new lines starting with the character -.
479    /// If not provided, the wallet must not include Resources in the message.
480    pub fn add_resource(&mut self, resource: &str) -> &mut Self {
481        self.resources
482            .to_mut()
483            .push(Cow::Owned(resource.to_string()));
484
485        self
486    }
487
488    /// Helper for [Self::add_resource()] when you want to add multiple resources at the same time
489    pub fn add_resources(&mut self, resources: &[&str]) -> &mut Self {
490        resources.iter().for_each(|resource| {
491            self.resources
492                .to_mut()
493                .push(Cow::Owned(resource.to_string()))
494        });
495
496        self
497    }
498
499    /// Get the `domain` field
500    pub fn domain(&self) -> Option<&str> {
501        self.domain.as_deref()
502    }
503
504    /// Get the `address` field
505    pub fn address(&self) -> Option<&str> {
506        self.address.as_deref()
507    }
508
509    /// Get the `statement` field
510    pub fn statement(&self) -> Option<&str> {
511        self.statement.as_deref()
512    }
513
514    /// Get the `uri` field
515    pub fn uri(&self) -> Option<&str> {
516        self.uri.as_deref()
517    }
518
519    /// Get the `version` field
520    pub fn version(&self) -> Option<&str> {
521        self.version.as_deref()
522    }
523
524    /// Get the `chain_id` field
525    pub fn chain_id(&self) -> Option<&str> {
526        self.chain_id.as_deref()
527    }
528
529    /// Get the `nonce` field
530    pub fn nonce(&self) -> Option<&str> {
531        self.nonce.as_deref()
532    }
533
534    /// Get the `issued_at` field
535    pub fn issued_at(&self) -> Option<&Cow<'_, str>> {
536        self.issued_at.as_ref()
537    }
538
539    /// Get the `expiration_time` field
540    pub fn expiration_time(&self) -> Option<&Cow<'_, str>> {
541        self.expiration_time.as_ref()
542    }
543
544    /// Get the `not_before` field
545    pub fn not_before(&self) -> Option<&Cow<'_, str>> {
546        self.not_before.as_ref()
547    }
548
549    /// Get the `issued_at` field
550    pub fn issued_at_system_time(&self) -> Option<SystemTime> {
551        self.issued_at
552            .as_ref()
553            .map(|value| humantime::parse_rfc3339(value).ok())?
554    }
555
556    /// Get the `expiration_time` field
557    pub fn expiration_time_system_time(&self) -> Option<SystemTime> {
558        self.expiration_time
559            .as_ref()
560            .map(|value| humantime::parse_rfc3339(value).ok())?
561    }
562
563    /// Get the `not_before` field
564    pub fn not_before_system_time(&self) -> Option<SystemTime> {
565        self.not_before
566            .as_ref()
567            .map(|value| humantime::parse_rfc3339(value).ok())?
568    }
569
570    /// Get the `request_id` field
571    pub fn request_id(&self) -> Option<&str> {
572        self.request_id.as_deref()
573    }
574
575    /// Get the `resources` field
576    pub fn resources(&'wa self) -> &'wa [Cow<'wa, str>] {
577        &self.resources
578    }
579}
580
581/// The output of Sign In With Solana (SIWS) response from a wallet
582#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
583pub struct SignInOutput<T: WalletAccount + Debug + Clone + Hash + Ord + PartialEq + Eq + Default> {
584    /// A [An Account](WalletAccountData)
585    pub account: T,
586    /// The UTF-8 encoded message
587    pub message: String,
588    /// The signature as a  byte array of 64 bytes in length corresponding to a
589    /// [Ed25519 Signature](ed25519_dalek::Signature)
590    pub signature: [u8; 64],
591    /// The public key as a  byte array of 32 bytes in length corresponding to a
592    /// [Ed25519 Public Key](ed25519_dalek::VerifyingKey)
593    pub public_key: [u8; 32],
594}
595
596#[cfg(test)]
597#[cfg(target_arch = "wasm32")]
598mod signin_input_sanity_checks {
599    use super::*;
600
601    #[test]
602    fn set_issued_at() {
603        let mut signin_input = SigninInput::default();
604
605        assert!(signin_input.issued_at().is_none());
606
607        signin_input.set_issued_at().unwrap();
608
609        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH)
610    }
611
612    #[test]
613    fn set_expiration_time() {
614        let mut signin_input = SigninInput::default();
615
616        let now = SigninInput::time_now().unwrap();
617
618        let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
619        assert_eq!(
620            Some(WalletError::ExpirationTimeIsInThePast),
621            signin_input.set_expiration_time(past_time).err()
622        );
623
624        signin_input.set_issued_at().unwrap();
625        assert_eq!(
626            Some(WalletError::ExpiryTimeEarlierThanIssuedTime),
627            signin_input.set_expiration_time(past_time).err()
628        );
629
630        let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
631        assert!(signin_input.set_expiration_time(valid_expiry).is_ok());
632
633        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
634
635        assert!(signin_input.set_expiration_time_millis(4000).is_ok());
636        assert!(signin_input.set_expiration_time_seconds(4).is_ok());
637    }
638
639    #[test]
640    fn set_not_before_time() {
641        let mut signin_input = SigninInput::default();
642
643        let now = SigninInput::time_now().unwrap();
644
645        let past_time = now.checked_sub(Duration::from_secs(300)).unwrap();
646        assert_eq!(
647            Some(WalletError::NotBeforeTimeIsInThePast),
648            signin_input.set_not_before_time(past_time).err()
649        );
650
651        signin_input.set_issued_at().unwrap();
652        let future_time = now.checked_sub(Duration::from_secs(3000000)).unwrap();
653        assert_eq!(
654            Some(WalletError::NotBeforeTimeEarlierThanIssuedTime),
655            signin_input.set_not_before_time(future_time).err()
656        );
657
658        signin_input.set_issued_at().unwrap();
659        let future_time = SigninInput::time_now()
660            .unwrap()
661            .checked_add(Duration::from_secs(30000))
662            .unwrap();
663        signin_input.set_expiration_time(future_time).unwrap();
664        let future_time = now.checked_add(Duration::from_secs(3000000)).unwrap();
665        assert_eq!(
666            Some(WalletError::NotBeforeTimeLaterThanExpirationTime),
667            signin_input.set_not_before_time(future_time).err()
668        );
669
670        let valid_expiry = now.checked_add(Duration::from_secs(300)).unwrap();
671        assert!(signin_input.set_not_before_time(valid_expiry).is_ok());
672
673        assert!(signin_input.issued_at.unwrap() > SystemTime::UNIX_EPOCH);
674
675        assert!(signin_input.set_not_before_time_millis(4000).is_ok());
676        assert!(signin_input.set_not_before_time_seconds(4).is_ok());
677    }
678}