Smart Contract
FixesHeartbeat
A.d2abb5dbf5e08666.FixesHeartbeat
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FixesHeartbeat
5
6The `FixesHeartbeat` contract is a contract that provides a heartbeat mechanism for the Flow blockchain.
7It allows developers to add hooks to the contract and execute them periodically.
8The contract is designed to be simple and easy to use.
9It is suitable for various use cases, such as updating the contract state, sending notifications, and more.
10
11*/
12
13/// The `FixesHeartbeat` contract
14///
15access(all) contract FixesHeartbeat {
16 /* --- Events --- */
17 /// Event emitted when the contract is initialized
18 access(all) event ContractInitialized()
19 /// Event emitted when a hook is added
20 access(all) event HookAdded(scope: String, hookAddr: Address, hookType: Type)
21 /// Event emitted when a hook is removed
22 access(all) event HookRemoved(scope: String, hookAddr: Address)
23 /// Event emitted when the heartbeat time is updated
24 access(all) event HeartbeatExecuted(scope: String, lastHeartbeatTime: UFix64, deltaTime: UFix64)
25
26 /* --- Variable, Enums and Structs --- */
27
28 access(all)
29 let storagePath: StoragePath
30
31 /// Record the last heartbeat time of each scope
32 /// Scope => Last Heartbeat Time
33 access(all)
34 let lastHeartbeatTime: {String: UFix64}
35
36 /// Record the hooks of each scope
37 /// Scope => Hooks' Addresses
38 access(all)
39 let heartbeatScopes: {String: {Address: PublicPath}}
40
41 /// The minimum interval between two heartbeats
42 access(all)
43 var heartbeatMinInterval: UFix64
44
45 /* --- Interfaces & Resources --- */
46
47 /// The interface that all the hooks must implement
48 ///
49 access(all) resource interface IHeartbeatHook {
50 /// The methods that is invoked when the heartbeat is executed
51 /// Before try-catch is deployed, please ensure that there will be no panic inside the method.
52 ///
53 access(account)
54 fun onHeartbeat(_ deltaTime: UFix64)
55 }
56
57 /// Heartbeat resource, provides the heartbeat function
58 /// The heartbeat function will invoke all the hooks that are bound to the specified scope
59 /// The heartbeatTime is the time when the heartbeat function is invoked, it is stored in the contract storage
60 /// The deltaTime is the time interval between the last heartbeat and the current heartbeat
61 ///
62 access(all) resource Heartbeat {
63 /// The heartbeat function
64 ///
65 /// - Parameter scope: The scope of the heartbeat
66 ///
67 access(all)
68 fun tick(scope: String) {
69 // Check if the scope exists
70 if FixesHeartbeat.heartbeatScopes[scope] == nil {
71 log("The scope does not exist: ".concat(scope))
72 return
73 } else {
74 log("Ticking Heartbeart for scope: ".concat(scope))
75 }
76
77 if let hooks = FixesHeartbeat.borrowHooksDictRef(scope: scope) {
78 let now = getCurrentBlock().timestamp
79 let lastHeartbeatTime = FixesHeartbeat.lastHeartbeatTime[scope] ?? (now - FixesHeartbeat.heartbeatMinInterval)
80 let deltaTime = now - lastHeartbeatTime
81 // Check if the interval is too short
82 if deltaTime < FixesHeartbeat.heartbeatMinInterval {
83 return
84 }
85
86 // iterate all the hooks
87 for hookAddr in hooks.keys {
88 if let hookRef = getAccount(hookAddr)
89 .capabilities.get<&{IHeartbeatHook}>(hooks[hookAddr]!)
90 .borrow()
91 {
92 hookRef.onHeartbeat(deltaTime)
93 }
94 }
95
96 // Update the last heartbeat time
97 FixesHeartbeat.lastHeartbeatTime[scope] = now
98
99 // Emit the event
100 emit HeartbeatExecuted(scope: scope, lastHeartbeatTime: now, deltaTime: deltaTime)
101 }
102 }
103 }
104
105 /// Borrow the hook dictionary reference
106 ///
107 access(contract)
108 view fun borrowHooksDictRef(scope: String): auth(Mutate) &{Address: PublicPath}? {
109 return &self.heartbeatScopes[scope]
110 }
111
112 /** --- Account Level Functions --- */
113
114 /// Add a hook to the specified scope
115 ///
116 /// - Parameter scope: The scope of the hook
117 /// - Parameter hook: The hook to be added
118 ///
119 access(account)
120 fun addHook(scope: String, hookAddr: Address, hookPath: PublicPath) {
121 // Check if the scope exists
122 if self.heartbeatScopes[scope] == nil {
123 self.heartbeatScopes[scope] = {}
124 }
125 let hookCap = getAccount(hookAddr)
126 .capabilities.get<&{IHeartbeatHook}>(hookPath)
127 if hookCap.check() {
128 if let hookRef = hookCap.borrow() {
129 let scopesRef = (self.borrowHooksDictRef(scope: scope))!
130 // check if the hook is already added
131 if scopesRef[hookAddr] != nil {
132 return
133 }
134 // Add the hook
135 scopesRef[hookAddr] = hookPath
136 // Emit the event
137 emit HookAdded(scope: scope, hookAddr: hookAddr, hookType: hookRef.getType())
138 }
139 }
140 }
141
142 /// Remove a hook from the specified scope
143 ///
144 access(account)
145 fun removeHook(scope: String, hookAddr: Address) {
146 if let hooks = FixesHeartbeat.borrowHooksDictRef(scope: scope) {
147 hooks.remove(key: hookAddr)
148
149 // Emit the event
150 emit HookRemoved(scope: scope, hookAddr: hookAddr)
151 }
152 }
153
154 /* --- Public Functions --- */
155
156 /// Create a new Heartbeat resource
157 ///
158 access(all)
159 fun createHeartbeat(): @Heartbeat {
160 return <-create Heartbeat()
161 }
162
163 /// Check if the hook is added to the specified scope
164 ///
165 access(all)
166 view fun hasHook(scope: String, hookAddr: Address): Bool {
167 if let hooks = FixesHeartbeat.borrowHooksDictRef(scope: scope) {
168 return hooks[hookAddr] != nil
169 }
170 return false
171 }
172
173 init() {
174 let identifier = "FixesHeartbeat_".concat(self.account.address.toString())
175 self.storagePath = StoragePath(identifier: identifier)!
176
177 self.lastHeartbeatTime = {}
178 self.heartbeatScopes = {}
179
180 // Set the default minimum interval between two heartbeats
181 // 60 seconds
182 self.heartbeatMinInterval = 60.0
183
184 // Register the hooks
185
186 emit ContractInitialized()
187 }
188}
189