Smart Contract

FlowTransactionSchedulerFlat

A.7d19efcd8e5b4a4a.FlowTransactionSchedulerFlat

Valid From

140,592,723

Deployed

1w ago
Feb 16, 2026, 08:35:37 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/// FlowTransactionSchedulerFlat - Flattened version without SharedScheduler resource
8/// All fields are directly on the contract, no borrow() calls needed
9access(all) contract FlowTransactionSchedulerFlat {
10
11    /// storage path for any auxiliary storage
12    access(all) let storagePath: StoragePath
13
14    /// Enums
15
16    access(all) enum Priority: UInt8 {
17        access(all) case High
18        access(all) case Medium
19        access(all) case Low
20    }
21
22    access(all) enum Status: UInt8 {
23        access(all) case Unknown
24        access(all) case Scheduled
25        access(all) case Executed
26        access(all) case Canceled
27    }
28
29    /// Events
30
31    access(all) event Scheduled(
32        id: UInt64,
33        priority: UInt8,
34        timestamp: UFix64,
35        executionEffort: UInt64,
36        fees: UFix64,
37        transactionHandlerOwner: Address,
38        transactionHandlerTypeIdentifier: String,
39        transactionHandlerUUID: UInt64,
40        transactionHandlerPublicPath: PublicPath?
41    )
42
43    access(all) event PendingExecution(
44        id: UInt64,
45        priority: UInt8,
46        executionEffort: UInt64,
47        fees: UFix64,
48        transactionHandlerOwner: Address,
49        transactionHandlerTypeIdentifier: String
50    )
51
52    access(all) event Executed(
53        id: UInt64,
54        priority: UInt8,
55        executionEffort: UInt64,
56        transactionHandlerOwner: Address,
57        transactionHandlerTypeIdentifier: String,
58        transactionHandlerUUID: UInt64,
59        transactionHandlerPublicPath: PublicPath?
60    )
61
62    access(all) event Canceled(
63        id: UInt64,
64        priority: UInt8,
65        feesReturned: UFix64,
66        feesDeducted: UFix64,
67        transactionHandlerOwner: Address,
68        transactionHandlerTypeIdentifier: String
69    )
70
71    access(all) event CollectionLimitReached(
72        collectionEffortLimit: UInt64?,
73        collectionTransactionsLimit: Int?
74    )
75
76    access(all) event RemovalLimitReached()
77    access(all) event ConfigUpdated()
78    access(all) event CriticalIssue(message: String)
79
80    /// Entitlements
81    access(all) entitlement Execute
82    access(all) entitlement Process
83    access(all) entitlement Cancel
84    access(all) entitlement UpdateConfig
85
86    /// Interfaces
87
88    access(all) resource interface TransactionHandler: ViewResolver.Resolver {
89        access(all) view fun getViews(): [Type] {
90            return []
91        }
92
93        access(all) fun resolveView(_ view: Type): AnyStruct? {
94            return nil
95        }
96
97        access(Execute) fun executeTransaction(id: UInt64, data: AnyStruct?)
98    }
99
100    /// Resources
101
102    access(all) resource ScheduledTransaction {
103        access(all) let id: UInt64
104        access(all) let timestamp: UFix64
105        access(all) let handlerTypeIdentifier: String
106
107        access(all) view fun status(): Status? {
108            return FlowTransactionSchedulerFlat.getStatus(id: self.id)
109        }
110
111        init(
112            id: UInt64,
113            timestamp: UFix64,
114            handlerTypeIdentifier: String
115        ) {
116            self.id = id
117            self.timestamp = timestamp
118            self.handlerTypeIdentifier = handlerTypeIdentifier
119        }
120
121        access(all) event ResourceDestroyed(id: UInt64 = self.id, timestamp: UFix64 = self.timestamp, handlerTypeIdentifier: String = self.handlerTypeIdentifier)
122    }
123
124    /// Structs
125
126    access(all) struct EstimatedScheduledTransaction {
127        access(all) let flowFee: UFix64?
128        access(all) let timestamp: UFix64?
129        access(all) let error: String?
130
131        access(contract) view init(flowFee: UFix64?, timestamp: UFix64?, error: String?) {
132            self.flowFee = flowFee
133            self.timestamp = timestamp
134            self.error = error
135        }
136    }
137
138    access(all) struct TransactionData {
139        access(all) let id: UInt64
140        access(all) let priority: Priority
141        access(all) let executionEffort: UInt64
142        access(all) var status: Status
143        access(all) let fees: UFix64
144        access(all) var scheduledTimestamp: UFix64
145        access(contract) let handler: Capability<auth(Execute) &{TransactionHandler}>
146        access(all) let handlerTypeIdentifier: String
147        access(all) let handlerAddress: Address
148        access(contract) let data: AnyStruct?
149
150        access(contract) init(
151            id: UInt64,
152            handler: Capability<auth(Execute) &{TransactionHandler}>,
153            scheduledTimestamp: UFix64,
154            data: AnyStruct?,
155            priority: Priority,
156            executionEffort: UInt64,
157            fees: UFix64,
158        ) {
159            self.id = id
160            self.handler = handler
161            self.data = data
162            self.priority = priority
163            self.executionEffort = executionEffort
164            self.fees = fees
165            self.status = Status.Scheduled
166            let handlerRef = handler.borrow()
167                ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
168            self.handlerAddress = handler.address
169            self.handlerTypeIdentifier = handlerRef.getType().identifier
170            self.scheduledTimestamp = scheduledTimestamp
171        }
172
173        access(contract) fun setStatus(newStatus: Status) {
174            pre {
175                newStatus != Status.Unknown: "Invalid status: New status cannot be Unknown"
176                self.status != Status.Executed && self.status != Status.Canceled:
177                    "Invalid status: Transaction with id \(self.id) is already finalized"
178                newStatus == Status.Executed ? self.status == Status.Scheduled : true:
179                    "Invalid status: Transaction with id \(self.id) can only be set as Executed if it is Scheduled"
180                newStatus == Status.Canceled ? self.status == Status.Scheduled : true:
181                    "Invalid status: Transaction with id \(self.id) can only be set as Canceled if it is Scheduled"
182            }
183            self.status = newStatus
184        }
185
186        access(contract) fun setScheduledTimestamp(newTimestamp: UFix64) {
187            pre {
188                self.status != Status.Executed && self.status != Status.Canceled:
189                    "Invalid status: Transaction with id \(self.id) is already finalized"
190            }
191            self.scheduledTimestamp = newTimestamp
192        }
193
194        access(contract) fun payAndRefundFees(refundMultiplier: UFix64): @FlowToken.Vault {
195            pre {
196                refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
197                    "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
198            }
199            if refundMultiplier == 0.0 {
200                FlowFees.deposit(from: <-FlowTransactionSchedulerFlat.withdrawFees(amount: self.fees))
201                return <-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
202            } else {
203                let amountToReturn = self.fees * refundMultiplier
204                let amountToKeep = self.fees - amountToReturn
205                let feesToReturn <- FlowTransactionSchedulerFlat.withdrawFees(amount: amountToReturn)
206                FlowFees.deposit(from: <-FlowTransactionSchedulerFlat.withdrawFees(amount: amountToKeep))
207                return <-feesToReturn
208            }
209        }
210
211        access(all) view fun getData(): AnyStruct? {
212            return self.data
213        }
214
215        access(all) view fun borrowHandler(): &{TransactionHandler} {
216            return self.handler.borrow() as? &{TransactionHandler}
217                ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
218        }
219    }
220
221    access(all) struct interface SchedulerConfig {
222        access(all) var maximumIndividualEffort: UInt64
223        access(all) var minimumExecutionEffort: UInt64
224        access(all) var slotTotalEffortLimit: UInt64
225        access(all) var slotSharedEffortLimit: UInt64
226        access(all) var priorityEffortReserve: {Priority: UInt64}
227        access(all) var priorityEffortLimit: {Priority: UInt64}
228        access(all) var maxDataSizeMB: UFix64
229        access(all) var priorityFeeMultipliers: {Priority: UFix64}
230        access(all) var refundMultiplier: UFix64
231        access(all) var canceledTransactionsLimit: UInt
232        access(all) var collectionEffortLimit: UInt64
233        access(all) var collectionTransactionsLimit: Int
234
235        access(all) init(
236            maximumIndividualEffort: UInt64,
237            minimumExecutionEffort: UInt64,
238            slotSharedEffortLimit: UInt64,
239            priorityEffortReserve: {Priority: UInt64},
240            lowPriorityEffortLimit: UInt64,
241            maxDataSizeMB: UFix64,
242            priorityFeeMultipliers: {Priority: UFix64},
243            refundMultiplier: UFix64,
244            canceledTransactionsLimit: UInt,
245            collectionEffortLimit: UInt64,
246            collectionTransactionsLimit: Int,
247            txRemovalLimit: UInt
248        ) {
249            post {
250                self.refundMultiplier >= 0.0 && self.refundMultiplier <= 1.0:
251                    "Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
252                self.priorityFeeMultipliers[Priority.Low]! >= 1.0:
253                    "Invalid priority fee multiplier: Low priority multiplier must be greater than or equal to 1.0 but got \(self.priorityFeeMultipliers[Priority.Low]!)"
254                self.priorityFeeMultipliers[Priority.Medium]! > self.priorityFeeMultipliers[Priority.Low]!:
255                    "Invalid priority fee multiplier: Medium priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Low]!) but got \(priorityFeeMultipliers[Priority.Medium]!)"
256                self.priorityFeeMultipliers[Priority.High]! > self.priorityFeeMultipliers[Priority.Medium]!:
257                    "Invalid priority fee multiplier: High priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Medium]!) but got \(priorityFeeMultipliers[Priority.High]!)"
258                self.priorityEffortLimit[Priority.High]! >= self.priorityEffortReserve[Priority.High]!:
259                    "Invalid priority effort limit: High priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.High]!)"
260                self.priorityEffortLimit[Priority.Medium]! >= self.priorityEffortReserve[Priority.Medium]!:
261                    "Invalid priority effort limit: Medium priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.Medium]!)"
262                self.priorityEffortLimit[Priority.Low]! >= self.priorityEffortReserve[Priority.Low]!:
263                    "Invalid priority effort limit: Low priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.Low]!)"
264                self.priorityEffortReserve[Priority.Low]! == 0:
265                    "Invalid priority effort reserve: Low priority effort reserve must be 0"
266                self.collectionTransactionsLimit >= 0:
267                    "Invalid collection transactions limit: Collection transactions limit must be greater than or equal to 0 but got \(collectionTransactionsLimit)"
268                self.canceledTransactionsLimit >= 1:
269                    "Invalid canceled transactions limit: Canceled transactions limit must be greater than or equal to 1 but got \(canceledTransactionsLimit)"
270                self.collectionEffortLimit > self.slotTotalEffortLimit:
271                    "Invalid collection effort limit: Collection effort limit must be greater than \(self.slotTotalEffortLimit) but got \(self.collectionEffortLimit)"
272            }
273        }
274
275        access(all) view fun getTxRemovalLimit(): UInt
276    }
277
278    access(all) struct Config: SchedulerConfig {
279        access(all) var maximumIndividualEffort: UInt64
280        access(all) var minimumExecutionEffort: UInt64
281        access(all) var slotTotalEffortLimit: UInt64
282        access(all) var slotSharedEffortLimit: UInt64
283        access(all) var priorityEffortReserve: {Priority: UInt64}
284        access(all) var priorityEffortLimit: {Priority: UInt64}
285        access(all) var maxDataSizeMB: UFix64
286        access(all) var priorityFeeMultipliers: {Priority: UFix64}
287        access(all) var refundMultiplier: UFix64
288        access(all) var canceledTransactionsLimit: UInt
289        access(all) var collectionEffortLimit: UInt64
290        access(all) var collectionTransactionsLimit: Int
291
292        access(all) init(
293            maximumIndividualEffort: UInt64,
294            minimumExecutionEffort: UInt64,
295            slotSharedEffortLimit: UInt64,
296            priorityEffortReserve: {Priority: UInt64},
297            lowPriorityEffortLimit: UInt64,
298            maxDataSizeMB: UFix64,
299            priorityFeeMultipliers: {Priority: UFix64},
300            refundMultiplier: UFix64,
301            canceledTransactionsLimit: UInt,
302            collectionEffortLimit: UInt64,
303            collectionTransactionsLimit: Int,
304            txRemovalLimit: UInt
305        ) {
306            self.maximumIndividualEffort = maximumIndividualEffort
307            self.minimumExecutionEffort = minimumExecutionEffort
308            self.slotTotalEffortLimit = slotSharedEffortLimit + priorityEffortReserve[Priority.High]! + priorityEffortReserve[Priority.Medium]!
309            self.slotSharedEffortLimit = slotSharedEffortLimit
310            self.priorityEffortReserve = priorityEffortReserve
311            self.priorityEffortLimit = {
312                Priority.High: priorityEffortReserve[Priority.High]! + slotSharedEffortLimit,
313                Priority.Medium: priorityEffortReserve[Priority.Medium]! + slotSharedEffortLimit,
314                Priority.Low: lowPriorityEffortLimit
315            }
316            self.maxDataSizeMB = maxDataSizeMB
317            self.priorityFeeMultipliers = priorityFeeMultipliers
318            self.refundMultiplier = refundMultiplier
319            self.canceledTransactionsLimit = canceledTransactionsLimit
320            self.collectionEffortLimit = collectionEffortLimit
321            self.collectionTransactionsLimit = collectionTransactionsLimit
322        }
323
324        access(all) view fun getTxRemovalLimit(): UInt {
325            return FlowTransactionSchedulerFlat.account.storage.copy<UInt>(from: /storage/txRemovalLimitFlat)
326                ?? 200
327        }
328    }
329
330    access(all) struct SortedTimestamps {
331        access(self) var timestamps: [UFix64]
332
333        access(all) init() {
334            self.timestamps = []
335        }
336
337        access(all) fun add(timestamp: UFix64) {
338            var insertIndex = 0
339            for i, ts in self.timestamps {
340                if timestamp < ts {
341                    insertIndex = i
342                    break
343                } else if timestamp == ts {
344                    return
345                }
346                insertIndex = i + 1
347            }
348            self.timestamps.insert(at: insertIndex, timestamp)
349        }
350
351        access(all) fun remove(timestamp: UFix64) {
352            let index = self.timestamps.firstIndex(of: timestamp)
353            if index != nil {
354                self.timestamps.remove(at: index!)
355            }
356        }
357
358        access(all) fun getBefore(current: UFix64): [UFix64] {
359            let pastTimestamps: [UFix64] = []
360            for timestamp in self.timestamps {
361                if timestamp <= current {
362                    pastTimestamps.append(timestamp)
363                } else {
364                    break
365                }
366            }
367            return pastTimestamps
368        }
369
370        access(all) fun hasBefore(current: UFix64): Bool {
371            return self.timestamps.length > 0 && self.timestamps[0] <= current
372        }
373
374        access(all) fun getAll(): [UFix64] {
375            return self.timestamps
376        }
377    }
378
379    // ============================================
380    // CONTRACT-LEVEL STATE (formerly in SharedScheduler)
381    // ============================================
382
383    access(contract) var nextID: UInt64
384    access(contract) var transactions: {UInt64: TransactionData}
385    access(contract) var slotQueue: {UFix64: {Priority: {UInt64: UInt64}}}
386    access(contract) var slotUsedEffort: {UFix64: {Priority: UInt64}}
387    access(contract) var sortedTimestamps: SortedTimestamps
388    access(contract) var canceledTransactions: [UInt64]
389    access(contract) var config: {SchedulerConfig}
390
391    // ============================================
392    // HELPER FUNCTIONS
393    // ============================================
394
395    access(contract) fun depositFees(from: @FlowToken.Vault) {
396        let vaultRef = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
397            ?? panic("Unable to borrow reference to the default token vault")
398        vaultRef.deposit(from: <-from)
399    }
400
401    access(contract) fun withdrawFees(amount: UFix64): @FlowToken.Vault {
402        let vaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
403            ?? panic("Unable to borrow reference to the default token vault")
404        return <-vaultRef.withdraw(amount: amount) as! @FlowToken.Vault
405    }
406
407    access(self) fun getNextIDAndIncrement(): UInt64 {
408        let nextID = self.nextID
409        self.nextID = self.nextID + 1
410        return nextID
411    }
412
413    // ============================================
414    // PUBLIC/CONTRACT FUNCTIONS (no borrow needed!)
415    // ============================================
416
417    access(all) view fun getConfig(): {SchedulerConfig} {
418        return self.config
419    }
420
421    access(all) view fun getStatus(id: UInt64): Status? {
422        if id == 0 as UInt64 || id >= self.nextID {
423            return nil
424        }
425
426        if let tx = &self.transactions[id] as &TransactionData? {
427            return tx.status
428        }
429
430        if self.canceledTransactions.contains(id) {
431            return Status.Canceled
432        }
433
434        let firstCanceledID = self.canceledTransactions[0]
435        if id > firstCanceledID {
436            return Status.Executed
437        }
438
439        return Status.Unknown
440    }
441
442    access(all) view fun getTransactionData(id: UInt64): TransactionData? {
443        return self.transactions[id]
444    }
445
446    access(all) view fun borrowHandlerForID(_ id: UInt64): &{TransactionHandler}? {
447        return self.getTransactionData(id: id)?.borrowHandler()
448    }
449
450    access(all) view fun getCanceledTransactions(): [UInt64] {
451        return self.canceledTransactions
452    }
453
454    access(all) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
455        var transactionsInTimeframe: {UFix64: {UInt8: [UInt64]}} = {}
456
457        if startTimestamp > endTimestamp {
458            return transactionsInTimeframe
459        }
460
461        let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp)
462
463        for timestamp in allTimestampsBeforeEnd {
464            if timestamp < startTimestamp { continue }
465
466            let transactionPriorities = self.slotQueue[timestamp] ?? {}
467            var timestampTransactions: {UInt8: [UInt64]} = {}
468
469            for priority in transactionPriorities.keys {
470                let transactionIDs = transactionPriorities[priority] ?? {}
471                var priorityTransactions: [UInt64] = []
472
473                for id in transactionIDs.keys {
474                    priorityTransactions.append(id)
475                }
476
477                if priorityTransactions.length > 0 {
478                    timestampTransactions[priority.rawValue] = priorityTransactions
479                }
480            }
481
482            if timestampTransactions.keys.length > 0 {
483                transactionsInTimeframe[timestamp] = timestampTransactions
484            }
485        }
486
487        return transactionsInTimeframe
488    }
489
490    access(all) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
491        let sanitizedTimestamp = UFix64(UInt64(timestamp))
492        let priorityLimit = self.config.priorityEffortLimit[priority]!
493
494        if !self.slotUsedEffort.containsKey(sanitizedTimestamp) {
495            return priorityLimit
496        }
497
498        let slotPriorityEffortsUsed = self.slotUsedEffort[sanitizedTimestamp]!
499        let highReserve = self.config.priorityEffortReserve[Priority.High]!
500        let mediumReserve = self.config.priorityEffortReserve[Priority.Medium]!
501        let highUsed = slotPriorityEffortsUsed[Priority.High] ?? 0
502        let mediumUsed = slotPriorityEffortsUsed[Priority.Medium] ?? 0
503
504        if priority == Priority.Low {
505            let highPlusMediumUsed = highUsed + mediumUsed
506            let totalEffortRemaining = self.config.slotTotalEffortLimit.saturatingSubtract(highPlusMediumUsed)
507            let lowEffortRemaining = totalEffortRemaining < priorityLimit ? totalEffortRemaining : priorityLimit
508            let lowUsed = slotPriorityEffortsUsed[Priority.Low] ?? 0
509            return lowEffortRemaining.saturatingSubtract(lowUsed)
510        }
511
512        let highSharedUsed: UInt64 = highUsed.saturatingSubtract(highReserve)
513        let mediumSharedUsed: UInt64 = mediumUsed.saturatingSubtract(mediumReserve)
514        let totalShared = (self.config.slotTotalEffortLimit.saturatingSubtract(highReserve)).saturatingSubtract(mediumReserve)
515        let highPlusMediumSharedUsed = highSharedUsed + mediumSharedUsed
516        let sharedAvailable = totalShared.saturatingSubtract(highPlusMediumSharedUsed)
517
518        let reserve = self.config.priorityEffortReserve[priority]!
519        let used = slotPriorityEffortsUsed[priority] ?? 0
520        let unusedReserve: UInt64 = reserve.saturatingSubtract(used)
521        let available = sharedAvailable + unusedReserve
522
523        return available
524    }
525
526    access(all) fun getSizeOfData(_ data: AnyStruct?): UFix64 {
527        if data == nil {
528            return 0.0
529        } else {
530            let type = data!.getType()
531            if type.isSubtype(of: Type<Number>())
532            || type.isSubtype(of: Type<Bool>())
533            || type.isSubtype(of: Type<Address>())
534            || type.isSubtype(of: Type<Character>())
535            || type.isSubtype(of: Type<Capability>())
536            {
537                return 0.0
538            }
539        }
540        let storagePath = /storage/dataTempFlat
541        let storageUsedBefore = self.account.storage.used
542        self.account.storage.save(data!, to: storagePath)
543        let storageUsedAfter = self.account.storage.used
544        self.account.storage.load<AnyStruct>(from: storagePath)
545
546        return FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(storageUsedAfter.saturatingSubtract(storageUsedBefore))
547    }
548
549    access(contract) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 {
550        let baseFee = FlowFees.computeFees(inclusionEffort: 1.0, executionEffort: UFix64(executionEffort)/100000000.0)
551        let scaledExecutionFee = baseFee * self.config.priorityFeeMultipliers[priority]!
552        let storageFee = FlowStorageFees.storageCapacityToFlow(dataSizeMB)
553        let inclusionFee = 0.00001
554        return scaledExecutionFee + storageFee + inclusionFee
555    }
556
557    access(contract) view fun calculateScheduledTimestamp(
558        timestamp: UFix64,
559        priority: Priority,
560        executionEffort: UInt64
561    ): UFix64? {
562        var timestampToSearch = timestamp
563
564        while true {
565            let used = self.slotUsedEffort[timestampToSearch]
566            if used == nil {
567                return timestampToSearch
568            }
569
570            let available = self.getSlotAvailableEffort(timestamp: timestampToSearch, priority: priority)
571            if executionEffort <= available {
572                return timestampToSearch
573            }
574
575            if priority == Priority.High {
576                return nil
577            }
578
579            timestampToSearch = timestampToSearch + 1.0
580        }
581
582        return nil
583    }
584
585    // ============================================
586    // MAIN FUNCTIONS
587    // ============================================
588
589    access(all) fun estimate(
590        data: AnyStruct?,
591        timestamp: UFix64,
592        priority: Priority,
593        executionEffort: UInt64
594    ): EstimatedScheduledTransaction {
595        let sanitizedTimestamp = UFix64(UInt64(timestamp))
596
597        if sanitizedTimestamp <= getCurrentBlock().timestamp {
598            return EstimatedScheduledTransaction(
599                flowFee: nil,
600                timestamp: nil,
601                error: "Invalid timestamp: \(sanitizedTimestamp) is in the past, current timestamp: \(getCurrentBlock().timestamp)"
602            )
603        }
604
605        if executionEffort > self.config.maximumIndividualEffort {
606            return EstimatedScheduledTransaction(
607                flowFee: nil,
608                timestamp: nil,
609                error: "Invalid execution effort: \(executionEffort) is greater than the maximum transaction effort of \(self.config.maximumIndividualEffort)"
610            )
611        }
612
613        if executionEffort > self.config.priorityEffortLimit[priority]! {
614            return EstimatedScheduledTransaction(
615                flowFee: nil,
616                timestamp: nil,
617                error: "Invalid execution effort: \(executionEffort) is greater than the priority's max effort of \(self.config.priorityEffortLimit[priority]!)"
618            )
619        }
620
621        if executionEffort < self.config.minimumExecutionEffort {
622            return EstimatedScheduledTransaction(
623                flowFee: nil,
624                timestamp: nil,
625                error: "Invalid execution effort: \(executionEffort) is less than the minimum execution effort of \(self.config.minimumExecutionEffort)"
626            )
627        }
628
629        let dataSizeMB = self.getSizeOfData(data)
630        if dataSizeMB > self.config.maxDataSizeMB {
631            return EstimatedScheduledTransaction(
632                flowFee: nil,
633                timestamp: nil,
634                error: "Invalid data size: \(dataSizeMB) is greater than the maximum data size of \(self.config.maxDataSizeMB)MB"
635            )
636        }
637
638        let fee = self.calculateFee(executionEffort: executionEffort, priority: priority, dataSizeMB: dataSizeMB)
639
640        let scheduledTimestamp = self.calculateScheduledTimestamp(
641            timestamp: sanitizedTimestamp,
642            priority: priority,
643            executionEffort: executionEffort
644        )
645
646        if scheduledTimestamp == nil {
647            return EstimatedScheduledTransaction(
648                flowFee: nil,
649                timestamp: nil,
650                error: "Invalid execution effort: \(executionEffort) is greater than the priority's available effort for the requested timestamp."
651            )
652        }
653
654        if priority == Priority.Low {
655            return EstimatedScheduledTransaction(
656                flowFee: fee,
657                timestamp: scheduledTimestamp,
658                error: "Invalid Priority: Cannot estimate for Low Priority transactions. They will be included in the first block with available space after their requested timestamp."
659            )
660        }
661
662        return EstimatedScheduledTransaction(flowFee: fee, timestamp: scheduledTimestamp, error: nil)
663    }
664
665    access(all) fun schedule(
666        handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
667        data: AnyStruct?,
668        timestamp: UFix64,
669        priority: Priority,
670        executionEffort: UInt64,
671        fees: @FlowToken.Vault
672    ): @ScheduledTransaction {
673
674        let estimate = self.estimate(
675            data: data,
676            timestamp: timestamp,
677            priority: priority,
678            executionEffort: executionEffort
679        )
680
681        if estimate.error != nil && estimate.timestamp == nil {
682            panic(estimate.error!)
683        }
684
685        assert(
686            fees.balance >= estimate.flowFee!,
687            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."
688        )
689
690        let transactionID = self.getNextIDAndIncrement()
691        let transactionData = TransactionData(
692            id: transactionID,
693            handler: handlerCap,
694            scheduledTimestamp: estimate.timestamp!,
695            data: data,
696            priority: priority,
697            executionEffort: executionEffort,
698            fees: fees.balance,
699        )
700
701        self.depositFees(from: <-fees)
702
703        let handlerRef = handlerCap.borrow()
704            ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
705
706        let handlerPublicPath = handlerRef.resolveView(Type<PublicPath>()) as? PublicPath
707
708        emit Scheduled(
709            id: transactionData.id,
710            priority: transactionData.priority.rawValue,
711            timestamp: transactionData.scheduledTimestamp,
712            executionEffort: transactionData.executionEffort,
713            fees: transactionData.fees,
714            transactionHandlerOwner: transactionData.handler.address,
715            transactionHandlerTypeIdentifier: transactionData.handlerTypeIdentifier,
716            transactionHandlerUUID: handlerRef.uuid,
717            transactionHandlerPublicPath: handlerPublicPath
718        )
719
720        self.addTransaction(slot: estimate.timestamp!, txData: transactionData)
721
722        return <-create ScheduledTransaction(
723            id: transactionID,
724            timestamp: estimate.timestamp!,
725            handlerTypeIdentifier: transactionData.handlerTypeIdentifier
726        )
727    }
728
729    access(all) fun cancel(scheduledTx: @ScheduledTransaction): @FlowToken.Vault {
730        let id = scheduledTx.id
731        destroy scheduledTx
732
733        let tx = &self.transactions[id] as &TransactionData?
734            ?? panic("Invalid ID: \(id) transaction not found")
735
736        assert(
737            tx.status == Status.Scheduled,
738            message: "Transaction must be in a scheduled state in order to be canceled"
739        )
740
741        let slotEfforts = self.slotUsedEffort[tx.scheduledTimestamp]!
742        slotEfforts[tx.priority] = slotEfforts[tx.priority]!.saturatingSubtract(tx.executionEffort)
743        self.slotUsedEffort[tx.scheduledTimestamp] = slotEfforts
744
745        let totalFees = tx.fees
746        let refundedFees <- tx.payAndRefundFees(refundMultiplier: self.config.refundMultiplier)
747
748        var insertIndex = 0
749        for i, canceledID in self.canceledTransactions {
750            if id < canceledID {
751                insertIndex = i
752                break
753            }
754            insertIndex = i + 1
755        }
756        self.canceledTransactions.insert(at: insertIndex, id)
757
758        if UInt(self.canceledTransactions.length) > self.config.canceledTransactionsLimit {
759            self.canceledTransactions.remove(at: 0)
760        }
761
762        emit Canceled(
763            id: tx.id,
764            priority: tx.priority.rawValue,
765            feesReturned: refundedFees.balance,
766            feesDeducted: totalFees - refundedFees.balance,
767            transactionHandlerOwner: tx.handler.address,
768            transactionHandlerTypeIdentifier: tx.handlerTypeIdentifier
769        )
770
771        self.removeTransaction(txData: tx)
772
773        return <-refundedFees
774    }
775
776    // ============================================
777    // INTERNAL FUNCTIONS
778    // ============================================
779
780    access(self) fun addTransaction(slot: UFix64, txData: TransactionData) {
781        if self.slotQueue[slot] == nil {
782            self.slotQueue[slot] = {}
783            self.slotUsedEffort[slot] = {
784                Priority.High: 0,
785                Priority.Medium: 0,
786                Priority.Low: 0
787            }
788            self.sortedTimestamps.add(timestamp: slot)
789        }
790
791        let slotQueue = self.slotQueue[slot]!
792        if let priorityQueue = slotQueue[txData.priority] {
793            priorityQueue[txData.id] = txData.executionEffort
794            slotQueue[txData.priority] = priorityQueue
795        } else {
796            slotQueue[txData.priority] = {
797                txData.id: txData.executionEffort
798            }
799        }
800        self.slotQueue[slot] = slotQueue
801
802        let slotEfforts = self.slotUsedEffort[slot]!
803        var newPriorityEffort = slotEfforts[txData.priority]! + txData.executionEffort
804        slotEfforts[txData.priority] = newPriorityEffort
805        var newTotalEffort: UInt64 = 0
806        for priority in slotEfforts.keys {
807            newTotalEffort = newTotalEffort.saturatingAdd(slotEfforts[priority]!)
808        }
809        self.slotUsedEffort[slot] = slotEfforts
810
811        let lowTransactionsToReschedule: [UInt64] = []
812        if newTotalEffort > self.config.slotTotalEffortLimit {
813            let lowPriorityTransactions = slotQueue[Priority.Low]!
814            for id in lowPriorityTransactions.keys {
815                if newTotalEffort <= self.config.slotTotalEffortLimit {
816                    break
817                }
818                lowTransactionsToReschedule.append(id)
819                newTotalEffort = newTotalEffort.saturatingSubtract(lowPriorityTransactions[id]!)
820            }
821        }
822
823        self.transactions[txData.id] = txData
824        self.rescheduleLowPriorityTransactions(slot: slot, transactions: lowTransactionsToReschedule)
825    }
826
827    access(self) fun rescheduleLowPriorityTransactions(slot: UFix64, transactions: [UInt64]) {
828        for id in transactions {
829            let tx = &self.transactions[id] as &TransactionData?
830            if tx == nil {
831                emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while rescheduling low priority transactions")
832                continue
833            }
834
835            if tx!.priority != Priority.Low {
836                emit CriticalIssue(message: "Invalid Priority: Cannot reschedule transaction with id \(id) because it is not low priority")
837                continue
838            }
839
840            if tx!.scheduledTimestamp != slot {
841                emit CriticalIssue(message: "Invalid Timestamp: Cannot reschedule transaction with id \(id) because it is not scheduled at the same slot as the new transaction")
842                continue
843            }
844
845            let newTimestamp = self.calculateScheduledTimestamp(
846                timestamp: slot + 1.0,
847                priority: Priority.Low,
848                executionEffort: tx!.executionEffort
849            )!
850
851            let effort = tx!.executionEffort
852            let transactionData = self.removeTransaction(txData: tx!)
853
854            let slotEfforts = self.slotUsedEffort[slot]!
855            slotEfforts[Priority.Low] = slotEfforts[Priority.Low]!.saturatingSubtract(effort)
856            self.slotUsedEffort[slot] = slotEfforts
857
858            transactionData.setScheduledTimestamp(newTimestamp: newTimestamp)
859            self.addTransaction(slot: newTimestamp, txData: transactionData)
860        }
861    }
862
863    access(self) fun removeTransaction(txData: &TransactionData): TransactionData {
864        let transactionID = txData.id
865        let slot = txData.scheduledTimestamp
866        let transactionPriority = txData.priority
867
868        let transactionObject = self.transactions.remove(key: transactionID)!
869
870        if let transactionQueue = self.slotQueue[slot] {
871            if let priorityQueue = transactionQueue[transactionPriority] {
872                priorityQueue[transactionID] = nil
873                if priorityQueue.keys.length == 0 {
874                    transactionQueue.remove(key: transactionPriority)
875                } else {
876                    transactionQueue[transactionPriority] = priorityQueue
877                }
878                self.slotQueue[slot] = transactionQueue
879            }
880
881            if transactionQueue.keys.length == 0 {
882                self.slotQueue.remove(key: slot)
883                self.slotUsedEffort.remove(key: slot)
884                self.sortedTimestamps.remove(timestamp: slot)
885            }
886        }
887
888        return transactionObject
889    }
890
891    // ============================================
892    // INIT
893    // ============================================
894
895    access(all) init() {
896        self.storagePath = /storage/schedulerFlatData
897        self.nextID = 1
898        self.canceledTransactions = [0 as UInt64]
899        self.transactions = {}
900        self.slotUsedEffort = {}
901        self.slotQueue = {}
902        self.sortedTimestamps = SortedTimestamps()
903
904        let sharedEffortLimit: UInt64 = 5_000
905        let highPriorityEffortReserve: UInt64 = 10_000
906        let mediumPriorityEffortReserve: UInt64 = 2_500
907
908        self.config = Config(
909            maximumIndividualEffort: 9999,
910            minimumExecutionEffort: 100,
911            slotSharedEffortLimit: sharedEffortLimit,
912            priorityEffortReserve: {
913                Priority.High: highPriorityEffortReserve,
914                Priority.Medium: mediumPriorityEffortReserve,
915                Priority.Low: 0
916            },
917            lowPriorityEffortLimit: 2_500,
918            maxDataSizeMB: 0.001,
919            priorityFeeMultipliers: {
920                Priority.High: 10.0,
921                Priority.Medium: 5.0,
922                Priority.Low: 2.0
923            },
924            refundMultiplier: 0.5,
925            canceledTransactionsLimit: 1000,
926            collectionEffortLimit: 500_000,
927            collectionTransactionsLimit: 150,
928            txRemovalLimit: 200
929        )
930    }
931}
932