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> {
let gas = 3_500_000 + (task.len() as u128) * 9_000;
sponsored_escrow_diamond_call(
sender,
fee_payer,
budget_wei,
encode_schedule_job(target_id, task, interval_secs, budget_wei, max_runs),
fee_token,
gas,
)
.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 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");
}
}