DeploySEALED
○&?^▪□▒&!^&╲?◆▓?◆^◆@^▪▓@░■▒●!◇^#◆╲■╲▓◆□╲*●?╳╱~**◇█#▪??%◆□╱%&?╲$*
Transaction ID
Execution Fee
0.01206 FLOWTransaction Summary
DeployContract deployment
Contract deployment
Script Arguments
0nameString
FlowTransactionSchedulerFlat
1codeString
import FungibleToken from 0xf233dcee88fe0abe
import FlowToken from 0x1654653399040a61
import FlowFees from 0xf919ee77447b7497
import FlowStorageFees from 0xe467b9dd11fa00df
import ViewResolver from 0x1d7e57aa55817448
/// FlowTransactionSchedulerFlat - Flattened version without SharedScheduler resource
/// All fields are directly on the contract, no borrow() calls needed
access(all) contract FlowTransactionSchedulerFlat {
/// storage path for any auxiliary storage
access(all) let storagePath: StoragePath
/// Enums
access(all) enum Priority: UInt8 {
access(all) case High
access(all) case Medium
access(all) case Low
}
access(all) enum Status: UInt8 {
access(all) case Unknown
access(all) case Scheduled
access(all) case Executed
access(all) case Canceled
}
/// Events
access(all) event Scheduled(
id: UInt64,
priority: UInt8,
timestamp: UFix64,
executionEffort: UInt64,
fees: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String,
transactionHandlerUUID: UInt64,
transactionHandlerPublicPath: PublicPath?
)
access(all) event PendingExecution(
id: UInt64,
priority: UInt8,
executionEffort: UInt64,
fees: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String
)
access(all) event Executed(
id: UInt64,
priority: UInt8,
executionEffort: UInt64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String,
transactionHandlerUUID: UInt64,
transactionHandlerPublicPath: PublicPath?
)
access(all) event Canceled(
id: UInt64,
priority: UInt8,
feesReturned: UFix64,
feesDeducted: UFix64,
transactionHandlerOwner: Address,
transactionHandlerTypeIdentifier: String
)
access(all) event CollectionLimitReached(
collectionEffortLimit: UInt64?,
collectionTransactionsLimit: Int?
)
access(all) event RemovalLimitReached()
access(all) event ConfigUpdated()
access(all) event CriticalIssue(message: String)
/// Entitlements
access(all) entitlement Execute
access(all) entitlement Process
access(all) entitlement Cancel
access(all) entitlement UpdateConfig
/// Interfaces
access(all) resource interface TransactionHandler: ViewResolver.Resolver {
access(all) view fun getViews(): [Type] {
return []
}
access(all) fun resolveView(_ view: Type): AnyStruct? {
return nil
}
access(Execute) fun executeTransaction(id: UInt64, data: AnyStruct?)
}
/// Resources
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 FlowTransactionSchedulerFlat.getStatus(id: self.id)
}
init(
id: UInt64,
timestamp: UFix64,
handlerTypeIdentifier: String
) {
self.id = id
self.timestamp = timestamp
self.handlerTypeIdentifier = handlerTypeIdentifier
}
access(all) event ResourceDestroyed(id: UInt64 = self.id, timestamp: UFix64 = self.timestamp, handlerTypeIdentifier: String = self.handlerTypeIdentifier)
}
/// Structs
access(all) struct EstimatedScheduledTransaction {
access(all) let flowFee: UFix64?
access(all) let timestamp: UFix64?
access(all) let error: String?
access(contract) view init(flowFee: UFix64?, timestamp: UFix64?, error: String?) {
self.flowFee = flowFee
self.timestamp = timestamp
self.error = error
}
}
access(all) struct TransactionData {
access(all) let id: UInt64
access(all) let priority: Priority
access(all) let executionEffort: UInt64
access(all) var status: Status
access(all) let fees: UFix64
access(all) var scheduledTimestamp: UFix64
access(contract) let handler: Capability<auth(Execute) &{TransactionHandler}>
access(all) let handlerTypeIdentifier: String
access(all) let handlerAddress: Address
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
}
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
}
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
}
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: <-FlowTransactionSchedulerFlat.withdrawFees(amount: self.fees))
return <-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
} else {
let amountToReturn = self.fees * refundMultiplier
let amountToKeep = self.fees - amountToReturn
let feesToReturn <- FlowTransactionSchedulerFlat.withdrawFees(amount: amountToReturn)
FlowFees.deposit(from: <-FlowTransactionSchedulerFlat.withdrawFees(amount: amountToKeep))
return <-feesToReturn
}
}
access(all) view fun getData(): AnyStruct? {
return self.data
}
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")
}
}
access(all) struct interface SchedulerConfig {
access(all) var maximumIndividualEffort: UInt64
access(all) var minimumExecutionEffort: UInt64
access(all) var slotTotalEffortLimit: UInt64
access(all) var slotSharedEffortLimit: UInt64
access(all) var priorityEffortReserve: {Priority: 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) init(
maximumIndividualEffort: UInt64,
minimumExecutionEffort: UInt64,
slotSharedEffortLimit: UInt64,
priorityEffortReserve: {Priority: UInt64},
lowPriorityEffortLimit: UInt64,
maxDataSizeMB: UFix64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
canceledTransactionsLimit: UInt,
collectionEffortLimit: UInt64,
collectionTransactionsLimit: Int,
txRemovalLimit: UInt
) {
post {
self.refundMultiplier >= 0.0 && self.refundMultiplier <= 1.0:
"Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
self.priorityFeeMultipliers[Priority.Low]! >= 1.0:
"Invalid priority fee multiplier: Low priority multiplier must be greater than or equal to 1.0 but got \(self.priorityFeeMultipliers[Priority.Low]!)"
self.priorityFeeMultipliers[Priority.Medium]! > self.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]!)"
self.priorityFeeMultipliers[Priority.High]! > self.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]!)"
self.priorityEffortLimit[Priority.High]! >= self.priorityEffortReserve[Priority.High]!:
"Invalid priority effort limit: High priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.High]!)"
self.priorityEffortLimit[Priority.Medium]! >= self.priorityEffortReserve[Priority.Medium]!:
"Invalid priority effort limit: Medium priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.Medium]!)"
self.priorityEffortLimit[Priority.Low]! >= self.priorityEffortReserve[Priority.Low]!:
"Invalid priority effort limit: Low priority effort limit must be greater than or equal to the priority effort reserve of \(priorityEffortReserve[Priority.Low]!)"
self.priorityEffortReserve[Priority.Low]! == 0:
"Invalid priority effort reserve: Low priority effort reserve must be 0"
self.collectionTransactionsLimit >= 0:
"Invalid collection transactions limit: Collection transactions limit must be greater than or equal to 0 but got \(collectionTransactionsLimit)"
self.canceledTransactionsLimit >= 1:
"Invalid canceled transactions limit: Canceled transactions limit must be greater than or equal to 1 but got \(canceledTransactionsLimit)"
self.collectionEffortLimit > self.slotTotalEffortLimit:
"Invalid collection effort limit: Collection effort limit must be greater than \(self.slotTotalEffortLimit) but got \(self.collectionEffortLimit)"
}
}
access(all) view fun getTxRemovalLimit(): UInt
}
access(all) struct Config: SchedulerConfig {
access(all) var maximumIndividualEffort: UInt64
access(all) var minimumExecutionEffort: UInt64
access(all) var slotTotalEffortLimit: UInt64
access(all) var slotSharedEffortLimit: UInt64
access(all) var priorityEffortReserve: {Priority: 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) init(
maximumIndividualEffort: UInt64,
minimumExecutionEffort: UInt64,
slotSharedEffortLimit: UInt64,
priorityEffortReserve: {Priority: UInt64},
lowPriorityEffortLimit: UInt64,
maxDataSizeMB: UFix64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
canceledTransactionsLimit: UInt,
collectionEffortLimit: UInt64,
collectionTransactionsLimit: Int,
txRemovalLimit: UInt
) {
self.maximumIndividualEffort = maximumIndividualEffort
self.minimumExecutionEffort = minimumExecutionEffort
self.slotTotalEffortLimit = slotSharedEffortLimit + priorityEffortReserve[Priority.High]! + priorityEffortReserve[Priority.Medium]!
self.slotSharedEffortLimit = slotSharedEffortLimit
self.priorityEffortReserve = priorityEffortReserve
self.priorityEffortLimit = {
Priority.High: priorityEffortReserve[Priority.High]! + slotSharedEffortLimit,
Priority.Medium: priorityEffortReserve[Priority.Medium]! + slotSharedEffortLimit,
Priority.Low: lowPriorityEffortLimit
}
self.maxDataSizeMB = maxDataSizeMB
self.priorityFeeMultipliers = priorityFeeMultipliers
self.refundMultiplier = refundMultiplier
self.canceledTransactionsLimit = canceledTransactionsLimit
self.collectionEffortLimit = collectionEffortLimit
self.collectionTransactionsLimit = collectionTransactionsLimit
}
access(all) view fun getTxRemovalLimit(): UInt {
return FlowTransactionSchedulerFlat.account.storage.copy<UInt>(from: /storage/txRemovalLimitFlat)
?? 200
}
}
access(all) struct SortedTimestamps {
access(self) var timestamps: [UFix64]
access(all) init() {
self.timestamps = []
}
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)
}
access(all) fun remove(timestamp: UFix64) {
let index = self.timestamps.firstIndex(of: timestamp)
if index != nil {
self.timestamps.remove(at: index!)
}
}
access(all) fun getBefore(current: UFix64): [UFix64] {
let pastTimestamps: [UFix64] = []
for timestamp in self.timestamps {
if timestamp <= current {
pastTimestamps.append(timestamp)
} else {
break
}
}
return pastTimestamps
}
access(all) fun hasBefore(current: UFix64): Bool {
return self.timestamps.length > 0 && self.timestamps[0] <= current
}
access(all) fun getAll(): [UFix64] {
return self.timestamps
}
}
// ============================================
// CONTRACT-LEVEL STATE (formerly in SharedScheduler)
// ============================================
access(contract) var nextID: UInt64
access(contract) var transactions: {UInt64: TransactionData}
access(contract) var slotQueue: {UFix64: {Priority: {UInt64: UInt64}}}
access(contract) var slotUsedEffort: {UFix64: {Priority: UInt64}}
access(contract) var sortedTimestamps: SortedTimestamps
access(contract) var canceledTransactions: [UInt64]
access(contract) var config: {SchedulerConfig}
// ============================================
// HELPER FUNCTIONS
// ============================================
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)
}
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(self) fun getNextIDAndIncrement(): UInt64 {
let nextID = self.nextID
self.nextID = self.nextID + 1
return nextID
}
// ============================================
// PUBLIC/CONTRACT FUNCTIONS (no borrow needed!)
// ============================================
access(all) view fun getConfig(): {SchedulerConfig} {
return self.config
}
access(all) view fun getStatus(id: UInt64): Status? {
if id == 0 as UInt64 || id >= self.nextID {
return nil
}
if let tx = &self.transactions[id] as &TransactionData? {
return tx.status
}
if self.canceledTransactions.contains(id) {
return Status.Canceled
}
let firstCanceledID = self.canceledTransactions[0]
if id > firstCanceledID {
return Status.Executed
}
return Status.Unknown
}
access(all) view fun getTransactionData(id: UInt64): TransactionData? {
return self.transactions[id]
}
access(all) view fun borrowHandlerForID(_ id: UInt64): &{TransactionHandler}? {
return self.getTransactionData(id: id)?.borrowHandler()
}
access(all) view fun getCanceledTransactions(): [UInt64] {
return self.canceledTransactions
}
access(all) fun getTransactionsForTimeframe(startTimestamp: UFix64, endTimestamp: UFix64): {UFix64: {UInt8: [UInt64]}} {
var transactionsInTimeframe: {UFix64: {UInt8: [UInt64]}} = {}
if startTimestamp > endTimestamp {
return transactionsInTimeframe
}
let allTimestampsBeforeEnd = self.sortedTimestamps.getBefore(current: endTimestamp)
for timestamp in allTimestampsBeforeEnd {
if timestamp < startTimestamp { continue }
let transactionPriorities = self.slotQueue[timestamp] ?? {}
var timestampTransactions: {UInt8: [UInt64]} = {}
for priority in transactionPriorities.keys {
let transactionIDs = transactionPriorities[priority] ?? {}
var priorityTransactions: [UInt64] = []
for id in transactionIDs.keys {
priorityTransactions.append(id)
}
if priorityTransactions.length > 0 {
timestampTransactions[priority.rawValue] = priorityTransactions
}
}
if timestampTransactions.keys.length > 0 {
transactionsInTimeframe[timestamp] = timestampTransactions
}
}
return transactionsInTimeframe
}
access(all) view fun getSlotAvailableEffort(timestamp: UFix64, priority: Priority): UInt64 {
let sanitizedTimestamp = UFix64(UInt64(timestamp))
let priorityLimit = self.config.priorityEffortLimit[priority]!
if !self.slotUsedEffort.containsKey(sanitizedTimestamp) {
return priorityLimit
}
let slotPriorityEffortsUsed = self.slotUsedEffort[sanitizedTimestamp]!
let highReserve = self.config.priorityEffortReserve[Priority.High]!
let mediumReserve = self.config.priorityEffortReserve[Priority.Medium]!
let highUsed = slotPriorityEffortsUsed[Priority.High] ?? 0
let mediumUsed = slotPriorityEffortsUsed[Priority.Medium] ?? 0
if priority == Priority.Low {
let highPlusMediumUsed = highUsed + mediumUsed
let totalEffortRemaining = self.config.slotTotalEffortLimit.saturatingSubtract(highPlusMediumUsed)
let lowEffortRemaining = totalEffortRemaining < priorityLimit ? totalEffortRemaining : priorityLimit
let lowUsed = slotPriorityEffortsUsed[Priority.Low] ?? 0
return lowEffortRemaining.saturatingSubtract(lowUsed)
}
let highSharedUsed: UInt64 = highUsed.saturatingSubtract(highReserve)
let mediumSharedUsed: UInt64 = mediumUsed.saturatingSubtract(mediumReserve)
let totalShared = (self.config.slotTotalEffortLimit.saturatingSubtract(highReserve)).saturatingSubtract(mediumReserve)
let highPlusMediumSharedUsed = highSharedUsed + mediumSharedUsed
let sharedAvailable = totalShared.saturatingSubtract(highPlusMediumSharedUsed)
let reserve = self.config.priorityEffortReserve[priority]!
let used = slotPriorityEffortsUsed[priority] ?? 0
let unusedReserve: UInt64 = reserve.saturatingSubtract(used)
let available = sharedAvailable + unusedReserve
return available
}
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/dataTempFlat
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(contract) fun calculateFee(executionEffort: UInt64, priority: Priority, dataSizeMB: UFix64): UFix64 {
let baseFee = FlowFees.computeFees(inclusionEffort: 1.0, executionEffort: UFix64(executionEffort)/100000000.0)
let scaledExecutionFee = baseFee * self.config.priorityFeeMultipliers[priority]!
let storageFee = FlowStorageFees.storageCapacityToFlow(dataSizeMB)
let inclusionFee = 0.00001
return scaledExecutionFee + storageFee + inclusionFee
}
access(contract) view fun calculateScheduledTimestamp(
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64
): UFix64? {
var timestampToSearch = timestamp
while true {
let used = self.slotUsedEffort[timestampToSearch]
if used == nil {
return timestampToSearch
}
let available = self.getSlotAvailableEffort(timestamp: timestampToSearch, priority: priority)
if executionEffort <= available {
return timestampToSearch
}
if priority == Priority.High {
return nil
}
timestampToSearch = timestampToSearch + 1.0
}
return nil
}
// ============================================
// MAIN FUNCTIONS
// ============================================
access(all) fun estimate(
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64
): EstimatedScheduledTransaction {
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 = self.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."
)
}
if priority == Priority.Low {
return EstimatedScheduledTransaction(
flowFee: fee,
timestamp: scheduledTimestamp,
error: "Invalid Priority: Cannot estimate for Low Priority transactions. They will be included in the first block with available space after their requested timestamp."
)
}
return EstimatedScheduledTransaction(flowFee: fee, timestamp: scheduledTimestamp, error: nil)
}
access(all) fun schedule(
handlerCap: Capability<auth(Execute) &{TransactionHandler}>,
data: AnyStruct?,
timestamp: UFix64,
priority: Priority,
executionEffort: UInt64,
fees: @FlowToken.Vault
): @ScheduledTransaction {
let estimate = self.estimate(
data: data,
timestamp: timestamp,
priority: priority,
executionEffort: executionEffort
)
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,
)
self.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
)
self.addTransaction(slot: estimate.timestamp!, txData: transactionData)
return <-create ScheduledTransaction(
id: transactionID,
timestamp: estimate.timestamp!,
handlerTypeIdentifier: transactionData.handlerTypeIdentifier
)
}
access(all) fun cancel(scheduledTx: @ScheduledTransaction): @FlowToken.Vault {
let id = scheduledTx.id
destroy scheduledTx
let tx = &self.transactions[id] as &TransactionData?
?? 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"
)
let slotEfforts = self.slotUsedEffort[tx.scheduledTimestamp]!
slotEfforts[tx.priority] = slotEfforts[tx.priority]!.saturatingSubtract(tx.executionEffort)
self.slotUsedEffort[tx.scheduledTimestamp] = slotEfforts
let totalFees = tx.fees
let refundedFees <- tx.payAndRefundFees(refundMultiplier: self.config.refundMultiplier)
var insertIndex = 0
for i, canceledID in self.canceledTransactions {
if id < canceledID {
insertIndex = i
break
}
insertIndex = i + 1
}
self.canceledTransactions.insert(at: insertIndex, id)
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
}
// ============================================
// INTERNAL FUNCTIONS
// ============================================
access(self) fun addTransaction(slot: UFix64, txData: TransactionData) {
if self.slotQueue[slot] == nil {
self.slotQueue[slot] = {}
self.slotUsedEffort[slot] = {
Priority.High: 0,
Priority.Medium: 0,
Priority.Low: 0
}
self.sortedTimestamps.add(timestamp: slot)
}
let slotQueue = self.slotQueue[slot]!
if let priorityQueue = slotQueue[txData.priority] {
priorityQueue[txData.id] = txData.executionEffort
slotQueue[txData.priority] = priorityQueue
} else {
slotQueue[txData.priority] = {
txData.id: txData.executionEffort
}
}
self.slotQueue[slot] = slotQueue
let slotEfforts = self.slotUsedEffort[slot]!
var newPriorityEffort = slotEfforts[txData.priority]! + txData.executionEffort
slotEfforts[txData.priority] = newPriorityEffort
var newTotalEffort: UInt64 = 0
for priority in slotEfforts.keys {
newTotalEffort = newTotalEffort.saturatingAdd(slotEfforts[priority]!)
}
self.slotUsedEffort[slot] = slotEfforts
let lowTransactionsToReschedule: [UInt64] = []
if newTotalEffort > self.config.slotTotalEffortLimit {
let lowPriorityTransactions = slotQueue[Priority.Low]!
for id in lowPriorityTransactions.keys {
if newTotalEffort <= self.config.slotTotalEffortLimit {
break
}
lowTransactionsToReschedule.append(id)
newTotalEffort = newTotalEffort.saturatingSubtract(lowPriorityTransactions[id]!)
}
}
self.transactions[txData.id] = txData
self.rescheduleLowPriorityTransactions(slot: slot, transactions: lowTransactionsToReschedule)
}
access(self) fun rescheduleLowPriorityTransactions(slot: UFix64, transactions: [UInt64]) {
for id in transactions {
let tx = &self.transactions[id] as &TransactionData?
if tx == nil {
emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while rescheduling low priority transactions")
continue
}
if tx!.priority != Priority.Low {
emit CriticalIssue(message: "Invalid Priority: Cannot reschedule transaction with id \(id) because it is not low priority")
continue
}
if tx!.scheduledTimestamp != slot {
emit CriticalIssue(message: "Invalid Timestamp: Cannot reschedule transaction with id \(id) because it is not scheduled at the same slot as the new transaction")
continue
}
let newTimestamp = self.calculateScheduledTimestamp(
timestamp: slot + 1.0,
priority: Priority.Low,
executionEffort: tx!.executionEffort
)!
let effort = tx!.executionEffort
let transactionData = self.removeTransaction(txData: tx!)
let slotEfforts = self.slotUsedEffort[slot]!
slotEfforts[Priority.Low] = slotEfforts[Priority.Low]!.saturatingSubtract(effort)
self.slotUsedEffort[slot] = slotEfforts
transactionData.setScheduledTimestamp(newTimestamp: newTimestamp)
self.addTransaction(slot: newTimestamp, txData: transactionData)
}
}
access(self) fun removeTransaction(txData: &TransactionData): TransactionData {
let transactionID = txData.id
let slot = txData.scheduledTimestamp
let transactionPriority = txData.priority
let transactionObject = self.transactions.remove(key: transactionID)!
if let transactionQueue = self.slotQueue[slot] {
if let priorityQueue = transactionQueue[transactionPriority] {
priorityQueue[transactionID] = nil
if priorityQueue.keys.length == 0 {
transactionQueue.remove(key: transactionPriority)
} else {
transactionQueue[transactionPriority] = priorityQueue
}
self.slotQueue[slot] = transactionQueue
}
if transactionQueue.keys.length == 0 {
self.slotQueue.remove(key: slot)
self.slotUsedEffort.remove(key: slot)
self.sortedTimestamps.remove(timestamp: slot)
}
}
return transactionObject
}
// ============================================
// INIT
// ============================================
access(all) init() {
self.storagePath = /storage/schedulerFlatData
self.nextID = 1
self.canceledTransactions = [0 as UInt64]
self.transactions = {}
self.slotUsedEffort = {}
self.slotQueue = {}
self.sortedTimestamps = SortedTimestamps()
let sharedEffortLimit: UInt64 = 5_000
let highPriorityEffortReserve: UInt64 = 10_000
let mediumPriorityEffortReserve: UInt64 = 2_500
self.config = Config(
maximumIndividualEffort: 9999,
minimumExecutionEffort: 100,
slotSharedEffortLimit: sharedEffortLimit,
priorityEffortReserve: {
Priority.High: highPriorityEffortReserve,
Priority.Medium: mediumPriorityEffortReserve,
Priority.Low: 0
},
lowPriorityEffortLimit: 2_500,
maxDataSizeMB: 0.001,
priorityFeeMultipliers: {
Priority.High: 10.0,
Priority.Medium: 5.0,
Priority.Low: 2.0
},
refundMultiplier: 0.5,
canceledTransactionsLimit: 1000,
collectionEffortLimit: 500_000,
collectionTransactionsLimit: 150,
txRemovalLimit: 200
)
}
}
Cadence Script
1transaction(name: String, code: String ) {
2 prepare(signer: auth(AddContract) &Account) {
3 signer.contracts.add(name: name, code: code.utf8 )
4 }
5 }