Smart Contract

DCAHandlerEVMV4

A.ca7ee55e4fc3251a.DCAHandlerEVMV4

Valid From

135,730,139

Deployed

5d ago
Feb 23, 2026, 12:48:07 AM UTC

Dependents

8 imports
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