Smart Contract
DCATransactionHandlerV2Loop
A.ca7ee55e4fc3251a.DCATransactionHandlerV2Loop
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import DCAControllerV2 from 0xca7ee55e4fc3251a
4import DCAPlanV2 from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import SwapRouter from 0xa6850776a94e6551
8import EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 from 0x1e4aa0b87d10b141
9
10/// DCATransactionHandlerV2Loop: Self-rescheduling DCA handler
11///
12/// Follows the official CounterLoopTransactionHandler pattern:
13/// - feeProviderCap passed directly in data (not fetched from controller)
14/// - Minimal rescheduling logic
15/// - 8 imports (reduced from 9)
16access(all) contract DCATransactionHandlerV2Loop {
17
18 /// Loop configuration - matches official scaffold pattern
19 access(all) struct LoopConfig {
20 access(all) let planId: UInt64
21 access(all) let intervalSeconds: UFix64
22 access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
23 access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
24 access(all) let priority: FlowTransactionScheduler.Priority
25 access(all) let executionEffort: UInt64
26
27 init(
28 planId: UInt64,
29 intervalSeconds: UFix64,
30 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
31 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
32 priority: FlowTransactionScheduler.Priority,
33 executionEffort: UInt64
34 ) {
35 self.planId = planId
36 self.intervalSeconds = intervalSeconds
37 self.schedulerManagerCap = schedulerManagerCap
38 self.feeProviderCap = feeProviderCap
39 self.priority = priority
40 self.executionEffort = executionEffort
41 }
42 }
43
44 access(all) event Executed(
45 transactionId: UInt64,
46 planId: UInt64,
47 amountIn: UFix64,
48 amountOut: UFix64,
49 nextScheduled: Bool
50 )
51
52 access(all) event Failed(
53 transactionId: UInt64,
54 planId: UInt64,
55 reason: String
56 )
57
58 /// Handler resource
59 access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
60 access(self) let controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
61
62 init(controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>) {
63 self.controllerCap = controllerCap
64 }
65
66 access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
67 // 1. Parse loop config (official pattern)
68 let config = data as! LoopConfig? ?? panic("LoopConfig required")
69
70 // 2. Execute swap
71 let swapResult = self.doSwap(planId: config.planId)
72
73 if !swapResult.success {
74 emit Failed(transactionId: id, planId: config.planId, reason: swapResult.error ?? "swap failed")
75 return
76 }
77
78 // 3. Reschedule if plan still active (official pattern)
79 var nextScheduled = false
80 let controller = self.controllerCap.borrow()
81 if controller != nil {
82 let plan = controller!.borrowPlan(id: config.planId)
83 if plan != nil && plan!.status == DCAPlanV2.PlanStatus.Active && !plan!.hasReachedMaxExecutions() {
84 nextScheduled = self.reschedule(config: config, data: data!)
85 }
86 }
87
88 emit Executed(
89 transactionId: id,
90 planId: config.planId,
91 amountIn: swapResult.amountIn ?? 0.0,
92 amountOut: swapResult.amountOut ?? 0.0,
93 nextScheduled: nextScheduled
94 )
95 }
96
97 /// Execute the swap - simplified
98 access(self) fun doSwap(planId: UInt64): SwapResult {
99 let controller = self.controllerCap.borrow() ?? panic("No controller")
100 let plan = controller.borrowPlan(id: planId) ?? panic("No plan")
101
102 if !plan.isReadyForExecution() || plan.hasReachedMaxExecutions() {
103 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "not ready")
104 }
105
106 let sourceVaultCap = controller.getSourceVaultCapability() ?? panic("no source")
107 let targetVaultCap = controller.getTargetVaultCapability() ?? panic("no target")
108
109 let sourceVault = sourceVaultCap.borrow() ?? panic("borrow source")
110 let amountIn = plan.amountPerInterval
111
112 if sourceVault.balance < amountIn {
113 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "insufficient balance")
114 }
115
116 let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
117
118 // Build path
119 let sourceTypeId = plan.sourceTokenType.identifier
120 let targetTypeId = plan.targetTokenType.identifier
121 let tokenPath: [String] = []
122
123 if sourceTypeId.contains("EVMVMBridgedToken") && targetTypeId.contains("FlowToken") {
124 tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
125 tokenPath.append("A.1654653399040a61.FlowToken")
126 } else if targetTypeId.contains("EVMVMBridgedToken") && sourceTypeId.contains("FlowToken") {
127 tokenPath.append("A.1654653399040a61.FlowToken")
128 tokenPath.append("A.1e4aa0b87d10b141.EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14")
129 } else {
130 destroy tokensToSwap
131 return SwapResult(success: false, amountIn: nil, amountOut: nil, error: "unsupported pair")
132 }
133
134 let expectedOut = SwapRouter.getAmountsOut(amountIn: amountIn, tokenKeyPath: tokenPath)
135 let minOut = expectedOut[expectedOut.length - 1] * (10000.0 - UFix64(plan.maxSlippageBps)) / 10000.0
136
137 let swapped <- SwapRouter.swapExactTokensForTokens(
138 exactVaultIn: <-tokensToSwap,
139 amountOutMin: minOut,
140 tokenKeyPath: tokenPath,
141 deadline: getCurrentBlock().timestamp + 300.0
142 )
143
144 let amountOut = swapped.balance
145 let targetVault = targetVaultCap.borrow() ?? panic("borrow target")
146 targetVault.deposit(from: <-swapped)
147
148 // Record execution
149 plan.recordExecution(amountIn: amountIn, amountOut: amountOut)
150 plan.scheduleNextExecution()
151
152 return SwapResult(success: true, amountIn: amountIn, amountOut: amountOut, error: nil)
153 }
154
155 /// Reschedule - follows official pattern exactly
156 access(self) fun reschedule(config: LoopConfig, data: AnyStruct): Bool {
157 let future = getCurrentBlock().timestamp + config.intervalSeconds
158
159 let estimate = FlowTransactionScheduler.estimate(
160 data: data,
161 timestamp: future,
162 priority: config.priority,
163 executionEffort: config.executionEffort
164 )
165
166 if estimate.timestamp == nil && config.priority != FlowTransactionScheduler.Priority.Low {
167 return false
168 }
169
170 // Get fees from feeProviderCap (official pattern)
171 let feeVault = config.feeProviderCap.borrow() ?? panic("fee provider")
172 let fees <- feeVault.withdraw(amount: estimate.flowFee ?? 0.0)
173
174 let manager = config.schedulerManagerCap.borrow() ?? panic("manager")
175
176 manager.scheduleByHandler(
177 handlerTypeIdentifier: self.getType().identifier,
178 handlerUUID: self.uuid,
179 data: data,
180 timestamp: future,
181 priority: config.priority,
182 executionEffort: config.executionEffort,
183 fees: <-fees as! @FlowToken.Vault
184 )
185
186 return true
187 }
188 }
189
190 access(all) struct SwapResult {
191 access(all) let success: Bool
192 access(all) let amountIn: UFix64?
193 access(all) let amountOut: UFix64?
194 access(all) let error: String?
195
196 init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, error: String?) {
197 self.success = success
198 self.amountIn = amountIn
199 self.amountOut = amountOut
200 self.error = error
201 }
202 }
203
204 access(all) fun createHandler(
205 controllerCap: Capability<auth(DCAControllerV2.Owner) &DCAControllerV2.Controller>
206 ): @Handler {
207 return <- create Handler(controllerCap: controllerCap)
208 }
209
210 access(all) fun createLoopConfig(
211 planId: UInt64,
212 intervalSeconds: UFix64,
213 schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
214 feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
215 priority: FlowTransactionScheduler.Priority,
216 executionEffort: UInt64
217 ): LoopConfig {
218 return LoopConfig(
219 planId: planId,
220 intervalSeconds: intervalSeconds,
221 schedulerManagerCap: schedulerManagerCap,
222 feeProviderCap: feeProviderCap,
223 priority: priority,
224 executionEffort: executionEffort
225 )
226 }
227}
228