use serde::{Deserialize, Serialize};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
pub const ETH_KEY: &str = "cyberia.eth.v1";
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct EthWallet {
pub address: String,
#[serde(default)]
pub chain_id: String,
#[serde(default)]
pub connected_ms: f64,
}
impl EthWallet {
pub fn short(&self) -> String {
let a = &self.address;
if a.len() > 12 {
format!("{}โฆ{}", &a[..6], &a[a.len() - 4..])
} else {
a.clone()
}
}
pub fn chain_name(&self) -> &'static str {
match self.chain_id.as_str() {
"0x1" => "Ethereum",
"0x2105" => "Base",
"0xaa36a7" => "Sepolia",
"0x89" => "Polygon",
"0xa4b1" => "Arbitrum",
"0xa" => "Optimism",
_ => "chain",
}
}
}
fn storage() -> Option<web_sys::Storage> {
web_sys::window().and_then(|w| w.local_storage().ok().flatten())
}
pub fn load_eth() -> Option<EthWallet> {
storage()
.and_then(|ls| ls.get_item(ETH_KEY).ok().flatten())
.and_then(|s| serde_json::from_str(&s).ok())
.filter(|w: &EthWallet| !w.address.is_empty())
}
pub fn save_eth(w: &EthWallet) {
if let (Some(ls), Ok(s)) = (storage(), serde_json::to_string(w)) {
let _ = ls.set_item(ETH_KEY, &s);
}
}
pub fn forget_eth() {
if let Some(ls) = storage() {
let _ = ls.remove_item(ETH_KEY);
}
}
fn provider() -> Option<JsValue> {
let win = web_sys::window()?;
let eth = js_sys::Reflect::get(&win, &JsValue::from_str("ethereum")).ok()?;
(!eth.is_undefined() && !eth.is_null()).then_some(eth)
}
pub fn has_provider() -> bool {
provider().is_some()
}
async fn request(eth: &JsValue, method: &str) -> Result<JsValue, String> {
let f: js_sys::Function = js_sys::Reflect::get(eth, &JsValue::from_str("request"))
.map_err(|_| "the provider has no request()")?
.dyn_into()
.map_err(|_| "the provider's request is not a function")?;
let args = js_sys::Object::new();
js_sys::Reflect::set(&args, &JsValue::from_str("method"), &JsValue::from_str(method))
.map_err(|_| "could not build the request")?;
let p: js_sys::Promise = f
.call1(eth, &args)
.map_err(|e| js_error(&e))?
.dyn_into()
.map_err(|_| "the provider returned no promise")?;
JsFuture::from(p).await.map_err(|e| js_error(&e))
}
fn js_error(e: &JsValue) -> String {
js_sys::Reflect::get(e, &JsValue::from_str("message"))
.ok()
.and_then(|m| m.as_string())
.unwrap_or_else(|| "the wallet declined".to_string())
}
pub async fn connect() -> Result<EthWallet, String> {
let eth = provider().ok_or("no wallet in this browser โ install MetaMask or open cyberia.my inside a wallet app")?;
let accounts = request(ð, "eth_requestAccounts").await?;
let address = js_sys::Array::from(&accounts)
.get(0)
.as_string()
.ok_or("the wallet gave no account")?;
let chain_id = request(ð, "eth_chainId")
.await
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
let w = EthWallet {
address,
chain_id,
connected_ms: js_sys::Date::now(),
};
save_eth(&w);
Ok(w)
}