Smart Contract
FlowTransactionSchedulerV3
A.7d19efcd8e5b4a4a.FlowTransactionSchedulerV3
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import FlowFees from 0xf919ee77447b7497
4import FlowStorageFees from 0xe467b9dd11fa00df
5import ViewResolver from 0x1d7e57aa55817448
6
7/// FlowTransactionSchedulerV3 enables smart contracts to schedule autonomous execution in the future.
8///
9/// This contract implements FLIP 330's scheduled transaction system, allowing contracts to "wake up" and execute
10/// logic at predefined times without external triggers.
11///
12/// Scheduled transactions are prioritized (High/Medium/Low) with different execution guarantees and fee multipliers:
13/// - High priority guarantees first-block execution,
14/// - Medium priority provides best-effort scheduling,
15/// - Low priority executes opportunistically when capacity allows after the time it was scheduled.
16///
17/// The system uses time slots with execution effort limits to manage network resources,
18/// ensuring predictable performance while enabling novel autonomous blockchain patterns like recurring
19/// payments, automated arbitrage, and time-based contract logic.
20///
21/// V3: Simplified priority pools - each priority has its own independent pool with no shared capacity.
22access(all) contract FlowTransactionSchedulerV3 {
23
24 /// singleton instance used to store all scheduled transaction data
25 /// and route all scheduled transaction functionality
26 access(self) var sharedScheduler: Capability<auth(Cancel) &SharedScheduler>
27
28 /// storage path for the singleton scheduler resource
29 access(all) let storagePath: StoragePath
30
31 /// Enums
32
33 /// Priority
34 access(all) enum Priority: UInt8 {
35 access(all) case High
36 access(all) case Medium
37 access(all) case Low
38 }
39
40 /// Status
41 access(all) enum Status: UInt8 {
42 /// unknown statuses are used for handling historic scheduled transactions with null statuses
43 access(all) case Unknown
44 /// mutable status
45 access(all) case Scheduled
46 /// finalized statuses
47 access(all) case Executed
48 access(all) case Canceled
49 }
50
51 /// Events
52
53 /// Emitted when a transaction is scheduled
54 access(all) event Scheduled(
55 id: UInt64,
56 priority: UInt8,
57 timestamp: UFix64,
58 executionEffort: UInt64,
59 fees: UFix64,
60 transactionHandlerOwner: Address,
61 transactionHandlerTypeIdentifier: String,
62 transactionHandlerUUID: UInt64,
63
64 // The public path of the transaction handler that can be used to resolve views
65 // DISCLAIMER: There is no guarantee that the public path is accurate
66 transactionHandlerPublicPath: PublicPath?
67 )
68
69 /// Emitted when a scheduled transaction's scheduled timestamp is reached and it is ready for execution
70 access(all) event PendingExecution(
71 id: UInt64,
72 priority: UInt8,
73 executionEffort: UInt64,
74 fees: UFix64,
75 transactionHandlerOwner: Address,
76 transactionHandlerTypeIdentifier: String
77 )
78
79 /// Emitted when a scheduled transaction is executed by the FVM
80 access(all) event Executed(
81 id: UInt64,
82 priority: UInt8,
83 executionEffort: UInt64,
84 transactionHandlerOwner: Address,
85 transactionHandlerTypeIdentifier: String,
86 transactionHandlerUUID: UInt64,
87
88 // The public path of the transaction handler that can be used to resolve views
89 // DISCLAIMER: There is no guarantee that the public path is accurate
90 transactionHandlerPublicPath: PublicPath?
91 )
92
93 /// Emitted when a scheduled transaction is canceled by the creator of the transaction
94 access(all) event Canceled(
95 id: UInt64,
96 priority: UInt8,
97 feesReturned: UFix64,
98 feesDeducted: UFix64,
99 transactionHandlerOwner: Address,
100 transactionHandlerTypeIdentifier: String
101 )
102
103 /// Emitted when a collection limit is reached
104 /// The limit that was reached is non-nil and is the limit that was reached
105 /// The other limit that was not reached is nil
106 access(all) event CollectionLimitReached(
107 collectionEffortLimit: UInt64?,
108 collectionTransactionsLimit: Int?
109 )
110
111 access(all) event RemovalLimitReached(id: UInt64, remainingLength: Int)
112 access(all) event TransactionAdded(id: UInt64)
113 access(all) event TransactionRemoved(id: UInt64)
114
115 // Emitted when one or more of the configuration details fields are updated
116 // Event listeners can listen to this and query the new configuration
117 // if they need to
118 access(all) event ConfigUpdated()
119
120 /// Entitlements
121 access(all) entitlement Execute
122 access(all) entitlement Process
123 access(all) entitlement Cancel
124 access(all) entitlement UpdateConfig
125
126 /// Interfaces
127
128 /// TransactionHandler is an interface that defines a single method executeTransaction that
129 /// must be implemented by the resource that contains the logic to be executed by the scheduled transaction.
130 /// An authorized capability to this resource is provided when scheduling a transaction.
131 /// The transaction scheduler uses this capability to execute the transaction when its scheduled timestamp arrives.
132 access(all) resource interface TransactionHandler: ViewResolver.Resolver {
133
134 access(all) view fun getViews(): [Type] {
135 return []
136 }
137
138 access(all) fun resolveView(_ view: Type): AnyStruct? {
139 return nil
140 }
141
142 /// Executes the implemented transaction logic
143 ///
144 /// @param id: The id of the scheduled transaction (this can be useful for any internal tracking)
145 /// @param data: The data that was passed when the transaction was originally scheduled
146 /// that may be useful for the execution of the transaction logic
147 access(Execute) fun executeTransaction(id: UInt64, data: AnyStruct?)
148 }
149
150 /// Structs
151
152 /// ScheduledTransaction is the resource that the user receives after scheduling a transaction.
153 /// It allows them to get the status of their transaction and can be passed back
154 /// to the scheduler contract to cancel the transaction if it has not yet been executed.
155 access(all) resource ScheduledTransaction {
156 access(all) let id: UInt64
157 access(all) let timestamp: UFix64
158 access(all) let handlerTypeIdentifier: String
159
160 access(all) view fun status(): Status? {
161 return FlowTransactionSchedulerV3.sharedScheduler.borrow()!.getStatus(id: self.id)
162 }
163
164 init(
165 id: UInt64,
166 timestamp: UFix64,
167 handlerTypeIdentifier: String
168 ) {
169 self.id = id
170 self.timestamp = timestamp
171 self.handlerTypeIdentifier = handlerTypeIdentifier
172 }
173
174 // event emitted when the resource is destroyed
175 access(all) event ResourceDestroyed(id: UInt64 = self.id, timestamp: UFix64 = self.timestamp, handlerTypeIdentifier: String = self.handlerTypeIdentifier)
176 }
177
178 /// EstimatedScheduledTransaction contains data for estimating transaction scheduling.
179 access(all) struct EstimatedScheduledTransaction {
180 /// flowFee is the estimated fee in Flow for the transaction to be scheduled
181 access(all) let flowFee: UFix64?
182 /// timestamp is estimated timestamp that the transaction will be executed at
183 access(all) let timestamp: UFix64?
184 /// error is an optional error message if the transaction cannot be scheduled
185 access(all) let error: String?
186
187 access(contract) view init(flowFee: UFix64?, timestamp: UFix64?, error: String?) {
188 self.flowFee = flowFee
189 self.timestamp = timestamp
190 self.error = error
191 }
192 }
193
194 /// Transaction data is a representation of a scheduled transaction
195 /// It is the source of truth for an individual transaction and stores the
196 /// capability to the handler that contains the logic that will be executed by the transaction.
197 access(all) struct TransactionData {
198 access(all) let id: UInt64
199 access(all) let priority: Priority
200 access(all) let executionEffort: UInt64
201 access(all) var status: Status
202
203 /// Fee amount to pay for the transaction
204 access(all) let fees: UFix64
205
206 /// The timestamp that the transaction is scheduled for
207 /// For medium priority transactions, it may be different than the requested timestamp
208 /// For low priority transactions, it is the requested timestamp,
209 /// but the timestamp where the transaction is actually executed may be different
210 access(all) var scheduledTimestamp: UFix64
211
212 /// Capability to the logic that the transaction will execute
213 access(contract) let handler: Capability<auth(Execute) &{TransactionHandler}>
214
215 /// Type identifier of the transaction handler
216 access(all) let handlerTypeIdentifier: String
217 access(all) let handlerAddress: Address
218
219 /// Optional data that can be passed to the handler
220 access(contract) let data: AnyStruct?
221
222 access(contract) init(
223 id: UInt64,
224 handler: Capability<auth(Execute) &{TransactionHandler}>,
225 scheduledTimestamp: UFix64,
226 data: AnyStruct?,
227 priority: Priority,
228 executionEffort: UInt64,
229 fees: UFix64,
230 ) {
231 self.id = id
232 self.handler = handler
233 self.data = data
234 self.priority = priority
235 self.executionEffort = executionEffort
236 self.fees = fees
237 self.status = Status.Scheduled
238 let handlerRef = handler.borrow()
239 ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
240 self.handlerAddress = handler.address
241 self.handlerTypeIdentifier = handlerRef.getType().identifier
242 self.scheduledTimestamp = scheduledTimestamp
243 }
244
245 /// setStatus updates the status of the transaction.
246 /// It panics if the transaction status is already finalized.
247 access(contract) fun setStatus(newStatus: Status) {
248 pre {
249 newStatus != Status.Unknown: "Invalid status: New status cannot be Unknown"
250 self.status != Status.Executed && self.status != Status.Canceled:
251 "Invalid status: Transaction with id \(self.id) is already finalized"
252 newStatus == Status.Executed ? self.status == Status.Scheduled : true:
253 "Invalid status: Transaction with id \(self.id) can only be set as Executed if it is Scheduled"
254 newStatus == Status.Canceled ? self.status == Status.Scheduled : true:
255 "Invalid status: Transaction with id \(self.id) can only be set as Canceled if it is Scheduled"
256 }
257
258 self.status = newStatus
259 }
260
261 /// setScheduledTimestamp updates the scheduled timestamp of the transaction.
262 /// It panics if the transaction status is already finalized.
263 access(contract) fun setScheduledTimestamp(newTimestamp: UFix64) {
264 pre {
265 self.status != Status.Executed && self.status != Status.Canceled:
266 "Invalid status: Transaction with id \(self.id) is already finalized"
267 }
268 self.scheduledTimestamp = newTimestamp
269 }
270
271 /// payAndRefundFees withdraws fees from the transaction based on the refund multiplier.
272 /// It deposits any leftover fees to the FlowFees vault to be used to pay node operator rewards
273 /// like any other transaction on the Flow network.
274 access(contract) fun payAndRefundFees(refundMultiplier: UFix64): @FlowToken.Vault {
275 pre {
276 refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
277 "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
278 }
279 if refundMultiplier == 0.0 {
280 FlowFees.deposit(from: <-FlowTransactionSchedulerV3.withdrawFees(amount: self.fees))
281 return <-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
282 } else {
283 let amountToReturn = self.fees * refundMultiplier
284 let amountToKeep = self.fees - amountToReturn
285 let feesToReturn <- FlowTransactionSchedulerV3.withdrawFees(amount: amountToReturn)
286 FlowFees.deposit(from: <-FlowTransactionSchedulerV3.withdrawFees(amount: amountToKeep))
287 return <-feesToReturn
288 }
289 }
290
291 /// getData copies and returns the data field
292 access(contract) view fun getData(): AnyStruct? {
293 return self.data
294 }
295
296 /// borrowHandler returns an un-entitled reference to the transaction handler
297 /// This allows users to query metadata views about the handler
298 /// @return: An un-entitled reference to the transaction handler
299 access(all) view fun borrowHandler(): &{TransactionHandler} {
300 return self.handler.borrow() as? &{TransactionHandler}
301 ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
302 }
303 }
304
305 /// Struct interface representing all the base configuration details in the Scheduler contract
306 /// that is used for governing the protocol
307 /// This is an interface to allow for the configuration details to be updated in the future
308 access(all) struct interface SchedulerConfig {
309
310 /// maximum effort that can be used for any transaction
311 access(all) var maximumIndividualEffort: UInt64
312
313 /// minimum execution effort is the minimum effort that can be
314 /// used for any transaction
315 access(all) var minimumExecutionEffort: UInt64
316
317 /// priority effort limit is the maximum cumulative effort per priority in a timeslot
318 /// Each priority has its own independent pool (no shared pool)
319 access(all) var priorityEffortLimit: {Priority: UInt64}
320
321 /// max data size is the maximum data size that can be stored for a transaction
322 access(all) var maxDataSizeMB: UFix64
323
324 /// priority fee multipliers are values we use to calculate the added
325 /// processing fee for each priority
326 access(all) var priorityFeeMultipliers: {Priority: UFix64}
327
328 /// refund multiplier is the portion of the fees that are refunded when any transaction is cancelled
329 access(all) var refundMultiplier: UFix64
330
331 /// canceledTransactionsLimit is the maximum number of canceled transactions
332 /// to keep in the canceledTransactions array
333 access(all) var canceledTransactionsLimit: UInt
334
335 /// collectionEffortLimit is the maximum effort that can be used for all transactions in a collection
336 access(all) var collectionEffortLimit: UInt64
337
338 /// collectionTransactionsLimit is the maximum number of transactions that can be processed in a collection
339 access(all) var collectionTransactionsLimit: Int
340
341 access(all) var removalTransactionsLimit: Int
342
343 /// maxTimestampSearchIterations is the maximum number of slots to search when looking for available capacity
344 access(all) var maxTimestampSearchIterations: Int
345
346 access(all) init(
347 maximumIndividualEffort: UInt64,
348 minimumExecutionEffort: UInt64,
349 priorityEffortLimit: {Priority: UInt64},
350 maxDataSizeMB: UFix64,
351 priorityFeeMultipliers: {Priority: UFix64},
352 refundMultiplier: UFix64,
353 canceledTransactionsLimit: UInt,
354 collectionEffortLimit: UInt64,
355 collectionTransactionsLimit: Int,
356 removalTransactionsLimit: Int,
357 maxTimestampSearchIterations: Int
358 ) {
359 pre {
360 refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
361 "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
362 priorityFeeMultipliers[Priority.Low]! >= 1.0:
363 "Invalid priority fee multiplier: Low priority multiplier must be greater than or equal to 1.0 but got \(priorityFeeMultipliers[Priority.Low]!)"
364 priorityFeeMultipliers[Priority.Medium]! > priorityFeeMultipliers[Priority.Low]!:
365 "Invalid priority fee multiplier: Medium priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Low]!) but got \(priorityFeeMultipliers[Priority.Medium]!)"
366 priorityFeeMultipliers[Priority.High]! > priorityFeeMultipliers[Priority.Medium]!:
367 "Invalid priority fee multiplier: High priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Medium]!) but got \(priorityFeeMultipliers[Priority.High]!)"
368 priorityEffortLimit[Priority.High]! > 0:
369 "Invalid priority effort limit: High priority effort limit must be greater than 0"
370 priorityEffortLimit[Priority.Medium]! > 0:
371 "Invalid priority effort limit: Medium priority effort limit must be greater than 0"
372 priorityEffortLimit[Priority.Low]! > 0:
373 "Invalid priority effort limit: Low priority effort limit must be greater than 0"
374 collectionTransactionsLimit >= 0:
375 "Invalid collection transactions limit: Collection transactions limit must be greater than or equal to 0 but got \(collectionTransactionsLimit)"
376 canceledTransactionsLimit >= 1:
377 "Invalid canceled transactions limit: Canceled transactions limit must be greater than or equal to 1 but got \(canceledTransactionsLimit)"
378 removalTransactionsLimit >= 0:
379 "Invalid removal transactions limit: Removal transactions limit must be greater than or equal to 0 but got \(removalTransactionsLimit)"
380 maxTimestampSearchIterations >= 1:
381 "Invalid max timestamp search iterations: Must be at least 1 but got \(maxTimestampSearchIterations)"
382 }
383 }
384 }
385
386 /// Concrete implementation of the SchedulerConfig interface
387 /// This struct is used to store the configuration details in the Scheduler contract
388 access(all) struct Config: SchedulerConfig {
389 access(all) var maximumIndividualEffort: UInt64
390 access(all) var minimumExecutionEffort: UInt64
391 access(all) var priorityEffortLimit: {Priority: UInt64}
392 access(all) var maxDataSizeMB: UFix64
393 access(all) var priorityFeeMultipliers: {Priority: UFix64}
394 access(all) var refundMultiplier: UFix64
395 access(all) var canceledTransactionsLimit: UInt
396 access(all) var collectionEffortLimit: UInt64
397 access(all) var collectionTransactionsLimit: Int
398 access(all) var removalTransactionsLimit: Int
399 access(all) var maxTimestampSearchIterations: Int
400
401 access(all) init(
402 maximumIndividualEffort: UInt64,
403 minimumExecutionEffort: UInt64,
404 priorityEffortLimit: {Priority: UInt64},
405 maxDataSizeMB: UFix64,
406 priorityFeeMultipliers: {Priority: UFix64},
407 refundMultiplier: UFix64,
408 canceledTransactionsLimit: UInt,
409 collectionEffortLimit: UInt64,
410 collectionTransactionsLimit: Int,
411 removalTransactionsLimit: Int,
412 maxTimestampSearchIterations: Int
413 ) {
414 self.maximumIndividualEffort = maximumIndividualEffort
415 self.minimumExecutionEffort = minimumExecutionEffort
416 self.priorityEffortLimit = priorityEffortLimit
417 self.maxDataSizeMB = maxDataSizeMB
418 self.priorityFeeMultipliers = priorityFeeMultipliers
419 self.refundMultiplier = refundMultiplier
420 self.canceledTransactionsLimit = canceledTransactionsLimit
421 self.collectionEffortLimit = collectionEffortLimit
422 self.collectionTransactionsLimit = collectionTransactionsLimit
423 self.removalTransactionsLimit = removalTransactionsLimit
424 self.maxTimestampSearchIterations = maxTimestampSearchIterations
425 }
426 }
427
428
429 /// SortedTimestamps maintains timestamps sorted in ascending order for efficient processing
430 /// It encapsulates all operations related to maintaining and querying sorted timestamps
431 access(all) struct SortedTimestamps {
432 /// Internal sorted array of timestamps
433 access(self) var timestamps: [UFix64]
434
435 access(all) init() {
436 self.timestamps = []
437 }
438
439 /// Add a timestamp to the sorted array maintaining sorted order
440 access(all) fun add(timestamp: UFix64) {
441
442 var insertIndex = 0
443 for i, ts in self.timestamps {
444 if timestamp < ts {
445 insertIndex = i
446 break
447 } else if timestamp == ts {
448 return
449 }
450 insertIndex = i + 1
451 }
452 self.timestamps.insert(at: insertIndex, timestamp)
453 }
454
455 /// Remove a timestamp from the sorted array
456 access(all) fun remove(timestamp: UFix64) {
457
458 let index = self.timestamps.firstIndex(of: timestamp)
459 if index != nil {
460 self.timestamps.remove(at: index!)
461 }
462 }
463
464 /// Get all timestamps that are in the past (less than or equal to current timestamp)
465 access(all) fun getBefore(current: UFix64): [UFix64] {
466 let pastTimestamps: [UFix64] = []
467 for timestamp in self.timestamps {
468 if timestamp <= current {
469 pastTimestamps.append(timestamp)
470 } else {
471 break // No need to check further since array is sorted
472 }
473 }
474 return pastTimestamps
475 }
476
477 /// Check if there are any timestamps that need processing
478 /// Returns true if processing is needed, false for early exit
479 access(all) fun hasBefore(current: UFix64): Bool {
480 return self.timestamps.length > 0 && self.timestamps[0] <= current
481 }
482
483 /// Get the whole array of timestamps
484 access(all) fun getAll(): [UFix64] {
485 return self.timestamps
486 }
487 }
488
489 /// Resources
490
491 /// Shared scheduler is a resource that is used as a singleton in the scheduler contract and contains
492 /// all the functionality to schedule, process and execute transactions as well as the internal state.
493 access(all) resource SharedScheduler {
494 /// nextID contains the next transaction ID to be assigned
495 /// This the ID is monotonically increasing and is used to identify each transaction
496 access(contract) var nextID: UInt64
497
498 /// transactions is a map of transaction IDs to TransactionData structs
499 access(contract) var transactions: {UInt64: TransactionData}
500
501 /// slot queues - separate maps per priority for O(1) access without nested dictionary copies
502 /// Maps timestamp -> (transactionID -> executionEffort)
503 access(contract) var slotQueueHigh: {UFix64: {UInt64: UInt64}}
504 access(contract) var slotQueueMedium: {UFix64: {UInt64: UInt64}}
505 access(contract) var slotQueueLow: {UFix64: {UInt64: UInt64}}
506
507 /// slot used effort - separate maps per priority for O(1) access
508 /// Maps timestamp -> total effort used for that priority
509 access(contract) var slotUsedEffortHigh: {UFix64: UInt64}
510 access(contract) var slotUsedEffortMedium: {UFix64: UInt64}
511 access(contract) var slotUsedEffortLow: {UFix64: UInt64}
512
513 /// sorted timestamps manager for efficient processing
514 access(contract) var sortedTimestamps: SortedTimestamps
515
516 /// canceled transactions keeps a record of canceled transaction IDs up to a canceledTransactionsLimit
517 access(contract) var canceledTransactions: [UInt64]
518
519 /// Struct that contains all the configuration details for the transaction scheduler protocol
520 /// Can be updated by the owner of the contract
521 access(contract) var config: {SchedulerConfig}
522
523 access(all) init() {
524 self.nextID = 1
525 self.canceledTransactions = [0 as UInt64]
526
527 self.transactions = {}
528 self.slotQueueHigh = {}
529 self.slotQueueMedium = {}
530 self.slotQueueLow = {}
531 self.slotUsedEffortHigh = {}
532 self.slotUsedEffortMedium = {}
533 self.slotUsedEffortLow = {}
534 self.sortedTimestamps = SortedTimestamps()
535
536 /* V2 Simplified slot efforts - each priority has its own independent pool:
537
538 Timestamp Slot (50kee total)
539 ┌─────────────────────────┐
540 │ ┌─────────────────────┐ │
541 │ │ High Pool (30kee) │ │ High: fail if full
542 │ └─────────────────────┘ │
543 │ ┌─────────────────────┐ │
544 │ │ Medium Pool (15kee) │ │ Medium: shift timestamp if full
545 │ └─────────────────────┘ │
546 │ ┌─────────────────────┐ │
547 │ │ Low Pool (5kee) │ │ Low: shift timestamp if full
548 │ └─────────────────────┘ │
549 └─────────────────────────┘
550 */
551
552 self.config = Config(
553 maximumIndividualEffort: 9999,
554 minimumExecutionEffort: 10,
555 priorityEffortLimit: {
556 Priority.High: 30_000,
557 Priority.Medium: 15_000,
558 Priority.Low: 5_000
559 },
560 maxDataSizeMB: 3.0,
561 priorityFeeMultipliers: {
562 Priority.High: 10.0,
563 Priority.Medium: 5.0,
564 Priority.Low: 2.0
565 },
566 refundMultiplier: 0.5,
567 canceledTransactionsLimit: 1000,
568 collectionEffortLimit: 500_000,
569 collectionTransactionsLimit: 150,
570 removalTransactionsLimit: 200,
571 maxTimestampSearchIterations: 1000
572 )
573 }
574
575 /// Gets a copy of the struct containing all the configuration details
576 /// of the Scheduler resource
577 access(contract) view fun getConfig(): {SchedulerConfig} {
578 return self.config
579 }
580
581 /// sets all the configuration details for the Scheduler resource
582 access(UpdateConfig) fun setConfig(newConfig: {SchedulerConfig}) {
583 self.config = newConfig
584 emit ConfigUpdated()
585 }
586
587 /// getTransaction returns a copy of the specified transaction
588 access(contract) view fun getTransaction(id: UInt64): TransactionData? {
589 return self.transactions[id]
590 }
591
592 /// borrowTransaction borrows a reference to the specified transaction
593 access(contract) view fun borrowTransaction(id: UInt64): &TransactionData? {
594 return &self.transactions[id]
595 }
596
597 /// getCanceledTransactions returns a copy of the canceled transactions array
598 access(contract) view fun getCanceledTransactions(): [UInt64] {
599 return self.canceledTransactions
600 }
601
602 /// getTransactionsForTimeframe returns a dictionary of transactions scheduled within a specified time range,
603 /// organized by timestamp and priority with arrays of transaction IDs.
604 /// WARNING: If you provide a time range that is too large, the function will likely fail to complete
605 /// because the function will run out of gas. Keep the time range small.
606 ///
607 /// @param startTimestamp: The start timestamp (inclusive) for the time range
608 /// @param endTimestamp: The end timestamp (inclusive) for the time range
609 /// @return {UFix64: {Priority: [UInt64]}}: A dictionary mapping timestamps to priorities to arrays of transaction IDs
610 access(contract) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
611 var transactionsInTimeframe: {UFix64: {UInt8: [UInt64]}} = {}
612
613 // Validate input parameters
614 if startTimestamp > endTimestamp {
615 return transactionsInTimeframe
616 }
617
618 // Get all timestamps that fall within the specified range
619 let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp)
620
621 for timestamp in allTimestampsBeforeEnd {
622 // Check if this timestamp falls within our range
623 if timestamp < startTimestamp { continue }
624
625 var timestampTransactions: {UInt8: [UInt64]} = {}
626
627 // Process high priority queue
628 let highQueue = self.slotQueueHigh[timestamp] ?? {}
629 if highQueue.keys.length > 0 {
630 timestampTransactions[Priority.High.rawValue] = highQueue.keys
631 }
632
633 // Process medium priority queue
634 let mediumQueue = self.slotQueueMedium[timestamp] ?? {}
635 if mediumQueue.keys.length > 0 {
636 timestampTransactions[Priority.Medium.rawValue] = mediumQueue.keys
637 }
638
639 // Process low priority queue
640 let lowQueue = self.slotQueueLow[timestamp] ?? {}
641 if lowQueue.keys.length > 0 {
642 timestampTransactions[Priority.Low.rawValue] = lowQueue.keys
643 }
644
645 if timestampTransactions.keys.length > 0 {
646 transactionsInTimeframe[timestamp] = timestampTransactions
647 }
648 }
649
650 return transactionsInTimeframe
651 }
652
653 /// calculate fee by converting execution effort to a fee in Flow tokens.
654 /// @param executionEffort: The execution effort of the transaction
655 /// @param priority: The priority of the transaction
656 /// @param dataSizeMB: The size of the data that was passed when the transaction was originally scheduled
657 /// @return UFix64: The fee in Flow tokens that is required to pay for the transaction
658 access(contract) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 {
659 // Use the official FlowFees calculation
660 let baseFee = FlowFees.computeFees(inclusionEffort: 1.0, executionEffort: UFix64(executionEffort)/100000000.0)
661
662 // Scale the execution fee by the multiplier for the priority
663 let scaledExecutionFee = baseFee * self.config.priorityFeeMultipliers[priority]!
664
665 // Calculate the FLOW required to pay for storage of the transaction data
666 let storageFee = FlowStorageFees.storageCapacityToFlow(dataSizeMB)
667
668 return scaledExecutionFee + storageFee
669 }
670
671 /// getNextIDAndIncrement returns the next ID and increments the ID counter
672 access(self) fun getNextIDAndIncrement(): UInt64 {
673 let nextID = self.nextID
674 self.nextID = self.nextID + 1
675 return nextID
676 }
677
678 /// get status of the scheduled transaction
679 /// @param id: The ID of the transaction to get the status of
680 /// @return Status: The status of the transaction, if the transaction is not found Unknown is returned.
681 access(contract) view fun getStatus(id: UInt64): Status? {
682 // if the transaction ID is greater than the next ID, it is not scheduled yet and has never existed
683 if id == 0 as UInt64 || id >= self.nextID {
684 return nil
685 }
686
687 // This should always return Scheduled or Executed
688 if let tx = self.borrowTransaction(id: id) {
689 return tx.status
690 }
691
692 // if the transaction was canceled and it is still not pruned from
693 // list return canceled status
694 if self.canceledTransactions.contains(id) {
695 return Status.Canceled
696 }
697
698 // if transaction ID is after first canceled ID it must be executed
699 // otherwise it would have been canceled and part of this list
700 let firstCanceledID = self.canceledTransactions[0]
701 if id > firstCanceledID {
702 return Status.Executed
703 }
704
705 // the transaction list was pruned and the transaction status might be
706 // either canceled or execute so we return unknown
707 return Status.Unknown
708 }
709
710 /// schedule is the primary entry point for scheduling a new transaction within the scheduler contract.
711 /// If scheduling the transaction is not possible either due to invalid arguments or due to
712 /// unavailable slots, the function panics.
713 //
714 /// The schedule function accepts the following arguments:
715 /// @param: transaction: A capability to a resource in storage that implements the transaction handler
716 /// interface. This handler will be invoked at execution time and will receive the specified data payload.
717 /// @param: timestamp: Specifies the earliest block timestamp at which the transaction is eligible for execution
718 /// (Unix timestamp so fractional seconds values are ignored). It must be set in the future.
719 /// @param: priority: An enum value (`High`, `Medium`, or `Low`) that influences the scheduling behavior and determines
720 /// how soon after the timestamp the transaction will be executed.
721 /// @param: executionEffort: Defines the maximum computational resources allocated to the transaction. This also determines
722 /// the fee charged. Unused execution effort is not refunded.
723 /// @param: fees: A Vault resource containing sufficient funds to cover the required execution effort.
724 access(contract) fun schedule(
725 handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
726 data: AnyStruct?,
727 timestamp: UFix64,
728 priority: Priority,
729 executionEffort: UInt64,
730 fees: @FlowToken.Vault
731 ): @ScheduledTransaction {
732 // Use the estimate function to validate inputs
733 let estimate = self.estimate(
734 data: data,
735 timestamp: timestamp,
736 priority: priority,
737 executionEffort: executionEffort
738 )
739
740 // Estimate returns an error for low priority transactions
741 // so need to check that the error is fine
742 // because low priority transactions are allowed in schedule
743 if estimate.error != nil && estimate.timestamp == nil {
744 panic(estimate.error!)
745 }
746
747 assert (
748 fees.balance >= estimate.flowFee!,
749 message: "Insufficient fees: The Fee balance of \(fees.balance) is not sufficient to pay the required amount of \(estimate.flowFee!) for execution of the transaction."
750 )
751
752 let transactionID = self.getNextIDAndIncrement()
753 let transactionData = TransactionData(
754 id: transactionID,
755 handler: handlerCap,
756 scheduledTimestamp: estimate.timestamp!,
757 data: data,
758 priority: priority,
759 executionEffort: executionEffort,
760 fees: fees.balance,
761 )
762
763 // Deposit the fees to the service account's vault
764 FlowTransactionSchedulerV3.depositFees(from: <-fees)
765
766 let handlerRef = handlerCap.borrow()
767 ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
768
769 let handlerPublicPath = handlerRef.resolveView(Type<PublicPath>()) as? PublicPath
770
771 emit Scheduled(
772 id: transactionData.id,
773 priority: transactionData.priority.rawValue,
774 timestamp: transactionData.scheduledTimestamp,
775 executionEffort: transactionData.executionEffort,
776 fees: transactionData.fees,
777 transactionHandlerOwner: transactionData.handler.address,
778 transactionHandlerTypeIdentifier: transactionData.handlerTypeIdentifier,
779 transactionHandlerUUID: handlerRef.uuid,
780 transactionHandlerPublicPath: handlerPublicPath
781 )
782
783 // Add the transaction to the slot queue and update the internal state
784 self.addTransaction(slot: estimate.timestamp!, txData: transactionData)
785
786 return <-create ScheduledTransaction(
787 id: transactionID,
788 timestamp: estimate.timestamp!,
789 handlerTypeIdentifier: transactionData.handlerTypeIdentifier
790 )
791 }
792
793 /// The estimate function calculates the required fee in Flow and expected execution timestamp for
794 /// a transaction based on the requested timestamp, priority, and execution effort.
795 ///
796 /// If the provided arguments are invalid or the transaction cannot be scheduled (e.g., due to
797 /// insufficient computation effort or unavailable time slots) the estimate function
798 /// returns an EstimatedScheduledTransaction struct with a non-nil error message.
799 ///
800 /// This helps developers ensure sufficient funding and preview the expected scheduling window,
801 /// reducing the risk of unnecessary cancellations.
802 ///
803 /// V2 Simplified: Each priority has its own independent pool. No shared pool logic.
804 ///
805 /// @param data: The data that was passed when the transaction was originally scheduled
806 /// @param timestamp: The requested timestamp for the transaction
807 /// @param priority: The priority of the transaction
808 /// @param executionEffort: The execution effort of the transaction
809 /// @return EstimatedScheduledTransaction: A struct containing the estimated fee, timestamp, and error message
810 access(contract) fun estimate(
811 data: AnyStruct?,
812 timestamp: UFix64,
813 priority: Priority,
814 executionEffort: UInt64
815 ): EstimatedScheduledTransaction {
816 // Remove fractional values from the timestamp
817 let sanitizedTimestamp = UFix64(UInt64(timestamp))
818
819 if sanitizedTimestamp <= getCurrentBlock().timestamp {
820 return EstimatedScheduledTransaction(
821 flowFee: nil,
822 timestamp: nil,
823 error: "Invalid timestamp: \(sanitizedTimestamp) is in the past, current timestamp: \(getCurrentBlock().timestamp)"
824 )
825 }
826
827 if executionEffort > self.config.maximumIndividualEffort {
828 return EstimatedScheduledTransaction(
829 flowFee: nil,
830 timestamp: nil,
831 error: "Invalid execution effort: \(executionEffort) is greater than the maximum transaction effort of \(self.config.maximumIndividualEffort)"
832 )
833 }
834
835 if executionEffort > self.config.priorityEffortLimit[priority]! {
836 return EstimatedScheduledTransaction(
837 flowFee: nil,
838 timestamp: nil,
839 error: "Invalid execution effort: \(executionEffort) is greater than the priority's max effort of \(self.config.priorityEffortLimit[priority]!)"
840 )
841 }
842
843 if executionEffort < self.config.minimumExecutionEffort {
844 return EstimatedScheduledTransaction(
845 flowFee: nil,
846 timestamp: nil,
847 error: "Invalid execution effort: \(executionEffort) is less than the minimum execution effort of \(self.config.minimumExecutionEffort)"
848 )
849 }
850
851 let dataSizeMB = FlowTransactionSchedulerV3.getSizeOfData(data)
852 if dataSizeMB > self.config.maxDataSizeMB {
853 return EstimatedScheduledTransaction(
854 flowFee: nil,
855 timestamp: nil,
856 error: "Invalid data size: \(dataSizeMB) is greater than the maximum data size of \(self.config.maxDataSizeMB)MB"
857 )
858 }
859
860 let fee = self.calculateFee(executionEffort: executionEffort, priority: priority, dataSizeMB: dataSizeMB)
861
862 let scheduledTimestamp = self.calculateScheduledTimestamp(
863 timestamp: sanitizedTimestamp,
864 priority: priority,
865 executionEffort: executionEffort
866 )
867
868 if scheduledTimestamp == nil {
869 return EstimatedScheduledTransaction(
870 flowFee: nil,
871 timestamp: nil,
872 error: "Invalid execution effort: \(executionEffort) is greater than the priority's available effort for the requested timestamp."
873 )
874 }
875
876 // V2: Low priority now has its own pool, so we can provide estimates for it
877 return EstimatedScheduledTransaction(flowFee: fee, timestamp: scheduledTimestamp, error: nil)
878 }
879
880 /// calculateScheduledTimestamp calculates the timestamp at which a transaction
881 /// can be scheduled. It takes into account the priority of the transaction and
882 /// the execution effort.
883 /// - If the transaction is high priority, it returns the timestamp if there is enough
884 /// space or nil if there is no space left.
885 /// - If the transaction is medium or low priority and there is space left in the requested timestamp,
886 /// it returns the requested timestamp. If there is not enough space, it finds the next timestamp with space.
887 ///
888 /// @param timestamp: The requested timestamp for the transaction
889 /// @param priority: The priority of the transaction
890 /// @param executionEffort: The execution effort of the transaction
891 /// @return UFix64?: The timestamp at which the transaction can be scheduled, or nil if there is no space left for a high priority transaction
892 ///
893 /// V2 Simplified: Iterative search instead of recursive. Each priority has its own pool.
894 /// High priority: O(1) - single check, fail if full
895 /// Medium/Low: O(n) worst case where n = maxTimestampSearchIterations
896 access(contract) view fun calculateScheduledTimestamp(
897 timestamp: UFix64,
898 priority: Priority,
899 executionEffort: UInt64
900 ): UFix64? {
901 var currentTimestamp = timestamp
902 var iterations = 0
903 let maxIterations = self.config.maxTimestampSearchIterations
904
905 while iterations < maxIterations {
906 let available = self.getSlotAvailableEffort(timestamp: currentTimestamp, priority: priority)
907
908 // If there's enough space, schedule here
909 if executionEffort <= available {
910 return currentTimestamp
911 }
912
913 // High priority: exact timestamp or fail immediately
914 if priority == Priority.High {
915 return nil
916 }
917
918 // Medium and Low priorities: try next timestamp
919 currentTimestamp = currentTimestamp + 1.0
920 iterations = iterations + 1
921 }
922
923 // Exceeded max iterations - cannot schedule
924 return nil
925 }
926
927 /// slot available effort returns the amount of effort that is available for a given timestamp and priority.
928 /// V2 Simplified: Each priority has its own independent pool, no shared pool.
929 /// O(1) lookup - single map access per priority.
930 access(contract) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
931 let sanitizedTimestamp = UFix64(UInt64(timestamp))
932 let limit = self.config.priorityEffortLimit[priority]!
933
934 switch priority {
935 case Priority.High:
936 return limit.saturatingSubtract(self.slotUsedEffortHigh[sanitizedTimestamp] ?? 0)
937 case Priority.Medium:
938 return limit.saturatingSubtract(self.slotUsedEffortMedium[sanitizedTimestamp] ?? 0)
939 case Priority.Low:
940 return limit.saturatingSubtract(self.slotUsedEffortLow[sanitizedTimestamp] ?? 0)
941 }
942 return 0
943 }
944
945 /// Returns true if any priority queue has transactions at the given slot
946 access(self) view fun slotHasTransactions(_ slot: UFix64): Bool {
947 return self.slotQueueHigh[slot] != nil
948 || self.slotQueueMedium[slot] != nil
949 || self.slotQueueLow[slot] != nil
950 }
951
952 /// Checks if a slot is empty across all priority queues and cleans up if so
953 access(self) fun cleanupSlotIfEmpty(_ slot: UFix64) {
954 let highEmpty = self.slotQueueHigh[slot] == nil || self.slotQueueHigh[slot]!.keys.length == 0
955 let mediumEmpty = self.slotQueueMedium[slot] == nil || self.slotQueueMedium[slot]!.keys.length == 0
956 let lowEmpty = self.slotQueueLow[slot] == nil || self.slotQueueLow[slot]!.keys.length == 0
957
958 if highEmpty && mediumEmpty && lowEmpty {
959 self.slotQueueHigh.remove(key: slot)
960 self.slotQueueMedium.remove(key: slot)
961 self.slotQueueLow.remove(key: slot)
962 self.slotUsedEffortHigh.remove(key: slot)
963 self.slotUsedEffortMedium.remove(key: slot)
964 self.slotUsedEffortLow.remove(key: slot)
965 self.sortedTimestamps.remove(timestamp: slot)
966 }
967 }
968
969 /// add transaction to the queue and updates all the internal state as well as emit an event
970 /// V2 Simplified: No low-priority rescheduling. Each priority has its own independent pool.
971 access(self) fun addTransaction(slot: UFix64, txData: TransactionData) {
972 // If nothing is in the queue for this slot, add to sorted timestamps
973 if !self.slotHasTransactions(slot) {
974 self.sortedTimestamps.add(timestamp: slot)
975 }
976
977 // Add transaction to appropriate priority queue and update effort tracking
978 switch txData.priority {
979 case Priority.High:
980 if self.slotQueueHigh[slot] == nil {
981 self.slotQueueHigh[slot] = {}
982 }
983 let queue = &self.slotQueueHigh[slot]! as auth(Mutate) &{UInt64: UInt64}
984 queue[txData.id] = txData.executionEffort
985 self.slotUsedEffortHigh[slot] = (self.slotUsedEffortHigh[slot] ?? 0) + txData.executionEffort
986
987 case Priority.Medium:
988 if self.slotQueueMedium[slot] == nil {
989 self.slotQueueMedium[slot] = {}
990 }
991 let queue = &self.slotQueueMedium[slot]! as auth(Mutate) &{UInt64: UInt64}
992 queue[txData.id] = txData.executionEffort
993 self.slotUsedEffortMedium[slot] = (self.slotUsedEffortMedium[slot] ?? 0) + txData.executionEffort
994
995 case Priority.Low:
996 if self.slotQueueLow[slot] == nil {
997 self.slotQueueLow[slot] = {}
998 }
999 let queue = &self.slotQueueLow[slot]! as auth(Mutate) &{UInt64: UInt64}
1000 queue[txData.id] = txData.executionEffort
1001 self.slotUsedEffortLow[slot] = (self.slotUsedEffortLow[slot] ?? 0) + txData.executionEffort
1002 }
1003
1004 // Store the transaction in the transactions map
1005 self.transactions[txData.id] = txData
1006 emit TransactionAdded(id: txData.id)
1007 }
1008
1009 /// remove the transaction from the slot queue.
1010 /// V2 Simplified: Updates priority-specific effort tracking.
1011 access(self) fun removeTransaction(txData: &TransactionData): TransactionData {
1012 let transactionID = txData.id
1013 let slot = txData.scheduledTimestamp
1014 let transactionPriority = txData.priority
1015 let effort = txData.executionEffort
1016
1017 // remove transaction object
1018 let transactionObject = self.transactions.remove(key: transactionID)!
1019 emit TransactionRemoved(id: transactionID)
1020
1021 // Remove from the appropriate priority queue and update effort tracking
1022 switch transactionPriority {
1023 case Priority.High:
1024 if let queue = &self.slotQueueHigh[slot] as auth(Mutate) &{UInt64: UInt64}? {
1025 queue.remove(key: transactionID)
1026 }
1027 if let currentEffort = self.slotUsedEffortHigh[slot] {
1028 self.slotUsedEffortHigh[slot] = currentEffort.saturatingSubtract(effort)
1029 }
1030
1031 case Priority.Medium:
1032 if let queue = &self.slotQueueMedium[slot] as auth(Mutate) &{UInt64: UInt64}? {
1033 queue.remove(key: transactionID)
1034 }
1035 if let currentEffort = self.slotUsedEffortMedium[slot] {
1036 self.slotUsedEffortMedium[slot] = currentEffort.saturatingSubtract(effort)
1037 }
1038
1039 case Priority.Low:
1040 if let queue = &self.slotQueueLow[slot] as auth(Mutate) &{UInt64: UInt64}? {
1041 queue.remove(key: transactionID)
1042 }
1043 if let currentEffort = self.slotUsedEffortLow[slot] {
1044 self.slotUsedEffortLow[slot] = currentEffort.saturatingSubtract(effort)
1045 }
1046 }
1047
1048 // Cleanup slot if empty across all priorities
1049 self.cleanupSlotIfEmpty(slot)
1050
1051 return transactionObject
1052 }
1053
1054 /// pendingQueue creates a list of transactions that are ready for execution.
1055 /// For transaction to be ready for execution it must be scheduled.
1056 ///
1057 /// The queue is sorted by timestamp and then by priority (high, medium, low).
1058 /// The queue will contain transactions from all timestamps that are in the past.
1059 /// Low priority transactions will only be added if there is effort available in the slot.
1060 /// The return value can be empty if there are no transactions ready for execution.
1061 access(Process) fun pendingQueue(): [&TransactionData] {
1062 let currentTimestamp = getCurrentBlock().timestamp
1063 var pendingTransactions: [&TransactionData] = []
1064
1065 // total effort across different timestamps guards collection being over the effort limit
1066 var collectionAvailableEffort = self.config.collectionEffortLimit
1067 var transactionsAvailableCount = self.config.collectionTransactionsLimit
1068 var limitReached = false
1069
1070 // Collect past timestamps efficiently from sorted array
1071 let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp)
1072
1073 for timestamp in pastTimestamps {
1074 if limitReached { break }
1075
1076 var high: [&TransactionData] = []
1077 var medium: [&TransactionData] = []
1078 var low: [&TransactionData] = []
1079
1080 // Process high priority queue
1081 let highQueue = self.slotQueueHigh[timestamp] ?? {}
1082 for id in highQueue.keys {
1083 let tx = self.borrowTransaction(id: id)
1084 ?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
1085 if tx.status != Status.Scheduled { continue }
1086 if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
1087 emit CollectionLimitReached(
1088 collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
1089 collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
1090 )
1091 limitReached = true
1092 break
1093 }
1094 collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
1095 transactionsAvailableCount = transactionsAvailableCount - 1
1096 high.append(tx)
1097 }
1098
1099 // Process medium priority queue
1100 if !limitReached {
1101 let mediumQueue = self.slotQueueMedium[timestamp] ?? {}
1102 for id in mediumQueue.keys {
1103 let tx = self.borrowTransaction(id: id)
1104 ?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
1105 if tx.status != Status.Scheduled { continue }
1106 if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
1107 emit CollectionLimitReached(
1108 collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
1109 collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
1110 )
1111 limitReached = true
1112 break
1113 }
1114 collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
1115 transactionsAvailableCount = transactionsAvailableCount - 1
1116 medium.append(tx)
1117 }
1118 }
1119
1120 // Process low priority queue
1121 if !limitReached {
1122 let lowQueue = self.slotQueueLow[timestamp] ?? {}
1123 for id in lowQueue.keys {
1124 let tx = self.borrowTransaction(id: id)
1125 ?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
1126 if tx.status != Status.Scheduled { continue }
1127 if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
1128 emit CollectionLimitReached(
1129 collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
1130 collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
1131 )
1132 limitReached = true
1133 break
1134 }
1135 collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
1136 transactionsAvailableCount = transactionsAvailableCount - 1
1137 low.append(tx)
1138 }
1139 }
1140
1141 pendingTransactions = pendingTransactions
1142 .concat(high)
1143 .concat(medium)
1144 .concat(low)
1145 }
1146
1147 return pendingTransactions
1148 }
1149
1150 /// removeExecutedTransactions removes all transactions that are marked as executed.
1151 access(self) fun removeExecutedTransactions(_ currentTimestamp: UFix64) {
1152 let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp)
1153 var removedCount = 0
1154
1155 for timestamp in pastTimestamps {
1156 // Process all three priority queues
1157 let queues: [{UInt64: UInt64}] = [
1158 self.slotQueueHigh[timestamp] ?? {},
1159 self.slotQueueMedium[timestamp] ?? {},
1160 self.slotQueueLow[timestamp] ?? {}
1161 ]
1162
1163 for transactionIDs in queues {
1164 for id in transactionIDs.keys {
1165 removedCount = removedCount + 1
1166 if removedCount >= self.config.removalTransactionsLimit {
1167 emit RemovalLimitReached(id: id, remainingLength: transactionIDs.keys.length)
1168 return
1169 }
1170
1171 let tx = self.borrowTransaction(id: id)
1172 ?? panic("Invalid ID: \(id) transaction not found while removing executed transactions")
1173
1174 // Only remove executed transactions
1175 if tx.status != Status.Executed {
1176 continue
1177 }
1178
1179 // charge the full fee for transaction execution
1180 destroy tx.payAndRefundFees(refundMultiplier: 0.0)
1181
1182 self.removeTransaction(txData: tx)
1183 }
1184 }
1185 }
1186 }
1187
1188 /// process scheduled transactions and prepare them for execution.
1189 ///
1190 /// First, it removes transactions that have already been executed.
1191 /// Then, it iterates over past timestamps in the queue and processes the transactions that are
1192 /// eligible for execution. It also emits an event for each transaction that is processed.
1193 ///
1194 /// This function is only called by the FVM to process transactions.
1195 access(Process) fun process() {
1196 let currentTimestamp = getCurrentBlock().timestamp
1197 // Early exit if no timestamps need processing
1198 if !self.sortedTimestamps.hasBefore(current: currentTimestamp) {
1199 return
1200 }
1201
1202 self.removeExecutedTransactions(currentTimestamp)
1203
1204 let pendingTransactions = self.pendingQueue()
1205
1206 if pendingTransactions.length == 0 {
1207 return
1208 }
1209
1210 for tx in pendingTransactions {
1211 // Only emit the pending execution event if the transaction handler capability is borrowable
1212 // This is to prevent a situation where the transaction handler is not available
1213 // In that case, the transaction is no longer valid because it cannot be executed
1214 if let transactionHandler = tx.handler.borrow() {
1215 emit PendingExecution(
1216 id: tx.id,
1217 priority: tx.priority.rawValue,
1218 executionEffort: tx.executionEffort,
1219 fees: tx.fees,
1220 transactionHandlerOwner: tx.handler.address,
1221 transactionHandlerTypeIdentifier: transactionHandler.getType().identifier
1222 )
1223 }
1224
1225 // after pending execution event is emitted we set the transaction as executed because we
1226 // must rely on execution node to actually execute it. Execution of the transaction is
1227 // done in a separate transaction that calls executeTransaction(id) function.
1228 // Executing the transaction can not update the status of transaction or any other shared state,
1229 // since that blocks concurrent transaction execution.
1230 // Therefore an optimistic update to executed is made here to avoid race condition.
1231 tx.setStatus(newStatus: Status.Executed)
1232 }
1233 }
1234
1235 /// cancel a scheduled transaction and return a portion of the fees that were paid.
1236 ///
1237 /// @param id: The ID of the transaction to cancel
1238 /// @return: The fees to be returned to the caller
1239 access(Cancel) fun cancel(id: UInt64): @FlowToken.Vault {
1240 let tx = self.borrowTransaction(id: id) ??
1241 panic("Invalid ID: \(id) transaction not found")
1242
1243 assert(
1244 tx.status == Status.Scheduled,
1245 message: "Transaction must be in a scheduled state in order to be canceled"
1246 )
1247
1248 // Note: Effort tracking is handled by removeTransaction()
1249 let totalFees = tx.fees
1250 let refundedFees <- tx.payAndRefundFees(refundMultiplier: self.config.refundMultiplier)
1251
1252 // if the transaction was canceled, add it to the canceled transactions array
1253 // maintain sorted order by inserting at the correct position
1254 var insertIndex = 0
1255 for i, canceledID in self.canceledTransactions {
1256 if id < canceledID {
1257 insertIndex = i
1258 break
1259 }
1260 insertIndex = i + 1
1261 }
1262 self.canceledTransactions.insert(at: insertIndex, id)
1263
1264 // keep the array under the limit
1265 if UInt(self.canceledTransactions.length) > self.config.canceledTransactionsLimit {
1266 self.canceledTransactions.remove(at: 0)
1267 }
1268
1269 emit Canceled(
1270 id: tx.id,
1271 priority: tx.priority.rawValue,
1272 feesReturned: refundedFees.balance,
1273 feesDeducted: totalFees - refundedFees.balance,
1274 transactionHandlerOwner: tx.handler.address,
1275 transactionHandlerTypeIdentifier: tx.handlerTypeIdentifier
1276 )
1277
1278 self.removeTransaction(txData: tx)
1279
1280 return <-refundedFees
1281 }
1282
1283 /// execute transaction is a system function that is called by FVM to execute a transaction by ID.
1284 /// The transaction must be found and in correct state or the function panics and this is a fatal error
1285 ///
1286 /// This function is only called by the FVM to execute transactions.
1287 /// WARNING: this function should not change any shared state, it will be run concurrently and it must not be blocking.
1288 access(Execute) fun executeTransaction(id: UInt64) {
1289 let tx = self.borrowTransaction(id: id) ??
1290 panic("Invalid ID: Transaction with id \(id) not found")
1291
1292 assert (
1293 tx.status == Status.Executed,
1294 message: "Invalid ID: Cannot execute transaction with id \(id) because it has incorrect status \(tx.status.rawValue)"
1295 )
1296
1297 let transactionHandler = tx.handler.borrow()
1298 ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
1299
1300 let handlerPublicPath = transactionHandler.resolveView(Type<PublicPath>()) as? PublicPath
1301
1302 emit Executed(
1303 id: tx.id,
1304 priority: tx.priority.rawValue,
1305 executionEffort: tx.executionEffort,
1306 transactionHandlerOwner: tx.handler.address,
1307 transactionHandlerTypeIdentifier: transactionHandler.getType().identifier,
1308 transactionHandlerUUID: transactionHandler.uuid,
1309 transactionHandlerPublicPath: handlerPublicPath
1310
1311 )
1312
1313 transactionHandler.executeTransaction(id: id, data: tx.getData())
1314 }
1315 }
1316
1317 /// Deposit fees to this contract's account's vault
1318 access(contract) fun depositFees(from: @FlowToken.Vault) {
1319 let vaultRef = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
1320 ?? panic("Unable to borrow reference to the default token vault")
1321 vaultRef.deposit(from: <-from)
1322 }
1323
1324 /// Withdraw fees from this contract's account's vault
1325 access(contract) fun withdrawFees(amount: UFix64): @FlowToken.Vault {
1326 let vaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
1327 ?? panic("Unable to borrow reference to the default token vault")
1328
1329 return <-vaultRef.withdraw(amount: amount) as! @FlowToken.Vault
1330 }
1331
1332 access(all) fun schedule(
1333 handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
1334 data: AnyStruct?,
1335 timestamp: UFix64,
1336 priority: Priority,
1337 executionEffort: UInt64,
1338 fees: @FlowToken.Vault
1339 ): @ScheduledTransaction {
1340 return <-self.sharedScheduler.borrow()!.schedule(
1341 handlerCap: handlerCap,
1342 data: data,
1343 timestamp: timestamp,
1344 priority: priority,
1345 executionEffort: executionEffort,
1346 fees: <-fees
1347 )
1348 }
1349
1350 access(all) fun estimate(
1351 data: AnyStruct?,
1352 timestamp: UFix64,
1353 priority: Priority,
1354 executionEffort: UInt64
1355 ): EstimatedScheduledTransaction {
1356 return self.sharedScheduler.borrow()!
1357 .estimate(
1358 data: data,
1359 timestamp: timestamp,
1360 priority: priority,
1361 executionEffort: executionEffort,
1362 )
1363 }
1364
1365 access(all) fun cancel(scheduledTx: @ScheduledTransaction): @FlowToken.Vault {
1366 let id = scheduledTx.id
1367 destroy scheduledTx
1368 return <-self.sharedScheduler.borrow()!.cancel(id: id)
1369 }
1370
1371 /// getTransactionData returns the transaction data for a given ID
1372 /// This function can only get the data for a transaction that is currently scheduled or pending execution
1373 /// because finalized transaction metadata is not stored in the contract
1374 /// @param id: The ID of the transaction to get the data for
1375 /// @return: The transaction data for the given ID
1376 access(all) view fun getTransactionData(id: UInt64): TransactionData? {
1377 return self.sharedScheduler.borrow()!.getTransaction(id: id)
1378 }
1379
1380 /// borrowHandlerForID returns an un-entitled reference to the transaction handler for a given ID
1381 /// The handler reference can be used to resolve views to get info about the handler and see where it is stored
1382 /// @param id: The ID of the transaction to get the handler for
1383 /// @return: An un-entitled reference to the transaction handler for the given ID
1384 access(all) view fun borrowHandlerForID(_ id: UInt64): &{TransactionHandler}? {
1385 return self.getTransactionData(id: id)?.borrowHandler()
1386 }
1387
1388 /// getCanceledTransactions returns the IDs of the transactions that have been canceled
1389 /// @return: The IDs of the transactions that have been canceled
1390 access(all) view fun getCanceledTransactions(): [UInt64] {
1391 return self.sharedScheduler.borrow()!.getCanceledTransactions()
1392 }
1393
1394
1395 access(all) view fun getStatus(id: UInt64): Status? {
1396 return self.sharedScheduler.borrow()!.getStatus(id: id)
1397 }
1398
1399 /// getTransactionsForTimeframe returns the IDs of the transactions that are scheduled for a given timeframe
1400 /// @param startTimestamp: The start timestamp to get the IDs for
1401 /// @param endTimestamp: The end timestamp to get the IDs for
1402 /// @return: The IDs of the transactions that are scheduled for the given timeframe
1403 access(all) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
1404 return self.sharedScheduler.borrow()!.getTransactionsForTimeframe(startTimestamp: startTimestamp, endTimestamp: endTimestamp)
1405 }
1406
1407 access(all) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
1408 return self.sharedScheduler.borrow()!.getSlotAvailableEffort(timestamp: timestamp, priority: priority)
1409 }
1410
1411 access(all) fun getConfig(): {SchedulerConfig} {
1412 return self.sharedScheduler.borrow()!.getConfig()
1413 }
1414
1415 /// getSizeOfData takes a transaction's data
1416 /// argument and stores it in the contract account's storage,
1417 /// checking storage used before and after to see how large the data is in MB
1418 /// If data is nil, the function returns 0.0
1419 access(all) fun getSizeOfData(_ data: AnyStruct?): UFix64 {
1420 if data == nil {
1421 return 0.0
1422 } else {
1423 let type = data!.getType()
1424 if type.isSubtype(of: Type<Number>())
1425 || type.isSubtype(of: Type<Bool>())
1426 || type.isSubtype(of: Type<Address>())
1427 || type.isSubtype(of: Type<Character>())
1428 || type.isSubtype(of: Type<Capability>())
1429 {
1430 return 0.0
1431 }
1432 }
1433 let storagePath = /storage/dataTemp
1434 let storageUsedBefore = self.account.storage.used
1435 self.account.storage.save(data!, to: storagePath)
1436 let storageUsedAfter = self.account.storage.used
1437 self.account.storage.load<AnyStruct>(from: storagePath)
1438
1439 return FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(storageUsedAfter.saturatingSubtract(storageUsedBefore))
1440 }
1441
1442 access(all) init() {
1443 self.storagePath = /storage/sharedSchedulerV3
1444 let scheduler <- create SharedScheduler()
1445 let oldScheduler <- self.account.storage.load<@AnyResource>(from: self.storagePath)
1446 destroy oldScheduler
1447 self.account.storage.save(<-scheduler, to: self.storagePath)
1448
1449 self.sharedScheduler = self.account.capabilities.storage
1450 .issue<auth(Cancel) &SharedScheduler>(self.storagePath)
1451 }
1452}