Smart Contract
DCATransactionHandlerUnified
A.ca7ee55e4fc3251a.DCATransactionHandlerUnified
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import DCAControllerUnified from 0xca7ee55e4fc3251a
4import DCAPlanUnified from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import SwapRouter from 0xa6850776a94e6551
8import EVM from 0xe467b9dd11fa00df
9import UniswapV3SwapperConnector from 0xca7ee55e4fc3251a
10import DeFiActions from 0xca7ee55e4fc3251a
11
12/// DCATransactionHandlerUnified: Unified DCA Handler with Auto-Rescheduling
13///
14/// Combines V2Loop's proven pattern with EVM swap support:
15/// - LoopConfig with feeProviderCap in data (scheduler-compatible)
16/// - Token-type detection for swap routing
17/// - IncrementFi for Cadence tokens (USDC), UniswapV3 for EVM tokens (USDF)
18///
19/// Storage: /storage/DCATransactionHandlerUnified
20access(all) contract DCATransactionHandlerUnified {
21
22 /// LoopConfig - proven pattern from CounterLoopTransactionHandler
23 access(all) struct LoopConfig {
24 access(all) let planId: UInt64
25 access(all) let intervalSeconds: UFix64
26 access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
27 access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
28 access(all) let priority: FlowTransactionScheduler.Priority
29 access(all) let executionEffort: UInt64
30
31 init(
32 planId: UInt64,
33 intervalSeconds: UFix64,
34 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
35 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
36 priority: FlowTransactionScheduler.Priority,
37 executionEffort: UInt64
38 ) {
39 self.planId = planId
40 self.intervalSeconds = intervalSeconds
41 self.schedulerManagerCap = schedulerManagerCap
42 self.feeProviderCap = feeProviderCap
43 self.priority = priority
44 self.executionEffort = executionEffort
45 }
46 }
47
48 access(all) event Executed(
49 transactionId: UInt64,
50 planId: UInt64,
51 amountIn: UFix64,
52 amountOut: UFix64,
53 swapType: String,
54 nextScheduled: Bool
55 )
56
57 access(all) event Failed(
58 transactionId: UInt64,
59 planId: UInt64,
60 reason: String
61 )
62
63 access(all) struct SwapResult {
64 access(all) let success: Bool
65 access(all) let amountIn: UFix64?
66 access(all) let amountOut: UFix64?
67 access(all) let swapType: String
68 access(all) let error: String?
69
70 init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, swapType: String, error: String?) {
71 self.success = success
72 self.amountIn = amountIn
73 self.amountOut = amountOut
74 self.swapType = swapType
75 self.error = error
76 }
77 }
78
79 /// Handler resource
80 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
81 access(self) let controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
82
83 init(controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>) {
84 self.controllerCap = controllerCap
85 }
86
87 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
88 let config = data as! LoopConfig? ?? panic("LoopConfig required")
89
90 // Execute swap with auto-routing
91 let swapResult = self.doSwap(planId: config.planId)
92
93 if !swapResult.success {
94 emit Failed(transactionId: id, planId: config.planId, reason: swapResult.error ?? "swap failed")
95 return
96 }
97
98 // Reschedule if plan still active
99 var nextScheduled = false
100 let controller = self.controllerCap.borrow()
101 if controller != nil {
102 let plan = controller!.borrowPlan(id: config.planId)
103 if plan != nil && plan!.status == DCAPlanUnified.PlanStatus.Active && !plan!.hasReachedMaxExecutions() {
104 nextScheduled = self.reschedule(config: config, data: data!)
105 }
106 }
107
108 emit Executed(
109 transactionId: id,
110 planId: config.planId,
111 amountIn: swapResult.amountIn ?? 0.0,
112 amountOut: swapResult.amountOut ?? 0.0,
113 swapType: swapResult.swapType,
114 nextScheduled: nextScheduled
115 )
116 }
117
118 /// Execute swap with auto-routing based on token types
119 access(self) fun doSwap(planId: UInt64): SwapResult {
120 let controller = self.controllerCap.borrow() ?? panic("No controller")
121 let plan = controller.borrowPlan(id: planId) ?? panic("No plan")
122
123 if !plan.isReadyForExecution() || plan.hasReachedMaxExecutions() {
124 return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "none", error: "not ready")
125 }
126
127 // Route based on token type
128 if plan.requiresEVM() {
129 return self.executeEVMSwap(controller: controller, plan: plan)
130 } else {
131 return self.executeCadenceSwap(controller: controller, plan: plan)
132 }
133 }
134
135 /// Execute Cadence-native swap via IncrementFi SwapRouter
136 access(self) fun executeCadenceSwap(
137 controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
138 plan: &DCAPlanUnified.Plan
139 ): SwapResult {
140 let sourceVaultCap = controller.getSourceVaultCapability() ?? panic("no source")
141 let targetVaultCap = controller.getTargetVaultCapability() ?? panic("no target")
142
143 let sourceVault = sourceVaultCap.borrow() ?? panic("borrow source")
144 let amountIn = plan.amountPerInterval
145
146 if sourceVault.balance < amountIn {
147 return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "Cadence", error: "insufficient balance")
148 }
149
150 let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
151
152 // Build token path for IncrementFi
153 let sourceTypeId = plan.sourceTokenType.identifier
154 let targetTypeId = plan.targetTokenType.identifier
155 var tokenPath: [String] = []
156
157 // USDC token contract on mainnet
158 let usdcTypeId = "A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14"
159 let flowTypeId = "A.1654653399040a61.FlowToken"
160
161 if sourceTypeId.contains("FlowToken") && targetTypeId.contains("EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14") {
162 // FLOW -> USDC
163 tokenPath = [flowTypeId, usdcTypeId]
164 } else if sourceTypeId.contains("EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14") && targetTypeId.contains("FlowToken") {
165 // USDC -> FLOW
166 tokenPath = [usdcTypeId, flowTypeId]
167 } else {
168 destroy tokensToSwap
169 return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "Cadence", error: "unsupported pair for Cadence swap")
170 }
171
172 let expectedOut = SwapRouter.getAmountsOut(amountIn: amountIn, tokenKeyPath: tokenPath)
173 let minOut = expectedOut[expectedOut.length - 1] * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
174
175 let swapped <- SwapRouter.swapExactTokensForTokens(
176 exactVaultIn: <-tokensToSwap,
177 amountOutMin: minOut,
178 tokenKeyPath: tokenPath,
179 deadline: getCurrentBlock().timestamp + 300.0
180 )
181
182 let amountOut = swapped.balance
183 let targetVault = targetVaultCap.borrow() ?? panic("borrow target")
184 targetVault.deposit(from: <-swapped)
185
186 plan.recordExecution(amountIn: amountIn, amountOut: amountOut)
187 plan.scheduleNextExecution()
188
189 return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, swapType: "Cadence/IncrementFi", error: nil)
190 }
191
192 /// Execute EVM swap via UniswapV3 COA
193 access(self) fun executeEVMSwap(
194 controller: auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller,
195 plan: &DCAPlanUnified.Plan
196 ): SwapResult {
197 let sourceVaultCap = controller.getSourceVaultCapability() ?? panic("no source")
198 let targetVaultCap = controller.getTargetVaultCapability() ?? panic("no target")
199 let coaCap = controller.getCOACapability() ?? panic("no COA for EVM swap")
200
201 let sourceVault = sourceVaultCap.borrow() ?? panic("borrow source")
202 let amountIn = plan.amountPerInterval
203
204 if sourceVault.balance < amountIn {
205 return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "EVM", error: "insufficient balance")
206 }
207
208 let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
209
210 // Determine token path and types for EVM swap
211 let sourceTypeId = plan.sourceTokenType.identifier
212 let targetTypeId = plan.targetTokenType.identifier
213
214 // USDF token contract (EVM-bridged)
215 let usdfEVMAddress = EVM.addressFromString("0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed")
216 // WFLOW on EVM
217 let wflowEVMAddress = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e")
218
219 var tokenPath: [EVM.EVMAddress] = []
220 var inVaultType: Type = plan.sourceTokenType
221 var outVaultType: Type = plan.targetTokenType
222
223 if sourceTypeId.contains("FlowToken") {
224 // FLOW -> USDF
225 tokenPath = [wflowEVMAddress, usdfEVMAddress]
226 } else if targetTypeId.contains("FlowToken") {
227 // USDF -> FLOW
228 tokenPath = [usdfEVMAddress, wflowEVMAddress]
229 } else {
230 destroy tokensToSwap
231 return SwapResult(success: false, amountIn: nil, amountOut: nil, swapType: "EVM", error: "unsupported pair for EVM swap")
232 }
233
234 // Create swapper with 0.3% fee tier (3000)
235 let swapper <- UniswapV3SwapperConnector.createSwapperWithDefaults(
236 tokenPath: tokenPath,
237 feePath: [3000],
238 inVaultType: inVaultType,
239 outVaultType: outVaultType,
240 coaCapability: coaCap
241 )
242
243 // Get quote with slippage
244 let quote = swapper.getQuote(
245 fromTokenType: inVaultType,
246 toTokenType: outVaultType,
247 amount: amountIn
248 )
249
250 // Apply plan's slippage
251 let adjustedMinAmount = quote.expectedAmount * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
252 let adjustedQuote = DeFiActions.Quote(
253 expectedAmount: quote.expectedAmount,
254 minAmount: adjustedMinAmount,
255 slippageTolerance: UFix64(plan.maxSlippageBps) / 10000.0,
256 deadline: nil,
257 data: quote.data
258 )
259
260 // Execute swap
261 let swapped <- swapper.swap(inVault: <-tokensToSwap, quote: adjustedQuote)
262 let amountOut = swapped.balance
263
264 // Deposit to target
265 let targetVault = targetVaultCap.borrow() ?? panic("borrow target")
266 targetVault.deposit(from: <-swapped)
267
268 // Cleanup swapper
269 destroy swapper
270
271 plan.recordExecution(amountIn: amountIn, amountOut: amountOut)
272 plan.scheduleNextExecution()
273
274 return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, swapType: "EVM/UniswapV3", error: nil)
275 }
276
277 /// Reschedule - follows official pattern exactly
278 access(self) fun reschedule(config: LoopConfig, data: AnyStruct): Bool {
279 let future = getCurrentBlock().timestamp + config.intervalSeconds
280
281 let estimate = FlowTransactionScheduler.estimate(
282 data: data,
283 timestamp: future,
284 priority: config.priority,
285 executionEffort: config.executionEffort
286 )
287
288 if estimate.timestamp == nil && config.priority != FlowTransactionScheduler.Priority.Low {
289 return false
290 }
291
292 let feeVault = config.feeProviderCap.borrow() ?? panic("fee provider")
293 let fees <- feeVault.withdraw(amount: estimate.flowFee ?? 0.0)
294
295 let manager = config.schedulerManagerCap.borrow() ?? panic("manager")
296
297 manager.scheduleByHandler(
298 handlerTypeIdentifier: self.getType().identifier,
299 handlerUUID: self.uuid,
300 data: data,
301 timestamp: future,
302 priority: config.priority,
303 executionEffort: config.executionEffort,
304 fees: <-fees as! @FlowToken.Vault
305 )
306
307 return true
308 }
309 }
310
311 access(all) fun createHandler(
312 controllerCap: Capability<auth(DCAControllerUnified.Owner) &DCAControllerUnified.Controller>
313 ): @Handler {
314 return <- create Handler(controllerCap: controllerCap)
315 }
316
317 access(all) fun createLoopConfig(
318 planId: UInt64,
319 intervalSeconds: UFix64,
320 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
321 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
322 priority: FlowTransactionScheduler.Priority,
323 executionEffort: UInt64
324 ): LoopConfig {
325 return LoopConfig(
326 planId: planId,
327 intervalSeconds: intervalSeconds,
328 schedulerManagerCap: schedulerManagerCap,
329 feeProviderCap: feeProviderCap,
330 priority: priority,
331 executionEffort: executionEffort
332 )
333 }
334}
335