Smart Contract
DCAHandlerEVMV4
A.ca7ee55e4fc3251a.DCAHandlerEVMV4
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import FlowToken from 0x1654653399040a61
4import FungibleToken from 0xf233dcee88fe0abe
5import DCAServiceEVM from 0xca7ee55e4fc3251a
6
7/// DCAHandlerEVMV4: Autonomous Scheduled Transaction Handler for EVM-Native DCA
8///
9/// This version follows the official scaffold pattern (CounterLoopTransactionHandler):
10/// - Capabilities are passed IN the TransactionData struct (LoopConfig pattern)
11/// - Handler does NOT store capabilities internally
12/// - Uses Manager.scheduleByHandler() for autonomous rescheduling
13///
14/// Key Difference from V3:
15/// V3 stored capabilities in the Handler resource and issued new caps each time.
16/// V4 passes capabilities through TransactionData, allowing proper serialization
17/// and retrieval via getControllers().
18///
19access(all) contract DCAHandlerEVMV4 {
20
21 // ============================================================
22 // Events
23 // ============================================================
24
25 access(all) event HandlerCreated(uuid: UInt64)
26 access(all) event ExecutionTriggered(planId: UInt64, success: Bool, nextScheduled: Bool)
27 access(all) event ExecutionSkipped(planId: UInt64, reason: String)
28 access(all) event NextExecutionScheduled(planId: UInt64, scheduledId: UInt64, timestamp: UFix64)
29 access(all) event NextExecutionSchedulingFailed(planId: UInt64, reason: String)
30
31 // ============================================================
32 // Storage Paths
33 // ============================================================
34
35 access(all) let HandlerStoragePath: StoragePath
36 access(all) let HandlerPublicPath: PublicPath
37
38 // ============================================================
39 // LoopConfig: Scheduling configuration passed in TransactionData
40 // ============================================================
41
42 /// Following the scaffold's LoopConfig pattern:
43 /// Capabilities are passed IN the data, not stored in handler.
44 access(all) struct LoopConfig {
45 /// Capability to the Manager for scheduling next transactions
46 access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
47
48 /// Capability to withdraw FLOW for scheduling fees
49 access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
50
51 /// Transaction priority
52 access(all) let priority: FlowTransactionScheduler.Priority
53
54 /// Execution effort (compute limit)
55 access(all) let executionEffort: UInt64
56
57 init(
58 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
59 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
60 priority: FlowTransactionScheduler.Priority,
61 executionEffort: UInt64
62 ) {
63 self.schedulerManagerCap = schedulerManagerCap
64 self.feeProviderCap = feeProviderCap
65 self.priority = priority
66 self.executionEffort = executionEffort
67 }
68 }
69
70 // ============================================================
71 // TransactionData: Carries plan ID + LoopConfig for rescheduling
72 // ============================================================
73
74 access(all) struct TransactionData {
75 access(all) let planId: UInt64
76 access(all) let loopConfig: LoopConfig
77
78 init(planId: UInt64, loopConfig: LoopConfig) {
79 self.planId = planId
80 self.loopConfig = loopConfig
81 }
82 }
83
84 // ============================================================
85 // Handler Resource
86 // ============================================================
87
88 /// Handler resource that implements TransactionHandler interface.
89 /// Does NOT store capabilities - they come from TransactionData.
90 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
91
92 /// Main execution entrypoint called by FlowTransactionScheduler
93 access(FlowTransactionScheduler.Execute)
94 fun executeTransaction(id: UInt64, data: AnyStruct?) {
95 // Parse transaction data
96 let txData = data as? TransactionData
97 if txData == nil {
98 log("DCAHandlerEVMV4: Invalid transaction data")
99 return
100 }
101
102 let planId = txData!.planId
103 let loopConfig = txData!.loopConfig
104
105 // Get plan details
106 let planOpt = DCAServiceEVM.getPlan(planId: planId)
107 if planOpt == nil {
108 emit ExecutionSkipped(planId: planId, reason: "Plan not found")
109 return
110 }
111 let plan = planOpt!
112
113 // Check if plan is active
114 if plan.getStatus() != DCAServiceEVM.PlanStatus.Active {
115 emit ExecutionSkipped(planId: planId, reason: "Plan not active")
116 return
117 }
118
119 // Execute the DCA plan via DCAServiceEVM
120 let success = DCAServiceEVM.executePlan(planId: planId)
121
122 // If successful and plan still active, schedule next execution
123 var nextScheduled = false
124 if success {
125 // Re-fetch plan to get updated nextExecutionTime
126 let updatedPlanOpt = DCAServiceEVM.getPlan(planId: planId)
127 if updatedPlanOpt != nil {
128 let updatedPlan = updatedPlanOpt!
129
130 // Only reschedule if plan is still active
131 if updatedPlan.getStatus() == DCAServiceEVM.PlanStatus.Active {
132 nextScheduled = self.scheduleNextExecution(
133 planId: planId,
134 nextExecutionTime: updatedPlan.nextExecutionTime,
135 loopConfig: loopConfig
136 )
137 }
138 }
139 }
140
141 emit ExecutionTriggered(planId: planId, success: success, nextScheduled: nextScheduled)
142 }
143
144 /// Schedule the next execution using capabilities from LoopConfig
145 access(self) fun scheduleNextExecution(
146 planId: UInt64,
147 nextExecutionTime: UFix64?,
148 loopConfig: LoopConfig
149 ): Bool {
150 // Verify nextExecutionTime is provided
151 if nextExecutionTime == nil {
152 emit NextExecutionSchedulingFailed(planId: planId, reason: "Next execution time not set")
153 return false
154 }
155
156 // Prepare next transaction data (pass same loopConfig for chaining)
157 let nextTxData = TransactionData(planId: planId, loopConfig: loopConfig)
158
159 // Estimate fees
160 let estimate = FlowTransactionScheduler.estimate(
161 data: nextTxData,
162 timestamp: nextExecutionTime!,
163 priority: loopConfig.priority,
164 executionEffort: loopConfig.executionEffort
165 )
166
167 let feeAmount = estimate.flowFee ?? 0.001
168 let feeWithBuffer = feeAmount * 1.1
169
170 // Borrow fee vault from capability
171 let feeVault = loopConfig.feeProviderCap.borrow()
172 if feeVault == nil {
173 emit NextExecutionSchedulingFailed(planId: planId, reason: "Could not borrow fee vault")
174 return false
175 }
176
177 // Check balance
178 if feeVault!.balance < feeWithBuffer {
179 emit NextExecutionSchedulingFailed(
180 planId: planId,
181 reason: "Insufficient fees. Required: ".concat(feeWithBuffer.toString()).concat(" Available: ").concat(feeVault!.balance.toString())
182 )
183 return false
184 }
185
186 // Withdraw fees
187 let fees <- feeVault!.withdraw(amount: feeWithBuffer)
188
189 // Borrow scheduler manager from capability
190 let schedulerManager = loopConfig.schedulerManagerCap.borrow()
191 if schedulerManager == nil {
192 // Return fees if we can't schedule
193 feeVault!.deposit(from: <-fees)
194 emit NextExecutionSchedulingFailed(planId: planId, reason: "Could not borrow scheduler manager")
195 return false
196 }
197
198 // Schedule next execution using Manager.scheduleByHandler()
199 // This is the key pattern from the scaffold
200 let scheduledId = schedulerManager!.scheduleByHandler(
201 handlerTypeIdentifier: self.getType().identifier,
202 handlerUUID: self.uuid,
203 data: nextTxData,
204 timestamp: nextExecutionTime!,
205 priority: loopConfig.priority,
206 executionEffort: loopConfig.executionEffort,
207 fees: <-fees as! @FlowToken.Vault
208 )
209
210 if scheduledId == 0 {
211 emit NextExecutionSchedulingFailed(planId: planId, reason: "scheduleByHandler returned 0")
212 return false
213 }
214
215 emit NextExecutionScheduled(planId: planId, scheduledId: scheduledId, timestamp: nextExecutionTime!)
216 return true
217 }
218
219 init() {
220 emit HandlerCreated(uuid: self.uuid)
221 }
222 }
223
224 // ============================================================
225 // Factory Functions
226 // ============================================================
227
228 access(all) fun createHandler(): @Handler {
229 return <- create Handler()
230 }
231
232 access(all) fun createLoopConfig(
233 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
234 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
235 priority: FlowTransactionScheduler.Priority,
236 executionEffort: UInt64
237 ): LoopConfig {
238 return LoopConfig(
239 schedulerManagerCap: schedulerManagerCap,
240 feeProviderCap: feeProviderCap,
241 priority: priority,
242 executionEffort: executionEffort
243 )
244 }
245
246 access(all) fun createTransactionData(planId: UInt64, loopConfig: LoopConfig): TransactionData {
247 return TransactionData(planId: planId, loopConfig: loopConfig)
248 }
249
250 // ============================================================
251 // Init
252 // ============================================================
253
254 init() {
255 self.HandlerStoragePath = /storage/DCAHandlerEVMV4
256 self.HandlerPublicPath = /public/DCAHandlerEVMV4
257 }
258}
259