Smart Contract
FlowTransactionSchedulerUtils
A.e467b9dd11fa00df.FlowTransactionSchedulerUtils
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FungibleToken from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4import EVM from 0xe467b9dd11fa00df
5import MetadataViews from 0x1d7e57aa55817448
6
7/// FlowTransactionSchedulerUtils provides utility functionality for working with scheduled transactions
8/// on the Flow blockchain.
9///
10/// In the future, this contract will be updated to include more functionality
11/// to make it more convenient for working with scheduled transactions for various use cases.
12///
13access(all) contract FlowTransactionSchedulerUtils {
14
15 /// Storage path for Manager resources
16 access(all) let managerStoragePath: StoragePath
17
18 /// Public path for Manager resources
19 access(all) let managerPublicPath: PublicPath
20
21 /// Entitlements
22 access(all) entitlement Owner
23
24 /// HandlerInfo is a struct that stores information about a single transaction handler
25 /// that has been used to schedule transactions.
26 /// It is stored in the manager's handlerInfos dictionary.
27 /// It stores the type identifier of the handler, the transaction IDs that have been scheduled for it,
28 /// and a capability to the handler.
29 /// The capability is used to borrow a reference to the handler when needed.
30 /// The transaction IDs are used to track the transactions that have been scheduled for the handler.
31 /// The type identifier is used to differentiate between handlers of the same type.
32 access(all) struct HandlerInfo {
33 /// The type identifier of the handler
34 access(all) let typeIdentifier: String
35
36 /// The transaction IDs that have been scheduled for the handler
37 access(all) let transactionIDs: [UInt64]
38
39 /// The capability to the handler
40 access(contract) let capability: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>
41
42 init(typeIdentifier: String, capability: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>) {
43 self.typeIdentifier = typeIdentifier
44 self.capability = capability
45 self.transactionIDs = []
46 }
47
48 /// Add a transaction ID to the handler's transaction IDs
49 /// @param id: The ID of the transaction to add
50 access(contract) fun addTransactionID(id: UInt64) {
51 self.transactionIDs.append(id)
52 }
53
54 /// Remove a transaction ID from the handler's transaction IDs
55 /// @param id: The ID of the transaction to remove
56 access(contract) fun removeTransactionID(id: UInt64) {
57 let index = self.transactionIDs.firstIndex(of: id)
58 if index != nil {
59 self.transactionIDs.remove(at: index!)
60 }
61 }
62
63 /// Borrow an un-entitled reference to the handler
64 /// @return: A reference to the handler, or nil if not found
65 access(contract) view fun borrow(): &{FlowTransactionScheduler.TransactionHandler}? {
66 return self.capability.borrow() as? &{FlowTransactionScheduler.TransactionHandler}
67 }
68 }
69
70 /// The Manager resource offers a convenient way for users and developers to
71 /// group, schedule, cancel, and query scheduled transactions through a single resource.
72 /// The Manager is defined as an interface to allow for multiple implementations of the manager
73 /// and to support upgrades that may be needed in the future to add additional storage fields and functionality.
74 ///
75 /// Key features:
76 /// - Organizes scheduled and executed transactions by handler type and timestamp
77 /// - Simplified scheduling interface that works with previously used transaction handlers
78 /// - Transaction tracking and querying capabilities by handler, timestamp, and ID
79 /// - Handler metadata and view resolution support
80 access(all) resource interface Manager {
81
82 /// Schedules a transaction by passing the arguments directly
83 /// to the FlowTransactionScheduler schedule function
84 /// This also should store the information about the transaction
85 /// and handler in the manager's fields
86 access(Owner) fun schedule(
87 handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>,
88 data: AnyStruct?,
89 timestamp: UFix64,
90 priority: FlowTransactionScheduler.Priority,
91 executionEffort: UInt64,
92 fees: @FlowToken.Vault
93 ): UInt64
94
95 /// Schedules a transaction that uses a previously used handler
96 /// This should also store the information about the transaction
97 /// and handler in the manager's fields
98 access(Owner) fun scheduleByHandler(
99 handlerTypeIdentifier: String,
100 handlerUUID: UInt64?,
101 data: AnyStruct?,
102 timestamp: UFix64,
103 priority: FlowTransactionScheduler.Priority,
104 executionEffort: UInt64,
105 fees: @FlowToken.Vault
106 ): UInt64
107
108 /// Cancels a scheduled transaction by its ID
109 /// This should also remove the information about the transaction from the manager's fields
110 access(Owner) fun cancel(id: UInt64): @FlowToken.Vault
111
112 access(all) view fun getTransactionData(_ id: UInt64): FlowTransactionScheduler.TransactionData?
113 access(all) view fun borrowTransactionHandlerForID(_ id: UInt64): &{FlowTransactionScheduler.TransactionHandler}?
114 access(all) fun getHandlerTypeIdentifiers(): {String: [UInt64]}
115 access(all) view fun borrowHandler(handlerTypeIdentifier: String, handlerUUID: UInt64?): &{FlowTransactionScheduler.TransactionHandler}?
116 access(all) fun getHandlerViews(handlerTypeIdentifier: String, handlerUUID: UInt64?): [Type]
117 access(all) fun resolveHandlerView(handlerTypeIdentifier: String, handlerUUID: UInt64?, viewType: Type): AnyStruct?
118 access(all) fun getHandlerViewsFromTransactionID(_ id: UInt64): [Type]
119 access(all) fun resolveHandlerViewFromTransactionID(_ id: UInt64, viewType: Type): AnyStruct?
120 access(all) view fun getTransactionIDs(): [UInt64]
121 access(all) view fun getTransactionIDsByHandler(handlerTypeIdentifier: String, handlerUUID: UInt64?): [UInt64]
122 access(all) view fun getTransactionIDsByTimestamp(_ timestamp: UFix64): [UInt64]
123 access(all) fun getTransactionIDsByTimestampRange(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: [UInt64]}
124 access(all) view fun getTransactionStatus(id: UInt64): FlowTransactionScheduler.Status?
125 access(all) view fun getSortedTimestamps(): FlowTransactionScheduler.SortedTimestamps
126 }
127
128 /// Manager resource is meant to provide users and developers with a simple way
129 /// to group the scheduled transactions that they own into one place to make it more
130 /// convenient to schedule/cancel transactions and get information about the transactions
131 /// that are managed.
132 /// It stores ScheduledTransaction resources in a dictionary and has other fields
133 /// to track the scheduled transactions by timestamp and handler
134 ///
135 access(all) resource ManagerV1: Manager {
136 /// Dictionary storing scheduled transactions by their ID
137 access(self) var scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction}
138
139 /// Sorted array of timestamps that this manager has transactions scheduled at
140 access(self) var sortedTimestamps: FlowTransactionScheduler.SortedTimestamps
141
142 /// Dictionary storing the IDs of the transactions scheduled at a given timestamp
143 access(self) let idsByTimestamp: {UFix64: [UInt64]}
144
145 /// Dictionary storing the handler UUIDs for transaction IDs
146 access(self) let handlerUUIDsByTransactionID: {UInt64: UInt64}
147
148 /// Dictionary storing the handlers that this manager has scheduled transactions for at one point
149 /// The field differentiates between handlers of the same type by their UUID because there can be multiple handlers of the same type
150 /// that perform the same functionality but maybe do it for different purposes
151 /// so it is important to differentiate between them in case the user needs to retrieve a specific handler
152 /// The metadata for each handler that potentially includes information about the handler's purpose
153 /// can be retrieved from the handler's reference via the getViews() and resolveView() functions
154 access(self) let handlerInfos: {String: {UInt64: HandlerInfo}}
155
156 init() {
157 self.scheduledTransactions <- {}
158 self.sortedTimestamps = FlowTransactionScheduler.SortedTimestamps()
159 self.idsByTimestamp = {}
160 self.handlerUUIDsByTransactionID = {}
161 self.handlerInfos = {}
162 }
163
164 /// scheduleByHandler schedules a transaction by a given handler that has been used before
165 /// @param handlerTypeIdentifier: The type identifier of the handler
166 /// @param data: Optional data to pass to the transaction when executed
167 /// @param timestamp: The timestamp when the transaction should be executed
168 /// @param priority: The priority of the transaction (High, Medium, or Low)
169 /// @param executionEffort: The execution effort for the transaction
170 /// @param fees: A FlowToken vault containing sufficient fees
171 /// @return: The ID of the scheduled transaction
172 access(Owner) fun scheduleByHandler(
173 handlerTypeIdentifier: String,
174 handlerUUID: UInt64?,
175 data: AnyStruct?,
176 timestamp: UFix64,
177 priority: FlowTransactionScheduler.Priority,
178 executionEffort: UInt64,
179 fees: @FlowToken.Vault
180 ): UInt64 {
181 pre {
182 self.handlerInfos.containsKey(handlerTypeIdentifier): "Invalid handler type identifier: Handler with type identifier \(handlerTypeIdentifier) not found in manager"
183 handlerUUID == nil || self.handlerInfos[handlerTypeIdentifier]!.containsKey(handlerUUID!): "Invalid handler UUID: Handler with type identifier \(handlerTypeIdentifier) and UUID \(handlerUUID!) not found in manager"
184 }
185 let handlers = self.handlerInfos[handlerTypeIdentifier]!
186 var id = handlerUUID
187 if handlerUUID == nil {
188 assert (
189 handlers.keys.length == 1,
190 message: "Invalid handler UUID: Handler with type identifier \(handlerTypeIdentifier) has more than one UUID, but no UUID was provided"
191 )
192 id = handlers.keys[0]
193 }
194 return self.schedule(handlerCap: handlers[id!]!.capability, data: data, timestamp: timestamp, priority: priority, executionEffort: executionEffort, fees: <-fees)
195 }
196
197 /// Schedule a transaction and store it in the manager's dictionary
198 /// @param handlerCap: A capability to a resource that implements the TransactionHandler interface
199 /// @param data: Optional data to pass to the transaction when executed
200 /// @param timestamp: The timestamp when the transaction should be executed
201 /// @param priority: The priority of the transaction (High, Medium, or Low)
202 /// @param executionEffort: The execution effort for the transaction
203 /// @param fees: A FlowToken vault containing sufficient fees
204 /// @return: The ID of the scheduled transaction
205 access(Owner) fun schedule(
206 handlerCap: Capability<auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}>,
207 data: AnyStruct?,
208 timestamp: UFix64,
209 priority: FlowTransactionScheduler.Priority,
210 executionEffort: UInt64,
211 fees: @FlowToken.Vault
212 ): UInt64 {
213 // Clean up any stale transactions before scheduling a new one
214 self.cleanup()
215
216 // Route to the main FlowTransactionScheduler
217 let scheduledTransaction <- FlowTransactionScheduler.schedule(
218 handlerCap: handlerCap,
219 data: data,
220 timestamp: timestamp,
221 priority: priority,
222 executionEffort: executionEffort,
223 fees: <-fees
224 )
225
226 // Store the handler capability in our dictionary for later retrieval
227 let id = scheduledTransaction.id
228 let actualTimestamp = scheduledTransaction.timestamp
229 let handlerRef = handlerCap.borrow()
230 ?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
231 let handlerTypeIdentifier = handlerRef.getType().identifier
232 let handlerUUID = handlerRef.uuid
233
234 self.handlerUUIDsByTransactionID[id] = handlerUUID
235
236 // Store the handler capability in the handlers dictionary for later retrieval
237 if self.handlerInfos[handlerTypeIdentifier] != nil {
238 let handlers = &self.handlerInfos[handlerTypeIdentifier]! as auth(Mutate) &{UInt64: HandlerInfo}
239 if let handlerInfo = handlers[handlerUUID] {
240 handlerInfo.addTransactionID(id: id)
241 } else {
242 let handlerInfo = HandlerInfo(typeIdentifier: handlerTypeIdentifier, capability: handlerCap)
243 handlerInfo.addTransactionID(id: id)
244 handlers[handlerUUID] = handlerInfo
245 }
246 } else {
247 let handlerInfo = HandlerInfo(typeIdentifier: handlerTypeIdentifier, capability: handlerCap)
248 handlerInfo.addTransactionID(id: id)
249 let uuidDictionary: {UInt64: HandlerInfo} = {handlerUUID: handlerInfo}
250 self.handlerInfos[handlerTypeIdentifier] = uuidDictionary
251 }
252
253 // Store the transaction in the transactions dictionary
254 self.scheduledTransactions[scheduledTransaction.id] <-! scheduledTransaction
255
256 // Add the transaction to the sorted timestamps array
257 self.sortedTimestamps.add(timestamp: actualTimestamp)
258
259 // Store the transaction in the ids by timestamp dictionary
260 if self.idsByTimestamp[actualTimestamp] != nil {
261 let ids = &self.idsByTimestamp[actualTimestamp]! as auth(Mutate) &[UInt64]
262 ids.append(id)
263 } else {
264 self.idsByTimestamp[actualTimestamp] = [id]
265 }
266
267 return id
268 }
269
270 /// Cancel a scheduled transaction by its ID
271 /// @param id: The ID of the transaction to cancel
272 /// @return: A FlowToken vault containing the refunded fees
273 access(Owner) fun cancel(id: UInt64): @FlowToken.Vault {
274 // Remove the transaction from the transactions dictionary
275 let tx <- self.scheduledTransactions.remove(key: id)
276 ?? panic("Invalid ID: Transaction with ID \(id) not found in manager")
277
278 self.removeID(id: id, timestamp: tx.timestamp, handlerTypeIdentifier: tx.handlerTypeIdentifier)
279
280 // Cancel the transaction through the main scheduler
281 let refundedFees <- FlowTransactionScheduler.cancel(scheduledTx: <-tx!)
282
283 return <-refundedFees
284 }
285
286 /// Remove an ID from the manager's fields
287 /// @param id: The ID of the transaction to remove
288 /// @param timestamp: The timestamp of the transaction to remove
289 /// @param handlerTypeIdentifier: The type identifier of the handler of the transaction to remove
290 access(self) fun removeID(id: UInt64, timestamp: UFix64, handlerTypeIdentifier: String) {
291 pre {
292 self.handlerInfos.containsKey(handlerTypeIdentifier): "Invalid handler type identifier: Handler with type identifier \(handlerTypeIdentifier) not found in manager"
293 }
294
295 if self.idsByTimestamp.containsKey(timestamp) {
296 let ids = &self.idsByTimestamp[timestamp]! as auth(Mutate) &[UInt64]
297 let index = ids.firstIndex(of: id)
298 ids.remove(at: index!)
299 if ids.length == 0 {
300 self.idsByTimestamp.remove(key: timestamp)
301 self.sortedTimestamps.remove(timestamp: timestamp)
302 }
303 }
304
305 if let handlerUUID = self.handlerUUIDsByTransactionID.remove(key: id) {
306 // Remove the transaction ID from the handler info array
307 let handlers = &self.handlerInfos[handlerTypeIdentifier]! as auth(Mutate) &{UInt64: HandlerInfo}
308 if let handlerInfo = handlers[handlerUUID] {
309 handlerInfo.removeTransactionID(id: id)
310 }
311 }
312 }
313
314 /// Clean up transactions that are no longer valid (return nil or Unknown status)
315 /// This removes and destroys transactions that have been executed, canceled, or are otherwise invalid
316 /// @return: The transactions that were cleaned up (removed from the manager)
317 access(Owner) fun cleanup(): [UInt64] {
318 let currentTimestamp = getCurrentBlock().timestamp
319 var transactionsToRemove: {UInt64: UFix64} = {}
320
321 let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp)
322 for timestamp in pastTimestamps {
323 let ids = self.idsByTimestamp[timestamp] ?? []
324 if ids.length == 0 {
325 self.sortedTimestamps.remove(timestamp: timestamp)
326 continue
327 }
328 for id in ids {
329 let status = FlowTransactionScheduler.getStatus(id: id)
330 if status == nil || status! != FlowTransactionScheduler.Status.Scheduled {
331 transactionsToRemove[id] = timestamp
332 // Need to temporarily limit the number of transactions to remove
333 // because some managers on mainnet have already hit the limit and we need to batch them
334 // to make sure they get cleaned up properly
335 // This will be removed eventually
336 if transactionsToRemove.length > 50 {
337 break
338 }
339 }
340 }
341 }
342
343 // Then remove and destroy the identified transactions
344 for id in transactionsToRemove.keys {
345 if let tx <- self.scheduledTransactions.remove(key: id) {
346 self.removeID(id: id, timestamp: transactionsToRemove[id]!, handlerTypeIdentifier: tx.handlerTypeIdentifier)
347 destroy tx
348 }
349 }
350
351 return transactionsToRemove.keys
352 }
353
354 /// Remove a handler capability from the manager
355 /// The specified handler must not have any transactions scheduled for it
356 /// @param handlerTypeIdentifier: The type identifier of the handler
357 /// @param handlerUUID: The UUID of the handler
358 access(Owner) fun removeHandler(handlerTypeIdentifier: String, handlerUUID: UInt64?) {
359 // Make sure the handler exists
360 if let handlers = self.handlerInfos[handlerTypeIdentifier] {
361 var id = handlerUUID
362 // If no UUID is provided, there must be only one handler of the type
363 if handlerUUID == nil {
364 if handlers.keys.length > 1 {
365 // No-op if we don't know which UUID to remove
366 return
367 } else if handlers.keys.length == 0 {
368 self.handlerInfos.remove(key: handlerTypeIdentifier)
369 return
370 }
371 id = handlers.keys[0]
372 }
373 // Make sure the handler has no transactions scheduled for it
374 if let handlerInfo = handlers[id!] {
375 if handlerInfo.transactionIDs.length > 0 {
376 return
377 }
378 }
379 // Remove the handler uuid from the handlers dictionary
380 handlers.remove(key: id!)
381
382 // If there are no more handlers of the type, remove the type from the handlers dictionary
383 if handlers.keys.length == 0 {
384 self.handlerInfos.remove(key: handlerTypeIdentifier)
385 } else {
386 self.handlerInfos[handlerTypeIdentifier] = handlers
387 }
388 }
389 }
390
391 /// Get transaction data by its ID
392 /// @param id: The ID of the transaction to retrieve
393 /// @return: The transaction data from FlowTransactionScheduler, or nil if not found
394 access(all) view fun getTransactionData(_ id: UInt64): FlowTransactionScheduler.TransactionData? {
395 if self.scheduledTransactions.containsKey(id) {
396 return FlowTransactionScheduler.getTransactionData(id: id)
397 }
398 return nil
399 }
400
401 /// Get an un-entitled reference to a transaction handler of a given ID
402 /// @param id: The ID of the transaction to retrieve
403 /// @return: A reference to the transaction handler, or nil if not found
404 access(all) view fun borrowTransactionHandlerForID(_ id: UInt64): &{FlowTransactionScheduler.TransactionHandler}? {
405 let txData = self.getTransactionData(id)
406 return txData?.borrowHandler()
407 }
408
409 /// Get all the handler type identifiers that the manager has scheduled transactions for
410 /// @return: A dictionary of all handler type identifiers and their UUIDs
411 access(all) fun getHandlerTypeIdentifiers(): {String: [UInt64]} {
412 var handlerTypeIdentifiers: {String: [UInt64]} = {}
413 for handlerTypeIdentifier in self.handlerInfos.keys {
414 let handlerUUIDs: [UInt64] = []
415 let handlerTypes = self.handlerInfos[handlerTypeIdentifier]!
416 for uuid in handlerTypes.keys {
417 let handlerInfo = handlerTypes[uuid]!
418 if !handlerInfo.capability.check() {
419 continue
420 }
421 handlerUUIDs.append(uuid)
422 }
423 handlerTypeIdentifiers[handlerTypeIdentifier] = handlerUUIDs
424 }
425 return handlerTypeIdentifiers
426 }
427
428 /// Get an un-entitled reference to a handler by a given type identifier
429 /// @param handlerTypeIdentifier: The type identifier of the handler
430 /// @param handlerUUID: The UUID of the handler, if nil, there must be only one handler of the type, otherwise nil will be returned
431 /// @return: An un-entitled reference to the handler, or nil if not found
432 access(all) view fun borrowHandler(handlerTypeIdentifier: String, handlerUUID: UInt64?): &{FlowTransactionScheduler.TransactionHandler}? {
433 if let handlers = self.handlerInfos[handlerTypeIdentifier] {
434 if handlerUUID != nil {
435 if let handlerInfo = handlers[handlerUUID!] {
436 return handlerInfo.borrow()
437 }
438 } else if handlers.keys.length == 1 {
439 // If no uuid is provided, we can just default to the only handler uuid
440 return handlers[handlers.keys[0]]!.borrow()
441 }
442 }
443 return nil
444 }
445
446 /// Get all the views that a handler implements
447 /// @param handlerTypeIdentifier: The type identifier of the handler
448 /// @param handlerUUID: The UUID of the handler, if nil, there must be only one handler of the type, otherwise nil will be returned
449 /// @return: An array of all views
450 access(all) fun getHandlerViews(handlerTypeIdentifier: String, handlerUUID: UInt64?): [Type] {
451 if let handler = self.borrowHandler(handlerTypeIdentifier: handlerTypeIdentifier, handlerUUID: handlerUUID) {
452 return handler.getViews()
453 }
454 return []
455 }
456
457 /// Resolve a view for a handler by a given type identifier
458 /// @param handlerTypeIdentifier: The type identifier of the handler
459 /// @param handlerUUID: The UUID of the handler, if nil, there must be only one handler of the type, otherwise nil will be returned
460 /// @param viewType: The type of the view to resolve
461 /// @return: The resolved view, or nil if not found
462 access(all) fun resolveHandlerView(handlerTypeIdentifier: String, handlerUUID: UInt64?, viewType: Type): AnyStruct? {
463 if let handler = self.borrowHandler(handlerTypeIdentifier: handlerTypeIdentifier, handlerUUID: handlerUUID) {
464 return handler.resolveView(viewType)
465 }
466 return nil
467 }
468
469 /// Get all the views that a handler implements from a given transaction ID
470 /// @param transactionId: The ID of the transaction
471 /// @return: An array of all views
472 access(all) fun getHandlerViewsFromTransactionID(_ id: UInt64): [Type] {
473 if let handler = self.borrowTransactionHandlerForID(id) {
474 return handler.getViews()
475 }
476 return []
477 }
478
479 /// Resolve a view for a handler from a given transaction ID
480 /// @param transactionId: The ID of the transaction
481 /// @param viewType: The type of the view to resolve
482 /// @return: The resolved view, or nil if not found
483 access(all) fun resolveHandlerViewFromTransactionID(_ id: UInt64, viewType: Type): AnyStruct? {
484 if let handler = self.borrowTransactionHandlerForID(id) {
485 return handler.resolveView(viewType)
486 }
487 return nil
488 }
489
490 /// Get all transaction IDs stored in the manager
491 /// @return: An array of all transaction IDs
492 access(all) view fun getTransactionIDs(): [UInt64] {
493 return self.scheduledTransactions.keys
494 }
495
496 /// Get all transaction IDs stored in the manager by a given handler
497 /// @param handlerTypeIdentifier: The type identifier of the handler
498 /// @return: An array of all transaction IDs
499 access(all) view fun getTransactionIDsByHandler(handlerTypeIdentifier: String, handlerUUID: UInt64?): [UInt64] {
500 if let handlers = self.handlerInfos[handlerTypeIdentifier] {
501 if handlerUUID != nil {
502 if let handlerInfo = handlers[handlerUUID!] {
503 return handlerInfo.transactionIDs
504 }
505 } else if handlers.keys.length == 1 {
506 // If no uuid is provided, we can just default to the only handler uuid
507 return handlers[handlers.keys[0]]!.transactionIDs
508 }
509 }
510 return []
511 }
512
513 /// Get all transaction IDs stored in the manager by a given timestamp
514 /// @param timestamp: The timestamp
515 /// @return: An array of all transaction IDs
516 access(all) view fun getTransactionIDsByTimestamp(_ timestamp: UFix64): [UInt64] {
517 return self.idsByTimestamp[timestamp] ?? []
518 }
519
520 /// Get all the timestamps and IDs from a given range of timestamps
521 /// @param startTimestamp: The start timestamp
522 /// @param endTimestamp: The end timestamp
523 /// @return: A dictionary of timestamps and IDs
524 access(all) fun getTransactionIDsByTimestampRange(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: [UInt64]} {
525 var transactionsInTimeframe: {UFix64: [UInt64]} = {}
526
527 // Validate input parameters
528 if startTimestamp > endTimestamp {
529 return transactionsInTimeframe
530 }
531
532 // Get all timestamps that fall within the specified range
533 let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp)
534
535 for timestamp in allTimestampsBeforeEnd {
536 // Check if this timestamp falls within our range
537 if timestamp < startTimestamp { continue }
538
539 var timestampTransactions: [UInt64] = self.idsByTimestamp[timestamp] ?? []
540
541 if timestampTransactions.length > 0 {
542 transactionsInTimeframe[timestamp] = timestampTransactions
543 }
544 }
545
546 return transactionsInTimeframe
547 }
548
549 /// Get the status of a transaction by its ID
550 /// @param id: The ID of the transaction
551 /// @return: The status of the transaction, or Status.Unknown if not found in manager
552 access(all) view fun getTransactionStatus(id: UInt64): FlowTransactionScheduler.Status? {
553 if self.scheduledTransactions.containsKey(id) {
554 return FlowTransactionScheduler.getStatus(id: id)
555 }
556 return FlowTransactionScheduler.Status.Unknown
557 }
558
559 /// Gets the sorted timestamps struct
560 /// @return: The sorted timestamps struct
561 access(all) view fun getSortedTimestamps(): FlowTransactionScheduler.SortedTimestamps {
562 return self.sortedTimestamps
563 }
564 }
565
566 /// Create a new Manager instance
567 /// @return: A new Manager resource
568 access(all) fun createManager(): @{Manager} {
569 return <-create ManagerV1()
570 }
571
572 access(all) init() {
573 self.managerStoragePath = /storage/flowTransactionSchedulerManager
574 self.managerPublicPath = /public/flowTransactionSchedulerManager
575 }
576
577 /// Get a public reference to a manager at the given address
578 /// @param address: The address of the manager
579 /// @return: A public reference to the manager
580 access(all) view fun borrowManager(at: Address): &{Manager}? {
581 return getAccount(at).capabilities.borrow<&{Manager}>(self.managerPublicPath)
582 }
583
584 /*********************************************
585
586 COA Handler Utils
587
588 **********************************************/
589
590 access(all) event COAHandlerExecutionError(id: UInt64, owner: Address?, coaAddress: String?, errorMessage: String)
591
592 access(all) view fun coaHandlerStoragePath(): StoragePath {
593 return /storage/coaScheduledTransactionHandler
594 }
595
596 access(all) view fun coaHandlerPublicPath(): PublicPath {
597 return /public/coaScheduledTransactionHandler
598 }
599
600 /// COATransactionHandler is a resource that wraps a capability to a COA (Cadence Owned Account)
601 /// and implements the TransactionHandler interface to allow scheduling transactions for COAs.
602 /// This handler enables users to schedule transactions that will be executed on behalf of their COA.
603 access(all) resource COATransactionHandler: FlowTransactionScheduler.TransactionHandler {
604 /// The capability to the COA resource
605 access(self) let coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>
606
607 /// The capability to the FlowToken vault
608 access(self) let flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
609
610 init(coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
611 flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
612 )
613 {
614 pre {
615 coaCapability.check(): "COA capability is invalid or expired"
616 flowTokenVaultCapability.check(): "FlowToken vault capability is invalid or expired"
617 }
618 self.coaCapability = coaCapability
619 self.flowTokenVaultCapability = flowTokenVaultCapability
620 }
621
622 access(self) fun emitError(id: UInt64, errorMessage: String) {
623 let coa = self.coaCapability.borrow()!
624 emit COAHandlerExecutionError(id: id, owner: self.owner?.address, coaAddress: coa.address().toString(),
625 errorMessage: errorMessage)
626 }
627
628 /// Execute the scheduled transaction using the COA
629 /// @param id: The ID of the scheduled transaction
630 /// @param data: Optional data passed to the transaction execution. In this case, the data must be a COAHandlerParams struct with valid values.
631 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
632
633 // Borrow the COA capability
634 let coa = self.coaCapability.borrow()
635 if coa == nil {
636 emit COAHandlerExecutionError(id: id, owner: self.owner?.address ?? Address(0x0), coaAddress: nil,
637 errorMessage: "COA capability is invalid or expired for scheduled transaction with ID \(id)")
638 return
639 }
640
641 // Parse the data into a list of COAHandlerParams
642 // If the data is a single COAHandlerParams struct, wrap it in a list
643 var params: [COAHandlerParams]? = data as? [COAHandlerParams]
644 if params == nil {
645 if let param = data as? COAHandlerParams {
646 params = [param]
647 }
648 }
649
650 // Iterate through all the COA transactions and execute them all
651 // If revertOnFailure is true for a transaction and any part of it fails, the entire scheduled transaction will be reverted
652 // If not but a part of the transaction fails, an error event will be emitted but the scheduled transaction will continue to execute the next transaction
653 //
654 if let transactions = params {
655 for index, txParams in transactions {
656 switch txParams.txType {
657 case COAHandlerTxType.DepositFLOW:
658 let vault = self.flowTokenVaultCapability.borrow()
659 if vault == nil {
660 if !txParams.revertOnFailure {
661 self.emitError(id: id, errorMessage: "FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
662 continue
663 } else {
664 panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
665 }
666 }
667
668 if txParams.amount! > vault!.balance && !txParams.revertOnFailure {
669 self.emitError(id: id, errorMessage: "Insufficient FLOW in FlowToken vault for deposit into COA for scheduled transaction with ID \(id) and index \(index)")
670 continue
671 }
672
673 // Deposit the FLOW into the COA vault. If there isn't enough FLOW in the vault,
674 //the transaction will be reverted because we know revertOnFailure is true
675 coa!.deposit(from: <-vault!.withdraw(amount: txParams.amount!) as! @FlowToken.Vault)
676 case COAHandlerTxType.WithdrawFLOW:
677 let vault = self.flowTokenVaultCapability.borrow()
678 if vault == nil {
679 if !txParams.revertOnFailure {
680 self.emitError(id: id, errorMessage: "FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
681 continue
682 } else {
683 panic("FlowToken vault capability is invalid or expired for scheduled transaction with ID \(id) and index \(index)")
684 }
685 }
686
687 let amount = EVM.Balance(attoflow: 0)
688 amount.setFLOW(flow: txParams.amount!)
689
690 if amount.attoflow > coa!.balance().attoflow && !txParams.revertOnFailure {
691 self.emitError(id: id, errorMessage: "Insufficient FLOW in COA vault for withdrawal from COA for scheduled transaction with ID \(id) and index \(index)")
692 continue
693 }
694
695 // Withdraw the FLOW from the COA vault. If there isn't enough FLOW in the COA,
696 // the transaction will be reverted because we know revertOnFailure is true
697 vault!.deposit(from: <-coa!.withdraw(balance: amount))
698 case COAHandlerTxType.Call:
699 let result = coa!.call(to: txParams.callToEVMAddress!, data: txParams.data!, gasLimit: txParams.gasLimit!, value: txParams.value!)
700
701 if result.status != EVM.Status.successful {
702 if !txParams.revertOnFailure {
703 self.emitError(id: id, errorMessage: "EVM call failed for scheduled transaction with ID \(id) and index \(index) with error: \(result.errorCode):\(result.errorMessage)")
704 continue
705 } else {
706 panic("EVM call failed for scheduled transaction with ID \(id) and index \(index) with error: \(result.errorCode):\(result.errorMessage)")
707 }
708 }
709 }
710 }
711 } else {
712 self.emitError(id: id, errorMessage: "Invalid scheduled transaction data type for COA handler execution for tx with ID \(id)! Expected [FlowTransactionSchedulerUtils.COAHandlerParams] but got \(data.getType().identifier)")
713 return
714 }
715 }
716
717 /// Get the views supported by this handler
718 /// @return: Array of view types
719 access(all) view fun getViews(): [Type] {
720 return [
721 Type<COAHandlerView>(),
722 Type<StoragePath>(),
723 Type<PublicPath>(),
724 Type<MetadataViews.Display>()
725 ]
726 }
727
728 /// Resolve a view for this handler
729 /// @param viewType: The type of view to resolve
730 /// @return: The resolved view data, or nil if not supported
731 access(all) fun resolveView(_ viewType: Type): AnyStruct? {
732 if viewType == Type<COAHandlerView>() {
733 return COAHandlerView(
734 coaOwner: self.coaCapability.borrow()?.owner?.address,
735 coaEVMAddress: self.coaCapability.borrow()?.address(),
736 coaBalance: self.coaCapability.borrow()?.balance(),
737 )
738 }
739 if viewType == Type<StoragePath>() {
740 return FlowTransactionSchedulerUtils.coaHandlerStoragePath()
741 } else if viewType == Type<PublicPath>() {
742 return FlowTransactionSchedulerUtils.coaHandlerPublicPath()
743 } else if viewType == Type<MetadataViews.Display>() {
744 return MetadataViews.Display(
745 name: "COA Scheduled Transaction Handler",
746 description: "Scheduled Transaction Handler that can execute transactions on behalf of a COA",
747 thumbnail: MetadataViews.HTTPFile(
748 url: ""
749 )
750 )
751 }
752 return nil
753 }
754 }
755
756 /// Enum for COA handler execution type
757 access(all) enum COAHandlerTxType: UInt8 {
758 access(all) case DepositFLOW
759 access(all) case WithdrawFLOW
760 access(all) case Call
761
762 // TODO: Should we have other transaction types??
763 }
764
765 access(all) struct COAHandlerParams {
766
767 /// The type of transaction to execute
768 access(all) let txType: COAHandlerTxType
769
770 /// Indicates if the whole set of scheduled transactions should be reverted
771 /// if this one transaction fails to execute in EVM
772 access(all) let revertOnFailure: Bool
773
774 /// The amount of FLOW to deposit or withdraw
775 /// Not required for the Call transaction type
776 access(all) let amount: UFix64?
777
778 /// The following fields are only required for the Call transaction type
779 access(all) let callToEVMAddress: EVM.EVMAddress?
780 access(all) let data: [UInt8]?
781 access(all) let gasLimit: UInt64?
782 access(all) let value: EVM.Balance?
783
784 init(txType: UInt8, revertOnFailure: Bool, amount: UFix64?, callToEVMAddress: String?, data: [UInt8]?, gasLimit: UInt64?, value: UInt?) {
785 self.txType = COAHandlerTxType(rawValue: txType)
786 ?? panic("Invalid COA transaction type enum")
787 self.revertOnFailure = revertOnFailure
788 if self.txType == COAHandlerTxType.DepositFLOW {
789 assert(amount != nil, message: "Amount is required for deposit but was not provided")
790 }
791 if self.txType == COAHandlerTxType.WithdrawFLOW {
792 assert(amount != nil, message: "Amount is required for withdrawal but was not provided")
793 }
794 if self.txType == COAHandlerTxType.Call {
795 assert(callToEVMAddress != nil, message: "Call to EVM address is required for EVM call but was not provided")
796 assert((data != nil && value != nil) || (data == nil ? value != nil : true), message: "Data and/or value are required for EVM call but neither were provided")
797 assert(gasLimit != nil, message: "Gas limit is required for EVM call but was not provided")
798 }
799 self.amount = amount
800 if callToEVMAddress != nil {
801 self.callToEVMAddress = EVM.addressFromString(callToEVMAddress!)
802 } else {
803 self.callToEVMAddress = nil
804 }
805 if data != nil {
806 self.data = data
807 } else {
808 self.data = []
809 }
810 self.gasLimit = gasLimit
811 if let unwrappedValue = value {
812 self.value = EVM.Balance(attoflow: unwrappedValue)
813 } else {
814 self.value = nil
815 }
816 }
817 }
818
819 /// View struct for COA handler metadata
820 access(all) struct COAHandlerView {
821 access(all) let coaOwner: Address?
822 access(all) let coaEVMAddress: EVM.EVMAddress?
823
824 access(all) let coaBalance: EVM.Balance?
825
826 init(coaOwner: Address?, coaEVMAddress: EVM.EVMAddress?, coaBalance: EVM.Balance?) {
827 self.coaOwner = coaOwner
828 self.coaEVMAddress = coaEVMAddress
829 self.coaBalance = coaBalance
830 }
831 }
832
833 /// Create a COA transaction handler
834 /// @param coaCapability: Capability to the COA resource
835 /// @param flowTokenVaultCapability: Capability to the FlowToken vault
836 /// @return: A new COATransactionHandler resource
837 access(all) fun createCOATransactionHandler(
838 coaCapability: Capability<auth(EVM.Owner) &EVM.CadenceOwnedAccount>,
839 flowTokenVaultCapability: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
840 ): @COATransactionHandler {
841 return <-create COATransactionHandler(
842 coaCapability: coaCapability,
843 flowTokenVaultCapability: flowTokenVaultCapability,
844 )
845 }
846
847 /********************************************
848
849 Scheduled Transactions Metadata Views
850
851 ***********************************************/
852
853}