Smart Contract
MarketFactory
A.6c1b12e35dca8863.MarketFactory
1// cadence/contracts/MarketFactory.cdc
2// Factory contract for creating and managing prediction markets - Fixed for Cadence 1.0
3
4import FlowWager from 0x6c1b12e35dca8863
5
6access(all) contract MarketFactory {
7
8 // ========================================
9 // EVENTS
10 // ========================================
11
12 access(all) event MarketTemplateCreated(templateId: UInt64, name: String, category: UInt8)
13 access(all) event MarketCreatedFromTemplate(marketId: UInt64, templateId: UInt64, creator: Address)
14 access(all) event MarketFactoryInitialized()
15
16 // ========================================
17 // STRUCTS
18 // ========================================
19
20 access(all) struct MarketTemplate {
21 access(all) let id: UInt64
22 access(all) let name: String
23 access(all) let description: String
24 access(all) let questionTemplate: String // e.g., "Will {EVENT} happen by {DATE}?"
25 access(all) let optionATemplate: String // e.g., "Yes"
26 access(all) let optionBTemplate: String // e.g., "No"
27 access(all) let category: FlowWager.MarketCategory
28 access(all) let defaultDuration: UFix64
29 access(all) let defaultMinBet: UFix64
30 access(all) let defaultMaxBet: UFix64
31 access(all) let isBreakingNewsTemplate: Bool
32 access(all) let createdAt: UFix64
33 access(all) let creator: Address
34 access(all) var active: Bool
35 access(all) var usageCount: UInt64
36
37 init(
38 id: UInt64,
39 name: String,
40 description: String,
41 questionTemplate: String,
42 optionATemplate: String,
43 optionBTemplate: String,
44 category: FlowWager.MarketCategory,
45 defaultDuration: UFix64,
46 defaultMinBet: UFix64,
47 defaultMaxBet: UFix64,
48 isBreakingNewsTemplate: Bool,
49 creator: Address
50 ) {
51 self.id = id
52 self.name = name
53 self.description = description
54 self.questionTemplate = questionTemplate
55 self.optionATemplate = optionATemplate
56 self.optionBTemplate = optionBTemplate
57 self.category = category
58 self.defaultDuration = defaultDuration
59 self.defaultMinBet = defaultMinBet
60 self.defaultMaxBet = defaultMaxBet
61 self.isBreakingNewsTemplate = isBreakingNewsTemplate
62 self.createdAt = getCurrentBlock().timestamp
63 self.creator = creator
64 self.active = true
65 self.usageCount = 0
66 }
67
68 access(contract) fun incrementUsage() {
69 self.usageCount = self.usageCount + 1
70 }
71
72 access(contract) fun setActive(active: Bool) {
73 self.active = active
74 }
75 }
76
77 access(all) struct MarketValidationResult {
78 access(all) let isValid: Bool
79 access(all) let errors: [String]
80 access(all) let warnings: [String]
81
82 init(isValid: Bool, errors: [String], warnings: [String]) {
83 self.isValid = isValid
84 self.errors = errors
85 self.warnings = warnings
86 }
87 }
88
89 access(all) struct MarketCreationRequest {
90 access(all) let question: String
91 access(all) let optionA: String
92 access(all) let optionB: String
93 access(all) let category: FlowWager.MarketCategory
94 access(all) let imageURI: String
95 access(all) let duration: UFix64
96 access(all) let isBreakingNews: Bool
97 access(all) let minBet: UFix64
98 access(all) let maxBet: UFix64
99 access(all) let templateId: UInt64?
100 access(all) let customMetadata: {String: String}
101
102 init(
103 question: String,
104 optionA: String,
105 optionB: String,
106 category: FlowWager.MarketCategory,
107 imageURI: String,
108 duration: UFix64,
109 isBreakingNews: Bool,
110 minBet: UFix64,
111 maxBet: UFix64,
112 templateId: UInt64?,
113 customMetadata: {String: String}
114 ) {
115 self.question = question
116 self.optionA = optionA
117 self.optionB = optionB
118 self.category = category
119 self.imageURI = imageURI
120 self.duration = duration
121 self.isBreakingNews = isBreakingNews
122 self.minBet = minBet
123 self.maxBet = maxBet
124 self.templateId = templateId
125 self.customMetadata = customMetadata
126 }
127 }
128
129 // ========================================
130 // RESOURCES
131 // ========================================
132
133 access(all) resource FactoryAdmin {
134 access(all) fun createMarketTemplate(
135 name: String,
136 description: String,
137 questionTemplate: String,
138 optionATemplate: String,
139 optionBTemplate: String,
140 category: FlowWager.MarketCategory,
141 defaultDuration: UFix64,
142 defaultMinBet: UFix64,
143 defaultMaxBet: UFix64,
144 isBreakingNewsTemplate: Bool
145 ): UInt64 {
146 pre {
147 name.length > 0: "Template name cannot be empty"
148 questionTemplate.length > 0: "Question template cannot be empty"
149 optionATemplate.length > 0: "Option A template cannot be empty"
150 optionBTemplate.length > 0: "Option B template cannot be empty"
151 defaultDuration > 0.0: "Default duration must be positive"
152 defaultMinBet > 0.0: "Default minimum bet must be positive"
153 defaultMaxBet >= defaultMinBet: "Default maximum bet must be >= minimum bet"
154 }
155
156 let templateId = MarketFactory.nextTemplateId
157 let template = MarketTemplate(
158 id: templateId,
159 name: name,
160 description: description,
161 questionTemplate: questionTemplate,
162 optionATemplate: optionATemplate,
163 optionBTemplate: optionBTemplate,
164 category: category,
165 defaultDuration: defaultDuration,
166 defaultMinBet: defaultMinBet,
167 defaultMaxBet: defaultMaxBet,
168 isBreakingNewsTemplate: isBreakingNewsTemplate,
169 creator: self.owner!.address
170 )
171
172 MarketFactory.templates[templateId] = template
173 MarketFactory.nextTemplateId = MarketFactory.nextTemplateId + 1
174 MarketFactory.totalTemplates = MarketFactory.totalTemplates + 1
175
176 emit MarketTemplateCreated(
177 templateId: templateId,
178 name: name,
179 category: category.rawValue
180 )
181
182 return templateId
183 }
184
185 access(all) fun updateMarketTemplate(
186 templateId: UInt64,
187 name: String?,
188 description: String?,
189 active: Bool?
190 ) {
191 pre {
192 MarketFactory.templates.containsKey(templateId): "Template does not exist"
193 }
194
195 // Fixed: Properly handle optional dictionary access
196 if let templateRef = &MarketFactory.templates[templateId] as &MarketTemplate? {
197 if active != nil {
198 templateRef.setActive(active: active!)
199 }
200 }
201 }
202
203 access(all) fun createMarketFromTemplate(
204 templateId: UInt64,
205 question: String,
206 optionA: String?,
207 optionB: String?,
208 imageURI: String,
209 duration: UFix64?,
210 minBet: UFix64?,
211 maxBet: UFix64?,
212 customMetadata: {String: String}
213 ): UInt64 {
214 pre {
215 MarketFactory.templates.containsKey(templateId): "Template does not exist"
216 }
217
218 // Fixed: Properly handle optional dictionary access
219 if let templateRef = &MarketFactory.templates[templateId] as &MarketTemplate? {
220 assert(templateRef.active, message: "Template is not active")
221
222 // Use template defaults if not provided
223 let finalOptionA = optionA ?? templateRef.optionATemplate
224 let finalOptionB = optionB ?? templateRef.optionBTemplate
225 let finalDuration = duration ?? templateRef.defaultDuration
226 let finalMinBet = minBet ?? templateRef.defaultMinBet
227 let finalMaxBet = maxBet ?? templateRef.defaultMaxBet
228
229 // This would integrate with FlowWager's market creation
230 let marketId: UInt64 = MarketFactory.totalMarketsFromTemplates + 1
231 MarketFactory.totalMarketsFromTemplates = MarketFactory.totalMarketsFromTemplates + 1
232
233 // Update template usage
234 templateRef.incrementUsage()
235
236 emit MarketCreatedFromTemplate(
237 marketId: marketId,
238 templateId: templateId,
239 creator: self.owner!.address
240 )
241
242 return marketId
243 }
244 panic("Template not found")
245 }
246 }
247
248 access(all) resource MarketValidator {
249 access(all) fun validateMarket(request: MarketCreationRequest): MarketValidationResult {
250 var errors: [String] = []
251 var warnings: [String] = []
252
253 // Validate question
254 if request.question.length == 0 {
255 errors.append("Question cannot be empty")
256 } else if request.question.length > 500 {
257 errors.append("Question too long (max 500 characters)")
258 }
259
260 // Validate options
261 if request.optionA.length == 0 {
262 errors.append("Option A cannot be empty")
263 } else if request.optionA.length > 100 {
264 errors.append("Option A too long (max 100 characters)")
265 }
266
267 if request.optionB.length == 0 {
268 errors.append("Option B cannot be empty")
269 } else if request.optionB.length > 100 {
270 errors.append("Option B too long (max 100 characters)")
271 }
272
273 if request.optionA == request.optionB {
274 errors.append("Options must be different")
275 }
276
277 // Validate duration
278 if request.duration <= 0.0 {
279 errors.append("Duration must be positive")
280 } else if request.duration < 3600.0 {
281 errors.append("Minimum duration is 1 hour")
282 } else if request.duration > 2592000.0 {
283 errors.append("Maximum duration is 30 days")
284 }
285
286 // Breaking news duration check
287 if request.isBreakingNews && request.duration > 86400.0 {
288 warnings.append("Breaking news markets typically last less than 24 hours")
289 }
290
291 // Validate betting limits
292 if request.minBet <= 0.0 {
293 errors.append("Minimum bet must be positive")
294 } else if request.minBet < 0.1 {
295 warnings.append("Very low minimum bet may attract spam")
296 }
297
298 if request.maxBet < request.minBet {
299 errors.append("Maximum bet must be >= minimum bet")
300 } else if request.maxBet > 10000.0 {
301 warnings.append("Very high maximum bet may limit participation")
302 }
303
304 // Validate image URI
305 if request.imageURI.length == 0 {
306 errors.append("Image URI cannot be empty")
307 }
308
309 return MarketValidationResult(
310 isValid: errors.length == 0,
311 errors: errors,
312 warnings: warnings
313 )
314 }
315
316 access(all) fun getMarketRecommendations(category: FlowWager.MarketCategory): [String] {
317 var recommendations: [String] = []
318
319 // Fixed: Use raw values for enum comparison
320 switch category.rawValue {
321 case FlowWager.MarketCategory.Crypto.rawValue:
322 recommendations.append("Consider price prediction markets")
323 recommendations.append("Include specific cryptocurrency names")
324 recommendations.append("Set reasonable price targets")
325 case FlowWager.MarketCategory.Sports.rawValue:
326 recommendations.append("Include team names and event details")
327 recommendations.append("Set end time after event completion")
328 recommendations.append("Consider weather conditions for outdoor sports")
329 case FlowWager.MarketCategory.BreakingNews.rawValue:
330 recommendations.append("Keep duration short (1-24 hours)")
331 recommendations.append("Ensure rapid resolution capability")
332 recommendations.append("Include news source verification")
333 default:
334 recommendations.append("Provide clear, objective criteria")
335 recommendations.append("Include relevant context and dates")
336 }
337
338 return recommendations
339 }
340 }
341
342 // ========================================
343 // CONTRACT STATE
344 // ========================================
345
346 access(contract) var templates: {UInt64: MarketTemplate}
347 access(contract) var nextTemplateId: UInt64
348 access(all) var totalTemplates: UInt64
349 access(all) var totalMarketsFromTemplates: UInt64
350
351 // Storage Paths
352 access(all) let FactoryAdminStoragePath: StoragePath
353 access(all) let MarketValidatorStoragePath: StoragePath
354 access(all) let MarketValidatorPublicPath: PublicPath
355
356 // ========================================
357 // PUBLIC FUNCTIONS
358 // ========================================
359
360 access(all) fun getTemplate(templateId: UInt64): MarketTemplate? {
361 return self.templates[templateId]
362 }
363
364 access(all) fun getAllTemplates(): [MarketTemplate] {
365 return self.templates.values
366 }
367
368 access(all) fun getTemplatesByCategory(category: FlowWager.MarketCategory): [MarketTemplate] {
369 let categoryTemplates: [MarketTemplate] = []
370
371 for template in self.templates.values {
372 if template.category.rawValue == category.rawValue && template.active {
373 categoryTemplates.append(template)
374 }
375 }
376
377 return categoryTemplates
378 }
379
380 access(all) fun getPopularTemplates(limit: UInt64): [MarketTemplate] {
381 let allTemplates = self.templates.values
382 return allTemplates
383 }
384
385 access(all) fun getFactoryStats(): {String: AnyStruct} {
386 var activeTemplates: UInt64 = 0
387 var totalUsage: UInt64 = 0
388
389 for template in self.templates.values {
390 if template.active {
391 activeTemplates = activeTemplates + 1
392 }
393 totalUsage = totalUsage + template.usageCount
394 }
395
396 return {
397 "totalTemplates": self.totalTemplates,
398 "activeTemplates": activeTemplates,
399 "totalMarketsFromTemplates": self.totalMarketsFromTemplates,
400 "totalUsage": totalUsage,
401 "averageUsagePerTemplate": activeTemplates > 0 ? totalUsage / activeTemplates : 0
402 }
403 }
404
405 access(all) fun validateMarketRequest(request: MarketCreationRequest): MarketValidationResult {
406 let validatorRef = self.account.capabilities.borrow<&MarketValidator>(self.MarketValidatorPublicPath)
407 ?? panic("Could not borrow MarketValidator reference")
408
409 return validatorRef.validateMarket(request: request)
410 }
411
412 access(all) fun getMarketRecommendations(category: FlowWager.MarketCategory): [String] {
413 let validatorRef = self.account.capabilities.borrow<&MarketValidator>(self.MarketValidatorPublicPath)
414 ?? panic("Could not borrow MarketValidator reference")
415
416 return validatorRef.getMarketRecommendations(category: category)
417 }
418
419 access(all) fun getSuggestedDuration(category: FlowWager.MarketCategory, isBreakingNews: Bool): UFix64 {
420 if isBreakingNews {
421 return 3600.0 // 1 hour for breaking news
422 }
423
424 // Fixed: Use raw values for enum comparison
425 switch category.rawValue {
426 case FlowWager.MarketCategory.Sports.rawValue:
427 return 86400.0 * 3.0 // 3 days for sports events
428 case FlowWager.MarketCategory.Crypto.rawValue:
429 return 86400.0 * 7.0 // 1 week for crypto predictions
430 case FlowWager.MarketCategory.Politics.rawValue:
431 return 86400.0 * 30.0 // 30 days for political events
432 case FlowWager.MarketCategory.Economics.rawValue:
433 return 86400.0 * 14.0 // 2 weeks for economic predictions
434 default:
435 return 86400.0 * 7.0 // 1 week default
436 }
437 }
438
439 // ========================================
440 // INITIALIZATION
441 // ========================================
442
443 init() {
444 self.templates = {}
445 self.nextTemplateId = 1
446 self.totalTemplates = 0
447 self.totalMarketsFromTemplates = 0
448
449 self.FactoryAdminStoragePath = /storage/MarketFactoryAdmin
450 self.MarketValidatorStoragePath = /storage/MarketValidator
451 self.MarketValidatorPublicPath = /public/MarketValidator
452
453 // Create and store Factory Admin resource
454 let factoryAdmin <- create FactoryAdmin()
455 self.account.storage.save(<-factoryAdmin, to: self.FactoryAdminStoragePath)
456
457 // Create and store Market Validator resource
458 let validator <- create MarketValidator()
459 self.account.storage.save(<-validator, to: self.MarketValidatorStoragePath)
460
461 // Link public capability for validator (Cadence 1.0 syntax)
462 let validatorCap = self.account.capabilities.storage.issue<&MarketValidator>(self.MarketValidatorStoragePath)
463 self.account.capabilities.publish(validatorCap, at: self.MarketValidatorPublicPath)
464
465 emit MarketFactoryInitialized()
466 }
467}