use k256::ecdsa::SigningKey;
use sha3::{Digest, Keccak256};
use super::*;
pub(crate) fn keccak32(data: &[u8]) -> [u8; 32] {
let mut h = Keccak256::new();
h.update(data);
let d = h.finalize();
let mut o = [0u8; 32];
o.copy_from_slice(&d);
o
}
pub(crate) fn addr_word(a: &[u8; 20]) -> [u8; 32] {
let mut w = [0u8; 32];
w[12..].copy_from_slice(a);
w
}
pub fn x402_domain_separator() -> Result<[u8; 32], String> {
let diamond = parse_eth_address(REGISTRY_ADDRESS)?;
let mut dom = Vec::with_capacity(160);
dom.extend_from_slice(&keccak32(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
));
dom.extend_from_slice(&keccak32(b"localharness-x402"));
dom.extend_from_slice(&keccak32(b"1"));
dom.extend_from_slice(&u256_be(CHAIN_ID as u128));
dom.extend_from_slice(&addr_word(&diamond));
Ok(keccak32(&dom))
}
pub fn x402_digest(
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
) -> Result<[u8; 32], String> {
let mut st = Vec::with_capacity(224);
st.extend_from_slice(&keccak32(
b"PaymentAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)",
));
st.extend_from_slice(&addr_word(from));
st.extend_from_slice(&addr_word(to));
st.extend_from_slice(&u256_be(value_wei));
st.extend_from_slice(&u256_be(valid_after as u128));
st.extend_from_slice(&u256_be(valid_before as u128));
st.extend_from_slice(nonce);
let struct_hash = keccak32(&st);
let mut pre = Vec::with_capacity(66);
pre.extend_from_slice(&[0x19, 0x01]);
pre.extend_from_slice(&x402_domain_separator()?);
pre.extend_from_slice(&struct_hash);
Ok(keccak32(&pre))
}
pub fn sign_x402(
signer: &SigningKey,
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
) -> Result<[u8; 65], String> {
let digest = x402_digest(from, to, value_wei, valid_after, valid_before, nonce)?;
Ok(crate::wallet::sign_hash(signer, &digest))
}
pub(crate) fn encode_settle(
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32 * 9 + 96);
out.extend_from_slice(&selector(
"settle(address,address,uint256,uint256,uint256,bytes32,bytes)",
));
out.extend_from_slice(&addr_word(from));
out.extend_from_slice(&addr_word(to));
out.extend_from_slice(&u256_be(value_wei));
out.extend_from_slice(&u256_be(valid_after as u128));
out.extend_from_slice(&u256_be(valid_before as u128));
out.extend_from_slice(nonce);
out.extend_from_slice(&u256_be(7 * 32)); out.extend_from_slice(&u256_be(signature.len() as u128)); out.extend_from_slice(signature);
out.resize(out.len() + 31, 0); out
}
#[allow(clippy::too_many_arguments)]
pub async fn settle_x402_sponsored(
submitter: &SigningKey,
fee_payer: &SigningKey,
from: &[u8; 20],
to: &[u8; 20],
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(
submitter,
fee_payer,
encode_settle(from, to, value_wei, valid_after, valid_before, nonce, signature),
fee_token,
1_200_000,
)
.await
}
pub const X402_PRICE_LABEL: &[u8] = b"localharness.x402_price";
pub const DEFAULT_ASK_PRICE_WEI: u128 = 10_000_000_000_000_000;
pub const REMOTE_CALL_MAX_AUTO_PAY_WEI: u128 = 1_000_000_000_000_000_000;
pub fn auto_pay_amount(advertised: Option<u128>, cap: u128) -> Result<u128, u128> {
let pay_wei = advertised.unwrap_or(DEFAULT_ASK_PRICE_WEI);
if pay_wei > cap {
Err(pay_wei)
} else {
Ok(pay_wei)
}
}
pub const PRICE_LOCK_OVERPAY_TOLERANCE_BPS: u128 = 1000;
pub fn price_lock_ceiling(required: u128) -> u128 {
let slack = required.saturating_mul(PRICE_LOCK_OVERPAY_TOLERANCE_BPS) / 10_000;
required.saturating_add(slack)
}
pub async fn x402_price_of(token_id: u64) -> Result<Option<u128>, String> {
match metadata_bytes_of(token_id, keccak_key(X402_PRICE_LABEL)).await? {
Some(b) => Ok(String::from_utf8(b)
.ok()
.and_then(|s| s.trim().parse::<u128>().ok())
.filter(|w| *w > 0)),
None => Ok(None),
}
}
pub async fn x402_ask_price_of(token_id: u64) -> Result<u128, String> {
Ok(x402_price_of(token_id).await?.unwrap_or(DEFAULT_ASK_PRICE_WEI))
}
pub fn encode_set_x402_price(token_id: u64, wei: u128) -> Vec<u8> {
let payload = if wei == 0 { String::new() } else { wei.to_string() };
encode_set_metadata_bytes(token_id, keccak_key(X402_PRICE_LABEL), payload.as_bytes())
}
pub async fn x402_authorization_state(
from_hex: &str,
nonce: &[u8; 32],
) -> Result<bool, String> {
let from = parse_eth_address(from_hex)?;
let result = read_view(
selector("authorizationState(address,bytes32)"),
&[addr_word(&from), *nonce],
)
.await?;
Ok(decode_u256_as_u64(&result).map(|v| v != 0).unwrap_or(false))
}
pub fn random_x402_nonce() -> [u8; 32] {
use rand_core::RngCore;
let mut n = [0u8; 32];
rand_core::OsRng.fill_bytes(&mut n);
n
}
pub fn mcp_endpoint_url() -> String {
let base = CREDIT_PROXY_URL.trim_end_matches('/');
format!("{base}/mcp")
}
#[allow(clippy::too_many_arguments)]
pub fn x402_authorization_json(
from_hex: &str,
to_hex: &str,
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
) -> serde_json::Value {
serde_json::json!({
"from": from_hex,
"to": to_hex,
"value": value_wei.to_string(),
"validAfter": valid_after,
"validBefore": valid_before,
"nonce": format!("0x{}", bytes_to_hex(nonce)),
"signature": format!("0x{}", bytes_to_hex(signature)),
})
}
pub fn x402_ask_agent_body(target: &str, message: &str) -> serde_json::Value {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ask_agent",
"arguments": { "name": target, "message": message }
}
})
}
pub fn parse_mcp_tool_reply(json: &serde_json::Value) -> Result<String, String> {
if let Some(err) = json.get("error") {
let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
let msg = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("(no message)");
return Err(format!("mcp error {code}: {msg}"));
}
let result = json
.get("result")
.ok_or_else(|| format!("response has neither result nor error: {json}"))?;
let text = result
.get("content")
.and_then(|c| c.as_array())
.and_then(|a| a.first())
.and_then(|c| c.get("text"))
.and_then(|t| t.as_str())
.ok_or_else(|| format!("response had no text content: {result}"))?;
if result.get("isError").and_then(|b| b.as_bool()).unwrap_or(false) {
return Err(text.trim().to_string());
}
Ok(text.trim().to_string())
}
#[cfg(test)]
mod x402_tests {
use super::*;
#[test]
fn x402_domain_matches_live_facet() {
let expected =
"54530933a67f96286ac528dbff39d00c0ea49f4c6bd0f034343a0c78927f0b7a";
let got = x402_domain_separator().unwrap();
assert_eq!(bytes_to_hex(&got), expected);
}
#[test]
fn x402_sign_recovers_payer() {
let w = crate::wallet::generate();
let from = w.address;
let to = [0x11u8; 20];
let nonce = [0x22u8; 32];
let sig = sign_x402(&w.signer, &from, &to, 1_000, 0, 9_999_999_999, &nonce).unwrap();
let digest = x402_digest(&from, &to, 1_000, 0, 9_999_999_999, &nonce).unwrap();
let recovered = crate::wallet::recover_address(&sig, &digest).unwrap();
assert_eq!(recovered, from);
}
#[test]
fn settle_calldata_layout() {
let from = [0x11u8; 20];
let to = [0x22u8; 20];
let nonce = [0x33u8; 32];
let sig = [0x44u8; 65];
let value = 7_000u128;
let cd = encode_settle(&from, &to, value, 1, 2, &nonce, &sig);
assert_eq!(
&cd[0..4],
&selector("settle(address,address,uint256,uint256,uint256,bytes32,bytes)")
);
assert_eq!(cd.len(), 4 + 6 * 32 + 32 + 32 + 96);
assert_eq!(&cd[4 + 12..4 + 32], &from); assert_eq!(&cd[4 + 44..4 + 64], &to); assert_eq!(u128::from_be_bytes(cd[4 + 80..4 + 96].try_into().unwrap()), value); assert_eq!(&cd[4 + 5 * 32..4 + 6 * 32], &nonce); assert_eq!(u64::from_be_bytes(cd[4 + 6 * 32 + 24..4 + 7 * 32].try_into().unwrap()), 7 * 32);
assert_eq!(u64::from_be_bytes(cd[4 + 7 * 32 + 24..4 + 8 * 32].try_into().unwrap()), 65);
assert_eq!(&cd[4 + 8 * 32..4 + 8 * 32 + 65], &sig);
assert_eq!(&cd[4 + 8 * 32 + 65..], &[0u8; 31]);
}
#[test]
fn x402_authorization_json_matches_proxy_shape() {
let from = "0x00000000000000000000000000000000000000aa";
let to = "0x00000000000000000000000000000000000000bb";
let nonce = [0x11u8; 32];
let sig = [0x22u8; 65];
let j = x402_authorization_json(from, to, 1_000_000_000_000_000, 0, 1_999_999_999, &nonce, &sig);
assert_eq!(j["from"], from);
assert_eq!(j["to"], to);
assert_eq!(j["value"], "1000000000000000");
assert!(j["value"].is_string());
assert_eq!(j["validAfter"], 0);
assert_eq!(j["validBefore"], 1_999_999_999u64);
let nonce_s = j["nonce"].as_str().unwrap();
let sig_s = j["signature"].as_str().unwrap();
assert_eq!(nonce_s.len(), 2 + 64);
assert!(nonce_s.starts_with("0x"));
assert_eq!(sig_s.len(), 2 + 130);
assert!(sig_s.starts_with("0x"));
}
#[test]
fn x402_ask_agent_body_is_jsonrpc_tools_call() {
let b = x402_ask_agent_body("claude", "hello");
assert_eq!(b["jsonrpc"], "2.0");
assert_eq!(b["method"], "tools/call");
assert_eq!(b["params"]["name"], "ask_agent");
assert_eq!(b["params"]["arguments"]["name"], "claude");
assert_eq!(b["params"]["arguments"]["message"], "hello");
}
#[test]
fn parse_mcp_tool_reply_extracts_text_and_failures() {
let ok = serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"result": { "content": [{ "type": "text", "text": " hi there\n" }], "isError": false }
});
assert_eq!(parse_mcp_tool_reply(&ok).unwrap(), "hi there");
let tool_err = serde_json::json!({
"result": { "content": [{ "type": "text", "text": "agent exploded" }], "isError": true }
});
assert_eq!(parse_mcp_tool_reply(&tool_err).unwrap_err(), "agent exploded");
let rpc_err = serde_json::json!({ "error": { "code": -32602, "message": "payment required" } });
let e = parse_mcp_tool_reply(&rpc_err).unwrap_err();
assert!(e.contains("-32602") && e.contains("payment required"), "{e}");
assert!(parse_mcp_tool_reply(&serde_json::json!({})).is_err());
assert!(parse_mcp_tool_reply(&serde_json::json!({ "result": {} })).is_err());
}
#[test]
fn auto_pay_cap_boundary() {
let cap = REMOTE_CALL_MAX_AUTO_PAY_WEI;
assert_eq!(auto_pay_amount(Some(cap), cap), Ok(cap));
assert_eq!(auto_pay_amount(Some(cap + 1), cap), Err(cap + 1));
assert_eq!(auto_pay_amount(None, cap), Ok(DEFAULT_ASK_PRICE_WEI));
assert!(DEFAULT_ASK_PRICE_WEI <= cap);
assert_eq!(auto_pay_amount(None, 1), Err(DEFAULT_ASK_PRICE_WEI));
}
#[test]
fn price_lock_ceiling_band() {
let p = DEFAULT_ASK_PRICE_WEI; assert_eq!(price_lock_ceiling(p), p + p / 10);
assert!(p <= price_lock_ceiling(p));
assert!(price_lock_ceiling(p) >= p + p * PRICE_LOCK_OVERPAY_TOLERANCE_BPS / 10_000);
assert!(price_lock_ceiling(p) + 1 > price_lock_ceiling(p));
assert_eq!(price_lock_ceiling(0), 0);
assert!(price_lock_ceiling(u128::MAX) >= u128::MAX - 1);
}
#[test]
fn mcp_endpoint_is_proxy_slash_mcp() {
let url = mcp_endpoint_url();
assert!(url.ends_with("/mcp"));
assert!(!url.contains("//mcp")); }
}