use k256::ecdsa::SigningKey;
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduledJob {
pub owner: String,
pub interval: u64,
pub status: u8,
pub next_run: u64,
pub budget_wei: u128,
pub runs_left: u32,
pub target_id: u64,
}
impl ScheduledJob {
pub fn status_label(&self) -> &'static str {
match self.status {
0 => "active",
1 => "paused",
2 => "cancelled",
3 => "exhausted",
_ => "unknown",
}
}
}
pub(crate) fn encode_cancel_job(job_id: u64) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 32);
out.extend_from_slice(&selector("cancelJob(uint256)"));
out.extend_from_slice(&u256_be(job_id as u128));
out
}
pub async fn cancel_job_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
job_id: u64,
fee_token: &str,
) -> Result<String, String> {
sponsored_diamond_call(sender, fee_payer, encode_cancel_job(job_id), fee_token, 400_000).await
}
pub async fn create_offchain_job(
signer: &SigningKey,
now_secs: u64,
kind: &str,
target: &str,
task: &str,
interval_secs: u64,
runs: u32,
) -> Result<String, String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/schedule");
let mut body = serde_json::json!({
"action": "create",
"kind": kind,
"task": task,
"intervalSecs": interval_secs,
"runs": runs,
});
if !target.is_empty() {
body["target"] = serde_json::Value::String(target.to_string());
}
let resp = http_post_json_authed_returning(&url, &token, &body).await?;
resp.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| format!("schedule: unexpected response {resp}"))
}
pub async fn cancel_offchain_job(signer: &SigningKey, now_secs: u64, id: &str) -> Result<(), String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/schedule");
let body = serde_json::json!({ "action": "cancel", "id": id });
http_post_json_authed_returning(&url, &token, &body).await.map(|_| ())
}
pub async fn list_offchain_jobs(
signer: &SigningKey,
now_secs: u64,
) -> Result<Vec<serde_json::Value>, String> {
let token = proxy_auth_token(signer, now_secs);
let url = format!("{CREDIT_PROXY_URL}api/schedule");
let body = serde_json::json!({ "action": "list" });
let resp = http_post_json_authed_returning(&url, &token, &body).await?;
Ok(resp
.get("jobs")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
pub async fn jobs_of(owner_hex: &str) -> Result<Vec<u64>, String> {
let owner = parse_eth_address(owner_hex)?;
let result = read_view(selector("jobsOf(address)"), &[addr_word(&owner)]).await?;
let bytes = hex_to_bytes(&result)?;
Ok(decode_u64_array(&bytes))
}
pub async fn get_job(job_id: u64) -> Result<ScheduledJob, String> {
let result = read_view(selector("getJob(uint256)"), &[u256_be(job_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 7 * 32 {
return Err(format!("getJob: short response {} bytes", bytes.len()));
}
let word = |i: usize| &bytes[i * 32..(i + 1) * 32];
let owner = format!("0x{}", bytes_to_hex(&word(0)[12..32])); Ok(ScheduledJob {
owner,
interval: u64_low(word(1)),
status: bytes[2 * 32 + 31], next_run: u64_low(word(3)),
budget_wei: u128_low(word(4)),
runs_left: u64_low(word(5)) as u32,
target_id: u64_low(word(6)),
})
}
pub async fn jobs_due(start_after: u64, limit: u64) -> Result<(Vec<u64>, u64), String> {
let result = read_view(
selector("jobsDue(uint256,uint256)"),
&[u256_be(start_after as u128), u256_be(limit as u128)],
)
.await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 64 {
return Err(format!("jobsDue: short response {} bytes", bytes.len()));
}
Ok(decode_jobs_due(&bytes))
}
fn decode_jobs_due(bytes: &[u8]) -> (Vec<u64>, u64) {
if bytes.len() < 64 {
return (Vec::new(), 0);
}
let next_cursor = u64_low(&bytes[32..64]); let off = u64_low(&bytes[0..32]) as usize; let mut ids = Vec::new();
if bytes.len() >= off + 32 {
let len = u64_low(&bytes[off..off + 32]) as usize;
for i in 0..len {
let s = off + 32 + i * 32;
let Some(word) = bytes.get(s..s + 32) else { break };
ids.push(u64_low(word));
}
}
(ids, next_cursor)
}
#[cfg(test)]
mod jobs_due_tests {
use super::*;
#[test]
fn decode_jobs_due_tuple_array_then_cursor() {
let mut b = Vec::new();
b.extend_from_slice(&u256_be(0x40));
b.extend_from_slice(&u256_be(128));
b.extend_from_slice(&u256_be(2));
b.extend_from_slice(&u256_be(5));
b.extend_from_slice(&u256_be(9));
assert_eq!(decode_jobs_due(&b), (vec![5, 9], 128));
let mut e = Vec::new();
e.extend_from_slice(&u256_be(0x40));
e.extend_from_slice(&u256_be(64));
e.extend_from_slice(&u256_be(0)); assert_eq!(decode_jobs_due(&e), (Vec::new(), 64));
assert_eq!(decode_jobs_due(&[]), (Vec::new(), 0));
assert_eq!(decode_jobs_due(&[0u8; 40]), (Vec::new(), 0));
let mut t = Vec::new();
t.extend_from_slice(&u256_be(0x40));
t.extend_from_slice(&u256_be(0));
t.extend_from_slice(&u256_be(3));
t.extend_from_slice(&u256_be(7));
assert_eq!(decode_jobs_due(&t), (vec![7], 0));
}
}
pub async fn all_due_job_ids() -> Result<Vec<u64>, String> {
let mut all = Vec::new();
let mut cursor = 0u64;
for _ in 0..64 {
let (ids, next) = jobs_due(cursor, 64).await?;
all.extend(ids);
if next <= cursor {
break; }
cursor = next;
}
Ok(all)
}
pub async fn last_run_of(job_id: u64) -> Result<(u64, u8), String> {
let result = read_view(selector("lastRunOf(uint256)"), &[u256_be(job_id as u128)]).await?;
let bytes = hex_to_bytes(&result)?;
if bytes.len() < 2 * 32 {
return Err(format!("lastRunOf: short response {} bytes", bytes.len()));
}
let timestamp = u64_low(&bytes[0..32]);
let status = bytes[2 * 32 - 1]; Ok((timestamp, status))
}
pub async fn task_of(job_id: u64) -> Result<String, String> {
let result = read_view(selector("taskOf(uint256)"), &[u256_be(job_id as u128)]).await?;
let raw = hex_to_bytes(&result)?;
if raw.len() < 64 {
return Err(format!("taskOf: short response {} bytes", raw.len()));
}
let len = u64::from_be_bytes(
raw[56..64].try_into().map_err(|e: std::array::TryFromSliceError| e.to_string())?,
) as usize;
let end = len
.checked_add(64)
.filter(|&end| end <= raw.len())
.ok_or_else(|| format!("taskOf: truncated body (len {}, have {})", len, raw.len()))?;
String::from_utf8(raw[64..end].to_vec()).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancel_job_calldata_layout() {
let cd = encode_cancel_job(9);
assert_eq!(&cd[0..4], &selector("cancelJob(uint256)"));
assert_eq!(cd.len(), 36);
assert_eq!(u64::from_be_bytes(cd[28..36].try_into().unwrap()), 9);
}
#[test]
fn scheduled_job_status_label_maps_enum() {
let mut j = ScheduledJob {
owner: "0x00".into(),
interval: 60,
status: 0,
next_run: 0,
budget_wei: 0,
runs_left: 0,
target_id: 0,
};
assert_eq!(j.status_label(), "active");
j.status = 1;
assert_eq!(j.status_label(), "paused");
j.status = 2;
assert_eq!(j.status_label(), "cancelled");
j.status = 3;
assert_eq!(j.status_label(), "exhausted");
j.status = 9;
assert_eq!(j.status_label(), "unknown");
}
}