radix_engine/blueprints/transaction_tracker/
package.rs1use crate::errors::{ApplicationError, RuntimeError};
2use crate::internal_prelude::*;
3use radix_blueprint_schema_init::{
4 BlueprintCollectionSchema, BlueprintEventSchemaInit, BlueprintFunctionsSchemaInit, FieldSchema,
5 FunctionSchemaInit, TypeRef,
6};
7use radix_blueprint_schema_init::{BlueprintSchemaInit, BlueprintStateSchemaInit};
8use radix_engine_interface::api::{AttachedModuleId, FieldValue, SystemApi};
9use radix_engine_interface::blueprints::package::{
10 AuthConfig, BlueprintDefinitionInit, BlueprintType, FunctionAuth, MethodAuthTemplate,
11 PackageDefinition,
12};
13use radix_engine_interface::blueprints::transaction_tracker::*;
14use radix_native_sdk::modules::metadata::Metadata;
15use radix_native_sdk::modules::role_assignment::RoleAssignment;
16use radix_native_sdk::runtime::Runtime;
17
18pub use radix_common::prelude::TRANSACTION_TRACKER_BLUEPRINT;
19
20pub type TransactionTrackerCreateOutput = ComponentAddress;
21
22pub struct TransactionTrackerNativePackage;
23
24pub const PARTITION_RANGE_START: u8 = MAIN_BASE_PARTITION.0 + 1;
25pub const PARTITION_RANGE_END: u8 = u8::MAX;
26pub const EPOCHS_PER_PARTITION: u64 = 100;
27
28impl TransactionTrackerNativePackage {
29 pub fn definition() -> PackageDefinition {
30 let mut aggregator = TypeAggregator::<ScryptoCustomTypeKind>::new();
31 let key_type_id = aggregator.add_child_type_and_descendents::<Hash>();
32 let value_type_id = aggregator.add_child_type_and_descendents::<TransactionStatus>();
33
34 let mut collections: Vec<BlueprintCollectionSchema<TypeRef<LocalTypeId>>> = vec![];
35 for _ in PARTITION_RANGE_START..=PARTITION_RANGE_END {
36 collections.push(BlueprintCollectionSchema::KeyValueStore(
37 BlueprintKeyValueSchema {
38 key: TypeRef::Static(key_type_id),
39 value: TypeRef::Static(value_type_id),
40 allow_ownership: false,
41 },
42 ))
43 }
44
45 let fields = vec![FieldSchema::static_field(
46 aggregator.add_child_type_and_descendents::<TransactionTrackerSubstate>(),
47 )];
48
49 let mut functions = index_map_new();
50 functions.insert(
51 TRANSACTION_TRACKER_CREATE_IDENT.to_string(),
52 FunctionSchemaInit {
53 receiver: None,
54 input: TypeRef::Static(
55 aggregator.add_child_type_and_descendents::<TransactionTrackerCreateInput>(),
56 ),
57 output: TypeRef::Static(
58 aggregator.add_child_type_and_descendents::<TransactionTrackerCreateOutput>(),
59 ),
60 export: TRANSACTION_TRACKER_CREATE_EXPORT_NAME.to_string(),
61 },
62 );
63
64 let schema = generate_full_schema(aggregator);
65 let blueprints = indexmap!(
66 TRANSACTION_TRACKER_BLUEPRINT.to_string() => BlueprintDefinitionInit {
67 blueprint_type: BlueprintType::default(),
68 is_transient: false,
69 dependencies: indexset!(
70 ),
71 feature_set: indexset!(),
72 schema: BlueprintSchemaInit {
73 generics: vec![],
74 schema,
75 state: BlueprintStateSchemaInit {
76 fields,
77 collections,
78 },
79 events: BlueprintEventSchemaInit::default(),
80 types: BlueprintTypeSchemaInit::default(),
81 functions: BlueprintFunctionsSchemaInit {
82 functions,
83 },
84 hooks: BlueprintHooksInit::default(),
85 },
86
87 royalty_config: PackageRoyaltyConfig::default(),
88 auth_config: AuthConfig {
89 function_auth: FunctionAuth::AccessRules(
90 indexmap!(
91 TRANSACTION_TRACKER_CREATE_IDENT.to_string() => rule!(require(system_execution(SystemExecution::Protocol))),
92 )
93 ),
94 method_auth: MethodAuthTemplate::default(),
95 },
96 }
97 );
98
99 PackageDefinition { blueprints }
100 }
101
102 pub fn invoke_export<Y: SystemApi<RuntimeError>>(
103 export_name: &str,
104 input: &IndexedScryptoValue,
105 api: &mut Y,
106 ) -> Result<IndexedScryptoValue, RuntimeError> {
107 match export_name {
108 TRANSACTION_TRACKER_CREATE_EXPORT_NAME => {
109 let input: TransactionTrackerCreateInput = input.as_typed().map_err(|e| {
110 RuntimeError::ApplicationError(ApplicationError::InputDecodeError(e))
111 })?;
112
113 let rtn = TransactionTrackerBlueprint::create(input.address_reservation, api)?;
114
115 Ok(IndexedScryptoValue::from_typed(&rtn))
116 }
117
118 _ => Err(RuntimeError::ApplicationError(
119 ApplicationError::ExportDoesNotExist(export_name.to_string()),
120 )),
121 }
122 }
123}
124
125#[derive(Debug, Clone, ScryptoSbor)]
126pub enum TransactionStatus {
127 V1(TransactionStatusV1),
128}
129
130impl TransactionStatus {
131 pub fn into_v1(self) -> TransactionStatusV1 {
132 match self {
133 TransactionStatus::V1(status) => status,
134 }
135 }
136}
137
138#[derive(Debug, Clone, ScryptoSbor)]
139pub enum TransactionStatusV1 {
140 CommittedSuccess,
141 CommittedFailure,
142 Cancelled,
143}
144
145pub type TransactionStatusSubstateContents = TransactionStatus;
146
147#[derive(Debug, Clone, ScryptoSbor)]
148pub enum TransactionTrackerSubstate {
149 V1(TransactionTrackerSubstateV1),
150}
151
152impl TransactionTrackerSubstate {
153 pub fn v1(&self) -> &TransactionTrackerSubstateV1 {
154 match self {
155 TransactionTrackerSubstate::V1(tracker) => tracker,
156 }
157 }
158
159 pub fn into_v1(self) -> TransactionTrackerSubstateV1 {
160 match self {
161 TransactionTrackerSubstate::V1(tracker) => tracker,
162 }
163 }
164
165 pub fn v1_mut(&mut self) -> &mut TransactionTrackerSubstateV1 {
166 match self {
167 TransactionTrackerSubstate::V1(tracker) => tracker,
168 }
169 }
170}
171
172#[derive(Debug, Clone, ScryptoSbor)]
173pub struct TransactionTrackerSubstateV1 {
174 pub start_epoch: u64,
175 pub start_partition: u8,
176
177 pub partition_range_start_inclusive: u8,
179 pub partition_range_end_inclusive: u8,
180 pub epochs_per_partition: u64,
181}
182
183impl TransactionTrackerSubstateV1 {
184 pub fn partition_for_expiry_epoch(&self, epoch: Epoch) -> Option<u8> {
185 let epoch = epoch.number();
186
187 let num_partitions =
189 self.partition_range_end_inclusive - self.partition_range_start_inclusive + 1;
190 let max_epoch_exclusive =
191 self.start_epoch + num_partitions as u64 * self.epochs_per_partition;
192 if epoch < self.start_epoch || epoch >= max_epoch_exclusive {
193 return None;
194 }
195
196 let mut partition_number =
198 self.start_partition as u64 + (epoch - self.start_epoch) / self.epochs_per_partition;
199 if partition_number > self.partition_range_end_inclusive as u64 {
200 partition_number -= num_partitions as u64;
201 }
202
203 assert!(partition_number >= self.partition_range_start_inclusive as u64);
204 assert!(partition_number <= self.partition_range_end_inclusive as u64);
205
206 Some(partition_number as u8)
207 }
208
209 pub fn advance(&mut self) -> u8 {
212 let old_start_partition = self.start_partition;
213 self.start_epoch += self.epochs_per_partition;
214 self.start_partition = if self.start_partition == self.partition_range_end_inclusive {
215 self.partition_range_start_inclusive
216 } else {
217 self.start_partition + 1
218 };
219 old_start_partition
220 }
221}
222
223pub struct TransactionTrackerBlueprint;
224
225impl TransactionTrackerBlueprint {
226 pub fn create<Y: SystemApi<RuntimeError>>(
227 address_reservation: GlobalAddressReservation,
228 api: &mut Y,
229 ) -> Result<GlobalAddress, RuntimeError> {
230 let current_epoch = Runtime::current_epoch(api)?;
231 let intent_store = api.new_simple_object(
232 TRANSACTION_TRACKER_BLUEPRINT,
233 indexmap!(
234 0u8 => FieldValue::new(TransactionTrackerSubstate::V1(TransactionTrackerSubstateV1{
235 start_epoch: current_epoch.number(),
236 start_partition: PARTITION_RANGE_START,
237 partition_range_start_inclusive: PARTITION_RANGE_START,
238 partition_range_end_inclusive: PARTITION_RANGE_END,
239 epochs_per_partition: EPOCHS_PER_PARTITION,
240 }))
241 ),
242 )?;
243 let role_assignment = RoleAssignment::create(OwnerRole::None, indexmap!(), api)?.0;
244 let metadata = Metadata::create(api)?;
245
246 let address = api.globalize(
247 intent_store,
248 indexmap!(
249 AttachedModuleId::RoleAssignment => role_assignment.0,
250 AttachedModuleId::Metadata => metadata.0,
251 ),
252 Some(address_reservation),
253 )?;
254 Ok(address)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use radix_transactions::validation::TransactionValidationConfig;
261
262 use super::*;
263
264 #[test]
265 fn calculate_coverage() {
266 let max_epoch_range = TransactionValidationConfig::latest().max_epoch_range;
267 let covered_epochs = (EPOCHS_PER_PARTITION as f64
268 * (PARTITION_RANGE_END as f64 - (PARTITION_RANGE_START as f64 - 1.0) - 1.0))
269 .floor() as u64;
270 let covered_days = covered_epochs
271 * 5 / 60
273 / 24;
274 assert!(covered_epochs >= max_epoch_range);
275 assert_eq!(covered_days, 65);
276 }
277
278 #[test]
279 fn test_partition_calculation() {
280 let mut store = TransactionTrackerSubstate::V1(TransactionTrackerSubstateV1 {
281 start_epoch: 256,
282 start_partition: 70,
283 partition_range_start_inclusive: PARTITION_RANGE_START,
284 partition_range_end_inclusive: PARTITION_RANGE_END,
285 epochs_per_partition: EPOCHS_PER_PARTITION,
286 });
287 let num_partitions = (PARTITION_RANGE_END - PARTITION_RANGE_START + 1) as u64;
288
289 assert_eq!(store.v1().partition_for_expiry_epoch(Epoch::of(0)), None);
290 assert_eq!(
291 store.v1().partition_for_expiry_epoch(Epoch::of(256)),
292 Some(70)
293 );
294 assert_eq!(
295 store
296 .v1()
297 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION - 1)),
298 Some(70)
299 );
300 assert_eq!(
301 store
302 .v1()
303 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION)),
304 Some(71)
305 );
306 assert_eq!(
307 store.v1().partition_for_expiry_epoch(Epoch::of(
308 256 + EPOCHS_PER_PARTITION * num_partitions - 1
309 )),
310 Some(69)
311 );
312 assert_eq!(
313 store
314 .v1()
315 .partition_for_expiry_epoch(Epoch::of(256 + EPOCHS_PER_PARTITION * num_partitions)),
316 None,
317 );
318
319 store.v1_mut().advance();
320 assert_eq!(store.v1().start_epoch, 256 + EPOCHS_PER_PARTITION);
321 assert_eq!(store.v1().start_partition, 71);
322 }
323}