Smart Contract

DCATransactionHandlerV2Loop

A.ca7ee55e4fc3251a.DCATransactionHandlerV2Loop

Valid From

135,616,807

Deployed

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

Dependents

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