Smart Contract

MarketFactory

A.6c1b12e35dca8863.MarketFactory

Valid From

118,667,520

Deployed

6d ago
Feb 22, 2026, 03:31:08 PM UTC

Dependents

0 imports
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}