use k256::ecdsa::SigningKey;
use super::*;
pub fn devices_topic(owner_addr: &str) -> [u8; 32] {
let mut pre = b"localharness.devices".to_vec();
if let Ok(a) = parse_eth_address(owner_addr) {
pre.extend_from_slice(&a);
}
keccak_key(&pre)
}
pub fn team_topic(team_id: u64) -> [u8; 32] {
let mut pre = b"localharness.team".to_vec();
pre.extend_from_slice(&u256_be(team_id as u128));
keccak_key(&pre)
}
pub(crate) fn address_word(addr: &[u8; 20]) -> [u8; 32] {
let mut w = [0u8; 32];
w[12..32].copy_from_slice(addr);
w
}
pub(crate) fn push_abi_bytes(d: &mut Vec<u8>, bytes: &[u8]) {
d.extend_from_slice(&u256_be(bytes.len() as u128));
d.extend_from_slice(bytes);
let pad = (32 - (bytes.len() % 32)) % 32;
d.extend(std::iter::repeat_n(0u8, pad));
}
pub fn announce_digest(topic: &[u8; 32], ephemeral: &[u8; 20], pubkey: &[u8]) -> [u8; 32] {
let mut pre = Vec::with_capacity(32 + 20 + pubkey.len());
pre.extend_from_slice(topic);
pre.extend_from_slice(ephemeral);
pre.extend_from_slice(pubkey);
keccak32(&pre)
}
pub fn leave_digest(topic: &[u8; 32], ephemeral: &[u8; 20]) -> [u8; 32] {
let mut pre = Vec::with_capacity(18 + 32 + 20);
pre.extend_from_slice(b"localharness.leave");
pre.extend_from_slice(topic);
pre.extend_from_slice(ephemeral);
keccak32(&pre)
}
pub(crate) fn encode_leave(
topic: &[u8; 32],
owner: &[u8; 20],
ephemeral: &[u8; 20],
sig: &[u8; 65],
) -> Vec<u8> {
let mut d = selector("leave(bytes32,address,address,bytes)").to_vec();
d.extend_from_slice(topic);
d.extend_from_slice(&address_word(owner));
d.extend_from_slice(&address_word(ephemeral));
d.extend_from_slice(&u256_be(0x80)); push_abi_bytes(&mut d, sig);
d
}
pub async fn leave_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
owner_key: &SigningKey,
owner: &[u8; 20],
topic: &[u8; 32],
ephemeral: &[u8; 20],
fee_token: &str,
) -> Result<String, String> {
let digest = leave_digest(topic, ephemeral);
let sig = crate::wallet::sign_hash(owner_key, &digest); sponsored_diamond_call(
sender,
fee_payer,
encode_leave(topic, owner, ephemeral, &sig),
fee_token,
1_200_000,
)
.await
}
pub(crate) fn encode_announce(
topic: &[u8; 32],
owner: &[u8; 20],
ephemeral: &[u8; 20],
pubkey: &[u8],
sig: &[u8; 65],
) -> Vec<u8> {
let mut d = selector("announce(bytes32,address,address,bytes,bytes)").to_vec();
d.extend_from_slice(topic);
d.extend_from_slice(&address_word(owner));
d.extend_from_slice(&address_word(ephemeral));
d.extend_from_slice(&u256_be(0xa0)); let pubkey_tail = 32 + pubkey.len().div_ceil(32) * 32;
d.extend_from_slice(&u256_be((0xa0 + pubkey_tail) as u128)); push_abi_bytes(&mut d, pubkey);
push_abi_bytes(&mut d, sig);
d
}
#[allow(clippy::too_many_arguments)]
pub async fn announce_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
owner_key: &SigningKey,
owner: &[u8; 20],
topic: &[u8; 32],
ephemeral: &[u8; 20],
pubkey: &[u8],
fee_token: &str,
) -> Result<String, String> {
let digest = announce_digest(topic, ephemeral, pubkey);
let sig = crate::wallet::sign_hash(owner_key, &digest); let gas = 1_200_000u128 + (pubkey.len() as u128) * 9_000;
sponsored_diamond_call(
sender,
fee_payer,
encode_announce(topic, owner, ephemeral, pubkey, &sig),
fee_token,
gas,
)
.await
}
pub(crate) fn encode_post_signal(to: &[u8; 20], blob: &[u8]) -> Vec<u8> {
let mut d = selector("postSignal(address,bytes)").to_vec();
d.extend_from_slice(&address_word(to));
d.extend_from_slice(&u256_be(0x40)); push_abi_bytes(&mut d, blob);
d
}
pub async fn post_signal_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
to: &[u8; 20],
blob: &[u8],
fee_token: &str,
) -> Result<String, String> {
let gas = 1_200_000u128 + (blob.len() as u128) * 9_000;
sponsored_diamond_call(sender, fee_payer, encode_post_signal(to, blob), fee_token, gas).await
}
pub type AddrTsBytes = (String, u64, Vec<u8>);
pub(crate) fn decode_addr_ts_bytes_array(result_hex: &str) -> Vec<AddrTsBytes> {
let raw = match hex_to_bytes(result_hex) {
Ok(b) => b,
Err(_) => return Vec::new(),
};
let read_usize = |off: usize| -> Option<usize> {
let end = off.checked_add(32)?;
let w = raw.get(off..end)?;
Some(u64::from_be_bytes(w[24..32].try_into().ok()?) as usize)
};
let mut out = Vec::new();
let arr_off = match read_usize(0) {
Some(o) => o,
None => return out,
};
let len = match read_usize(arr_off) {
Some(l) => l,
None => return out,
};
let heads = match arr_off.checked_add(32) {
Some(h) => h, None => return out,
};
for i in 0..len {
let head_slot = match i.checked_mul(32).and_then(|o| heads.checked_add(o)) {
Some(s) => s,
None => break,
};
let elem = match read_usize(head_slot) {
Some(rel) => match heads.checked_add(rel) {
Some(e) => e,
None => break,
},
None => break,
};
let addr = match elem
.checked_add(12)
.zip(elem.checked_add(32))
.and_then(|(a, b)| raw.get(a..b))
{
Some(a) => format!("0x{}", bytes_to_hex(a)),
None => break,
};
let ts = match elem
.checked_add(56)
.zip(elem.checked_add(64))
.and_then(|(a, b)| raw.get(a..b))
{
Some(t) => u64::from_be_bytes(t.try_into().unwrap_or_default()),
None => break,
};
let boff = match elem.checked_add(64).and_then(read_usize) {
Some(rel) => match elem.checked_add(rel) {
Some(b) => b,
None => break,
},
None => break,
};
let blen = match read_usize(boff) {
Some(l) => l,
None => break,
};
let bytes = boff
.checked_add(32)
.and_then(|start| start.checked_add(blen).map(|end| (start, end)))
.and_then(|(start, end)| raw.get(start..end))
.map(|s| s.to_vec())
.unwrap_or_default();
out.push((addr, ts, bytes));
}
out
}
pub async fn peers_of(topic: &[u8; 32]) -> Result<Vec<AddrTsBytes>, String> {
let res = read_view(selector("peersOf(bytes32)"), &[*topic]).await?;
Ok(decode_addr_ts_bytes_array(&res))
}
pub async fn inbox_of(peer: &[u8; 20], from_index: u64) -> Result<Vec<AddrTsBytes>, String> {
let res = read_view(
selector("inboxOf(address,uint256)"),
&[address_word(peer), u256_be(from_index as u128)],
)
.await?;
Ok(decode_addr_ts_bytes_array(&res))
}
pub async fn inbox_length(peer: &[u8; 20]) -> Result<u64, String> {
let res = read_view(selector("inboxLength(address)"), &[address_word(peer)]).await?;
let raw = hex_to_bytes(&res)?;
if raw.len() < 32 {
return Ok(0);
}
Ok(u64::from_be_bytes(raw[24..32].try_into().map_err(|_| "bad len")?))
}
pub async fn signal_post(
signer: &SigningKey,
now_secs: u64,
room: &str,
slot: &str,
sdp: &str,
) -> Result<(), String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/signal");
let body = serde_json::json!({ "room": room, "slot": slot, "sdp": sdp });
http_post_json_authed(&url, &token, &body).await
}
pub async fn signal_get(room: &str, slot: &str) -> Result<Option<String>, String> {
let url = format!("{CREDIT_PROXY_URL}api/signal?room={room}&slot={slot}");
match http_get_bytes(&url).await? {
Some(bytes) => {
let v: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|e| format!("signal_get: bad json: {e}"))?;
Ok(v.get("sdp").and_then(|s| s.as_str()).map(String::from))
}
None => Ok(None),
}
}
pub async fn signal_clear(signer: &SigningKey, now_secs: u64, room: &str) -> Result<(), String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/signal");
let body = serde_json::json!({ "action": "clear", "room": room });
http_post_json_authed(&url, &token, &body).await
}
pub async fn signal_join(
signer: &SigningKey,
now_secs: u64,
room: &str,
joiner_id: &str,
) -> Result<(), String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/signal");
let body = serde_json::json!({ "action": "join", "room": room, "joiner": joiner_id });
http_post_json_authed(&url, &token, &body).await
}
pub async fn signal_get_joiners(room: &str) -> Result<Vec<String>, String> {
let url = format!("{CREDIT_PROXY_URL}api/signal?room={room}&slot=join");
match http_get_bytes(&url).await? {
Some(bytes) => {
let v: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| format!("signal_get_joiners: bad json: {e}"))?;
Ok(v.get("joiners")
.and_then(|j| j.as_array())
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
.unwrap_or_default())
}
None => Ok(Vec::new()),
}
}
pub const MESH_SLOTS: usize = 8;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SlotEntry {
pub id: String,
pub addr: String,
pub ts: u64,
}
pub struct MeshSlots {
pub slots: Vec<Option<SlotEntry>>,
pub now: u64,
pub sha: Option<String>,
}
pub async fn signal_get_slots(room: &str) -> Result<MeshSlots, String> {
let url = format!("{CREDIT_PROXY_URL}api/signal?room={room}&slot=slots");
let mut slots: Vec<Option<SlotEntry>> = vec![None; MESH_SLOTS];
let mut now: u64 = 0;
let mut sha: Option<String> = None;
if let Some(bytes) = http_get_bytes(&url).await? {
let v: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|e| format!("get_slots: bad json: {e}"))?;
now = v.get("now").and_then(|x| x.as_u64()).unwrap_or(0);
sha = v.get("sha").and_then(|x| x.as_str()).map(String::from);
if let Some(arr) = v.get("slots").and_then(|s| s.as_array()) {
for (i, e) in arr.iter().take(MESH_SLOTS).enumerate() {
slots[i] = serde_json::from_value::<SlotEntry>(e.clone()).ok();
}
}
}
Ok(MeshSlots { slots, now, sha })
}
pub enum PutSlots {
Written,
Conflict,
}
pub async fn signal_put_slots(
signer: &SigningKey,
now_secs: u64,
room: &str,
slots: &[Option<SlotEntry>],
my_idx: usize,
sha: Option<&str>,
) -> Result<PutSlots, String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/signal");
let body = serde_json::json!({
"action": "put-slots",
"room": room,
"slots": slots,
"my": my_idx,
"sha": sha.unwrap_or(""),
});
let (code, _v) = http_post_json_authed_with_status(&url, &token, &body).await?;
match code {
200 => Ok(PutSlots::Written),
409 => Ok(PutSlots::Conflict),
c => Err(format!("put-slots: HTTP {c}")),
}
}
pub async fn fetch_ice_json() -> Result<String, String> {
let url = format!("{CREDIT_PROXY_URL}api/turn");
match http_get_bytes(&url).await? {
Some(b) => String::from_utf8(b).map_err(|e| format!("ice json: {e}")),
None => Err("ice config unavailable".to_string()),
}
}
pub async fn chat_post(signer: &SigningKey, now_secs: u64, room: &str, text: &str) -> Result<(), String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/chat");
let body = serde_json::json!({ "room": room, "text": text });
http_post_json_authed(&url, &token, &body).await
}
pub async fn chat_poll(room: &str, after: i64) -> Result<Vec<(i64, String, String)>, String> {
let url = format!("{CREDIT_PROXY_URL}api/chat?room={room}&after={after}");
let bytes = match http_get_bytes(&url).await? {
Some(b) => b,
None => return Ok(Vec::new()),
};
let v: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|e| format!("chat poll: bad json: {e}"))?;
let mut out = Vec::new();
if let Some(msgs) = v.get("messages").and_then(|m| m.as_array()) {
for m in msgs {
let n = m.get("n").and_then(|x| x.as_i64()).unwrap_or(0);
let name = m.get("name").and_then(|x| x.as_str()).unwrap_or("?").to_string();
let text = m.get("text").and_then(|x| x.as_str()).unwrap_or("").to_string();
out.push((n, name, text));
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_presence_signal_array() {
let hex = String::from("0x")
+ "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000001111111111111111111111111111111111111111" + "0000000000000000000000000000000000000000000000000000000000000005" + "0000000000000000000000000000000000000000000000000000000000000060" + "0000000000000000000000000000000000000000000000000000000000000002" + "aabb000000000000000000000000000000000000000000000000000000000000"; let out = decode_addr_ts_bytes_array(&hex);
assert_eq!(out.len(), 1);
assert_eq!(out[0].0, "0x1111111111111111111111111111111111111111");
assert_eq!(out[0].1, 5);
assert_eq!(out[0].2, vec![0xAA, 0xBB]);
let empty = String::from("0x")
+ "0000000000000000000000000000000000000000000000000000000000000020"
+ "0000000000000000000000000000000000000000000000000000000000000000";
assert!(decode_addr_ts_bytes_array(&empty).is_empty());
}
#[test]
fn devices_topic_preimage_is_label_then_raw_address() {
let owner = "0x1111111111111111111111111111111111111111";
let topic = devices_topic(owner);
let mut pre = b"localharness.devices".to_vec();
pre.extend_from_slice(&parse_eth_address(owner).unwrap());
assert_eq!(topic, keccak_key(&pre));
assert_eq!(pre.len(), 40);
}
#[test]
fn announce_digest_is_packed_topic_ephemeral_pubkey() {
let topic = [0xABu8; 32];
let eph = [0x22u8; 20];
let pubkey = vec![0x02u8; 33];
let mut pre = Vec::new();
pre.extend_from_slice(&topic);
pre.extend_from_slice(&eph);
pre.extend_from_slice(&pubkey);
assert_eq!(pre.len(), 32 + 20 + 33);
assert_eq!(announce_digest(&topic, &eph, &pubkey), keccak32(&pre));
}
#[test]
fn leave_digest_is_prefixed_packed_topic_ephemeral() {
let topic = [0xABu8; 32];
let eph = [0x22u8; 20];
let mut pre = b"localharness.leave".to_vec();
pre.extend_from_slice(&topic);
pre.extend_from_slice(&eph);
assert_eq!(pre.len(), 18 + 32 + 20);
assert_eq!(leave_digest(&topic, &eph), keccak32(&pre));
let pubkey = vec![0x02u8; 33];
assert_ne!(leave_digest(&topic, &eph), announce_digest(&topic, &eph, &pubkey));
}
#[test]
fn encode_leave_4arg_layout() {
let topic = [0x11u8; 32];
let owner = [0x22u8; 20];
let eph = [0x33u8; 20];
let sig = [0x44u8; 65];
let cd = encode_leave(&topic, &owner, &eph, &sig);
assert_eq!(&cd[..4], &selector("leave(bytes32,address,address,bytes)"));
assert_eq!(&cd[4..36], &topic[..]);
assert_eq!(&cd[36..68], &address_word(&owner)[..]);
assert_eq!(&cd[68..100], &address_word(&eph)[..]);
assert_eq!(&cd[100..132], &u256_be(0x80)[..]); let sig_off = 4 + 0x80;
assert_eq!(&cd[sig_off..sig_off + 32], &u256_be(65)[..]);
assert_eq!(&cd[sig_off + 32..sig_off + 32 + 65], &sig[..]);
assert_eq!(cd.len(), 4 + 4 * 32 + (32 + 96));
}
#[test]
fn leave_digest_signature_recovers_to_owner() {
let w = crate::wallet::generate();
let owner = crate::wallet::address(&w.signer); let topic = [0x11u8; 32];
let eph = [0x99u8; 20];
let digest = leave_digest(&topic, &eph);
let sig = crate::wallet::sign_hash(&w.signer, &digest); let recovered = crate::wallet::recover_address(&sig, &digest)
.expect("sig recovers");
assert_eq!(recovered, owner, "leave sig recovers to the owner");
}
#[test]
fn announce_digest_signature_recovers_to_owner() {
let w = crate::wallet::generate();
let owner = crate::wallet::address(&w.signer); let topic = [0x11u8; 32];
let eph = [0x99u8; 20];
let pubkey = vec![0x03u8; 33];
let digest = announce_digest(&topic, &eph, &pubkey);
let sig = crate::wallet::sign_hash(&w.signer, &digest); let recovered = crate::wallet::recover_address(&sig, &digest)
.expect("sig recovers");
assert_eq!(recovered, owner, "announce sig recovers to the owner");
}
#[test]
fn encode_announce_5arg_layout() {
let topic = [0x11u8; 32];
let owner = [0x22u8; 20];
let eph = [0x33u8; 20];
let pubkey = vec![0x02u8; 33]; let sig = [0x44u8; 65];
let cd = encode_announce(&topic, &owner, &eph, &pubkey, &sig);
assert_eq!(
&cd[..4],
&selector("announce(bytes32,address,address,bytes,bytes)")
);
assert_eq!(&cd[4..36], &topic[..]);
assert_eq!(&cd[36..68], &address_word(&owner)[..]);
assert_eq!(&cd[68..100], &address_word(&eph)[..]);
assert_eq!(&cd[100..132], &u256_be(0xa0)[..]); assert_eq!(&cd[132..164], &u256_be(0x100)[..]); let pk_off = 4 + 0xa0;
assert_eq!(&cd[pk_off..pk_off + 32], &u256_be(33)[..]);
assert_eq!(&cd[pk_off + 32..pk_off + 32 + 33], &pubkey[..]);
let sig_off = 4 + 0x100;
assert_eq!(&cd[sig_off..sig_off + 32], &u256_be(65)[..]);
assert_eq!(&cd[sig_off + 32..sig_off + 32 + 65], &sig[..]);
assert_eq!(cd.len(), 4 + 5 * 32 + (32 + 64) + (32 + 96));
}
#[test]
fn addr_ts_bytes_array_empty_and_short_inputs() {
assert!(decode_addr_ts_bytes_array("0x").is_empty());
assert!(decode_addr_ts_bytes_array("0x00").is_empty());
assert!(decode_addr_ts_bytes_array("0xabc").is_empty());
assert!(decode_addr_ts_bytes_array("0xzz").is_empty());
assert!(decode_addr_ts_bytes_array("nonsense").is_empty());
let off_oob = format!("0x{}", word_usize(0x40)); assert!(decode_addr_ts_bytes_array(&off_oob).is_empty());
}
#[test]
fn addr_ts_bytes_array_hostile_offsets_dont_overflow() {
let huge_off = format!("0x{}", word_u64_max());
assert!(decode_addr_ts_bytes_array(&huge_off).is_empty());
let huge_len = format!("0x{}{}", word_usize(0x20), word_u64_max());
assert!(decode_addr_ts_bytes_array(&huge_len).is_empty());
let bad_head = String::from("0x")
+ &word_usize(0x20) + &word_usize(1) + &word_u64_max(); assert!(decode_addr_ts_bytes_array(&bad_head).is_empty());
let bad_bytes_off = String::from("0x")
+ &word_usize(0x20) + &word_usize(1) + &word_usize(0x20) + &word_usize(0x1111) + &word_usize(7) + &word_u64_max(); assert!(decode_addr_ts_bytes_array(&bad_bytes_off).is_empty());
}
#[test]
fn addr_ts_bytes_array_multi_element_decodes() {
let elem0 = String::from("")
+ "0000000000000000000000001111111111111111111111111111111111111111" + &word_usize(1) + &word_usize(0x60) + &word_usize(1) + "aa00000000000000000000000000000000000000000000000000000000000000"; let elem1 = String::from("")
+ "0000000000000000000000002222222222222222222222222222222222222222"
+ &word_usize(2)
+ &word_usize(0x60)
+ &word_usize(2)
+ "bbcc000000000000000000000000000000000000000000000000000000000000";
let hex = String::from("0x")
+ &word_usize(0x20) + &word_usize(2) + &word_usize(0x40) + &word_usize(0xE0) + &elem0
+ &elem1;
let out = decode_addr_ts_bytes_array(&hex);
assert_eq!(out.len(), 2);
assert_eq!(out[0].0, "0x1111111111111111111111111111111111111111");
assert_eq!(out[0].1, 1);
assert_eq!(out[0].2, vec![0xAA]);
assert_eq!(out[1].0, "0x2222222222222222222222222222222222222222");
assert_eq!(out[1].1, 2);
assert_eq!(out[1].2, vec![0xBB, 0xCC]);
}
}