Smart Contract

DCATransactionHandler

A.ca7ee55e4fc3251a.DCATransactionHandler

Valid From

134,619,625

Deployed

6d ago
Feb 22, 2026, 02:49:55 AM UTC

Dependents

5 imports
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import DCAController from 0xca7ee55e4fc3251a
3import DCAPlan from 0xca7ee55e4fc3251a
4import DeFiMath from 0xca7ee55e4fc3251a
5import FungibleToken from 0xf233dcee88fe0abe
6import FlowToken from 0x1654653399040a61
7import SwapRouter from 0xa6850776a94e6551
8import TeleportedTetherToken from 0xcfdd90d4a00f7b5b
9
10/// DCATransactionHandler: Scheduled transaction handler for DCA execution
11///
12/// This contract implements the FlowTransactionScheduler.TransactionHandler interface
13/// to enable autonomous DCA plan execution via Forte Scheduled Transactions.
14///
15/// Architecture:
16/// 1. User creates DCA plan with DCAController
17/// 2. Plan schedules execution via FlowTransactionScheduler
18/// 3. At scheduled time, scheduler calls this handler's executeTransaction()
19/// 4. Handler:
20///    - Validates plan is ready
21///    - Builds DeFi Actions stack (Source → Swapper → Sink)
22///    - Executes swap
23///    - Updates plan accounting
24///    - Reschedules next execution if plan is still active
25///
26/// Educational Notes:
27/// - Implements FlowTransactionScheduler.TransactionHandler interface
28/// - Access control: executeTransaction is access(FlowTransactionScheduler.Execute)
29/// - Uses DeFi Actions for composable swap execution
30/// - Stores metadata via getViews/resolveView for discoverability
31access(all) contract DCATransactionHandler {
32
33    /// Event emitted when handler starts execution
34    access(all) event HandlerExecutionStarted(
35        transactionId: UInt64,
36        planId: UInt64,
37        owner: Address,
38        timestamp: UFix64
39    )
40
41    /// Event emitted when handler completes successfully
42    access(all) event HandlerExecutionCompleted(
43        transactionId: UInt64,
44        planId: UInt64,
45        owner: Address,
46        amountIn: UFix64,
47        amountOut: UFix64,
48        nextExecutionScheduled: Bool,
49        timestamp: UFix64
50    )
51
52    /// Event emitted when handler execution fails
53    access(all) event HandlerExecutionFailed(
54        transactionId: UInt64,
55        planId: UInt64?,
56        owner: Address?,
57        reason: String,
58        timestamp: UFix64
59    )
60
61    /// Handler resource that implements the Scheduled Transaction interface
62    ///
63    /// Each user has one instance of this stored in their account.
64    /// The scheduler calls executeTransaction() when a DCA plan is due.
65    access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
66
67        /// Reference to the user's DCA controller
68        /// This capability allows the handler to access and update plans
69        access(self) let controllerCap: Capability<auth(DCAController.Owner) &DCAController.Controller>
70
71        init(controllerCap: Capability<auth(DCAController.Owner) &DCAController.Controller>) {
72            pre {
73                controllerCap.check(): "Invalid controller capability"
74            }
75            self.controllerCap = controllerCap
76        }
77
78        /// Main execution entrypoint called by FlowTransactionScheduler
79        ///
80        /// @param id: Transaction ID from the scheduler
81        /// @param data: Encoded plan data (contains planId)
82        ///
83        /// This function has restricted access - only FlowTransactionScheduler can call it
84        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
85            let timestamp = getCurrentBlock().timestamp
86
87            // Parse plan ID from transaction data
88            let planData = data as! {String: UInt64}? ?? panic("Invalid transaction data format")
89            let planId = planData["planId"] ?? panic("Plan ID not found in transaction data")
90            let ownerAddress = self.controllerCap.address
91
92            emit HandlerExecutionStarted(
93                transactionId: id,
94                planId: planId,
95                owner: ownerAddress,
96                timestamp: timestamp
97            )
98
99            // Borrow controller
100            let controller = self.controllerCap.borrow()
101                ?? panic("Could not borrow DCA controller")
102
103            // Borrow plan
104            let planRef = controller.borrowPlan(id: planId)
105                ?? panic("Could not borrow plan with ID: ".concat(planId.toString()))
106
107            // Validate plan is ready
108            if !planRef.isReadyForExecution() {
109                emit HandlerExecutionFailed(
110                    transactionId: id,
111                    planId: planId,
112                    owner: ownerAddress,
113                    reason: "Plan not ready for execution",
114                    timestamp: timestamp
115                )
116                return
117            }
118
119            // Check max executions
120            if planRef.hasReachedMaxExecutions() {
121                emit HandlerExecutionFailed(
122                    transactionId: id,
123                    planId: planId,
124                    owner: ownerAddress,
125                    reason: "Plan has reached maximum executions",
126                    timestamp: timestamp
127                )
128                return
129            }
130
131            // Get vault capabilities
132            let sourceVaultCap = controller.getSourceVaultCapability()
133                ?? panic("Source vault capability not configured")
134
135            let targetVaultCap = controller.getTargetVaultCapability()
136                ?? panic("Target vault capability not configured")
137
138            // Validate capabilities
139            if !sourceVaultCap.check() {
140                emit HandlerExecutionFailed(
141                    transactionId: id,
142                    planId: planId,
143                    owner: ownerAddress,
144                    reason: "Invalid source vault capability",
145                    timestamp: timestamp
146                )
147                return
148            }
149
150            if !targetVaultCap.check() {
151                emit HandlerExecutionFailed(
152                    transactionId: id,
153                    planId: planId,
154                    owner: ownerAddress,
155                    reason: "Invalid target vault capability",
156                    timestamp: timestamp
157                )
158                return
159            }
160
161            // Execute the swap
162            let result = self.executeSwap(
163                planRef: planRef,
164                sourceVaultCap: sourceVaultCap,
165                targetVaultCap: targetVaultCap
166            )
167
168            if result.success {
169                // Record successful execution
170                planRef.recordExecution(
171                    amountIn: result.amountIn!,
172                    amountOut: result.amountOut!
173                )
174
175                // Schedule next execution if plan is still active
176                var nextScheduled = false
177                if planRef.status == DCAPlan.PlanStatus.Active && !planRef.hasReachedMaxExecutions() {
178                    planRef.scheduleNextExecution()
179                    nextScheduled = true
180
181                    // TODO: In production, call FlowTransactionScheduler.schedule() here
182                    // to schedule the next execution. See CLAUDE.md for integration notes.
183                }
184
185                emit HandlerExecutionCompleted(
186                    transactionId: id,
187                    planId: planId,
188                    owner: ownerAddress,
189                    amountIn: result.amountIn!,
190                    amountOut: result.amountOut!,
191                    nextExecutionScheduled: nextScheduled,
192                    timestamp: timestamp
193                )
194            } else {
195                emit HandlerExecutionFailed(
196                    transactionId: id,
197                    planId: planId,
198                    owner: ownerAddress,
199                    reason: result.errorMessage ?? "Unknown error",
200                    timestamp: timestamp
201                )
202            }
203        }
204
205        /// Execute swap using IncrementFi SwapRouter
206        ///
207        /// This uses IncrementFi's production swap infrastructure to swap tokens.
208        /// Supports USDT ↔ FLOW swaps with slippage protection.
209        ///
210        /// @param planRef: Reference to the DCA plan
211        /// @param sourceVaultCap: Capability to withdraw from source vault
212        /// @param targetVaultCap: Capability to deposit to target vault
213        /// @return ExecutionResult with success status and amounts
214        access(self) fun executeSwap(
215            planRef: &DCAPlan.Plan,
216            sourceVaultCap: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
217            targetVaultCap: Capability<&{FungibleToken.Receiver}>
218        ): ExecutionResult {
219            // Get amount to invest
220            let amountIn = planRef.amountPerInterval
221
222            // Borrow source vault and check balance
223            let sourceVault = sourceVaultCap.borrow()
224                ?? panic("Could not borrow source vault")
225
226            if sourceVault.balance < amountIn {
227                return ExecutionResult(
228                    success: false,
229                    amountIn: nil,
230                    amountOut: nil,
231                    errorMessage: "Insufficient balance in source vault. Required: ".concat(amountIn.toString()).concat(", Available: ").concat(sourceVault.balance.toString())
232                )
233            }
234
235            // Withdraw tokens to swap
236            let tokensToSwap <- sourceVault.withdraw(amount: amountIn)
237
238            // Determine swap path based on token types
239            let sourceTypeId = planRef.sourceTokenType.identifier
240            let targetTypeId = planRef.targetTokenType.identifier
241
242            let tokenPath: [String] = []
243            if sourceTypeId.contains("TeleportedTetherToken") && targetTypeId.contains("FlowToken") {
244                // USDT → FLOW
245                tokenPath.append("A.cfdd90d4a00f7b5b.TeleportedTetherToken")
246                tokenPath.append("A.1654653399040a61.FlowToken")
247            } else if sourceTypeId.contains("FlowToken") && targetTypeId.contains("TeleportedTetherToken") {
248                // FLOW → USDT
249                tokenPath.append("A.1654653399040a61.FlowToken")
250                tokenPath.append("A.cfdd90d4a00f7b5b.TeleportedTetherToken")
251            } else {
252                // Unsupported swap path
253                destroy tokensToSwap
254                return ExecutionResult(
255                    success: false,
256                    amountIn: nil,
257                    amountOut: nil,
258                    errorMessage: "Unsupported token pair. Only USDT ↔ FLOW swaps are currently supported."
259                )
260            }
261
262            // Get expected output amount from IncrementFi
263            let expectedAmountsOut = SwapRouter.getAmountsOut(
264                amountIn: amountIn,
265                tokenKeyPath: tokenPath
266            )
267            let expectedAmountOut = expectedAmountsOut[expectedAmountsOut.length - 1]
268
269            // Calculate minimum output with slippage protection
270            // maxSlippageBps is in basis points (100 = 1%)
271            let slippageMultiplier = UInt64(10000) - planRef.maxSlippageBps
272            let minAmountOut = expectedAmountOut * UFix64(slippageMultiplier) / 10000.0
273
274            // Set deadline (5 minutes from now)
275            let deadline = getCurrentBlock().timestamp + 300.0
276
277            // Execute swap via IncrementFi SwapRouter
278            let swappedTokens <- SwapRouter.swapExactTokensForTokens(
279                exactVaultIn: <-tokensToSwap,
280                amountOutMin: minAmountOut,
281                tokenKeyPath: tokenPath,
282                deadline: deadline
283            )
284
285            // Get actual output amount
286            let actualAmountOut = swappedTokens.balance
287
288            // Deposit to target vault
289            let targetVault = targetVaultCap.borrow()
290                ?? panic("Could not borrow target vault")
291
292            targetVault.deposit(from: <-swappedTokens)
293
294            return ExecutionResult(
295                success: true,
296                amountIn: amountIn,
297                amountOut: actualAmountOut,
298                errorMessage: nil
299            )
300        }
301
302        /// Get supported view types (for resource metadata)
303        access(all) view fun getViews(): [Type] {
304            return [Type<StoragePath>(), Type<PublicPath>()]
305        }
306
307        /// Resolve a specific view type
308        access(all) fun resolveView(_ view: Type): AnyStruct? {
309            switch view {
310                case Type<StoragePath>():
311                    return /storage/DCATransactionHandler
312                case Type<PublicPath>():
313                    return /public/DCATransactionHandler
314                default:
315                    return nil
316            }
317        }
318    }
319
320    /// Result struct for swap execution
321    access(all) struct ExecutionResult {
322        access(all) let success: Bool
323        access(all) let amountIn: UFix64?
324        access(all) let amountOut: UFix64?
325        access(all) let errorMessage: String?
326
327        init(success: Bool, amountIn: UFix64?, amountOut: UFix64?, errorMessage: String?) {
328            self.success = success
329            self.amountIn = amountIn
330            self.amountOut = amountOut
331            self.errorMessage = errorMessage
332        }
333    }
334
335    /// Factory function to create a new handler
336    ///
337    /// @param controllerCap: Capability to the user's DCA controller
338    /// @return New handler resource
339    access(all) fun createHandler(
340        controllerCap: Capability<auth(DCAController.Owner) &DCAController.Controller>
341    ): @Handler {
342        return <- create Handler(controllerCap: controllerCap)
343    }
344}
345