v0.0.23: ETH display everywhere, local build, web UX fixes

Version: 0.0.22 → 0.0.23, SW cache wz-v3 → wz-v4

TUI:
- Own messages show ETH address (0x...) instead of fingerprint
- Received messages: async ETH cache lookup (resolve on first sight)
- /info shows Identity + Fingerprint
- Welcome message shows ETH address

Web:
- Header shows only ETH address (single element, click to copy)
- Own messages show ETH format
- Received messages resolve sender ETH via /v1/resolve/
- /peer 0x... resolves via /v1/resolve/ endpoint
- Click messages area → focuses text input

Client:
- register_bundle sends eth_address to server
- ETH↔fingerprint mapping stored on registration

Build:
- --local: build on current machine (auto-detect apt/dnf/pacman/brew)
- --local-ship: build locally + deploy to all servers
- --local-clean: build + clean cargo cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 08:50:31 +04:00
parent 2aa58a4319
commit ea04405199
9 changed files with 226 additions and 55 deletions

View File

@@ -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(())

View File

@@ -14,6 +14,8 @@ pub struct ServerClient {
struct RegisterRequest {
fingerprint: String,
bundle: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
eth_address: Option<String>,
}
#[derive(Serialize)]
@@ -43,6 +45,7 @@ impl ServerClient {
&self,
fingerprint: &str,
bundle: &PreKeyBundle,
eth_address: Option<String>,
) -> 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

View File

@@ -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,

View File

@@ -43,6 +43,38 @@ fn send_receipt(
});
}
/// ETH address cache: fingerprint → ETH address (populated async, read sync).
pub type EthCache = Arc<std::sync::Mutex<HashMap<String, String>>>;
/// 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[..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::<serde_json::Value>().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<Mutex<HashMap<String, PendingFileTransfer>>>,
our_fp: &str,
client: &ServerClient,
eth_cache: &EthCache,
last_dm_peer: &Arc<Mutex<Option<String>>>,
) {
match bincode::deserialize::<WireMessage>(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<Mutex<Option<String>>>,
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<AtomicBool>,
) {
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, &eth_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, &eth_cache, &last_dm_peer);
}
}
}

View File

@@ -63,9 +63,19 @@ pub struct ChatLine {
impl App {
pub fn new(our_fp: String, peer_fp: Option<String>, 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,