use sha3::{Digest, Keccak256};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::MessageEvent;
use crate::wallet;
const DOMAIN_TAG: &[u8] = b"localharness-auth-v0:";
pub(crate) fn install_signer_listener() -> Result<(), JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
let handler = Closure::<dyn FnMut(_)>::new(move |event: MessageEvent| {
if let Err(err) = handle_message(&event) {
web_sys::console::warn_1(&JsValue::from_str(&format!("signer: {err}")));
}
});
window.add_event_listener_with_callback("message", handler.as_ref().unchecked_ref())?;
handler.forget();
Ok(())
}
fn handle_message(event: &MessageEvent) -> Result<(), String> {
let data = event.data();
if data.is_null() || data.is_undefined() {
return Ok(());
}
let msg_type = js_sys::Reflect::get(&data, &JsValue::from_str("type"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
if !matches!(
msg_type.as_str(),
"lh-sign-challenge"
| "lh-sign-digest"
| "lh-create-wallet"
| "lh-reveal-seed"
| "lh-import-seed"
| "lh-claim-name"
) {
return Ok(());
}
let origin = event.origin();
if !is_trusted_origin(&origin) {
return Err(format!("untrusted origin: {origin}"));
}
let id = js_sys::Reflect::get(&data, &JsValue::from_str("id"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
let source = event
.source()
.ok_or_else(|| "no source window on the message event".to_string())?;
let source_jsval: JsValue = source.into();
let reply = match msg_type.as_str() {
"lh-sign-challenge" => {
let nonce_hex = js_sys::Reflect::get(&data, &JsValue::from_str("nonce"))
.ok()
.and_then(|v| v.as_string())
.ok_or_else(|| "nonce not a string".to_string())?;
match build_challenge_response(&id, &nonce_hex) {
Ok(obj) => obj,
Err(err) => error_response(&id, &err),
}
}
"lh-sign-digest" => {
let purpose = js_sys::Reflect::get(&data, &JsValue::from_str("purpose"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_else(|| "sign digest".into());
let digest_hex = js_sys::Reflect::get(&data, &JsValue::from_str("digest"))
.ok()
.and_then(|v| v.as_string())
.ok_or_else(|| "digest not a string".to_string())?;
match build_digest_response(&id, &digest_hex, &purpose) {
Ok(obj) => obj,
Err(err) => error_response(&id, &err),
}
}
"lh-reveal-seed" => match build_reveal_seed_response(&id) {
Ok(obj) => obj,
Err(err) => error_response(&id, &err),
},
"lh-create-wallet" => {
let overwrite = js_sys::Reflect::get(&data, &JsValue::from_str("overwrite"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
spawn_create_wallet(
id.clone(),
overwrite,
source_jsval.clone(),
origin.clone(),
);
return Ok(());
}
"lh-import-seed" => {
let phrase = js_sys::Reflect::get(&data, &JsValue::from_str("phrase"))
.ok()
.and_then(|v| v.as_string())
.ok_or_else(|| "phrase not a string".to_string())?;
spawn_import_seed(id.clone(), phrase, source_jsval.clone(), origin.clone());
return Ok(());
}
"lh-claim-name" => {
let name = js_sys::Reflect::get(&data, &JsValue::from_str("name"))
.ok()
.and_then(|v| v.as_string())
.ok_or_else(|| "name not a string".to_string())?;
spawn_claim_name(id.clone(), name, source_jsval.clone(), origin.clone());
return Ok(());
}
_ => return Ok(()), };
post_reply(&source_jsval, &reply, &origin)?;
Ok(())
}
fn post_reply(source: &JsValue, reply: &JsValue, origin: &str) -> Result<(), String> {
let post_msg = js_sys::Reflect::get(source, &JsValue::from_str("postMessage"))
.map_err(|_| "source has no postMessage".to_string())?;
let post_fn: js_sys::Function = post_msg
.dyn_into()
.map_err(|_| "source.postMessage isn't a function".to_string())?;
post_fn
.call2(source, reply, &JsValue::from_str(origin))
.map_err(|e| format!("postMessage call: {e:?}"))?;
Ok(())
}
fn build_reveal_seed_response(id: &str) -> Result<JsValue, String> {
let phrase = super::APP
.with(|cell| {
cell.borrow()
.wallet
.as_ref()
.map(|w| w.mnemonic.to_string())
})
.ok_or_else(|| "no identity on this device".to_string())?;
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(id));
set(&obj, "phrase", JsValue::from_str(&phrase));
Ok(JsValue::from(obj))
}
fn spawn_create_wallet(id: String, overwrite: bool, source: JsValue, origin: String) {
wasm_bindgen_futures::spawn_local(async move {
if !overwrite {
let existing = super::APP
.with(|cell| cell.borrow().wallet.as_ref().map(|w| w.address_hex()));
if let Some(addr) = existing {
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(&id));
set(&obj, "address", JsValue::from_str(&addr));
let reply = JsValue::from(obj);
if let Err(err) = post_reply(&source, &reply, &origin) {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"signer: create-wallet (cached) reply: {err}"
)));
}
return;
}
}
let reply = match super::wallet_store::create_and_persist().await {
Ok(wallet) => {
let addr = wallet.address_hex();
super::APP.with(|cell| cell.borrow_mut().wallet = Some(wallet));
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(&id));
set(&obj, "address", JsValue::from_str(&addr));
JsValue::from(obj)
}
Err(err) => error_response(&id, &err),
};
if let Err(err) = post_reply(&source, &reply, &origin) {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"signer: create-wallet reply: {err}"
)));
}
});
}
fn spawn_claim_name(id: String, name: String, source: JsValue, origin: String) {
wasm_bindgen_futures::spawn_local(async move {
let reply = match run_claim_name(&name).await {
Ok((address, tx_hash)) => {
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(&id));
set(&obj, "address", JsValue::from_str(&address));
set(&obj, "tx_hash", JsValue::from_str(&tx_hash));
JsValue::from(obj)
}
Err(err) => error_response(&id, &err),
};
if let Err(err) = post_reply(&source, &reply, &origin) {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"signer: claim-name reply: {err}"
)));
}
});
}
async fn run_claim_name(name: &str) -> Result<(String, String), String> {
let (signer, address) = wallet_handle()?;
let address_hex = hex_addr(&address);
let fee_payer = super::sponsor::signer()?;
let tx_hash = crate::registry::claim_and_maybe_set_main_sponsored(
&signer,
&fee_payer,
name,
crate::registry::ALPHA_USD_ADDRESS,
)
.await?;
Ok((address_hex, tx_hash))
}
fn spawn_import_seed(id: String, phrase: String, source: JsValue, origin: String) {
wasm_bindgen_futures::spawn_local(async move {
let reply = match super::wallet_store::import(&phrase).await {
Ok(wallet) => {
let addr = wallet.address_hex();
super::APP.with(|cell| cell.borrow_mut().wallet = Some(wallet));
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(&id));
set(&obj, "address", JsValue::from_str(&addr));
JsValue::from(obj)
}
Err(err) => error_response(&id, &err),
};
if let Err(err) = post_reply(&source, &reply, &origin) {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"signer: import-seed reply: {err}"
)));
}
});
}
fn build_challenge_response(id: &str, nonce_hex: &str) -> Result<JsValue, String> {
let nonce = parse_nonce(nonce_hex)?;
let mut hasher = Keccak256::new();
hasher.update(DOMAIN_TAG);
hasher.update(&nonce);
let mut prehash = [0u8; 32];
prehash.copy_from_slice(&hasher.finalize());
let (signer, address) = wallet_handle()?;
let signature = wallet::sign_hash(&signer, &prehash);
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(id));
set(&obj, "address", JsValue::from_str(&hex_addr(&address)));
set(&obj, "signature", JsValue::from_str(&hex_bytes(&signature)));
Ok(JsValue::from(obj))
}
fn build_digest_response(id: &str, digest_hex: &str, purpose: &str) -> Result<JsValue, String> {
let digest_bytes = decode_hex(digest_hex)?;
if digest_bytes.len() != 32 {
return Err(format!(
"digest must be 32 bytes, got {}",
digest_bytes.len()
));
}
let mut digest = [0u8; 32];
digest.copy_from_slice(&digest_bytes);
let (signer, address) = wallet_handle()?;
let from_hex = hex_addr(&address);
web_sys::console::log_1(&JsValue::from_str(&format!(
"lh-sign-digest auto-approved: {purpose} (digest=0x{})",
digest_hex.trim_start_matches("0x"),
)));
let sig = wallet::sign_hash(&signer, &digest);
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(id));
set(&obj, "address", JsValue::from_str(&from_hex));
set(&obj, "signature", JsValue::from_str(&hex_bytes(&sig)));
Ok(JsValue::from(obj))
}
fn decode_hex(hex: &str) -> Result<Vec<u8>, String> {
let trimmed = hex.trim().trim_start_matches("0x").trim_start_matches("0X");
if trimmed.len() % 2 != 0 {
return Err("data hex odd length".into());
}
let mut out = Vec::with_capacity(trimmed.len() / 2);
let bytes = trimmed.as_bytes();
let mut i = 0;
while i < bytes.len() {
let hi = nibble(bytes[i])?;
let lo = nibble(bytes[i + 1])?;
out.push((hi << 4) | lo);
i += 2;
}
Ok(out)
}
fn wallet_handle() -> Result<(k256::ecdsa::SigningKey, [u8; 20]), String> {
super::APP
.with(|cell| {
cell.borrow()
.wallet
.as_ref()
.map(|w| (w.signer.clone(), w.address))
})
.ok_or_else(|| "no identity on this device — create one at the apex".to_string())
}
fn error_response(id: &str, err: &str) -> JsValue {
let obj = js_sys::Object::new();
set(&obj, "type", JsValue::from_str("lh-sign-response"));
set(&obj, "id", JsValue::from_str(id));
set(&obj, "error", JsValue::from_str(err));
JsValue::from(obj)
}
fn set(obj: &js_sys::Object, key: &str, value: JsValue) {
let _ = js_sys::Reflect::set(obj, &JsValue::from_str(key), &value);
}
fn is_trusted_origin(origin: &str) -> bool {
let stripped = origin
.strip_prefix("https://")
.or_else(|| origin.strip_prefix("http://"))
.unwrap_or(origin);
let host = stripped.split(':').next().unwrap_or(stripped);
host == "localharness.xyz"
|| host.ends_with(".localharness.xyz")
|| host == "localhost"
|| host.ends_with(".localhost")
}
fn parse_nonce(hex: &str) -> Result<Vec<u8>, String> {
let trimmed = hex.trim().trim_start_matches("0x").trim_start_matches("0X");
if trimmed.len() % 2 != 0 {
return Err("nonce hex odd length".into());
}
let mut out = Vec::with_capacity(trimmed.len() / 2);
let bytes = trimmed.as_bytes();
let mut i = 0;
while i < bytes.len() {
let hi = nibble(bytes[i])?;
let lo = nibble(bytes[i + 1])?;
out.push((hi << 4) | lo);
i += 2;
}
Ok(out)
}
fn nibble(b: u8) -> Result<u8, String> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(format!("non-hex byte {b}")),
}
}
fn hex_addr(addr: &[u8; 20]) -> String {
let mut s = String::with_capacity(42);
s.push_str("0x");
for b in addr {
s.push_str(&format!("{b:02x}"));
}
s
}
fn hex_bytes(bytes: &[u8]) -> String {
let mut s = String::with_capacity(2 + bytes.len() * 2);
s.push_str("0x");
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}