Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ coverage.lcov
lcov.info
*.pkey

imports*

# Private flow.jsons

private.flow.json
148 changes: 106 additions & 42 deletions contracts/FlowCallbackScheduler.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ access(all) contract FlowCallbackScheduler {
}

access(all) enum Status: UInt8 {
/// unknown statuses are used for handling historic callbacks with null statuses
access(all) case Unknown
/// mutable statuses
access(all) case Scheduled
access(all) case Processed
/// finalized statuses
access(all) case Executed
access(all) case Succeeded
access(all) case Failed
access(all) case Canceled
}

Expand All @@ -47,7 +50,8 @@ access(all) contract FlowCallbackScheduler {
priority: UInt8,
executionEffort: UInt64,
fees: UFix64,
callbackOwner: Address
callbackOwner: Address,
status: UInt8
)

access(all) event Canceled(
Expand All @@ -58,7 +62,7 @@ access(all) contract FlowCallbackScheduler {
callbackOwner: Address
)

// Emmitted when one or more of the configuration details fields are updated
// 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()
Expand Down Expand Up @@ -176,10 +180,12 @@ access(all) contract FlowCallbackScheduler {
/// It panics if the callback status is already finalized.
access(contract) fun setStatus(newStatus: Status) {
pre {
self.status != Status.Executed && self.status != Status.Canceled:
self.status != Status.Succeeded && self.status != Status.Failed && self.status != Status.Canceled:
"Invalid status: Callback with id \(self.id) is already finalized"
newStatus == Status.Executed ? self.status == Status.Processed : true:
"Invalid status: Callback with id \(self.id) cannot be marked as Executed until after it is Processed"
newStatus == Status.Succeeded ? self.status == Status.Processed : true:
"Invalid status: Callback with id \(self.id) cannot be marked as Succeeded until after it is Processed"
newStatus == Status.Failed ? self.status == Status.Processed : true:
"Invalid status: Callback with id \(self.id) cannot be marked as Failed until after it is Processed"
newStatus == Status.Processed ? self.status == Status.Scheduled : true:
"Invalid status: Callback with id \(self.id) can only be set as Processed if it is Scheduled"
}
Expand Down Expand Up @@ -243,8 +249,9 @@ access(all) contract FlowCallbackScheduler {
/// refund multiplier is the portion of the fees that are refunded when a callback is cancelled
access(all) var refundMultiplier: UFix64

/// historic status limit is the maximum age of a historic canceled callback status we keep before getting pruned
access(all) var historicStatusLimit: UFix64
/// historic status age limit is the maximum age in timestamp seconds
/// of a historic canceled callback status to keep before getting pruned
access(all) var historicStatusAgeLimit: UFix64

access(all) init(
slotSharedEffortLimit: UInt64,
Expand All @@ -253,13 +260,13 @@ access(all) contract FlowCallbackScheduler {
minimumExecutionEffort: UInt64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
historicStatusLimit: UFix64
historicStatusAgeLimit: UFix64
) {
pre {
refundMultiplier >= 0.0 && refundMultiplier <= 1.0:
"Invalid refund multiplier: The multiplier must be between 0.0 and 1.0 but got \(refundMultiplier)"
historicStatusLimit >= 1.0 && historicStatusLimit < getCurrentBlock().timestamp:
"Invalid historic status limit: Limit must be greater than 1.0 and less than the current timestamp but got \(historicStatusLimit)"
historicStatusAgeLimit >= 1.0 && historicStatusAgeLimit < getCurrentBlock().timestamp:
"Invalid historic status limit: Limit must be greater than 1.0 and less than the current timestamp but got \(historicStatusAgeLimit)"
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]!:
Expand All @@ -286,7 +293,7 @@ access(all) contract FlowCallbackScheduler {
access(all) var minimumExecutionEffort: UInt64
access(all) var priorityFeeMultipliers: {Priority: UFix64}
access(all) var refundMultiplier: UFix64
access(all) var historicStatusLimit: UFix64
access(all) var historicStatusAgeLimit: UFix64

access(all) init(
slotSharedEffortLimit: UInt64,
Expand All @@ -295,7 +302,7 @@ access(all) contract FlowCallbackScheduler {
minimumExecutionEffort: UInt64,
priorityFeeMultipliers: {Priority: UFix64},
refundMultiplier: UFix64,
historicStatusLimit: UFix64
historicStatusAgeLimit: UFix64
) {
self.slotTotalEffortLimit = slotSharedEffortLimit + priorityEffortReserve[Priority.High]! + priorityEffortReserve[Priority.Medium]!
self.slotSharedEffortLimit = slotSharedEffortLimit
Expand All @@ -304,7 +311,24 @@ access(all) contract FlowCallbackScheduler {
self.minimumExecutionEffort = minimumExecutionEffort
self.priorityFeeMultipliers = priorityFeeMultipliers
self.refundMultiplier = refundMultiplier
self.historicStatusLimit = historicStatusLimit
self.historicStatusAgeLimit = historicStatusAgeLimit
}
}

/// HistoricCallback is a struct that contains the timestamp and status of a callback
/// that has been finalized and is stored in the historicCallbacks map
access(all) struct HistoricCallback {
access(all) let timestamp: UFix64
access(all) let status: Status

access(contract) init(timestamp: UFix64, status: Status) {
pre {
status == Status.Canceled || status == Status.Failed:
"Invalid status: Historic callbacks can only be Canceled or Failed"
}

self.timestamp = timestamp
self.status = status
}
}

Expand All @@ -319,8 +343,8 @@ access(all) contract FlowCallbackScheduler {
/// callbacks is a map of callback IDs to callback data
access(contract) var callbacks: @{UInt64: CallbackData}

/// callback status maps historic canceled callback IDs to their original timestamps
access(contract) var historicCanceledCallbacks: {UInt64: UFix64}
/// callback status maps historic callback IDs to their statuses
access(contract) var historicCallbacks: {UInt64: HistoricCallback}

/// slot queue is a map of timestamps to callback IDs and their execution efforts
access(contract) var slotQueue: {UFix64: {UInt64: UInt64}}
Expand All @@ -333,16 +357,20 @@ access(all) contract FlowCallbackScheduler {
/// so we use this special value
access(contract) let lowPriorityScheduledTimestamp: UFix64

/// used for querying historic statuses so that we don't have to store all succeeded statuses
access(contract) var earliestHistoricID: UInt64

/// Struct that contains all the configuration details for the callback scheduler protocol
/// Can be updated by the owner of the contract
access(contract) var configurationDetails: {SchedulerConfig}

access(all) init() {
self.nextID = 1
self.lowPriorityScheduledTimestamp = 0.0
self.earliestHistoricID = 0

self.callbacks <- {}
self.historicCanceledCallbacks = {}
self.historicCallbacks = {}
self.slotUsedEffort = {
self.lowPriorityScheduledTimestamp: {
Priority.High: 0,
Expand Down Expand Up @@ -401,7 +429,7 @@ access(all) contract FlowCallbackScheduler {
Priority.Low: 2.0
},
refundMultiplier: 0.5,
historicStatusLimit: 30.0 * 24.0 * 60.0 * 60.0 // 30 days
historicStatusAgeLimit: 30.0 * 24.0 * 60.0 * 60.0 // 30 days
)
}

Expand Down Expand Up @@ -451,18 +479,13 @@ access(all) contract FlowCallbackScheduler {
}

// if the callback is not found in the callbacks map, we check the callback status map for historic status
if let historic = self.historicCanceledCallbacks[id] {
return Status.Canceled
} else if id < self.nextID {
// historicCanceledCallbacks only stores canceled callbacks
// because the only other possible status for finalized callbacks is Executed
// Since the ID is a monotonically increasing number,
// we know that any ID that is less than the next ID and not in the
// active callbacks map must have been executed
return Status.Executed
if let historic = self.historicCallbacks[id] {
return historic.status
} else if id > self.earliestHistoricID {
return Status.Succeeded
}

return nil
return Status.Unknown
}

/// schedule is the primary entry point for scheduling a new callback within the scheduler contract.
Expand Down Expand Up @@ -749,6 +772,11 @@ access(all) contract FlowCallbackScheduler {
for id in callbackIDs.keys {
let callback = self.borrowCallback(id: id)!

if callback.status == Status.Processed {
self.failCallback(id: id)
continue
}

if callback.priority == Priority.High {
highPriorityIDs.append(id)
} else if callback.priority == Priority.Medium {
Expand All @@ -768,7 +796,6 @@ access(all) contract FlowCallbackScheduler {
let callbackEffort = lowPriorityCallbacks[lowCallbackID]!
if callbackEffort <= lowPriorityEffortAvailable {
lowPriorityEffortAvailable = lowPriorityEffortAvailable - callbackEffort
callbackIDs[lowCallbackID] = callbackEffort
lowPriorityCallbacks[lowCallbackID] = nil
sortedCallbackIDs.append(lowCallbackID)
}
Expand Down Expand Up @@ -797,11 +824,14 @@ access(all) contract FlowCallbackScheduler {

// garbage collect historic statuses that are older than the limit
// todo: maybe not do this every time, but only each X blocks to save compute
let historicCallbacks = self.historicCanceledCallbacks.keys
let historicCallbacks = self.historicCallbacks.keys
for id in historicCallbacks {
let historicTimestamp = self.historicCanceledCallbacks[id]!
if historicTimestamp < currentTimestamp - self.configurationDetails.historicStatusLimit {
self.historicCanceledCallbacks.remove(key: id)
let historicCallback = self.historicCallbacks[id]!
if historicCallback.timestamp < currentTimestamp - self.configurationDetails.historicStatusAgeLimit {
self.historicCallbacks.remove(key: id)
if id > self.earliestHistoricID {
self.earliestHistoricID = id
}
}
}
}
Expand Down Expand Up @@ -836,11 +866,11 @@ access(all) contract FlowCallbackScheduler {
callbackOwner: callback.handler.address
)

// keep historic Canceled status for future queries after garbage collection
// We don't keep executed statuses because we can just assume
// they every ID that is less than the current ID counter
// that is not Canceled, Scheduled, or Processed is Executed
self.historicCanceledCallbacks[callback.id] = callback.scheduledTimestamp
// keep historic statuses for future queries after garbage collection
self.historicCallbacks[callback.id] = HistoricCallback(
timestamp: callback.scheduledTimestamp,
status: Status.Canceled
)

self.finalizeCallback(callback: callback, status: Status.Canceled)

Expand All @@ -865,13 +895,47 @@ access(all) contract FlowCallbackScheduler {
priority: callback.priority.rawValue,
executionEffort: callback.executionEffort,
fees: callback.fees.balance,
callbackOwner: callback.handler.address
callbackOwner: callback.handler.address,
status: Status.Succeeded.rawValue
)

// Deposit all the fees into the FlowFees vault
destroy callback.payAndWithdrawFees(multiplierToWithdraw: 0.0)


self.finalizeCallback(callback: callback, status: Status.Executed)
self.finalizeCallback(callback: callback, status: Status.Succeeded)
}

/// fail callback fail a Processed callback by ID.
/// The callback must be found and in correct state or the function panics and this is a fatal error
access(contract) fun failCallback(id: UInt64) {
let callback = self.borrowCallback(id: id) ??
panic("Invalid ID: Callback with id \(id) not found")

assert (
callback.status == Status.Processed,
message: "Invalid ID: Cannot fail callback with id \(id) because it has not been processed yet"
)

// keep historic statuses for future queries after garbage collection
self.historicCallbacks[callback.id] = HistoricCallback(
timestamp: callback.scheduledTimestamp,
status: Status.Failed
)

// Deposit all the fees into the FlowFees vault
destroy callback.payAndWithdrawFees(multiplierToWithdraw: 0.0)

emit Executed(
id: callback.id,
priority: callback.priority.rawValue,
executionEffort: callback.executionEffort,
fees: callback.fees.balance,
callbackOwner: callback.handler.address,
status: Status.Failed.rawValue
)

self.finalizeCallback(callback: callback, status: Status.Failed)
}

/// finalizes the callback by setting the status to executed or canceled and emitting the appropriate event.
Expand All @@ -881,8 +945,8 @@ access(all) contract FlowCallbackScheduler {
/// in the same block after it is processed so it won't get processed twice
access(contract) fun finalizeCallback(callback: &CallbackData, status: Status) {
pre {
status == Status.Executed || status == Status.Canceled:
"Invalid status: The provided status to finalizeCallback must be Executed or Canceled"
status == Status.Succeeded || status == Status.Failed || status == Status.Canceled:
"Invalid status: The provided status to finalizeCallback must be Succeeded, Failed, or Canceled"
}

callback.setStatus(newStatus: status)
Expand Down
10 changes: 5 additions & 5 deletions contracts/testContracts/TestFlowCallbackHandler.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "FungibleToken"
// TestFlowCallbackHandler is a simplified test contract for testing CallbackScheduler
access(all) contract TestFlowCallbackHandler {
access(all) var scheduledCallbacks: {UInt64: FlowCallbackScheduler.ScheduledCallback}
access(all) var executedCallbacks: [UInt64]
access(all) var succeededCallbacks: [UInt64]

access(all) let HandlerStoragePath: StoragePath
access(all) let HandlerPublicPath: PublicPath
Expand All @@ -21,7 +21,7 @@ access(all) contract TestFlowCallbackHandler {
panic("Callback \(id) failed")
} else {
// All other regular test cases should succeed
TestFlowCallbackHandler.executedCallbacks.append(id)
TestFlowCallbackHandler.succeededCallbacks.append(id)
}
} else if let dataCap = data as? Capability<auth(FlowCallbackScheduler.Execute) &{FlowCallbackScheduler.CallbackHandler}> {
// Testing scheduling a callback with a callback
Expand Down Expand Up @@ -55,8 +55,8 @@ access(all) contract TestFlowCallbackHandler {
return <-FlowCallbackScheduler.cancel(callback: callback)
}

access(all) fun getExecutedCallbacks(): [UInt64] {
return self.executedCallbacks
access(all) fun getSucceededCallbacks(): [UInt64] {
return self.succeededCallbacks
}

access(contract) fun getFeeFromVault(amount: UFix64): @FlowToken.Vault {
Expand All @@ -69,7 +69,7 @@ access(all) contract TestFlowCallbackHandler {

access(all) init() {
self.scheduledCallbacks = {}
self.executedCallbacks = []
self.succeededCallbacks = []

self.HandlerStoragePath = /storage/testCallbackHandler
self.HandlerPublicPath = /public/testCallbackHandler
Expand Down
4 changes: 2 additions & 2 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"source": "./contracts/FlowCallbackScheduler.cdc",
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"testing": "0000000000000007"
"testing": "0000000000000001"
}
},
"FlowClusterQC": {
Expand Down Expand Up @@ -169,7 +169,7 @@
"source": "./contracts/testContracts/TestFlowCallbackHandler.cdc",
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"testing": "0000000000000007"
"testing": "0000000000000001"
}
}
},
Expand Down
12 changes: 6 additions & 6 deletions lib/go/contracts/internal/assets/assets.go

Large diffs are not rendered by default.

Loading
Loading