diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 77a0dd7..9b2c073 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -70,6 +70,16 @@ pub enum Error { #[error("Signing error: {0}")] Sign(#[from] SignTransactionError), + + #[error("Grant timestamp {0} is outside the representable range")] + InvalidTimestamp(i64), +} + +/// Converts a grant boundary from Unix seconds. `None` in means "unbounded"; an +/// unrepresentable value is an error, never a silently unbounded grant. +fn grant_timestamp(secs: Option) -> Result>, Error> { + secs.map(|s| chrono::DateTime::from_timestamp(s, 0).ok_or(Error::InvalidTimestamp(s))) + .transpose() } #[derive(Actor)] @@ -343,12 +353,8 @@ impl EvmActor { let basic = SharedGrantSettings { wallet_access_id: grant.wallet_access_id, chain: grant.chain_id, - valid_from: grant - .valid_from_secs - .and_then(|s| chrono::DateTime::from_timestamp(s, 0)), - valid_until: grant - .valid_until_secs - .and_then(|s| chrono::DateTime::from_timestamp(s, 0)), + valid_from: grant_timestamp(grant.valid_from_secs)?, + valid_until: grant_timestamp(grant.valid_until_secs)?, max_gas_fee_per_gas: grant.max_gas_fee_per_gas.map(U256::from_be_bytes), max_priority_fee_per_gas: grant.max_priority_fee_per_gas.map(U256::from_be_bytes), rate_limit: grant.rate_limit.map(|r| TransactionRateLimit { @@ -414,3 +420,27 @@ impl EvmActor { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{Error, grant_timestamp}; + + #[test] + fn absent_timestamp_stays_absent() { + assert!(grant_timestamp(None).unwrap().is_none()); + } + + #[test] + fn in_range_timestamp_is_converted() { + let converted = grant_timestamp(Some(1_800_000_000)).unwrap(); + assert_eq!(converted.unwrap().timestamp(), 1_800_000_000); + } + + /// An unrepresentable expiry must not silently become "no expiry": that would widen the + /// grant beyond what was voted on. + #[test] + fn out_of_range_timestamp_is_an_error() { + let err = grant_timestamp(Some(i64::MAX)).unwrap_err(); + assert!(matches!(err, Error::InvalidTimestamp(i64::MAX))); + } +}