use crate::{hash, settings::Settings, solana::SolPubkey, time::get_current_time, with_settings};
use candid::CandidType;
use ic_certified_map::Hash;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt};
use time::{macros::format_description, OffsetDateTime};
#[derive(Debug)]
pub enum SiwsMessageError {
MessageNotFound,
}
impl fmt::Display for SiwsMessageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SiwsMessageError::MessageNotFound => write!(f, "Message not found"),
}
}
}
impl From<SiwsMessageError> for String {
fn from(error: SiwsMessageError) -> Self {
error.to_string()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, CandidType)]
pub struct SiwsMessage {
pub domain: String,
pub address: String,
pub statement: String,
pub uri: String,
pub version: u32,
pub chain_id: String,
pub nonce: String,
pub issued_at: u64,
pub expiration_time: u64,
}
impl SiwsMessage {
pub fn new(pubkey: &SolPubkey, nonce: &str) -> SiwsMessage {
let current_time = get_current_time();
with_settings!(|settings: &Settings| {
SiwsMessage {
domain: settings.domain.clone(),
address: pubkey.to_string(),
statement: settings.statement.clone(),
uri: settings.uri.clone(),
version: 1,
chain_id: settings.chain_id.clone(),
nonce: nonce.to_string(),
issued_at: get_current_time(),
expiration_time: current_time.saturating_add(settings.sign_in_expires_in),
}
})
}
pub fn is_expired(&self) -> bool {
let current_time = get_current_time();
self.issued_at < current_time || current_time > self.expiration_time
}
}
impl fmt::Display for SiwsMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
write!(f, "{}", json)
}
}
impl From<SiwsMessage> for String {
fn from(val: SiwsMessage) -> Self {
let js_iso_format = format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
);
let issued_at_datetime =
OffsetDateTime::from_unix_timestamp_nanos(val.issued_at as i128).unwrap();
let issued_at_iso_8601 = issued_at_datetime.format(&js_iso_format).unwrap();
let expiration_datetime =
OffsetDateTime::from_unix_timestamp_nanos(val.expiration_time as i128).unwrap();
let expiration_iso_8601 = expiration_datetime.format(&js_iso_format).unwrap();
format!(
"{domain} wants you to sign in with your Solana account:\n\
{address}\n\
\n\
{statement}\n\
\n\
URI: {uri}\n\
Version: {version}\n\
Chain ID: {chain_id}\n\
Nonce: {nonce}\n\
Issued At: {issued_at_iso_8601}\n\
Expiration Time: {expiration_iso_8601}",
domain = val.domain,
address = val.address,
statement = val.statement,
uri = val.uri,
version = val.version,
chain_id = val.chain_id,
nonce = val.nonce,
)
}
}
pub fn siws_message_map_hash(pubkey: &SolPubkey, nonce: &str) -> Hash {
let mut bytes: Vec<u8> = vec![];
let pubkey_bytes = pubkey.to_bytes();
bytes.push(pubkey_bytes.len() as u8);
bytes.extend(pubkey_bytes);
let nonce_bytes = nonce.as_bytes();
bytes.push(nonce_bytes.len() as u8);
bytes.extend(nonce_bytes);
hash::hash_bytes(bytes)
}
pub struct SiwsMessageMap {
map: HashMap<[u8; 32], SiwsMessage>,
}
impl SiwsMessageMap {
pub fn new() -> SiwsMessageMap {
SiwsMessageMap {
map: HashMap::new(),
}
}
pub fn prune_expired(&mut self) {
let current_time = get_current_time();
self.map
.retain(|_, message| message.expiration_time > current_time);
}
pub fn insert(&mut self, pubkey: &SolPubkey, message: SiwsMessage, nonce: &str) {
let hash = siws_message_map_hash(pubkey, nonce);
self.map.insert(hash, message);
}
pub fn get(&self, pubkey: &SolPubkey, nonce: &str) -> Result<SiwsMessage, SiwsMessageError> {
let hash = siws_message_map_hash(pubkey, nonce);
self.map
.get(&hash)
.cloned()
.ok_or(SiwsMessageError::MessageNotFound)
}
pub fn remove(&mut self, pubkey: &SolPubkey, nonce: &str) {
let hash = siws_message_map_hash(pubkey, nonce);
self.map.remove(&hash);
}
}
impl Default for SiwsMessageMap {
fn default() -> Self {
Self::new()
}
}