DeploySEALED
□░▒○◆○!□▫╳╳@?*▒╳%█?◇░▓!▫□╲▪□*■^□#╲▪^~◆▪*╱▓$*%□█▒●&#□╱@╲!$◇*@◇◇╱▒
Transaction ID
Execution Fee
0.02006 FLOWTransaction Summary
DeployContract deployment
Contract deployment
Script Arguments
0nameString
FlowTransactionSchedulerV3
1codeString
import FungibleToken from 0xf233dcee88fe0abe
import FlowToken from 0x1654653399040a61
import FlowFees from 0xf919ee77447b7497
import FlowStorageFees from 0xe467b9dd11fa00df
import ViewResolver from 0x1d7e57aa55817448
/// FlowTransactionSchedulerV3 enables smart contracts to schedule autonomous execution in the future.
///
/// This contract implements FLIP 330's scheduled transaction system, allowing contracts to "wake up" and execute
/// logic at predefined times without external triggers.
///
/// Scheduled transactions are prioritized (High/Medium/Low) with different execution guarantees and fee multipliers:
/// - High priority guarantees first-block execution,
/// - Medium priority provides best-effort scheduling,
/// - Low priority executes opportunistically when capacity allows after the time it was scheduled.
///
/// The system uses time slots with execution effort limits to manage network resources,
/// ensuring predictable performance while enabling novel autonomous blockchain patterns like recurring
/// payments, automated arbitrage, and time-based contract logic.
///
/// V3: Simplified priority pools - each priority has its own independent pool with no shared capacity.
access(all) contract FlowTransactionSchedulerV3 {
/// singleton instance used to store all scheduled transaction data
/// and route all scheduled transaction functionality
access(self) var sharedScheduler: Capability<auth(Cancel) &SharedScheduler>
/// storage path for the singleton scheduler resource
access(all) let storagePath: StoragePath
/// Enums
/// Priority
access(all) enum Priority: UInt8 {
access(all) case High
access(all) case Medium
access(all) case Low
}
/// Status
access(all) enum Status: UInt8 {
/// unknown statuses are used for handling historic scheduled transactions with null statuses
access(all) case Unknown
/// mutable status
access(all) case Scheduled
/// finalized statuses
access(all) case Executed
access(all) case Canceled
}
/// Events
/// Emitted when a transaction is scheduled
access(all) event Scheduled(
id: UInt64,
priority: UInt8,
timestamp: UFix64,
executionEffort: UInt64,
fees: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String,
transactionHandlerUUID: UInt64,
// The public path of the transaction handler that can be used to resolve views
// DISCLAIMER: There is no guarantee that the public path is accurate
transactionHandlerPublicPath: PublicPath?
)
/// Emitted when a scheduled transaction's scheduled timestamp is reached and it is ready for execution
access(all) event PendingExecution(
id: UInt64,
priority: UInt8,
executionEffort: UInt64,
fees: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String
)
/// Emitted when a scheduled transaction is executed by the FVM
access(all) event Executed(
id: UInt64,
priority: UInt8,
executionEffort: UInt64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String,
transactionHandlerUUID: UInt64,
// The public path of the transaction handler that can be used to resolve views
// DISCLAIMER: There is no guarantee that the public path is accurate
transactionHandlerPublicPath: PublicPath?
)
/// Emitted when a scheduled transaction is canceled by the creator of the transaction
access(all) event Canceled(
id: UInt64,
priority: UInt8,
feesReturned: UFix64,
feesDeducted: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String
)
/// Emitted when a collection limit is reached
/// The limit that was reached is non-nil and is the limit that was reached
/// The other limit that was not reached is nil
access(all) event CollectionLimitReached(
collectionEffortLimit: UInt64?,
collectionTransactionsLimit: Int?
)
access(all) event RemovalLimitReached(id: UInt64, remainingLength: Int)
access(all) event TransactionAdded(id: UInt64)
access(all) event TransactionRemoved(id: UInt64)
// Emitted when one or more of the configuration details fields are updated
// Event listeners can listen to this and query the new configuration
// if they need to
access(all) event ConfigUpdated()
/// Entitlements
access(all) entitlement Execute
access(all) entitlement Process
access(all) entitlement Cancel
access(all) entitlement UpdateConfig
/// Interfaces
/// TransactionHandler is an interface that defines a single method executeTransaction that
/// must be implemented by the resource that contains the logic to be executed by the scheduled transaction.
/// An authorized capability to this resource is provided when scheduling a transaction.
/// The transaction scheduler uses this capability to execute the transaction when its scheduled timestamp arrives.
access(all) resource interface TransactionHandler: ViewResolver.Resolver {
access(all) view fun getViews(): [Type] {
return []
}
access(all) fun resolveView(_ view: Type): AnyStruct? {
return nil
}
/// Executes the implemented transaction logic
///
/// @param id: The id of the scheduled transaction (this can be useful for any internal tracking)
/// @param data: The data that was passed when the transaction was originally scheduled
/// that may be useful for the execution of the transaction logic
access(Execute) fun executeTransaction(id: UInt64, data: AnyStruct?)
}
/// Structs
/// ScheduledTransaction is the resource that the user receives after scheduling a transaction.
/// It allows them to get the status of their transaction and can be passed back
/// to the scheduler contract to cancel the transaction if it has not yet been executed.
access(all) resource ScheduledTransaction {
access(all) let id: UInt64
access(all) let timestamp: UFix64
access(all) let handlerTypeIdentifier: String
access(all) view fun status(): Status? {
return FlowTransactionSchedulerV3.sharedScheduler.borrow()!.getStatus(id: self.id)
}
init(
id: UInt64,
timestamp: UFix64,
handlerTypeIdentifier: String
) {
self.id = id
self.timestamp = timestamp
self.handlerTypeIdentifier = handlerTypeIdentifier
}
// event emitted when the resource is destroyed
access(all) event ResourceDestroyed(id: UInt64 = self.id, timestamp: UFix64 = self.timestamp, handlerTypeIdentifier: String = self.handlerTypeIdentifier)
}
/// EstimatedScheduledTransaction contains data for estimating transaction scheduling.
access(all) struct EstimatedScheduledTransaction {
/// flowFee is the estimated fee in Flow for the transaction to be scheduled
access(all) let flowFee: UFix64?
/// timestamp is estimated timestamp that the transaction will be executed at
access(all) let timestamp: UFix64?
/// error is an optional error message if the transaction cannot be scheduled
access(all) let error: String?
access(contract) view init(flowFee: UFix64?, timestamp: UFix64?, error: String?) {
self.flowFee = flowFee
self.timestamp = timestamp
self.error = error
}
}
/// Transaction data is a representation of a scheduled transaction
/// It is the source of truth for an individual transaction and stores the
/// capability to the handler that contains the logic that will be executed by the transaction.
access(all) struct TransactionData {
access(all) let id: UInt64
access(all) let priority: Priority
access(all) let executionEffort: UInt64
access(all) var status: Status
/// Fee amount to pay for the transaction
access(all) let fees: UFix64
/// The timestamp that the transaction is scheduled for
/// For medium priority transactions, it may be different than the requested timestamp
/// For low priority transactions, it is the requested timestamp,
/// but the timestamp where the transaction is actually executed may be different
access(all) var scheduledTimestamp: UFix64
/// Capability to the logic that the transaction will execute
access(contract) let handler: Capability<auth(Execute) &{TransactionHandler}>
/// Type identifier of the transaction handler
access(all) let handlerTypeIdentifier: String
access(all) let handlerAddress: Address
/// Optional data that can be passed to the handler
access(contract) let data: AnyStruct?
access(contract) init(
id: UInt64,
handler: Capability<auth(Execute) &{TransactionHandler}>,
scheduledTimestamp: UFix64,
data: AnyStruct?,
priority: Priority,
executionEffort: UInt64,
fees: UFix64,
) {
self.id = id
self.handler = handler
self.data = data
self.priority = priority
self.executionEffort = executionEffort
self.fees = fees
self.status = Status.Scheduled
let handlerRef = handler.borrow()
?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
self.handlerAddress = handler.address
self.handlerTypeIdentifier = handlerRef.getType().identifier
self.scheduledTimestamp = scheduledTimestamp
}
/// setStatus updates the status of the transaction.
/// It panics if the transaction status is already finalized.
access(contract) fun setStatus(newStatus: Status) {
pre {
newStatus != Status.Unknown: "Invalid status: New status cannot be Unknown"
self.status != Status.Executed && self.status != Status.Canceled:
"Invalid status: Transaction with id \(self.id) is already finalized"
newStatus == Status.Executed ? self.status == Status.Scheduled : true:
"Invalid status: Transaction with id \(self.id) can only be set as Executed if it is Scheduled"
newStatus == Status.Canceled ? self.status == Status.Scheduled : true:
"Invalid status: Transaction with id \(self.id) can only be set as Canceled if it is Scheduled"
}
self.status = newStatus
}
/// setScheduledTimestamp updates the scheduled timestamp of the transaction.
/// It panics if the transaction status is already finalized.
access(contract) fun setScheduledTimestamp(newTimestamp: UFix64) {
pre {
self.status != Status.Executed && self.status != Status.Canceled:
"Invalid status: Transaction with id \(self.id) is already finalized"
}
self.scheduledTimestamp = newTimestamp
}
/// payAndRefundFees withdraws fees from the transaction based on the refund multiplier.
/// It deposits any leftover fees to the FlowFees vault to be used to pay node operator rewards
/// like any other transaction on the Flow network.
access(contract) fun payAndRefundFees(refundMultiplier: UFix64): @FlowToken.Vault {
pre {
refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
"Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
}
if refundMultiplier == 0.0 {
FlowFees.deposit(from: <-FlowTransactionSchedulerV3.withdrawFees(amount: self.fees))
return <-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
} else {
let amountToReturn = self.fees * refundMultiplier
let amountToKeep = self.fees - amountToReturn
let feesToReturn <- FlowTransactionSchedulerV3.withdrawFees(amount: amountToReturn)
FlowFees.deposit(from: <-FlowTransactionSchedulerV3.withdrawFees(amount: amountToKeep))
return <-feesToReturn
}
}
/// getData copies and returns the data field
access(contract) view fun getData(): AnyStruct? {
return self.data
}
/// borrowHandler returns an un-entitled reference to the transaction handler
/// This allows users to query metadata views about the handler
/// @return: An un-entitled reference to the transaction handler
access(all) view fun borrowHandler(): &{TransactionHandler} {
return self.handler.borrow() as? &{TransactionHandler}
?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
}
}
/// Struct interface representing all the base configuration details in the Scheduler contract
/// that is used for governing the protocol
/// This is an interface to allow for the configuration details to be updated in the future
access(all) struct interface SchedulerConfig {
/// maximum effort that can be used for any transaction
access(all) var maximumIndividualEffort: UInt64
/// minimum execution effort is the minimum effort that can be
/// used for any transaction
access(all) var minimumExecutionEffort: UInt64
/// priority effort limit is the maximum cumulative effort per priority in a timeslot
/// Each priority has its own independent pool (no shared pool)
access(all) var priorityEffortLimit: {Priority: UInt64}
/// max data size is the maximum data size that can be stored for a transaction
access(all) var maxDataSizeMB: UFix64
/// priority fee multipliers are values we use to calculate the added
/// processing fee for each priority
access(all) var priorityFeeMultipliers: {Priority: UFix64}
/// refund multiplier is the portion of the fees that are refunded when any transaction is cancelled
access(all) var refundMultiplier: UFix64
/// canceledTransactionsLimit is the maximum number of canceled transactions
/// to keep in the canceledTransactions array
access(all) var canceledTransactionsLimit: UInt
/// collectionEffortLimit is the maximum effort that can be used for all transactions in a collection
access(all) var collectionEffortLimit: UInt64
/// collectionTransactionsLimit is the maximum number of transactions that can be processed in a collection
access(all) var collectionTransactionsLimit: Int
access(all) var removalTransactionsLimit: Int
/// maxTimestampSearchIterations is the maximum number of slots to search when looking for available capacity
access(all) var maxTimestampSearchIterations: Int
access(all) init(
maximumIndividualEffort: UInt64,
minimumExecutionEffort: UInt64,
priorityEffortLimit: {Priority: UInt64},
maxDataSizeMB: UFix64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
canceledTransactionsLimit: UInt,
collectionEffortLimit: UInt64,
collectionTransactionsLimit: Int,
removalTransactionsLimit: Int,
maxTimestampSearchIterations: Int
) {
pre {
refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
"Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
priorityFeeMultipliers[Priority.Low]! >= 1.0:
"Invalid priority fee multiplier: Low priority multiplier must be greater than or equal to 1.0 but got \(priorityFeeMultipliers[Priority.Low]!)"
priorityFeeMultipliers[Priority.Medium]! > priorityFeeMultipliers[Priority.Low]!:
"Invalid priority fee multiplier: Medium priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Low]!) but got \(priorityFeeMultipliers[Priority.Medium]!)"
priorityFeeMultipliers[Priority.High]! > priorityFeeMultipliers[Priority.Medium]!:
"Invalid priority fee multiplier: High priority multiplier must be greater than or equal to \(priorityFeeMultipliers[Priority.Medium]!) but got \(priorityFeeMultipliers[Priority.High]!)"
priorityEffortLimit[Priority.High]! > 0:
"Invalid priority effort limit: High priority effort limit must be greater than 0"
priorityEffortLimit[Priority.Medium]! > 0:
"Invalid priority effort limit: Medium priority effort limit must be greater than 0"
priorityEffortLimit[Priority.Low]! > 0:
"Invalid priority effort limit: Low priority effort limit must be greater than 0"
collectionTransactionsLimit >= 0:
"Invalid collection transactions limit: Collection transactions limit must be greater than or equal to 0 but got \(collectionTransactionsLimit)"
canceledTransactionsLimit >= 1:
"Invalid canceled transactions limit: Canceled transactions limit must be greater than or equal to 1 but got \(canceledTransactionsLimit)"
removalTransactionsLimit >= 0:
"Invalid removal transactions limit: Removal transactions limit must be greater than or equal to 0 but got \(removalTransactionsLimit)"
maxTimestampSearchIterations >= 1:
"Invalid max timestamp search iterations: Must be at least 1 but got \(maxTimestampSearchIterations)"
}
}
}
/// Concrete implementation of the SchedulerConfig interface
/// This struct is used to store the configuration details in the Scheduler contract
access(all) struct Config: SchedulerConfig {
access(all) var maximumIndividualEffort: UInt64
access(all) var minimumExecutionEffort: UInt64
access(all) var priorityEffortLimit: {Priority: UInt64}
access(all) var maxDataSizeMB: UFix64
access(all) var priorityFeeMultipliers: {Priority: UFix64}
access(all) var refundMultiplier: UFix64
access(all) var canceledTransactionsLimit: UInt
access(all) var collectionEffortLimit: UInt64
access(all) var collectionTransactionsLimit: Int
access(all) var removalTransactionsLimit: Int
access(all) var maxTimestampSearchIterations: Int
access(all) init(
maximumIndividualEffort: UInt64,
minimumExecutionEffort: UInt64,
priorityEffortLimit: {Priority: UInt64},
maxDataSizeMB: UFix64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
canceledTransactionsLimit: UInt,
collectionEffortLimit: UInt64,
collectionTransactionsLimit: Int,
removalTransactionsLimit: Int,
maxTimestampSearchIterations: Int
) {
self.maximumIndividualEffort = maximumIndividualEffort
self.minimumExecutionEffort = minimumExecutionEffort
self.priorityEffortLimit = priorityEffortLimit
self.maxDataSizeMB = maxDataSizeMB
self.priorityFeeMultipliers = priorityFeeMultipliers
self.refundMultiplier = refundMultiplier
self.canceledTransactionsLimit = canceledTransactionsLimit
self.collectionEffortLimit = collectionEffortLimit
self.collectionTransactionsLimit = collectionTransactionsLimit
self.removalTransactionsLimit = removalTransactionsLimit
self.maxTimestampSearchIterations = maxTimestampSearchIterations
}
}
/// SortedTimestamps maintains timestamps sorted in ascending order for efficient processing
/// It encapsulates all operations related to maintaining and querying sorted timestamps
access(all) struct SortedTimestamps {
/// Internal sorted array of timestamps
access(self) var timestamps: [UFix64]
access(all) init() {
self.timestamps = []
}
/// Add a timestamp to the sorted array maintaining sorted order
access(all) fun add(timestamp: UFix64) {
var insertIndex = 0
for i, ts in self.timestamps {
if timestamp < ts {
insertIndex = i
break
} else if timestamp == ts {
return
}
insertIndex = i + 1
}
self.timestamps.insert(at: insertIndex, timestamp)
}
/// Remove a timestamp from the sorted array
access(all) fun remove(timestamp: UFix64) {
let index = self.timestamps.firstIndex(of: timestamp)
if index != nil {
self.timestamps.remove(at: index!)
}
}
/// Get all timestamps that are in the past (less than or equal to current timestamp)
access(all) fun getBefore(current: UFix64): [UFix64] {
let pastTimestamps: [UFix64] = []
for timestamp in self.timestamps {
if timestamp <= current {
pastTimestamps.append(timestamp)
} else {
break // No need to check further since array is sorted
}
}
return pastTimestamps
}
/// Check if there are any timestamps that need processing
/// Returns true if processing is needed, false for early exit
access(all) fun hasBefore(current: UFix64): Bool {
return self.timestamps.length > 0 && self.timestamps[0] <= current
}
/// Get the whole array of timestamps
access(all) fun getAll(): [UFix64] {
return self.timestamps
}
}
/// Resources
/// Shared scheduler is a resource that is used as a singleton in the scheduler contract and contains
/// all the functionality to schedule, process and execute transactions as well as the internal state.
access(all) resource SharedScheduler {
/// nextID contains the next transaction ID to be assigned
/// This the ID is monotonically increasing and is used to identify each transaction
access(contract) var nextID: UInt64
/// transactions is a map of transaction IDs to TransactionData structs
access(contract) var transactions: {UInt64: TransactionData}
/// slot queues - separate maps per priority for O(1) access without nested dictionary copies
/// Maps timestamp -> (transactionID -> executionEffort)
access(contract) var slotQueueHigh: {UFix64: {UInt64: UInt64}}
access(contract) var slotQueueMedium: {UFix64: {UInt64: UInt64}}
access(contract) var slotQueueLow: {UFix64: {UInt64: UInt64}}
/// slot used effort - separate maps per priority for O(1) access
/// Maps timestamp -> total effort used for that priority
access(contract) var slotUsedEffortHigh: {UFix64: UInt64}
access(contract) var slotUsedEffortMedium: {UFix64: UInt64}
access(contract) var slotUsedEffortLow: {UFix64: UInt64}
/// sorted timestamps manager for efficient processing
access(contract) var sortedTimestamps: SortedTimestamps
/// canceled transactions keeps a record of canceled transaction IDs up to a canceledTransactionsLimit
access(contract) var canceledTransactions: [UInt64]
/// Struct that contains all the configuration details for the transaction scheduler protocol
/// Can be updated by the owner of the contract
access(contract) var config: {SchedulerConfig}
access(all) init() {
self.nextID = 1
self.canceledTransactions = [0 as UInt64]
self.transactions = {}
self.slotQueueHigh = {}
self.slotQueueMedium = {}
self.slotQueueLow = {}
self.slotUsedEffortHigh = {}
self.slotUsedEffortMedium = {}
self.slotUsedEffortLow = {}
self.sortedTimestamps = SortedTimestamps()
/* V2 Simplified slot efforts - each priority has its own independent pool:
Timestamp Slot (50kee total)
┌─────────────────────────┐
│ ┌─────────────────────┐ │
│ │ High Pool (30kee) │ │ High: fail if full
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ Medium Pool (15kee) │ │ Medium: shift timestamp if full
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ Low Pool (5kee) │ │ Low: shift timestamp if full
│ └─────────────────────┘ │
└─────────────────────────┘
*/
self.config = Config(
maximumIndividualEffort: 9999,
minimumExecutionEffort: 10,
priorityEffortLimit: {
Priority.High: 30_000,
Priority.Medium: 15_000,
Priority.Low: 5_000
},
maxDataSizeMB: 3.0,
priorityFeeMultipliers: {
Priority.High: 10.0,
Priority.Medium: 5.0,
Priority.Low: 2.0
},
refundMultiplier: 0.5,
canceledTransactionsLimit: 1000,
collectionEffortLimit: 500_000,
collectionTransactionsLimit: 150,
removalTransactionsLimit: 200,
maxTimestampSearchIterations: 1000
)
}
/// Gets a copy of the struct containing all the configuration details
/// of the Scheduler resource
access(contract) view fun getConfig(): {SchedulerConfig} {
return self.config
}
/// sets all the configuration details for the Scheduler resource
access(UpdateConfig) fun setConfig(newConfig: {SchedulerConfig}) {
self.config = newConfig
emit ConfigUpdated()
}
/// getTransaction returns a copy of the specified transaction
access(contract) view fun getTransaction(id: UInt64): TransactionData? {
return self.transactions[id]
}
/// borrowTransaction borrows a reference to the specified transaction
access(contract) view fun borrowTransaction(id: UInt64): &TransactionData? {
return &self.transactions[id]
}
/// getCanceledTransactions returns a copy of the canceled transactions array
access(contract) view fun getCanceledTransactions(): [UInt64] {
return self.canceledTransactions
}
/// getTransactionsForTimeframe returns a dictionary of transactions scheduled within a specified time range,
/// organized by timestamp and priority with arrays of transaction IDs.
/// WARNING: If you provide a time range that is too large, the function will likely fail to complete
/// because the function will run out of gas. Keep the time range small.
///
/// @param startTimestamp: The start timestamp (inclusive) for the time range
/// @param endTimestamp: The end timestamp (inclusive) for the time range
/// @return {UFix64: {Priority: [UInt64]}}: A dictionary mapping timestamps to priorities to arrays of transaction IDs
access(contract) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
var transactionsInTimeframe: {UFix64: {UInt8: [UInt64]}} = {}
// Validate input parameters
if startTimestamp > endTimestamp {
return transactionsInTimeframe
}
// Get all timestamps that fall within the specified range
let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp)
for timestamp in allTimestampsBeforeEnd {
// Check if this timestamp falls within our range
if timestamp < startTimestamp { continue }
var timestampTransactions: {UInt8: [UInt64]} = {}
// Process high priority queue
let highQueue = self.slotQueueHigh[timestamp] ?? {}
if highQueue.keys.length > 0 {
timestampTransactions[Priority.High.rawValue] = highQueue.keys
}
// Process medium priority queue
let mediumQueue = self.slotQueueMedium[timestamp] ?? {}
if mediumQueue.keys.length > 0 {
timestampTransactions[Priority.Medium.rawValue] = mediumQueue.keys
}
// Process low priority queue
let lowQueue = self.slotQueueLow[timestamp] ?? {}
if lowQueue.keys.length > 0 {
timestampTransactions[Priority.Low.rawValue] = lowQueue.keys
}
if timestampTransactions.keys.length > 0 {
transactionsInTimeframe[timestamp] = timestampTransactions
}
}
return transactionsInTimeframe
}
/// calculate fee by converting execution effort to a fee in Flow tokens.
/// @param executionEffort: The execution effort of the transaction
/// @param priority: The priority of the transaction
/// @param dataSizeMB: The size of the data that was passed when the transaction was originally scheduled
/// @return UFix64: The fee in Flow tokens that is required to pay for the transaction
access(contract) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 {
// Use the official FlowFees calculation
let baseFee = FlowFees.computeFees(inclusionEffort: 1.0, executionEffort: UFix64(executionEffort)/100000000.0)
// Scale the execution fee by the multiplier for the priority
let scaledExecutionFee = baseFee * self.config.priorityFeeMultipliers[priority]!
// Calculate the FLOW required to pay for storage of the transaction data
let storageFee = FlowStorageFees.storageCapacityToFlow(dataSizeMB)
return scaledExecutionFee + storageFee
}
/// getNextIDAndIncrement returns the next ID and increments the ID counter
access(self) fun getNextIDAndIncrement(): UInt64 {
let nextID = self.nextID
self.nextID = self.nextID + 1
return nextID
}
/// get status of the scheduled transaction
/// @param id: The ID of the transaction to get the status of
/// @return Status: The status of the transaction, if the transaction is not found Unknown is returned.
access(contract) view fun getStatus(id: UInt64): Status? {
// if the transaction ID is greater than the next ID, it is not scheduled yet and has never existed
if id == 0 as UInt64 || id >= self.nextID {
return nil
}
// This should always return Scheduled or Executed
if let tx = self.borrowTransaction(id: id) {
return tx.status
}
// if the transaction was canceled and it is still not pruned from
// list return canceled status
if self.canceledTransactions.contains(id) {
return Status.Canceled
}
// if transaction ID is after first canceled ID it must be executed
// otherwise it would have been canceled and part of this list
let firstCanceledID = self.canceledTransactions[0]
if id > firstCanceledID {
return Status.Executed
}
// the transaction list was pruned and the transaction status might be
// either canceled or execute so we return unknown
return Status.Unknown
}
/// schedule is the primary entry point for scheduling a new transaction within the scheduler contract.
/// If scheduling the transaction is not possible either due to invalid arguments or due to
/// unavailable slots, the function panics.
//
/// The schedule function accepts the following arguments:
/// @param: transaction: A capability to a resource in storage that implements the transaction handler
/// interface. This handler will be invoked at execution time and will receive the specified data payload.
/// @param: timestamp: Specifies the earliest block timestamp at which the transaction is eligible for execution
/// (Unix timestamp so fractional seconds values are ignored). It must be set in the future.
/// @param: priority: An enum value (`High`, `Medium`, or `Low`) that influences the scheduling behavior and determines
/// how soon after the timestamp the transaction will be executed.
/// @param: executionEffort: Defines the maximum computational resources allocated to the transaction. This also determines
/// the fee charged. Unused execution effort is not refunded.
/// @param: fees: A Vault resource containing sufficient funds to cover the required execution effort.
access(contract) fun schedule(
handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64,
fees: @FlowToken.Vault
): @ScheduledTransaction {
// Use the estimate function to validate inputs
let estimate = self.estimate(
data: data,
timestamp: timestamp,
priority: priority,
executionEffort: executionEffort
)
// Estimate returns an error for low priority transactions
// so need to check that the error is fine
// because low priority transactions are allowed in schedule
if estimate.error != nil && estimate.timestamp == nil {
panic(estimate.error!)
}
assert (
fees.balance >= estimate.flowFee!,
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."
)
let transactionID = self.getNextIDAndIncrement()
let transactionData = TransactionData(
id: transactionID,
handler: handlerCap,
scheduledTimestamp: estimate.timestamp!,
data: data,
priority: priority,
executionEffort: executionEffort,
fees: fees.balance,
)
// Deposit the fees to the service account's vault
FlowTransactionSchedulerV3.depositFees(from: <-fees)
let handlerRef = handlerCap.borrow()
?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
let handlerPublicPath = handlerRef.resolveView(Type<PublicPath>()) as? PublicPath
emit Scheduled(
id: transactionData.id,
priority: transactionData.priority.rawValue,
timestamp: transactionData.scheduledTimestamp,
executionEffort: transactionData.executionEffort,
fees: transactionData.fees,
transactionHandlerOwner: transactionData.handler.address,
transactionHandlerTypeIdentifier: transactionData.handlerTypeIdentifier,
transactionHandlerUUID: handlerRef.uuid,
transactionHandlerPublicPath: handlerPublicPath
)
// Add the transaction to the slot queue and update the internal state
self.addTransaction(slot: estimate.timestamp!, txData: transactionData)
return <-create ScheduledTransaction(
id: transactionID,
timestamp: estimate.timestamp!,
handlerTypeIdentifier: transactionData.handlerTypeIdentifier
)
}
/// The estimate function calculates the required fee in Flow and expected execution timestamp for
/// a transaction based on the requested timestamp, priority, and execution effort.
///
/// If the provided arguments are invalid or the transaction cannot be scheduled (e.g., due to
/// insufficient computation effort or unavailable time slots) the estimate function
/// returns an EstimatedScheduledTransaction struct with a non-nil error message.
///
/// This helps developers ensure sufficient funding and preview the expected scheduling window,
/// reducing the risk of unnecessary cancellations.
///
/// V2 Simplified: Each priority has its own independent pool. No shared pool logic.
///
/// @param data: The data that was passed when the transaction was originally scheduled
/// @param timestamp: The requested timestamp for the transaction
/// @param priority: The priority of the transaction
/// @param executionEffort: The execution effort of the transaction
/// @return EstimatedScheduledTransaction: A struct containing the estimated fee, timestamp, and error message
access(contract) fun estimate(
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64
): EstimatedScheduledTransaction {
// Remove fractional values from the timestamp
let sanitizedTimestamp = UFix64(UInt64(timestamp))
if sanitizedTimestamp <= getCurrentBlock().timestamp {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid timestamp: \(sanitizedTimestamp) is in the past, current timestamp: \(getCurrentBlock().timestamp)"
)
}
if executionEffort > self.config.maximumIndividualEffort {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid execution effort: \(executionEffort) is greater than the maximum transaction effort of \(self.config.maximumIndividualEffort)"
)
}
if executionEffort > self.config.priorityEffortLimit[priority]! {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid execution effort: \(executionEffort) is greater than the priority's max effort of \(self.config.priorityEffortLimit[priority]!)"
)
}
if executionEffort < self.config.minimumExecutionEffort {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid execution effort: \(executionEffort) is less than the minimum execution effort of \(self.config.minimumExecutionEffort)"
)
}
let dataSizeMB = FlowTransactionSchedulerV3.getSizeOfData(data)
if dataSizeMB > self.config.maxDataSizeMB {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid data size: \(dataSizeMB) is greater than the maximum data size of \(self.config.maxDataSizeMB)MB"
)
}
let fee = self.calculateFee(executionEffort: executionEffort, priority: priority, dataSizeMB: dataSizeMB)
let scheduledTimestamp = self.calculateScheduledTimestamp(
timestamp: sanitizedTimestamp,
priority: priority,
executionEffort: executionEffort
)
if scheduledTimestamp == nil {
return EstimatedScheduledTransaction(
flowFee: nil,
timestamp: nil,
error: "Invalid execution effort: \(executionEffort) is greater than the priority's available effort for the requested timestamp."
)
}
// V2: Low priority now has its own pool, so we can provide estimates for it
return EstimatedScheduledTransaction(flowFee: fee, timestamp: scheduledTimestamp, error: nil)
}
/// calculateScheduledTimestamp calculates the timestamp at which a transaction
/// can be scheduled. It takes into account the priority of the transaction and
/// the execution effort.
/// - If the transaction is high priority, it returns the timestamp if there is enough
/// space or nil if there is no space left.
/// - If the transaction is medium or low priority and there is space left in the requested timestamp,
/// it returns the requested timestamp. If there is not enough space, it finds the next timestamp with space.
///
/// @param timestamp: The requested timestamp for the transaction
/// @param priority: The priority of the transaction
/// @param executionEffort: The execution effort of the transaction
/// @return UFix64?: The timestamp at which the transaction can be scheduled, or nil if there is no space left for a high priority transaction
///
/// V2 Simplified: Iterative search instead of recursive. Each priority has its own pool.
/// High priority: O(1) - single check, fail if full
/// Medium/Low: O(n) worst case where n = maxTimestampSearchIterations
access(contract) view fun calculateScheduledTimestamp(
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64
): UFix64? {
var currentTimestamp = timestamp
var iterations = 0
let maxIterations = self.config.maxTimestampSearchIterations
while iterations < maxIterations {
let available = self.getSlotAvailableEffort(timestamp: currentTimestamp, priority: priority)
// If there's enough space, schedule here
if executionEffort <= available {
return currentTimestamp
}
// High priority: exact timestamp or fail immediately
if priority == Priority.High {
return nil
}
// Medium and Low priorities: try next timestamp
currentTimestamp = currentTimestamp + 1.0
iterations = iterations + 1
}
// Exceeded max iterations - cannot schedule
return nil
}
/// slot available effort returns the amount of effort that is available for a given timestamp and priority.
/// V2 Simplified: Each priority has its own independent pool, no shared pool.
/// O(1) lookup - single map access per priority.
access(contract) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
let sanitizedTimestamp = UFix64(UInt64(timestamp))
let limit = self.config.priorityEffortLimit[priority]!
switch priority {
case Priority.High:
return limit.saturatingSubtract(self.slotUsedEffortHigh[sanitizedTimestamp] ?? 0)
case Priority.Medium:
return limit.saturatingSubtract(self.slotUsedEffortMedium[sanitizedTimestamp] ?? 0)
case Priority.Low:
return limit.saturatingSubtract(self.slotUsedEffortLow[sanitizedTimestamp] ?? 0)
}
return 0
}
/// Returns true if any priority queue has transactions at the given slot
access(self) view fun slotHasTransactions(_ slot: UFix64): Bool {
return self.slotQueueHigh[slot] != nil
|| self.slotQueueMedium[slot] != nil
|| self.slotQueueLow[slot] != nil
}
/// Checks if a slot is empty across all priority queues and cleans up if so
access(self) fun cleanupSlotIfEmpty(_ slot: UFix64) {
let highEmpty = self.slotQueueHigh[slot] == nil || self.slotQueueHigh[slot]!.keys.length == 0
let mediumEmpty = self.slotQueueMedium[slot] == nil || self.slotQueueMedium[slot]!.keys.length == 0
let lowEmpty = self.slotQueueLow[slot] == nil || self.slotQueueLow[slot]!.keys.length == 0
if highEmpty && mediumEmpty && lowEmpty {
self.slotQueueHigh.remove(key: slot)
self.slotQueueMedium.remove(key: slot)
self.slotQueueLow.remove(key: slot)
self.slotUsedEffortHigh.remove(key: slot)
self.slotUsedEffortMedium.remove(key: slot)
self.slotUsedEffortLow.remove(key: slot)
self.sortedTimestamps.remove(timestamp: slot)
}
}
/// add transaction to the queue and updates all the internal state as well as emit an event
/// V2 Simplified: No low-priority rescheduling. Each priority has its own independent pool.
access(self) fun addTransaction(slot: UFix64, txData: TransactionData) {
// If nothing is in the queue for this slot, add to sorted timestamps
if !self.slotHasTransactions(slot) {
self.sortedTimestamps.add(timestamp: slot)
}
// Add transaction to appropriate priority queue and update effort tracking
switch txData.priority {
case Priority.High:
if self.slotQueueHigh[slot] == nil {
self.slotQueueHigh[slot] = {}
}
let queue = &self.slotQueueHigh[slot]! as auth(Mutate) &{UInt64: UInt64}
queue[txData.id] = txData.executionEffort
self.slotUsedEffortHigh[slot] = (self.slotUsedEffortHigh[slot] ?? 0) + txData.executionEffort
case Priority.Medium:
if self.slotQueueMedium[slot] == nil {
self.slotQueueMedium[slot] = {}
}
let queue = &self.slotQueueMedium[slot]! as auth(Mutate) &{UInt64: UInt64}
queue[txData.id] = txData.executionEffort
self.slotUsedEffortMedium[slot] = (self.slotUsedEffortMedium[slot] ?? 0) + txData.executionEffort
case Priority.Low:
if self.slotQueueLow[slot] == nil {
self.slotQueueLow[slot] = {}
}
let queue = &self.slotQueueLow[slot]! as auth(Mutate) &{UInt64: UInt64}
queue[txData.id] = txData.executionEffort
self.slotUsedEffortLow[slot] = (self.slotUsedEffortLow[slot] ?? 0) + txData.executionEffort
}
// Store the transaction in the transactions map
self.transactions[txData.id] = txData
emit TransactionAdded(id: txData.id)
}
/// remove the transaction from the slot queue.
/// V2 Simplified: Updates priority-specific effort tracking.
access(self) fun removeTransaction(txData: &TransactionData): TransactionData {
let transactionID = txData.id
let slot = txData.scheduledTimestamp
let transactionPriority = txData.priority
let effort = txData.executionEffort
// remove transaction object
let transactionObject = self.transactions.remove(key: transactionID)!
emit TransactionRemoved(id: transactionID)
// Remove from the appropriate priority queue and update effort tracking
switch transactionPriority {
case Priority.High:
if let queue = &self.slotQueueHigh[slot] as auth(Mutate) &{UInt64: UInt64}? {
queue.remove(key: transactionID)
}
if let currentEffort = self.slotUsedEffortHigh[slot] {
self.slotUsedEffortHigh[slot] = currentEffort.saturatingSubtract(effort)
}
case Priority.Medium:
if let queue = &self.slotQueueMedium[slot] as auth(Mutate) &{UInt64: UInt64}? {
queue.remove(key: transactionID)
}
if let currentEffort = self.slotUsedEffortMedium[slot] {
self.slotUsedEffortMedium[slot] = currentEffort.saturatingSubtract(effort)
}
case Priority.Low:
if let queue = &self.slotQueueLow[slot] as auth(Mutate) &{UInt64: UInt64}? {
queue.remove(key: transactionID)
}
if let currentEffort = self.slotUsedEffortLow[slot] {
self.slotUsedEffortLow[slot] = currentEffort.saturatingSubtract(effort)
}
}
// Cleanup slot if empty across all priorities
self.cleanupSlotIfEmpty(slot)
return transactionObject
}
/// pendingQueue creates a list of transactions that are ready for execution.
/// For transaction to be ready for execution it must be scheduled.
///
/// The queue is sorted by timestamp and then by priority (high, medium, low).
/// The queue will contain transactions from all timestamps that are in the past.
/// Low priority transactions will only be added if there is effort available in the slot.
/// The return value can be empty if there are no transactions ready for execution.
access(Process) fun pendingQueue(): [&TransactionData] {
let currentTimestamp = getCurrentBlock().timestamp
var pendingTransactions: [&TransactionData] = []
// total effort across different timestamps guards collection being over the effort limit
var collectionAvailableEffort = self.config.collectionEffortLimit
var transactionsAvailableCount = self.config.collectionTransactionsLimit
var limitReached = false
// Collect past timestamps efficiently from sorted array
let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp)
for timestamp in pastTimestamps {
if limitReached { break }
var high: [&TransactionData] = []
var medium: [&TransactionData] = []
var low: [&TransactionData] = []
// Process high priority queue
let highQueue = self.slotQueueHigh[timestamp] ?? {}
for id in highQueue.keys {
let tx = self.borrowTransaction(id: id)
?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
if tx.status != Status.Scheduled { continue }
if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
emit CollectionLimitReached(
collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
)
limitReached = true
break
}
collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
transactionsAvailableCount = transactionsAvailableCount - 1
high.append(tx)
}
// Process medium priority queue
if !limitReached {
let mediumQueue = self.slotQueueMedium[timestamp] ?? {}
for id in mediumQueue.keys {
let tx = self.borrowTransaction(id: id)
?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
if tx.status != Status.Scheduled { continue }
if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
emit CollectionLimitReached(
collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
)
limitReached = true
break
}
collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
transactionsAvailableCount = transactionsAvailableCount - 1
medium.append(tx)
}
}
// Process low priority queue
if !limitReached {
let lowQueue = self.slotQueueLow[timestamp] ?? {}
for id in lowQueue.keys {
let tx = self.borrowTransaction(id: id)
?? panic("Invalid ID: \(id) transaction not found while preparing pending queue")
if tx.status != Status.Scheduled { continue }
if tx.executionEffort >= collectionAvailableEffort || transactionsAvailableCount == 0 {
emit CollectionLimitReached(
collectionEffortLimit: transactionsAvailableCount == 0 ? nil : self.config.collectionEffortLimit,
collectionTransactionsLimit: transactionsAvailableCount == 0 ? self.config.collectionTransactionsLimit : nil
)
limitReached = true
break
}
collectionAvailableEffort = collectionAvailableEffort.saturatingSubtract(tx.executionEffort)
transactionsAvailableCount = transactionsAvailableCount - 1
low.append(tx)
}
}
pendingTransactions = pendingTransactions
.concat(high)
.concat(medium)
.concat(low)
}
return pendingTransactions
}
/// removeExecutedTransactions removes all transactions that are marked as executed.
access(self) fun removeExecutedTransactions(_ currentTimestamp: UFix64) {
let pastTimestamps = self.sortedTimestamps.getBefore(current: currentTimestamp)
var removedCount = 0
for timestamp in pastTimestamps {
// Process all three priority queues
let queues: [{UInt64: UInt64}] = [
self.slotQueueHigh[timestamp] ?? {},
self.slotQueueMedium[timestamp] ?? {},
self.slotQueueLow[timestamp] ?? {}
]
for transactionIDs in queues {
for id in transactionIDs.keys {
removedCount = removedCount + 1
if removedCount >= self.config.removalTransactionsLimit {
emit RemovalLimitReached(id: id, remainingLength: transactionIDs.keys.length)
return
}
let tx = self.borrowTransaction(id: id)
?? panic("Invalid ID: \(id) transaction not found while removing executed transactions")
// Only remove executed transactions
if tx.status != Status.Executed {
continue
}
// charge the full fee for transaction execution
destroy tx.payAndRefundFees(refundMultiplier: 0.0)
self.removeTransaction(txData: tx)
}
}
}
}
/// process scheduled transactions and prepare them for execution.
///
/// First, it removes transactions that have already been executed.
/// Then, it iterates over past timestamps in the queue and processes the transactions that are
/// eligible for execution. It also emits an event for each transaction that is processed.
///
/// This function is only called by the FVM to process transactions.
access(Process) fun process() {
let currentTimestamp = getCurrentBlock().timestamp
// Early exit if no timestamps need processing
if !self.sortedTimestamps.hasBefore(current: currentTimestamp) {
return
}
self.removeExecutedTransactions(currentTimestamp)
let pendingTransactions = self.pendingQueue()
if pendingTransactions.length == 0 {
return
}
for tx in pendingTransactions {
// Only emit the pending execution event if the transaction handler capability is borrowable
// This is to prevent a situation where the transaction handler is not available
// In that case, the transaction is no longer valid because it cannot be executed
if let transactionHandler = tx.handler.borrow() {
emit PendingExecution(
id: tx.id,
priority: tx.priority.rawValue,
executionEffort: tx.executionEffort,
fees: tx.fees,
transactionHandlerOwner: tx.handler.address,
transactionHandlerTypeIdentifier: transactionHandler.getType().identifier
)
}
// after pending execution event is emitted we set the transaction as executed because we
// must rely on execution node to actually execute it. Execution of the transaction is
// done in a separate transaction that calls executeTransaction(id) function.
// Executing the transaction can not update the status of transaction or any other shared state,
// since that blocks concurrent transaction execution.
// Therefore an optimistic update to executed is made here to avoid race condition.
tx.setStatus(newStatus: Status.Executed)
}
}
/// cancel a scheduled transaction and return a portion of the fees that were paid.
///
/// @param id: The ID of the transaction to cancel
/// @return: The fees to be returned to the caller
access(Cancel) fun cancel(id: UInt64): @FlowToken.Vault {
let tx = self.borrowTransaction(id: id) ??
panic("Invalid ID: \(id) transaction not found")
assert(
tx.status == Status.Scheduled,
message: "Transaction must be in a scheduled state in order to be canceled"
)
// Note: Effort tracking is handled by removeTransaction()
let totalFees = tx.fees
let refundedFees <- tx.payAndRefundFees(refundMultiplier: self.config.refundMultiplier)
// if the transaction was canceled, add it to the canceled transactions array
// maintain sorted order by inserting at the correct position
var insertIndex = 0
for i, canceledID in self.canceledTransactions {
if id < canceledID {
insertIndex = i
break
}
insertIndex = i + 1
}
self.canceledTransactions.insert(at: insertIndex, id)
// keep the array under the limit
if UInt(self.canceledTransactions.length) > self.config.canceledTransactionsLimit {
self.canceledTransactions.remove(at: 0)
}
emit Canceled(
id: tx.id,
priority: tx.priority.rawValue,
feesReturned: refundedFees.balance,
feesDeducted: totalFees - refundedFees.balance,
transactionHandlerOwner: tx.handler.address,
transactionHandlerTypeIdentifier: tx.handlerTypeIdentifier
)
self.removeTransaction(txData: tx)
return <-refundedFees
}
/// execute transaction is a system function that is called by FVM to execute a transaction by ID.
/// The transaction must be found and in correct state or the function panics and this is a fatal error
///
/// This function is only called by the FVM to execute transactions.
/// WARNING: this function should not change any shared state, it will be run concurrently and it must not be blocking.
access(Execute) fun executeTransaction(id: UInt64) {
let tx = self.borrowTransaction(id: id) ??
panic("Invalid ID: Transaction with id \(id) not found")
assert (
tx.status == Status.Executed,
message: "Invalid ID: Cannot execute transaction with id \(id) because it has incorrect status \(tx.status.rawValue)"
)
let transactionHandler = tx.handler.borrow()
?? panic("Invalid transaction handler: Could not borrow a reference to the transaction handler")
let handlerPublicPath = transactionHandler.resolveView(Type<PublicPath>()) as? PublicPath
emit Executed(
id: tx.id,
priority: tx.priority.rawValue,
executionEffort: tx.executionEffort,
transactionHandlerOwner: tx.handler.address,
transactionHandlerTypeIdentifier: transactionHandler.getType().identifier,
transactionHandlerUUID: transactionHandler.uuid,
transactionHandlerPublicPath: handlerPublicPath
)
transactionHandler.executeTransaction(id: id, data: tx.getData())
}
}
/// Deposit fees to this contract's account's vault
access(contract) fun depositFees(from: @FlowToken.Vault) {
let vaultRef = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("Unable to borrow reference to the default token vault")
vaultRef.deposit(from: <-from)
}
/// Withdraw fees from this contract's account's vault
access(contract) fun withdrawFees(amount: UFix64): @FlowToken.Vault {
let vaultRef = self.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
?? panic("Unable to borrow reference to the default token vault")
return <-vaultRef.withdraw(amount: amount) as! @FlowToken.Vault
}
access(all) fun schedule(
handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64,
fees: @FlowToken.Vault
): @ScheduledTransaction {
return <-self.sharedScheduler.borrow()!.schedule(
handlerCap: handlerCap,
data: data,
timestamp: timestamp,
priority: priority,
executionEffort: executionEffort,
fees: <-fees
)
}
access(all) fun estimate(
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64
): EstimatedScheduledTransaction {
return self.sharedScheduler.borrow()!
.estimate(
data: data,
timestamp: timestamp,
priority: priority,
executionEffort: executionEffort,
)
}
access(all) fun cancel(scheduledTx: @ScheduledTransaction): @FlowToken.Vault {
let id = scheduledTx.id
destroy scheduledTx
return <-self.sharedScheduler.borrow()!.cancel(id: id)
}
/// getTransactionData returns the transaction data for a given ID
/// This function can only get the data for a transaction that is currently scheduled or pending execution
/// because finalized transaction metadata is not stored in the contract
/// @param id: The ID of the transaction to get the data for
/// @return: The transaction data for the given ID
access(all) view fun getTransactionData(id: UInt64): TransactionData? {
return self.sharedScheduler.borrow()!.getTransaction(id: id)
}
/// borrowHandlerForID returns an un-entitled reference to the transaction handler for a given ID
/// The handler reference can be used to resolve views to get info about the handler and see where it is stored
/// @param id: The ID of the transaction to get the handler for
/// @return: An un-entitled reference to the transaction handler for the given ID
access(all) view fun borrowHandlerForID(_ id: UInt64): &{TransactionHandler}? {
return self.getTransactionData(id: id)?.borrowHandler()
}
/// getCanceledTransactions returns the IDs of the transactions that have been canceled
/// @return: The IDs of the transactions that have been canceled
access(all) view fun getCanceledTransactions(): [UInt64] {
return self.sharedScheduler.borrow()!.getCanceledTransactions()
}
access(all) view fun getStatus(id: UInt64): Status? {
return self.sharedScheduler.borrow()!.getStatus(id: id)
}
/// getTransactionsForTimeframe returns the IDs of the transactions that are scheduled for a given timeframe
/// @param startTimestamp: The start timestamp to get the IDs for
/// @param endTimestamp: The end timestamp to get the IDs for
/// @return: The IDs of the transactions that are scheduled for the given timeframe
access(all) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
return self.sharedScheduler.borrow()!.getTransactionsForTimeframe(startTimestamp: startTimestamp, endTimestamp: endTimestamp)
}
access(all) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
return self.sharedScheduler.borrow()!.getSlotAvailableEffort(timestamp: timestamp, priority: priority)
}
access(all) fun getConfig(): {SchedulerConfig} {
return self.sharedScheduler.borrow()!.getConfig()
}
/// getSizeOfData takes a transaction's data
/// argument and stores it in the contract account's storage,
/// checking storage used before and after to see how large the data is in MB
/// If data is nil, the function returns 0.0
access(all) fun getSizeOfData(_ data: AnyStruct?): UFix64 {
if data == nil {
return 0.0
} else {
let type = data!.getType()
if type.isSubtype(of: Type<Number>())
|| type.isSubtype(of: Type<Bool>())
|| type.isSubtype(of: Type<Address>())
|| type.isSubtype(of: Type<Character>())
|| type.isSubtype(of: Type<Capability>())
{
return 0.0
}
}
let storagePath = /storage/dataTemp
let storageUsedBefore = self.account.storage.used
self.account.storage.save(data!, to: storagePath)
let storageUsedAfter = self.account.storage.used
self.account.storage.load<AnyStruct>(from: storagePath)
return FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(storageUsedAfter.saturatingSubtract(storageUsedBefore))
}
access(all) init() {
self.storagePath = /storage/sharedSchedulerV3
let scheduler <- create SharedScheduler()
let oldScheduler <- self.account.storage.load<@AnyResource>(from: self.storagePath)
destroy oldScheduler
self.account.storage.save(<-scheduler, to: self.storagePath)
self.sharedScheduler = self.account.capabilities.storage
.issue<auth(Cancel) &SharedScheduler>(self.storagePath)
}
}
Cadence Script
1transaction(name: String, code: String ) {
2 prepare(signer: auth(AddContract) &Account) {
3 signer.contracts.add(name: name, code: code.utf8 )
4 }
5 }