1use iota_protocol_config::ProtocolConfig;
5use iota_stardust_sdk::types::block::output::{BasicOutput, OutputId};
6use move_core_types::{
7 ident_str,
8 identifier::IdentStr,
9 language_storage::{StructTag, TypeTag},
10};
11use serde::{Deserialize, Serialize};
12
13use super::{
14 label::label_struct_tag_to_string, stardust_upgrade_label::stardust_upgrade_label_type,
15};
16use crate::{
17 IOTA_FRAMEWORK_ADDRESS,
18 balance::Balance,
19 base_types::{IotaAddress, MoveObjectType, ObjectID, SequenceNumber, TxContext},
20 error::{ExecutionError, IotaError},
21 gas_coin::GasCoin,
22 id::UID,
23 object::{Data, MoveObject, Object, Owner},
24};
25
26#[cfg(test)]
27#[path = "../unit_tests/timelock/timelock_tests.rs"]
28mod timelock_tests;
29
30pub const TIMELOCK_MODULE_NAME: &IdentStr = ident_str!("timelock");
31pub const TIMELOCK_STRUCT_NAME: &IdentStr = ident_str!("TimeLock");
32
33pub const VESTED_REWARD_ID_PREFIX: &str =
36 "0xb191c4bc825ac6983789e50545d5ef07a1d293a98ad974fc9498cb18";
37
38#[derive(Debug, thiserror::Error)]
39pub enum VestedRewardError {
40 #[error("failed to create genesis move object, owner: {owner}, timelock: {timelock:#?}")]
41 ObjectCreation {
42 owner: IotaAddress,
43 timelock: TimeLock<Balance>,
44 source: ExecutionError,
45 },
46 #[error("a vested reward must not contain native tokens")]
47 NativeTokensNotSupported,
48 #[error("a basic output is not a vested reward")]
49 NotVestedReward,
50 #[error("a vested reward must have two unlock conditions")]
51 UnlockConditionsNumberMismatch,
52 #[error("only timelocked vested rewards can be migrated as `TimeLock<Balance<IOTA>>`")]
53 UnlockedVestedReward,
54}
55
56pub fn is_timelocked_vested_reward(
58 output_id: OutputId,
59 basic_output: &BasicOutput,
60 target_milestone_timestamp_sec: u32,
61) -> bool {
62 is_vested_reward(output_id, basic_output)
63 && basic_output
64 .unlock_conditions()
65 .is_time_locked(target_milestone_timestamp_sec)
66}
67
68pub fn is_vested_reward(output_id: OutputId, basic_output: &BasicOutput) -> bool {
73 let has_vesting_prefix = output_id.to_string().starts_with(VESTED_REWARD_ID_PREFIX);
74
75 has_vesting_prefix
76 && basic_output.unlock_conditions().timelock().is_some()
77 && basic_output.native_tokens().is_empty()
78 && basic_output.unlock_conditions().len() == 2
79 && basic_output.unlock_conditions().address().is_some()
80}
81
82pub fn try_from_stardust(
85 output_id: OutputId,
86 basic_output: &BasicOutput,
87 target_milestone_timestamp_sec: u32,
88) -> Result<TimeLock<Balance>, VestedRewardError> {
89 if !is_vested_reward(output_id, basic_output) {
90 return Err(VestedRewardError::NotVestedReward);
91 }
92
93 if !basic_output
94 .unlock_conditions()
95 .is_time_locked(target_milestone_timestamp_sec)
96 {
97 return Err(VestedRewardError::UnlockedVestedReward);
98 }
99
100 if basic_output.unlock_conditions().len() != 2 {
101 return Err(VestedRewardError::UnlockConditionsNumberMismatch);
102 }
103
104 if !basic_output.native_tokens().is_empty() {
105 return Err(VestedRewardError::NativeTokensNotSupported);
106 }
107
108 let id = UID::new(ObjectID::new(output_id.hash()));
109 let locked = Balance::new(basic_output.amount());
110
111 let timelock_uc = basic_output
114 .unlock_conditions()
115 .timelock()
116 .expect("a vested reward should contain a timelock unlock condition");
117 let expiration_timestamp_ms = Into::<u64>::into(timelock_uc.timestamp()) * 1000;
118
119 let label = Option::Some(label_struct_tag_to_string(stardust_upgrade_label_type()));
120
121 Ok(TimeLock::new(id, locked, expiration_timestamp_ms, label))
122}
123
124pub fn to_genesis_object(
126 timelock: TimeLock<Balance>,
127 owner: IotaAddress,
128 protocol_config: &ProtocolConfig,
129 tx_context: &TxContext,
130 version: SequenceNumber,
131) -> Result<Object, VestedRewardError> {
132 let move_object = {
133 MoveObject::new_from_execution(
134 MoveObjectType::timelocked_iota_balance(),
135 version,
136 timelock.to_bcs_bytes(),
137 protocol_config,
138 )
139 .map_err(|source| VestedRewardError::ObjectCreation {
140 owner,
141 timelock,
142 source,
143 })?
144 };
145
146 Ok(Object::new_from_genesis(
147 Data::Move(move_object),
148 Owner::AddressOwner(owner),
149 tx_context.digest(),
150 ))
151}
152
153#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
155pub struct TimeLock<T> {
156 id: UID,
157 locked: T,
159 expiration_timestamp_ms: u64,
161 label: Option<String>,
163}
164
165impl<T> TimeLock<T> {
166 pub fn new(id: UID, locked: T, expiration_timestamp_ms: u64, label: Option<String>) -> Self {
168 Self {
169 id,
170 locked,
171 expiration_timestamp_ms,
172 label,
173 }
174 }
175
176 pub fn type_(type_param: TypeTag) -> StructTag {
178 StructTag {
179 address: IOTA_FRAMEWORK_ADDRESS,
180 module: TIMELOCK_MODULE_NAME.to_owned(),
181 name: TIMELOCK_STRUCT_NAME.to_owned(),
182 type_params: vec![type_param],
183 }
184 }
185
186 pub fn id(&self) -> &ObjectID {
188 self.id.object_id()
189 }
190
191 pub fn locked(&self) -> &T {
193 &self.locked
194 }
195
196 pub fn expiration_timestamp_ms(&self) -> u64 {
198 self.expiration_timestamp_ms
199 }
200
201 pub fn label(&self) -> &Option<String> {
203 &self.label
204 }
205}
206
207impl<'de, T> TimeLock<T>
208where
209 T: Serialize + Deserialize<'de>,
210{
211 pub fn from_bcs_bytes(content: &'de [u8]) -> Result<Self, IotaError> {
213 bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
214 error: format!("Unable to deserialize TimeLock object: {:?}", err),
215 })
216 }
217
218 pub fn to_bcs_bytes(&self) -> Vec<u8> {
220 bcs::to_bytes(&self).unwrap()
221 }
222}
223
224pub fn is_timelock(other: &StructTag) -> bool {
226 other.address == IOTA_FRAMEWORK_ADDRESS
227 && other.module.as_ident_str() == TIMELOCK_MODULE_NAME
228 && other.name.as_ident_str() == TIMELOCK_STRUCT_NAME
229}
230
231pub fn is_timelocked_balance(other: &StructTag) -> bool {
233 if !is_timelock(other) {
234 return false;
235 }
236
237 if other.type_params.len() != 1 {
238 return false;
239 }
240
241 match &other.type_params[0] {
242 TypeTag::Struct(tag) => Balance::is_balance(tag),
243 _ => false,
244 }
245}
246
247pub fn is_timelocked_gas_balance(other: &StructTag) -> bool {
249 if !is_timelock(other) {
250 return false;
251 }
252
253 if other.type_params.len() != 1 {
254 return false;
255 }
256
257 match &other.type_params[0] {
258 TypeTag::Struct(tag) => GasCoin::is_gas_balance(tag),
259 _ => false,
260 }
261}
262
263impl<'de, T> TryFrom<&'de Object> for TimeLock<T>
264where
265 T: Serialize + Deserialize<'de>,
266{
267 type Error = IotaError;
268
269 fn try_from(object: &'de Object) -> Result<Self, Self::Error> {
270 match &object.data {
271 Data::Move(o) => {
272 if o.type_().is_timelock() {
273 return TimeLock::from_bcs_bytes(o.contents());
274 }
275 }
276 Data::Package(_) => {}
277 }
278
279 Err(IotaError::Type {
280 error: format!("Object type is not a TimeLock: {:?}", object),
281 })
282 }
283}