use soroban_sdk::{
contracttype, panic_with_error, xdr::ToXdr, Address, Bytes, BytesN, Env, Symbol, Val, Vec,
};
use crate::timelock::{
emit_min_delay_changed, emit_operation_cancelled, emit_operation_executed,
emit_operation_scheduled, TimelockError, DONE_LEDGER, TIMELOCK_EXTEND_AMOUNT,
TIMELOCK_TTL_THRESHOLD, UNSET_LEDGER,
};
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Operation {
pub target: Address,
pub function: Symbol,
pub args: Vec<Val>,
pub predecessor: BytesN<32>,
pub salt: BytesN<32>,
}
#[contracttype]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum OperationState {
Unset,
Waiting,
Ready,
Done,
}
#[derive(Clone)]
#[contracttype]
pub enum TimelockStorageKey {
MinDelay,
OperationLedger(BytesN<32>),
}
pub fn get_min_delay(e: &Env) -> u32 {
e.storage()
.instance()
.get(&TimelockStorageKey::MinDelay)
.unwrap_or_else(|| panic_with_error!(e, TimelockError::MinDelayNotSet))
}
pub fn get_operation_ledger(e: &Env, operation_id: &BytesN<32>) -> u32 {
let key = TimelockStorageKey::OperationLedger(operation_id.clone());
if let Some(ready_ledger) = e.storage().persistent().get::<_, u32>(&key) {
e.storage().persistent().extend_ttl(&key, TIMELOCK_TTL_THRESHOLD, TIMELOCK_EXTEND_AMOUNT);
ready_ledger
} else {
UNSET_LEDGER
}
}
pub fn get_operation_state(e: &Env, operation_id: &BytesN<32>) -> OperationState {
let ready_ledger = get_operation_ledger(e, operation_id);
let current_ledger = e.ledger().sequence();
match ready_ledger {
UNSET_LEDGER => OperationState::Unset,
DONE_LEDGER => OperationState::Done,
ready if ready > current_ledger => OperationState::Waiting,
_ => OperationState::Ready,
}
}
pub fn operation_exists(e: &Env, operation_id: &BytesN<32>) -> bool {
get_operation_state(e, operation_id) != OperationState::Unset
}
pub fn is_operation_pending(e: &Env, operation_id: &BytesN<32>) -> bool {
let state = get_operation_state(e, operation_id);
state == OperationState::Waiting || state == OperationState::Ready
}
pub fn is_operation_ready(e: &Env, operation_id: &BytesN<32>) -> bool {
get_operation_state(e, operation_id) == OperationState::Ready
}
pub fn is_operation_done(e: &Env, operation_id: &BytesN<32>) -> bool {
get_operation_state(e, operation_id) == OperationState::Done
}
pub fn set_min_delay(e: &Env, min_delay: u32) {
let old_delay =
e.storage().instance().get::<_, u32>(&TimelockStorageKey::MinDelay).unwrap_or(0);
e.storage().instance().set(&TimelockStorageKey::MinDelay, &min_delay);
emit_min_delay_changed(e, old_delay, min_delay);
}
pub fn schedule_operation(e: &Env, operation: &Operation, delay: u32) -> BytesN<32> {
let id = hash_operation(e, operation);
if operation_exists(e, &id) {
panic_with_error!(e, TimelockError::OperationAlreadyScheduled);
}
let min_delay = get_min_delay(e);
if delay < min_delay {
panic_with_error!(e, TimelockError::InsufficientDelay);
}
let current_ledger = e.ledger().sequence();
let ready_ledger = current_ledger.saturating_add(delay);
let key = TimelockStorageKey::OperationLedger(id.clone());
e.storage().persistent().set(&key, &ready_ledger);
emit_operation_scheduled(
e,
&id,
&operation.target,
&operation.function,
&operation.args,
&operation.predecessor,
&operation.salt,
delay,
);
id
}
pub fn execute_operation(e: &Env, operation: &Operation) -> Val {
set_execute_operation(e, operation);
e.invoke_contract::<Val>(&operation.target, &operation.function, operation.args.clone())
}
pub fn set_execute_operation(e: &Env, operation: &Operation) {
let id = hash_operation(e, operation);
if !is_operation_ready(e, &id) {
panic_with_error!(e, TimelockError::InvalidOperationState);
}
let no_predecessor = BytesN::<32>::from_array(e, &[0u8; 32]);
if operation.predecessor != no_predecessor && !is_operation_done(e, &operation.predecessor) {
panic_with_error!(e, TimelockError::UnexecutedPredecessor);
}
let key = TimelockStorageKey::OperationLedger(id.clone());
e.storage().persistent().set(&key, &DONE_LEDGER);
emit_operation_executed(
e,
&id,
&operation.target,
&operation.function,
&operation.args,
&operation.predecessor,
&operation.salt,
);
}
pub fn cancel_operation(e: &Env, operation_id: &BytesN<32>) {
if !is_operation_pending(e, operation_id) {
panic_with_error!(e, TimelockError::InvalidOperationState);
}
let key = TimelockStorageKey::OperationLedger(operation_id.clone());
e.storage().persistent().remove(&key);
emit_operation_cancelled(e, operation_id);
}
pub fn hash_operation(e: &Env, operation: &Operation) -> BytesN<32> {
let mut data = Bytes::new(e);
data.append(&operation.target.clone().to_xdr(e));
data.append(&operation.function.clone().to_xdr(e));
data.append(&operation.args.clone().to_xdr(e));
data.append(&operation.predecessor.clone().into());
data.append(&operation.salt.clone().into());
e.crypto().keccak256(&data).into()
}