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_schedule_job(
target_id: u64,
task: &[u8],
interval_secs: u64,
budget_wei: u128,
max_runs: u32,
) -> Vec<u8> {
let padded_len = task.len().div_ceil(32) * 32;
let mut out = Vec::with_capacity(4 + 5 * 32 + 32 + padded_len);
out.extend_from_slice(&selector("scheduleJob(uint256,bytes,uint64,uint128,uint32)"));
out.extend_from_slice(&u256_be(target_id as u128));
out.extend_from_slice(&u256_be(5 * 32));
out.extend_from_slice(&u256_be(interval_secs as u128));
out.extend_from_slice(&u256_be(budget_wei));
out.extend_from_slice(&u256_be(max_runs as u128));
out.extend_from_slice(&u256_be(task.len() as u128));
out.extend_from_slice(task);
out.resize(out.len() + (padded_len - task.len()), 0);
out
}
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
}
#[allow(clippy::too_many_arguments)]
pub async fn schedule_job_sponsored(
sender: &SigningKey,
fee_payer: &SigningKey,
target_id: u64,
task: &[u8],
interval_secs: u64,
budget_wei: u128,
max_runs: u32,
fee_token: &str,
) -> Result<String, String> {
schedule_job_sponsored_bridged(
sender, fee_payer, target_id, task, interval_secs, budget_wei, max_runs, fee_token, 0,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn schedule_job_sponsored_bridged(
sender: &SigningKey,
fee_payer: &SigningKey,
target_id: u64,
task: &[u8],
interval_secs: u64,
budget_wei: u128,
max_runs: u32,
fee_token: &str,
bridge_wei: u128,
) -> Result<String, String> {
let gas = 3_500_000 + (task.len() as u128) * 9_000;
sponsored_escrow_diamond_call_bridged(
sender,
fee_payer,
budget_wei,
encode_schedule_job(target_id, task, interval_secs, budget_wei, max_runs),
fee_token,
gas,
bridge_wei,
)
.await
}
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 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 schedule_job_calldata_layout() {
let task = b"ping the oracle"; let cd = encode_schedule_job(0x42, task, 300, 1_500_000_000_000_000_000u128, 100);
assert_eq!(&cd[0..4], &selector("scheduleJob(uint256,bytes,uint64,uint128,uint32)"));
assert_eq!(cd.len(), 4 + 5 * 32 + 32 + 32);
assert_eq!(u64::from_be_bytes(cd[4 + 24..4 + 32].try_into().unwrap()), 0x42);
assert_eq!(u64::from_be_bytes(cd[4 + 32 + 24..4 + 2 * 32].try_into().unwrap()), 5 * 32);
assert_eq!(u64::from_be_bytes(cd[4 + 2 * 32 + 24..4 + 3 * 32].try_into().unwrap()), 300);
assert_eq!(
u128::from_be_bytes(cd[4 + 3 * 32 + 16..4 + 4 * 32].try_into().unwrap()),
1_500_000_000_000_000_000u128
);
assert_eq!(u64::from_be_bytes(cd[4 + 4 * 32 + 24..4 + 5 * 32].try_into().unwrap()), 100);
assert_eq!(
u64::from_be_bytes(cd[4 + 5 * 32 + 24..4 + 6 * 32].try_into().unwrap()),
task.len() as u64
);
assert_eq!(&cd[4 + 6 * 32..4 + 6 * 32 + task.len()], task);
assert_eq!(&cd[4 + 6 * 32 + task.len()..], &[0u8; 32 - 15]);
}
#[test]
fn schedule_job_task_exact_multiple_no_extra_pad() {
let task = [0xABu8; 32];
let cd = encode_schedule_job(1, &task, 60, 1, 1);
assert_eq!(cd.len(), 4 + 5 * 32 + 32 + 32);
assert_eq!(&cd[4 + 6 * 32..], &task);
}
#[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");
}
}