Smart Contract

FlowTransactionScheduler

A.e467b9dd11fa00df.FlowTransactionScheduler

Valid From

143,609,306

Deployed

1w ago
Feb 14, 2026, 03:27:11 PM UTC

Dependents

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