Smart Contract

DCATransactionHandlerV2Simple

A.ca7ee55e4fc3251a.DCATransactionHandlerV2Simple

Valid From

135,615,341

Deployed

5d ago
Feb 23, 2026, 12:44:10 AM UTC

Dependents

4 imports
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import DCAControllerV2 from 0xca7ee55e4fc3251a
3import DCAPlanV2 from 0xca7ee55e4fc3251a
4import DeFiMath from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import SwapRouter from 0xa6850776a94e6551
8import EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 from 0x1e4aa0b87d10b141
9
10/// DCATransactionHandlerV2Simple: Simplified handler WITHOUT autonomous rescheduling
11///
12/// This is a stripped-down version of DCATransactionHandlerV2 that:
13/// - Executes DCA swaps via IncrementFi
14/// - Records execution results
15/// - Does NOT reschedule itself (external process handles rescheduling)
16///
17/// This reduces handler complexity from ~550 lines to ~200 lines,
18/// removing the scheduleNextExecution() logic that may exceed scheduler limits.
19access(all) contract DCATransactionHandlerV2Simple {
20
21    /// Simple transaction data - just the plan ID
22    access(all) struct SimpleTransactionData {
23        access(all) let planId: UInt64
24
25        init(planId: UInt64) {
26            self.planId = planId
27        }
28    }
29
30    /// Event emitted when handler starts execution
31    access(all) event HandlerExecutionStarted(
32        transactionId: UInt64,
33        planId: UInt64,
34        owner: Address,
35        timestamp: UFix64
36    )
37
38    /// Event emitted when handler completes successfully
39    access(all) event HandlerExecutionCompleted(
40        transactionId: UInt64,
41        planId: UInt64,
42        owner: Address,
43        amountIn: UFix64,
44        amountOut: UFix64,
45        executionCount: UInt64,
46        timestamp: UFix64
47    )
48
49    /// Event emitted when handler execution fails
50    access(all) event HandlerExecutionFailed(
51        transactionId: UInt64,
52        planId: UInt64?,
53        owner: Address?,
54        reason: String,
55        timestamp: UFix64
56    )
57
58    /// Handler resource - simplified version
59    access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
60
61        /// Reference to the user's DCA controller
62        access(self) let controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
63
64        init(controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>) {
65            pre {
66                controllerCap.check(): "Invalid controller capability"
67            }
68            self.controllerCap = controllerCap
69        }
70
71        /// Main execution entrypoint - SIMPLIFIED (no rescheduling)
72        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
73            let timestamp = getCurrentBlock().timestamp
74
75            // Parse plan ID - simple format
76            let txData = data as! SimpleTransactionData? ?? panic("Invalid transaction data")
77            let planId = txData.planId
78            let ownerAddress = self.controllerCap.address
79
80            emit HandlerExecutionStarted(
81                transactionId: id,
82                planId: planId,
83                owner: ownerAddress,
84                timestamp: timestamp
85            )
86
87            // Borrow controller
88            let controller = self.controllerCap.borrow()
89                ?? panic("Could not borrow DCA controller")
90
91            // Borrow plan
92            let planRef = controller.borrowPlan(id: planId)
93                ?? panic("Could not borrow plan")
94
95            // Validate plan is ready
96            if !planRef.isReadyForExecution() {
97                emit HandlerExecutionFailed(
98                    transactionId: id,
99                    planId: planId,
100                    owner: ownerAddress,
101                    reason: "Plan not ready for execution",
102                    timestamp: timestamp
103                )
104                return
105            }
106
107            // Check max executions
108            if planRef.hasReachedMaxExecutions() {
109                emit HandlerExecutionFailed(
110                    transactionId: id,
111                    planId: planId,
112                    owner: ownerAddress,
113                    reason: "Plan has reached maximum executions",
114                    timestamp: timestamp
115                )
116                return
117            }
118
119            // Get vault capabilities
120            let sourceVaultCap = controller.getSourceVaultCapability()
121                ?? panic("Source vault capability not configured")
122            let targetVaultCap = controller.getTargetVaultCapability()
123                ?? panic("Target vault capability not configured")
124
125            // Validate capabilities
126            if !sourceVaultCap.check() {
127                emit HandlerExecutionFailed(
128                    transactionId: id,
129                    planId: planId,
130                    owner: ownerAddress,
131                    reason: "Invalid source vault capability",
132                    timestamp: timestamp
133                )
134                return
135            }
136
137            if !targetVaultCap.check() {
138                emit HandlerExecutionFailed(
139                    transactionId: id,
140                    planId: planId,
141                    owner: ownerAddress,
142                    reason: "Invalid target vault capability",
143                    timestamp: timestamp
144                )
145                return
146            }
147
148            // Execute the swap
149            let result = self.executeSwap(
150                planRef: planRef,
151                sourceVaultCap: sourceVaultCap,
152                targetVaultCap: targetVaultCap
153            )
154
155            if result.success {
156                // Record successful execution
157                planRef.recordExecution(
158                    amountIn: result.amountIn!,
159                    amountOut: result.amountOut!
160                )
161
162                // Update next execution time (but DON'T schedule - external process does that)
163                if planRef.status == DCAPlanV2.PlanStatus.Active && !planRef.hasReachedMaxExecutions() {
164                    planRef.scheduleNextExecution()
165                }
166
167                emit HandlerExecutionCompleted(
168                    transactionId: id,
169                    planId: planId,
170                    owner: ownerAddress,
171                    amountIn: result.amountIn!,
172                    amountOut: result.amountOut!,
173                    executionCount: planRef.executionCount,
174                    timestamp: timestamp
175                )
176            } else {
177                emit HandlerExecutionFailed(
178                    transactionId: id,
179                    planId: planId,
180                    owner: ownerAddress,
181                    reason: result.errorMessage ?? "Unknown error",
182                    timestamp: timestamp
183                )
184            }
185        }
186
187        /// Execute swap using IncrementFi SwapRouter
188        access(self) fun executeSwap(
189            planRef: &DCAPlanV2.Plan,
190            sourceVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
191            targetVaultCap: Capability<&{FungibleToken.Receiver}>
192        ): ExecutionResult {
193            let amountIn = planRef.amountPerInterval
194
195            // Borrow source vault
196            let sourceVault = sourceVaultCap.borrow()
197                ?? panic("Could not borrow source vault")
198
199            if sourceVault.balance < amountIn {
200                return ExecutionResult(
201                    success: false,
202                    amountIn: nil,
203                    amountOut: nil,
204                    errorMessage: "Insufficient balance"
205                )
206            }
207
208            // Withdraw tokens
209            let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
210
211            // Determine swap path
212            let sourceTypeId = planRef.sourceTokenType.identifier
213            let targetTypeId = planRef.targetTokenType.identifier
214
215            let tokenPath: [String] = []
216            if sourceTypeId.contains("EVMVMBridgedToken") && targetTypeId.contains("FlowToken") {
217                tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
218                tokenPath.append("A.1654653399040a61.FlowToken")
219            } else if targetTypeId.contains("EVMVMBridgedToken") && sourceTypeId.contains("FlowToken") {
220                tokenPath.append("A.1654653399040a61.FlowToken")
221                tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
222            } else {
223                destroy tokensToSwap
224                return ExecutionResult(
225                    success: false,
226                    amountIn: nil,
227                    amountOut: nil,
228                    errorMessage: "Unsupported token pair"
229                )
230            }
231
232            // Get expected output
233            let expectedAmountsOut = SwapRouter.getAmountsOut(
234                amountIn: amountIn,
235                tokenKeyPath: tokenPath
236            )
237            let expectedAmountOut = expectedAmountsOut[expectedAmountsOut.length - 1]
238
239            // Calculate minimum with slippage
240            let slippageMultiplier = UInt64(10000) - planRef.maxSlippageBps
241            let minAmountOut = expectedAmountOut * UFix64(slippageMultiplier) / 10000.0
242
243            let deadline = getCurrentBlock().timestamp + 300.0
244
245            // Execute swap
246            let swappedTokens <- SwapRouter.swapExactTokensForTokens(
247                exactVaultIn: <-tokensToSwap,
248                amountOutMin: minAmountOut,
249                tokenKeyPath: tokenPath,
250                deadline: deadline
251            )
252
253            let actualAmountOut = swappedTokens.balance
254
255            // Deposit to target
256            let targetVault = targetVaultCap.borrow()
257                ?? panic("Could not borrow target vault")
258            targetVault.deposit(from: <-swappedTokens)
259
260            return ExecutionResult(
261                success: true,
262                amountIn: amountIn,
263                amountOut: actualAmountOut,
264                errorMessage: nil
265            )
266        }
267
268        access(all) view fun getViews(): [Type] {
269            return [Type<StoragePath>(), Type<PublicPath>()]
270        }
271
272        access(all) fun resolveView(_ view: Type): AnyStruct? {
273            switch view {
274                case Type<StoragePath>():
275                    return /storage/DCATransactionHandlerV2Simple
276                case Type<PublicPath>():
277                    return /public/DCATransactionHandlerV2Simple
278                default:
279                    return nil
280            }
281        }
282    }
283
284    /// Result struct for swap execution
285    access(all) struct ExecutionResult {
286        access(all) let success: Bool
287        access(all) let amountIn: UFix64?
288        access(all) let amountOut: UFix64?
289        access(all) let errorMessage: String?
290
291        init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, errorMessage: String?) {
292            self.success = success
293            self.amountIn = amountIn
294            self.amountOut = amountOut
295            self.errorMessage = errorMessage
296        }
297    }
298
299    /// Factory function
300    access(all) fun createHandler(
301        controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
302    ): @Handler {
303        return <- create Handler(controllerCap: controllerCap)
304    }
305
306    /// Helper to create transaction data
307    access(all) fun createTransactionData(planId: UInt64): SimpleTransactionData {
308        return SimpleTransactionData(planId: planId)
309    }
310}
311