use k256::ecdsa::SigningKey;
use super::*;
const SLOT_BYTE_BUDGET: usize = 4000;
pub fn merge_push_sub(slot: Option<&str>, current: &str) -> Option<String> {
let cur: serde_json::Value = serde_json::from_str(current).ok()?;
let cur_ep = cur.get("endpoint")?.as_str()?.to_string();
let mut entries: Vec<serde_json::Value> = match slot.map(str::trim).filter(|s| !s.is_empty()) {
None => Vec::new(),
Some(s) => match serde_json::from_str::<serde_json::Value>(s) {
Ok(serde_json::Value::Array(a)) => a,
Ok(v @ serde_json::Value::Object(_)) => vec![v],
_ => Vec::new(),
},
};
entries.retain(|e| e.get("endpoint").and_then(|v| v.as_str()).is_some());
if entries.contains(&cur) {
return None; }
entries.retain(|e| e.get("endpoint").and_then(|v| v.as_str()) != Some(cur_ep.as_str()));
entries.insert(0, cur);
loop {
let json = serde_json::Value::Array(entries.clone()).to_string();
if json.len() <= SLOT_BYTE_BUDGET || entries.len() <= 1 {
return Some(json);
}
entries.pop();
}
}
pub fn encode_set_addr_push_sub(sub_json: &[u8]) -> Vec<u8> {
let len = sub_json.len();
let padded = len.div_ceil(32) * 32;
let mut buf = Vec::with_capacity(4 + 32 + 32 + padded);
buf.extend_from_slice(&selector("setPushSub(bytes)"));
buf.extend_from_slice(&u256_be(0x20)); buf.extend_from_slice(&u256_be(len as u128));
buf.extend_from_slice(sub_json);
buf.resize(4 + 32 + 32 + padded, 0);
buf
}
pub async fn addr_push_sub_of(addr_hex: &str) -> Result<Option<String>, String> {
let addr = parse_eth_address(addr_hex)?;
let result = read_view(selector("pushSubOf(address)"), &[addr_word(&addr)]).await?;
Ok(decode_string(&result)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()))
}
fn set_push_sub_gas(len: usize) -> u128 {
1_500_000 + (len as u128) * 9_000
}
pub async fn set_push_sub_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
sub_json: &[u8],
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
sender,
fee_payer,
encode_set_addr_push_sub(sub_json),
fee_token,
set_push_sub_gas(sub_json.len()),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
fn sub(ep: &str, key: &str) -> String {
format!(r#"{{"endpoint":"https://push.example/{ep}","keys":{{"p256dh":"{key}","auth":"a"}}}}"#)
}
#[test]
fn merge_into_empty_slot_makes_one_element_array() {
let cur = sub("phone", "k1");
let merged = merge_push_sub(None, &cur).unwrap();
let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
assert_eq!(v.as_array().unwrap().len(), 1);
assert_eq!(v[0], serde_json::from_str::<serde_json::Value>(&cur).unwrap());
}
#[test]
fn merge_promotes_legacy_single_object_and_appends() {
let phone = sub("phone", "k1");
let desktop = sub("desktop", "k2");
let merged = merge_push_sub(Some(&desktop), &phone).unwrap();
let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["endpoint"], "https://push.example/phone"); assert_eq!(arr[1]["endpoint"], "https://push.example/desktop");
}
#[test]
fn merge_is_idempotent_when_already_present() {
let phone = sub("phone", "k1");
let slot = merge_push_sub(None, &phone).unwrap();
assert!(merge_push_sub(Some(&slot), &phone).is_none()); }
#[test]
fn merge_replaces_same_endpoint_with_new_keys() {
let old = sub("phone", "OLD");
let new = sub("phone", "NEW");
let slot = merge_push_sub(None, &old).unwrap();
let merged = merge_push_sub(Some(&slot), &new).unwrap();
let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["keys"]["p256dh"], "NEW");
}
#[test]
fn merge_evicts_oldest_past_byte_budget() {
let mut slot: Option<String> = None;
for i in 0..40 {
let s = sub(&format!("dev{i}-{}", "x".repeat(120)), "k");
if let Some(m) = merge_push_sub(slot.as_deref(), &s) {
slot = Some(m);
}
}
let out = slot.unwrap();
assert!(out.len() <= SLOT_BYTE_BUDGET);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(v[0]["endpoint"].as_str().unwrap().contains("dev39"));
}
#[test]
fn set_push_sub_calldata_layout() {
let sub = br#"{"endpoint":"https://x"}"#;
let cd = encode_set_addr_push_sub(sub);
assert_eq!(&cd[0..4], &selector("setPushSub(bytes)"));
assert_eq!(&cd[4..36], &u256_be(0x20));
assert_eq!(&cd[36..68], &u256_be(sub.len() as u128));
assert_eq!(cd.len(), 4 + 32 + 32 + sub.len().div_ceil(32) * 32);
}
}