Smart Contract

FlowTransactionSchedulerV3

A.7d19efcd8e5b4a4a.FlowTransactionSchedulerV3

Valid From

138,273,366

Deployed

1w ago
Feb 15, 2026, 10:03:15 PM UTC

Dependents

2 imports
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}