Smart Contract
FlowTransactionSchedulerFlat
A.7d19efcd8e5b4a4a.FlowTransactionSchedulerFlat
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