Smart Contract
DCATransactionHandlerV2Simple
A.ca7ee55e4fc3251a.DCATransactionHandlerV2Simple
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