diff --git a/warzone/Cargo.toml b/warzone/Cargo.toml index 132b47a..6ae20ae 100644 --- a/warzone/Cargo.toml +++ b/warzone/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.0.22" +version = "0.0.23" edition = "2021" license = "MIT" rust-version = "1.75" diff --git a/warzone/crates/warzone-client/src/cli/init.rs b/warzone/crates/warzone-client/src/cli/init.rs index ee2292d..b571431 100644 --- a/warzone/crates/warzone-client/src/cli/init.rs +++ b/warzone/crates/warzone-client/src/cli/init.rs @@ -85,8 +85,16 @@ pub async fn register_with_server_identity( .map_err(|_| anyhow::anyhow!("No bundle found. Run `warzone init` first."))?; let bundle: PreKeyBundle = bincode::deserialize(&bundle_bytes)?; + // Derive ETH address from seed + let eth_address = crate::keystore::load_seed_raw() + .map(|seed| { + let eth = warzone_protocol::ethereum::derive_eth_identity(&seed); + eth.address.to_checksum() + }) + .ok(); + let client = ServerClient::new(server_url); - client.register_bundle(&fp, &bundle).await?; + client.register_bundle(&fp, &bundle, eth_address).await?; println!("Bundle registered with {}", server_url); Ok(()) diff --git a/warzone/crates/warzone-client/src/net.rs b/warzone/crates/warzone-client/src/net.rs index 79184f5..4648c0d 100644 --- a/warzone/crates/warzone-client/src/net.rs +++ b/warzone/crates/warzone-client/src/net.rs @@ -14,6 +14,8 @@ pub struct ServerClient { struct RegisterRequest { fingerprint: String, bundle: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + eth_address: Option, } #[derive(Serialize)] @@ -43,6 +45,7 @@ impl ServerClient { &self, fingerprint: &str, bundle: &PreKeyBundle, + eth_address: Option, ) -> Result<()> { let encoded = bincode::serialize(bundle).context("failed to serialize bundle")?; @@ -51,6 +54,7 @@ impl ServerClient { .json(&RegisterRequest { fingerprint: fingerprint.to_string(), bundle: encoded, + eth_address, }) .send() .await diff --git a/warzone/crates/warzone-client/src/tui/commands.rs b/warzone/crates/warzone-client/src/tui/commands.rs index 1bcc431..50fa676 100644 --- a/warzone/crates/warzone-client/src/tui/commands.rs +++ b/warzone/crates/warzone-client/src/tui/commands.rs @@ -34,13 +34,10 @@ impl App { return; } if text == "/info" { - self.add_message(ChatLine { - sender: "system".into(), - text: format!("Your fingerprint: {}", self.our_fp), - is_system: true, - is_self: false, - message_id: None, timestamp: Local::now(), - }); + if !self.our_eth.is_empty() { + self.add_message(ChatLine { sender: "system".into(), text: format!("Identity: {}", self.our_eth), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); + } + self.add_message(ChatLine { sender: "system".into(), text: format!("Fingerprint: {}", self.our_fp), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); return; } if text == "/help" || text == "/?" { @@ -634,7 +631,7 @@ impl App { let _ = db.touch_contact(&peer, None); let _ = db.store_message(&peer, &self.our_fp, &text, true); self.add_message(ChatLine { - sender: self.our_fp[..12].to_string(), + sender: if self.our_eth.is_empty() { self.our_fp[..12].to_string() } else { format!("{}...", &self.our_eth[..self.our_eth.len().min(12)]) }, text: text.clone(), is_system: false, is_self: true, @@ -879,7 +876,7 @@ impl App { { Ok(_) => { self.add_message(ChatLine { - sender: format!("{} [#{}]", &self.our_fp[..12], group_name), + sender: format!("{} [#{}]", if self.our_eth.is_empty() { &self.our_fp[..12] } else { &self.our_eth[..self.our_eth.len().min(12)] }, group_name), text: text.to_string(), is_system: false, is_self: true, diff --git a/warzone/crates/warzone-client/src/tui/network.rs b/warzone/crates/warzone-client/src/tui/network.rs index c2dfb42..47eacce 100644 --- a/warzone/crates/warzone-client/src/tui/network.rs +++ b/warzone/crates/warzone-client/src/tui/network.rs @@ -43,6 +43,38 @@ fn send_receipt( }); } +/// ETH address cache: fingerprint → ETH address (populated async, read sync). +pub type EthCache = Arc>>; + +/// Display a fingerprint as short ETH address if cached, otherwise truncated fingerprint. +fn display_sender(fp: &str, eth_cache: &EthCache) -> String { + let cache = eth_cache.lock().unwrap(); + if let Some(eth) = cache.get(fp) { + format!("{}...", ð[..eth.len().min(12)]) + } else { + fp[..fp.len().min(12)].to_string() + } +} + +/// Async: look up ETH address for a fingerprint and cache it. +fn cache_eth_lookup(fp: &str, client: &ServerClient, eth_cache: &EthCache) { + let fp = fp.to_string(); + let client = client.clone(); + let cache = eth_cache.clone(); + // Check if already cached + if cache.lock().unwrap().contains_key(&fp) { return; } + tokio::spawn(async move { + let url = format!("{}/v1/resolve/{}", client.base_url, fp); + if let Ok(resp) = client.client.get(&url).send().await { + if let Ok(data) = resp.json::().await { + if let Some(eth) = data.get("eth_address").and_then(|v| v.as_str()) { + cache.lock().unwrap().insert(fp, eth.to_string()); + } + } + } + }); +} + fn store_received(db: &LocalDb, sender_fp: &str, text: &str) { let _ = db.touch_contact(sender_fp, None); let _ = db.store_message(sender_fp, sender_fp, text, false); @@ -58,10 +90,11 @@ pub fn process_incoming( pending_files: &Arc>>, our_fp: &str, client: &ServerClient, + eth_cache: &EthCache, last_dm_peer: &Arc>>, ) { match bincode::deserialize::(raw) { - Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer), + Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer, eth_cache), Err(_) => {} } } @@ -76,6 +109,7 @@ fn process_wire_message( our_fp: &str, client: &ServerClient, last_dm_peer: &Arc>>, + eth_cache: &EthCache, ) { match wire { WireMessage::KeyExchange { @@ -117,7 +151,7 @@ fn process_wire_message( } store_received(db, &sender_fingerprint, &text); messages.lock().unwrap().push(ChatLine { - sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(), + sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) }, text, is_system: false, is_self: false, @@ -166,7 +200,7 @@ fn process_wire_message( } store_received(db, &sender_fingerprint, &text); messages.lock().unwrap().push(ChatLine { - sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(), + sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) }, text, is_system: false, is_self: false, @@ -464,7 +498,7 @@ fn process_wire_message( } => { let type_str = format!("{:?}", signal_type); messages.lock().unwrap().push(ChatLine { - sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(), + sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) }, text: format!("\u{1f4de} Call signal: {}", type_str), is_system: false, is_self: false, @@ -487,6 +521,7 @@ pub async fn poll_loop( connected: Arc, ) { let fp = normfp(&our_fp); + let eth_cache: EthCache = Arc::new(std::sync::Mutex::new(HashMap::new())); // Try WebSocket first let ws_url = client.base_url @@ -511,7 +546,7 @@ pub async fn poll_loop( while let Some(Ok(msg)) = read.next().await { if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg { - process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer); + process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer); } } @@ -534,7 +569,7 @@ pub async fn poll_loop( Err(_) => continue, }; for raw in &raw_msgs { - process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer); + process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer); } } } diff --git a/warzone/crates/warzone-client/src/tui/types.rs b/warzone/crates/warzone-client/src/tui/types.rs index 518d94a..ec2de23 100644 --- a/warzone/crates/warzone-client/src/tui/types.rs +++ b/warzone/crates/warzone-client/src/tui/types.rs @@ -63,9 +63,19 @@ pub struct ChatLine { impl App { pub fn new(our_fp: String, peer_fp: Option, server_url: String) -> Self { + // Derive ETH address from seed first (used in welcome messages) + let our_eth = crate::keystore::load_seed_raw() + .map(|seed| { + let eth = warzone_protocol::ethereum::derive_eth_identity(&seed); + eth.address.to_checksum() + }) + .unwrap_or_default(); + + let identity_display = if our_eth.is_empty() { our_fp.clone() } else { our_eth.clone() }; + let messages = Arc::new(Mutex::new(vec![ChatLine { sender: "system".into(), - text: format!("You are {}", our_fp), + text: format!("You are {}", identity_display), is_system: true, is_self: false, message_id: None, @@ -101,14 +111,6 @@ impl App { timestamp: Local::now(), }); - // Derive ETH address from seed if available - let our_eth = crate::keystore::load_seed_raw() - .map(|seed| { - let eth = warzone_protocol::ethereum::derive_eth_identity(&seed); - eth.address.to_checksum() - }) - .unwrap_or_default(); - App { input: String::new(), messages, diff --git a/warzone/crates/warzone-protocol/Cargo.toml b/warzone/crates/warzone-protocol/Cargo.toml index b118c06..5b08ee6 100644 --- a/warzone/crates/warzone-protocol/Cargo.toml +++ b/warzone/crates/warzone-protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warzone-protocol" -version = "0.0.22" +version = "0.0.23" edition = "2021" license = "MIT" description = "Core crypto & wire protocol for featherChat (Warzone messenger)" diff --git a/warzone/crates/warzone-server/src/routes/web.rs b/warzone/crates/warzone-server/src/routes/web.rs index 5ee6af0..dbabbe2 100644 --- a/warzone/crates/warzone-server/src/routes/web.rs +++ b/warzone/crates/warzone-server/src/routes/web.rs @@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse { async fn service_worker() -> impl IntoResponse { ([(header::CONTENT_TYPE, "application/javascript")], r##" -const CACHE = 'wz-v3'; +const CACHE = 'wz-v4'; const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json']; self.addEventListener('install', e => { @@ -209,8 +209,8 @@ const WEB_HTML: &str = r##"
- - + + @@ -242,7 +242,7 @@ let pollTimer = null; let ws = null; // WebSocket connection let wasmReady = false; -const VERSION = '0.0.22'; +const VERSION = '0.0.23'; let DEBUG = true; // toggle with /debug command // ── Receipt tracking ── @@ -616,13 +616,16 @@ async function handleIncomingMessage(bytes) { let fromLabel = result.sender.slice(0, 19); try { - const ar = await fetch(SERVER + '/v1/alias/whois/' + senderFP); + const ar = await fetch(SERVER + '/v1/resolve/' + senderFP); const ad = await ar.json(); - if (ad.alias) fromLabel = '@' + ad.alias; + if (ad.eth_address) fromLabel = ad.eth_address.slice(0, 12) + '...'; + // Alias overrides ETH + const aw = await fetch(SERVER + '/v1/alias/whois/' + senderFP); + const adata = await aw.json(); + if (adata.alias) fromLabel = '@' + adata.alias; } catch(e) {} addMsg(fromLabel, result.text, false); - // Send delivery receipt if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered'); lastDmPeer = normFP(result.sender); return; @@ -653,13 +656,16 @@ async function handleIncomingMessage(bytes) { let fromLabel = result.sender.slice(0, 19); try { - const ar = await fetch(SERVER + '/v1/alias/whois/' + normFP(result.sender)); + const rfp = normFP(result.sender); + const ar = await fetch(SERVER + '/v1/resolve/' + rfp); const ad = await ar.json(); - if (ad.alias) fromLabel = '@' + ad.alias; + if (ad.eth_address) fromLabel = ad.eth_address.slice(0, 12) + '...'; + const aw = await fetch(SERVER + '/v1/alias/whois/' + rfp); + const adata = await aw.json(); + if (adata.alias) fromLabel = '@' + adata.alias; } catch(e2) {} addMsg(fromLabel, result.text, false); - // Send delivery receipt if (result.message_id) sendReceipt(result.sender, result.message_id, 'delivered'); lastDmPeer = normFP(result.sender); return; @@ -859,18 +865,21 @@ let pendingFiles = {}; // file_id -> { filename, chunks: [], total, received, async function enterChat() { document.getElementById('setup').classList.remove('active'); document.getElementById('chat').classList.add('active'); - document.getElementById('hdr-fp').textContent = myFingerprint.slice(0, 19); document.getElementById('hdr-server').textContent = SERVER; await registerKey(); - addSys('Identity: ' + myEthAddress); - addSys('Fingerprint: ' + myFingerprint); - addSys('Key registered with server'); - + // Show ETH in header, fallback to fingerprint + const hdrFp = document.getElementById('hdr-fp'); if (myEthAddress) { - document.getElementById('hdr-eth').textContent = myEthAddress.slice(0, 10) + '...'; - document.getElementById('hdr-eth').title = myEthAddress; + hdrFp.textContent = myEthAddress.slice(0, 12) + '...'; + hdrFp.title = myEthAddress; + hdrFp.onclick = function() { navigator.clipboard.writeText(myEthAddress); addSys('Copied: ' + myEthAddress); }; + } else { + hdrFp.textContent = (myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)); + hdrFp.title = myFingerprint; } + addSys('Identity: ' + (myEthAddress || myFingerprint)); + addSys('Key registered with server'); addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above'); addSys('/alias · /g · /gleave · /gkick · /gmembers · /glist · /friend · /file · /info'); @@ -986,7 +995,7 @@ async function sendToGroup(groupName, text) { body: JSON.stringify({ from: myFP, messages }) }); - addMsg(myFingerprint.slice(0, 19) + ' [' + groupName + ']', text, true, null); + addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)) + ' [' + groupName + ']', text, true, null); } // ── Send handler ── @@ -1092,18 +1101,19 @@ async function doSend() { $peerInput.value = lastDmPeer; try { await sendEncrypted(lastDmPeer, replyText.trim()); - addMsg(myFingerprint.slice(0, 19), replyText.trim(), true); + addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)), replyText.trim(), true); } catch(e) { addSys('Reply failed: ' + e.message); } return; } if (text.startsWith('/p ') || text.startsWith('/peer ')) { let val = text.startsWith('/p ') ? text.slice(3).trim() : text.slice(6).trim(); - if (val.startsWith('@')) { - const resp = await fetch(SERVER + '/v1/alias/resolve/' + val.slice(1)); + if (val.startsWith('@') || val.startsWith('0x') || val.startsWith('0X')) { + const endpoint = val.startsWith('@') ? '/v1/alias/resolve/' + val.slice(1) : '/v1/resolve/' + val; + const resp = await fetch(SERVER + endpoint); const data = await resp.json(); - if (data.error) { addSys('Unknown alias ' + val); return; } + if (data.error) { addSys('Cannot resolve ' + val + ': ' + data.error); return; } $peerInput.value = data.fingerprint; - addSys(val + ' → ' + data.fingerprint.slice(0,16) + '...'); + addSys(val + ' \u2192 ' + data.fingerprint.slice(0,16) + '...'); } else { $peerInput.value = val; } @@ -1197,7 +1207,7 @@ async function doSend() { try { const msgId = await sendEncrypted(peer, text); sentMsgReceipts[msgId] = { status: 'sent', el: null }; - addMsg(myFingerprint.slice(0, 19), text, true, msgId); + addMsg((myEthAddress ? myEthAddress.slice(0,12) + '...' : myFingerprint.slice(0,19)), text, true, msgId); } catch(e) { addSys('Send failed: ' + e.message); } @@ -1218,6 +1228,7 @@ document.getElementById('btn-show-recover').onclick = () => document.getElementB document.getElementById('btn-recover').onclick = () => doRecover(); document.getElementById('btn-enter').onclick = () => enterChat(); document.getElementById('send-btn').onclick = () => doSend(); +document.getElementById('messages').onclick = () => document.getElementById('msg-input').focus(); document.getElementById('hdr-eth').onclick = function() { if (myEthAddress) navigator.clipboard.writeText(myEthAddress).then(() => addSys('Copied ETH address')); }; diff --git a/warzone/scripts/build-linux.sh b/warzone/scripts/build-linux.sh index 7b440ff..e819d00 100755 --- a/warzone/scripts/build-linux.sh +++ b/warzone/scripts/build-linux.sh @@ -384,6 +384,108 @@ do_logs() { ssh "$host" "journalctl -u $PROD_SERVICE -f --no-pager" } +# --------------------------------------------------------------------------- +# --local: Build locally on this machine (auto-detect package manager) +# --------------------------------------------------------------------------- + +detect_pkg_manager() { + if command -v apt-get &>/dev/null; then echo "apt" + elif command -v dnf &>/dev/null; then echo "dnf" + elif command -v pacman &>/dev/null; then echo "pacman" + elif command -v brew &>/dev/null; then echo "brew" + else echo "unknown"; fi +} + +do_local_deps() { + local pm + pm=$(detect_pkg_manager) + echo "[1/4] Installing dependencies ($pm)..." + + case "$pm" in + apt) + sudo apt-get update -qq + sudo apt-get install -y -qq build-essential pkg-config libssl-dev curl >/dev/null 2>&1 + ;; + dnf) + sudo dnf install -y gcc gcc-c++ make pkg-config openssl-devel curl >/dev/null 2>&1 + ;; + pacman) + sudo pacman -Sy --noconfirm base-devel pkg-config openssl curl >/dev/null 2>&1 + ;; + brew) + brew install openssl pkg-config 2>/dev/null || true + ;; + *) + echo "WARNING: Unknown package manager. Ensure build-essential, pkg-config, libssl-dev are installed." + ;; + esac + + # Ensure Rust is installed + if ! command -v cargo &>/dev/null; then + echo " Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source "$HOME/.cargo/env" + fi + + # Ensure wasm-pack is installed + if ! command -v wasm-pack &>/dev/null; then + echo " Installing wasm-pack..." + cargo install wasm-pack 2>/dev/null || true + fi + + # Ensure wasm target + rustup target add wasm32-unknown-unknown 2>/dev/null || true +} + +do_local_build() { + local arch + arch=$(uname -m) + local os + os=$(uname -s | tr '[:upper:]' '[:lower:]') + local out_dir="target/${os}-${arch}" + + echo "=== Local Build (${os}-${arch}) ===" + + do_local_deps + + echo "[2/4] Building WASM..." + wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg 2>&1 | tail -3 + + echo "[3/4] Building release binaries..." + cargo build --release --bin warzone-server --bin warzone-client 2>&1 + + echo "[4/4] Copying to ${out_dir}..." + mkdir -p "$out_dir" + cp target/release/warzone-server target/release/warzone-client "$out_dir/" + cp federation.example.json "$out_dir/" 2>/dev/null || true + + # Clean cargo cache if requested + if [ "${CLEAN_CACHE:-}" = "1" ]; then + echo " Cleaning build cache..." + cargo clean 2>/dev/null || true + fi + + echo "" + echo "=== Local Build Complete ===" + ls -lh "$out_dir"/warzone-* + echo "" + echo "Run:" + echo " $out_dir/warzone-server --bind 0.0.0.0:7700" + echo " $out_dir/warzone-client tui --server http://localhost:7700" +} + +do_local_ship() { + do_local_build + echo "" + do_update_all + echo "" + do_status + echo "" + echo "========================================" + echo " LOCAL SHIP COMPLETE" + echo "========================================" +} + # --------------------------------------------------------------------------- # --ship: Build + deploy to all servers + destroy VM (full pipeline) # --------------------------------------------------------------------------- @@ -453,18 +555,30 @@ case "${1:-}" in --ship) do_ship ;; + --local) + do_local_build + ;; + --local-ship) + do_local_ship + ;; + --local-clean) + CLEAN_CACHE=1 do_local_build + ;; --upload) do_upload ;; *) echo "Usage: $0 [args]" echo "" - echo "One command:" - echo " --ship Build + deploy to all servers + destroy VM" + echo "Local build:" + echo " --local Build locally (auto-detect OS, install deps)" + echo " --local-ship Build locally + deploy to all servers" + echo " --local-clean Build locally + clean cargo cache after" echo "" - echo "Build (Hetzner VM):" + echo "Remote build (Hetzner VM):" + echo " --ship Build on VM + deploy + destroy VM" echo " --prepare Create VM, install deps, upload source" - echo " --build Build release binaries" + echo " --build Build release binaries on VM" echo " --transfer Download binaries to $OUTPUT_DIR" echo " --destroy Delete the build VM" echo " --all prepare + build + transfer (VM persists)"