Smart Contract

PaymentCronTransactionHandler

A.651079a7b572ef10.PaymentCronTransactionHandler

Valid From

131,254,944

Deployed

1w ago
Feb 21, 2026, 03:46:44 PM UTC

Dependents

2 imports
1import FlowTransactionScheduler from 0xe467b9dd11fa00df
2import FlowTransactionSchedulerUtils from 0xe467b9dd11fa00df
3import FlowToken from 0x1654653399040a61
4import FungibleToken from 0xf233dcee88fe0abe
5
6/// CRON Payment Handler for recurring FLOW token transfers
7/// Schedules automatic payments at specified intervals to a recipient address
8access(all) contract PaymentCronTransactionHandler {
9
10    /// Event emitted when a payment is successfully executed
11    access(all) event PaymentExecuted(
12        transactionId: UInt64,
13        executionNumber: UInt64,
14        recipient: Address,
15        amount: UFix64,
16        nextExecutionTime: UFix64?
17    )
18
19    /// Event emitted when a payment cron job completes all executions
20    access(all) event PaymentCronCompleted(executionCount: UInt64)
21
22    /// Struct to hold payment cron configuration data
23    access(all) struct PaymentCronConfig {
24        access(all) let intervalSeconds: UFix64
25        access(all) let baseTimestamp: UFix64
26        access(all) let maxExecutions: UInt64?
27        access(all) let executionCount: UInt64
28        access(all) let recipientAddress: Address
29        access(all) let paymentAmount: UFix64
30        access(all) let schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>
31        access(all) let feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>
32        access(all) let priority: FlowTransactionScheduler.Priority
33        access(all) let executionEffort: UInt64
34
35        init(
36            intervalSeconds: UFix64,
37            baseTimestamp: UFix64,
38            maxExecutions: UInt64?,
39            executionCount: UInt64,
40            recipientAddress: Address,
41            paymentAmount: UFix64,
42            schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
43            feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
44            priority: FlowTransactionScheduler.Priority,
45            executionEffort: UInt64
46        ) {
47            pre {
48                paymentAmount > 0.0: "Payment amount must be greater than zero"
49                intervalSeconds > 0.0: "Interval must be greater than zero"
50                executionEffort >= 10: "Execution effort must be at least 10"
51            }
52            self.intervalSeconds = intervalSeconds
53            self.baseTimestamp = baseTimestamp
54            self.maxExecutions = maxExecutions
55            self.executionCount = executionCount
56            self.recipientAddress = recipientAddress
57            self.paymentAmount = paymentAmount
58            self.schedulerManagerCap = schedulerManagerCap
59            self.feeProviderCap = feeProviderCap
60            self.priority = priority
61            self.executionEffort = executionEffort
62        }
63
64        access(all) fun withIncrementedCount(): PaymentCronConfig {
65            return PaymentCronConfig(
66                intervalSeconds: self.intervalSeconds,
67                baseTimestamp: self.baseTimestamp,
68                maxExecutions: self.maxExecutions,
69                executionCount: self.executionCount + 1,
70                recipientAddress: self.recipientAddress,
71                paymentAmount: self.paymentAmount,
72                schedulerManagerCap: self.schedulerManagerCap,
73                feeProviderCap: self.feeProviderCap,
74                priority: self.priority,
75                executionEffort: self.executionEffort
76            )
77        }
78
79        access(all) fun shouldContinue(): Bool {
80            if let max = self.maxExecutions {
81                return self.executionCount < max
82            }
83            return true
84        }
85
86        access(all) fun getNextExecutionTime(): UFix64 {
87            let currentTime = getCurrentBlock().timestamp
88            
89            // If baseTimestamp is in the future, use it as the first execution time
90            if self.baseTimestamp > currentTime {
91                return self.baseTimestamp
92            }
93            
94            // Calculate next execution time based on elapsed intervals
95            let elapsed = currentTime - self.baseTimestamp
96            let intervals = UFix64(UInt64(elapsed / self.intervalSeconds)) + 1.0
97            return self.baseTimestamp + (intervals * self.intervalSeconds)
98        }
99    }
100
101    /// Handler resource that implements the Scheduled Transaction interface
102    access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
103        access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) {
104            // Extract payment configuration from transaction data
105            let paymentConfig = data as! PaymentCronConfig? ?? panic("PaymentCronConfig data is required")
106            
107            // Perform the payment
108            self.executePayment(transactionId: id, paymentConfig: paymentConfig)
109            
110            let updatedConfig = paymentConfig.withIncrementedCount()
111
112            // Check if we should continue scheduling
113            if !updatedConfig.shouldContinue() {
114                emit PaymentCronCompleted(executionCount: updatedConfig.executionCount)
115                log("Payment cron job completed after ".concat(updatedConfig.executionCount.toString()).concat(" executions"))
116                return
117            }
118
119            // Calculate the next precise execution time
120            let nextExecutionTime = paymentConfig.getNextExecutionTime()
121
122            let estimate = FlowTransactionScheduler.estimate(
123                data: updatedConfig,
124                timestamp: nextExecutionTime,
125                priority: paymentConfig.priority,
126                executionEffort: paymentConfig.executionEffort
127            )
128
129            assert(
130                estimate.timestamp != nil || paymentConfig.priority == FlowTransactionScheduler.Priority.Low,
131                message: estimate.error ?? "estimation failed"
132            )
133
134            // Borrow fee provider and withdraw fees
135            let feeVault = paymentConfig.feeProviderCap.borrow()
136                ?? panic("Cannot borrow fee provider capability")
137            let fees <- feeVault.withdraw(amount: estimate.flowFee ?? 0.0)
138
139            // Schedule next transaction through the manager
140            let schedulerManager = paymentConfig.schedulerManagerCap.borrow()
141                ?? panic("Cannot borrow scheduler manager capability")
142            
143            // Use scheduleByHandler since this handler has already been used
144            let transactionId = schedulerManager.scheduleByHandler(
145                handlerTypeIdentifier: self.getType().identifier,
146                handlerUUID: self.uuid,
147                data: updatedConfig,
148                timestamp: nextExecutionTime,
149                priority: paymentConfig.priority,
150                executionEffort: paymentConfig.executionEffort,
151                fees: <-fees as! @FlowToken.Vault
152            )
153
154            emit PaymentExecuted(
155                transactionId: id,
156                executionNumber: updatedConfig.executionCount,
157                recipient: paymentConfig.recipientAddress,
158                amount: paymentConfig.paymentAmount,
159                nextExecutionTime: nextExecutionTime
160            )
161
162            log("Next payment cron transaction scheduled (id: ".concat(transactionId.toString()).concat(") at ").concat(nextExecutionTime.toString()).concat(" (execution #").concat(updatedConfig.executionCount.toString()).concat(")"))
163        }
164
165        /// Execute the actual payment
166        access(all) fun executePayment(transactionId: UInt64, paymentConfig: PaymentCronConfig) {
167            // Borrow the fee provider vault which has withdraw capability
168            let feeVault = paymentConfig.feeProviderCap.borrow()
169                ?? panic("Cannot borrow fee provider capability")
170            
171            // Withdraw the payment amount
172            let tokens <- feeVault.withdraw(amount: paymentConfig.paymentAmount)
173            
174            // Get the recipient account
175            let recipient = getAccount(paymentConfig.recipientAddress)
176            
177            // Get the recipient's receiver capability
178            let recipientReceiverCap = recipient.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
179            
180            if recipientReceiverCap.check() {
181                let recipientReceiver = recipientReceiverCap.borrow()
182                    ?? panic("Cannot borrow recipient receiver")
183                recipientReceiver.deposit(from: <-tokens)
184                log("Payment executed: ".concat(paymentConfig.paymentAmount.toString()).concat(" FLOW sent to ").concat(paymentConfig.recipientAddress.toString()).concat(" (execution #").concat((paymentConfig.executionCount + 1).toString()).concat(")"))
185            } else {
186                // If recipient doesn't have a receiver capability, panic and return tokens
187                destroy tokens
188                panic("Recipient account ".concat(paymentConfig.recipientAddress.toString()).concat(" does not have a valid FlowToken receiver capability. Recipient must set up their account to receive tokens."))
189            }
190        }
191
192        /// Returns the list of metadata views this handler supports
193        access(all) view fun getViews(): [Type] {
194            return [Type<StoragePath>(), Type<PublicPath>()]
195        }
196
197        /// Resolves a specific metadata view
198        access(all) fun resolveView(_ view: Type): AnyStruct? {
199            switch view {
200                case Type<StoragePath>():
201                    return /storage/PaymentCronTransactionHandler
202                case Type<PublicPath>():
203                    return /public/PaymentCronTransactionHandler
204                default:
205                    return nil
206            }
207        }
208    }
209
210    /// Factory for the handler resource
211    access(all) fun createHandler(): @Handler {
212        return <- create Handler()
213    }
214
215    /// Helper function to create a payment cron configuration
216    access(all) fun createPaymentCronConfig(
217        intervalSeconds: UFix64,
218        baseTimestamp: UFix64?,
219        maxExecutions: UInt64?,
220        recipientAddress: Address,
221        paymentAmount: UFix64,
222        schedulerManagerCap: Capability<auth(FlowTransactionSchedulerUtils.Owner) &{FlowTransactionSchedulerUtils.Manager}>,
223        feeProviderCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>,
224        priority: FlowTransactionScheduler.Priority,
225        executionEffort: UInt64
226    ): PaymentCronConfig {
227        let base = baseTimestamp ?? getCurrentBlock().timestamp
228        return PaymentCronConfig(
229            intervalSeconds: intervalSeconds,
230            baseTimestamp: base,
231            maxExecutions: maxExecutions,
232            executionCount: 0,
233            recipientAddress: recipientAddress,
234            paymentAmount: paymentAmount,
235            schedulerManagerCap: schedulerManagerCap,
236            feeProviderCap: feeProviderCap,
237            priority: priority,
238            executionEffort: executionEffort
239        )
240    }
241}
242
243