Smart Contract
PaymentCronTransactionHandler
A.651079a7b572ef10.PaymentCronTransactionHandler
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