Smart Contract
FlowWager
A.6c1b12e35dca8863.FlowWager
1access(all) contract FlowWager {
2
3 // =====================================
4 // EVENTS
5 // =====================================
6
7 access(all) event ContractInitialized()
8 access(all) event MarketCreated(marketId: UInt64, title: String, creator: Address)
9 access(all) event SharesPurchased(marketId: UInt64, buyer: Address, option: UInt8, shares: UFix64, amount: UFix64)
10 access(all) event MarketResolved(marketId: UInt64, outcome: UInt8)
11 access(all) event WinningsClaimed(marketId: UInt64, claimer: Address, amount: UFix64)
12 access(all) event UserRegistered(address: Address, username: String)
13
14
15 access(all) enum MarketCategory: UInt8 {
16 access(all) case Sports
17 access(all) case Entertainment
18 access(all) case Technology
19 access(all) case Economics
20 access(all) case Weather
21 access(all) case Crypto
22 access(all) case Politics
23 access(all) case BreakingNews
24 access(all) case Other
25 }
26
27 access(all) enum MarketStatus: UInt8 {
28 access(all) case Active
29 access(all) case Paused
30 access(all) case Resolved
31 access(all) case Cancelled
32 }
33
34 access(all) enum MarketOutcome: UInt8 {
35 access(all) case OptionA
36 access(all) case OptionB
37 access(all) case Draw
38 access(all) case Cancelled
39 }
40
41 // =====================================
42 // STRUCTS - All fields immutable
43 // =====================================
44
45 access(all) struct Market {
46 access(all) let id: UInt64
47 access(all) let title: String
48 access(all) let description: String
49 access(all) let category: MarketCategory
50 access(all) let optionA: String
51 access(all) let optionB: String
52 access(all) let creator: Address
53 access(all) let createdAt: UFix64
54 access(all) let endTime: UFix64
55 access(all) let minBet: UFix64
56 access(all) let maxBet: UFix64
57 access(all) let status: MarketStatus
58 access(all) let outcome: MarketOutcome?
59 access(all) let resolved: Bool
60 access(all) let totalOptionAShares: UFix64
61 access(all) let totalOptionBShares: UFix64
62 access(all) let totalPool: UFix64
63
64 init(
65 id: UInt64,
66 title: String,
67 description: String,
68 category: MarketCategory,
69 optionA: String,
70 optionB: String,
71 creator: Address,
72 endTime: UFix64,
73 minBet: UFix64,
74 maxBet: UFix64,
75 status: MarketStatus,
76 outcome: MarketOutcome?,
77 resolved: Bool,
78 totalOptionAShares: UFix64,
79 totalOptionBShares: UFix64,
80 totalPool: UFix64
81 ) {
82 self.id = id
83 self.title = title
84 self.description = description
85 self.category = category
86 self.optionA = optionA
87 self.optionB = optionB
88 self.creator = creator
89 self.createdAt = getCurrentBlock().timestamp
90 self.endTime = endTime
91 self.minBet = minBet
92 self.maxBet = maxBet
93 self.status = status
94 self.outcome = outcome
95 self.resolved = resolved
96 self.totalOptionAShares = totalOptionAShares
97 self.totalOptionBShares = totalOptionBShares
98 self.totalPool = totalPool
99 }
100 }
101
102 access(all) struct UserPosition {
103 access(all) let marketId: UInt64
104 access(all) let optionAShares: UFix64
105 access(all) let optionBShares: UFix64
106 access(all) let totalInvested: UFix64
107 access(all) let averagePrice: UFix64
108 access(all) let claimed: Bool
109
110 init(marketId: UInt64, optionAShares: UFix64, optionBShares: UFix64, totalInvested: UFix64, claimed: Bool) {
111 self.marketId = marketId
112 self.optionAShares = optionAShares
113 self.optionBShares = optionBShares
114 self.totalInvested = totalInvested
115 self.averagePrice = totalInvested / (optionAShares + optionBShares)
116 self.claimed = claimed
117 }
118 }
119
120 access(all) struct UserProfile {
121 access(all) let address: Address
122 access(all) let username: String
123 access(all) let joinedAt: UFix64
124 access(all) let displayName: String
125 access(all) let bio: String
126 access(all) let profileImageUrl: String
127
128 init(address: Address, username: String, displayName: String, bio: String, profileImageUrl: String) {
129 self.address = address
130 self.username = username
131 self.displayName = displayName
132 self.joinedAt = getCurrentBlock().timestamp
133 self.bio = bio
134 self.profileImageUrl = profileImageUrl
135 }
136 }
137
138 access(all) struct UserStats {
139 access(all) let totalMarketsParticipated: UInt64
140 access(all) let totalWinnings: UFix64
141 access(all) let totalLosses: UFix64
142 access(all) let winStreak: UInt64
143 access(all) let currentStreak: UInt64
144 access(all) let longestWinStreak: UInt64
145 access(all) let roi: UFix64
146 access(all) let averageBetSize: UFix64
147
148 init(
149 totalMarketsParticipated: UInt64,
150 totalWinnings: UFix64,
151 totalLosses: UFix64,
152 winStreak: UInt64,
153 currentStreak: UInt64,
154 longestWinStreak: UInt64,
155 roi: UFix64,
156 averageBetSize: UFix64
157 ) {
158 self.totalMarketsParticipated = totalMarketsParticipated
159 self.totalWinnings = totalWinnings
160 self.totalLosses = totalLosses
161 self.winStreak = winStreak
162 self.currentStreak = currentStreak
163 self.longestWinStreak = longestWinStreak
164 self.roi = roi
165 self.averageBetSize = averageBetSize
166 }
167 }
168
169 access(all) struct PlatformStats {
170 access(all) let totalMarkets: UInt64
171 access(all) let activeMarkets: UInt64
172 access(all) let totalUsers: UInt64
173 access(all) let totalVolume: UFix64
174 access(all) let totalFees: UFix64
175
176 init(
177 totalMarkets: UInt64,
178 activeMarkets: UInt64,
179 totalUsers: UInt64,
180 totalVolume: UFix64,
181 totalFees: UFix64
182 ) {
183 self.totalMarkets = totalMarkets
184 self.activeMarkets = activeMarkets
185 self.totalUsers = totalUsers
186 self.totalVolume = totalVolume
187 self.totalFees = totalFees
188 }
189 }
190
191 // =====================================
192 // CONTRACT STATE
193 // =====================================
194
195 access(all) var nextMarketId: UInt64
196 access(all) var platformFeePercentage: UFix64
197 access(all) var totalPlatformFees: UFix64
198 access(all) var totalVolumeTraded: UFix64
199
200 // Storage paths
201 access(all) let UserPositionsStoragePath: StoragePath
202 access(all) let UserPositionsPublicPath: PublicPath
203 access(all) let UserStatsStoragePath: StoragePath
204 access(all) let UserStatsPublicPath: PublicPath
205
206 // Markets storage
207 access(contract) let markets: {UInt64: Market}
208 access(contract) let userProfiles: {Address: UserProfile}
209 access(contract) let userStats: {Address: UserStats}
210
211 // =====================================
212 // RESOURCES
213 // =====================================
214
215 access(all) resource UserPositions {
216 access(all) var positions: {UInt64: UserPosition}
217
218 init() {
219 self.positions = {}
220 }
221
222 access(all) fun addPosition(_ position: UserPosition) {
223 if let existingPosition = self.positions[position.marketId] {
224 let newPosition = UserPosition(
225 marketId: position.marketId,
226 optionAShares: existingPosition.optionAShares + position.optionAShares,
227 optionBShares: existingPosition.optionBShares + position.optionBShares,
228 totalInvested: existingPosition.totalInvested + position.totalInvested,
229 claimed: false
230 )
231 self.positions[position.marketId] = newPosition
232 } else {
233 self.positions[position.marketId] = position
234 }
235 }
236
237 access(all) fun getPosition(marketId: UInt64): UserPosition? {
238 return self.positions[marketId]
239 }
240
241 access(all) fun getPositions(): {UInt64: UserPosition} {
242 return self.positions
243 }
244 }
245
246 access(all) resource interface UserPositionsPublic {
247 access(all) fun getPosition(marketId: UInt64): UserPosition?
248 access(all) fun getPositions(): {UInt64: UserPosition}
249 }
250
251 access(all) resource UserStatsResource {
252 access(all) var stats: UserStats
253
254 init() {
255 self.stats = UserStats(
256 totalMarketsParticipated: 0,
257 totalWinnings: 0.0,
258 totalLosses: 0.0,
259 winStreak: 0,
260 currentStreak: 0,
261 longestWinStreak: 0,
262 roi: 0.0,
263 averageBetSize: 0.0
264 )
265 }
266
267 access(all) fun getStats(): UserStats {
268 return self.stats
269 }
270 }
271
272 access(all) resource interface UserStatsPublic {
273 access(all) fun getStats(): UserStats
274 }
275
276 access(all) resource Admin {
277 access(all) fun createMarket(
278 title: String,
279 description: String,
280 category: MarketCategory,
281 optionA: String,
282 optionB: String,
283 endTime: UFix64,
284 minBet: UFix64,
285 maxBet: UFix64
286 ): UInt64 {
287 let marketId = FlowWager.nextMarketId
288 let market = Market(
289 id: marketId,
290 title: title,
291 description: description,
292 category: category,
293 optionA: optionA,
294 optionB: optionB,
295 creator: self.owner!.address,
296 endTime: endTime,
297 minBet: minBet,
298 maxBet: maxBet,
299 status: MarketStatus.Active,
300 outcome: nil,
301 resolved: false,
302 totalOptionAShares: 0.0,
303 totalOptionBShares: 0.0,
304 totalPool: 0.0
305 )
306
307 FlowWager.markets[marketId] = market
308 FlowWager.nextMarketId = FlowWager.nextMarketId + 1
309
310 emit MarketCreated(marketId: marketId, title: title, creator: self.owner!.address)
311 return marketId
312 }
313
314 access(all) fun resolveMarket(marketId: UInt64, outcome: MarketOutcome) {
315 pre {
316 FlowWager.markets[marketId] != nil: "Market does not exist"
317 FlowWager.markets[marketId]!.resolved == false: "Market already resolved"
318 }
319
320 if let currentMarket = FlowWager.markets[marketId] {
321 // Create new market instance with updated values
322 let resolvedMarket = Market(
323 id: currentMarket.id,
324 title: currentMarket.title,
325 description: currentMarket.description,
326 category: currentMarket.category,
327 optionA: currentMarket.optionA,
328 optionB: currentMarket.optionB,
329 creator: currentMarket.creator,
330 endTime: currentMarket.endTime,
331 minBet: currentMarket.minBet,
332 maxBet: currentMarket.maxBet,
333 status: MarketStatus.Resolved,
334 outcome: outcome,
335 resolved: true,
336 totalOptionAShares: currentMarket.totalOptionAShares,
337 totalOptionBShares: currentMarket.totalOptionBShares,
338 totalPool: currentMarket.totalPool
339 )
340
341 FlowWager.markets[marketId] = resolvedMarket
342 emit MarketResolved(marketId: marketId, outcome: outcome.rawValue)
343 }
344 }
345
346 access(all) fun updatePlatformFee(newFeePercentage: UFix64) {
347 pre {
348 newFeePercentage >= 0.0 && newFeePercentage <= 10.0: "Fee must be between 0% and 10%"
349 }
350 FlowWager.platformFeePercentage = newFeePercentage
351 }
352 }
353
354 // =====================================
355 // PUBLIC FUNCTIONS
356 // =====================================
357
358 access(all) fun createUserAccount(username: String, displayName: String) {
359 pre {
360 username.length > 0: "Username cannot be empty"
361 displayName.length > 0: "Display name cannot be empty"
362 }
363
364 let userProfile = UserProfile(
365 address: self.account.address,
366 username: username,
367 displayName: displayName,
368 bio: "",
369 profileImageUrl: ""
370 )
371
372 let userStatsData = UserStats(
373 totalMarketsParticipated: 0,
374 totalWinnings: 0.0,
375 totalLosses: 0.0,
376 winStreak: 0,
377 currentStreak: 0,
378 longestWinStreak: 0,
379 roi: 0.0,
380 averageBetSize: 0.0
381 )
382
383 FlowWager.userProfiles[self.account.address] = userProfile
384 FlowWager.userStats[self.account.address] = userStatsData
385
386 emit UserRegistered(address: self.account.address, username: username)
387 }
388
389 access(all) fun buyShares(
390 marketId: UInt64,
391 option: UInt8,
392 amount: UFix64
393 ) {
394 pre {
395 option == 0 || option == 1: "Option must be 0 (Option A) or 1 (Option B)"
396 FlowWager.markets[marketId] != nil: "Market does not exist"
397 FlowWager.markets[marketId]!.status == MarketStatus.Active: "Market is not active"
398 FlowWager.markets[marketId]!.endTime > getCurrentBlock().timestamp: "Market has ended"
399 amount >= FlowWager.markets[marketId]!.minBet: "Amount below minimum bet"
400 amount <= FlowWager.markets[marketId]!.maxBet: "Amount above maximum bet"
401 }
402
403 let betAmount = amount
404 let platformFee = betAmount * FlowWager.platformFeePercentage / 100.0
405 let netAmount = betAmount - platformFee
406 let shares = netAmount
407
408 // Update platform totals
409 FlowWager.totalPlatformFees = FlowWager.totalPlatformFees + platformFee
410 FlowWager.totalVolumeTraded = FlowWager.totalVolumeTraded + betAmount
411
412 // Update market by creating new instance
413 if let currentMarket = FlowWager.markets[marketId] {
414 let updatedMarket = Market(
415 id: currentMarket.id,
416 title: currentMarket.title,
417 description: currentMarket.description,
418 category: currentMarket.category,
419 optionA: currentMarket.optionA,
420 optionB: currentMarket.optionB,
421 creator: currentMarket.creator,
422 endTime: currentMarket.endTime,
423 minBet: currentMarket.minBet,
424 maxBet: currentMarket.maxBet,
425 status: currentMarket.status,
426 outcome: currentMarket.outcome,
427 resolved: currentMarket.resolved,
428 totalOptionAShares: option == 0 ? currentMarket.totalOptionAShares + shares : currentMarket.totalOptionAShares,
429 totalOptionBShares: option == 1 ? currentMarket.totalOptionBShares + shares : currentMarket.totalOptionBShares,
430 totalPool: currentMarket.totalPool + netAmount
431 )
432
433 FlowWager.markets[marketId] = updatedMarket
434 }
435
436 // Track individual user position
437 let userAddress = self.account.address
438 let newPosition = UserPosition(
439 marketId: marketId,
440 optionAShares: option == 0 ? shares : 0.0,
441 optionBShares: option == 1 ? shares : 0.0,
442 totalInvested: betAmount,
443 claimed: false
444 )
445
446 // Store user position in user's account storage
447 let userPositionsRef = self.account.storage.borrow<&UserPositions>(from: FlowWager.UserPositionsStoragePath)
448 if userPositionsRef != nil {
449 userPositionsRef!.addPosition(newPosition)
450 } else {
451 // Create new UserPositions resource if it doesn't exist
452 let userPositions <- create UserPositions()
453 userPositions.addPosition(newPosition)
454 self.account.storage.save(<-userPositions, to: FlowWager.UserPositionsStoragePath)
455
456 // Link capability
457 self.account.capabilities.publish(
458 self.account.capabilities.storage.issue<&UserPositions>(FlowWager.UserPositionsStoragePath),
459 at: FlowWager.UserPositionsPublicPath
460 )
461 }
462
463 emit SharesPurchased(
464 marketId: marketId,
465 buyer: self.account.address,
466 option: option,
467 shares: shares,
468 amount: betAmount
469 )
470 }
471
472 // =====================================
473 // READ FUNCTIONS
474 // =====================================
475
476 access(all) fun getMarket(marketId: UInt64): Market? {
477 return FlowWager.markets[marketId]
478 }
479
480 access(all) fun getAllMarkets(): [Market] {
481 return FlowWager.markets.values
482 }
483
484 access(all) fun getActiveMarkets(): [Market] {
485 let activeMarkets: [Market] = []
486 for market in FlowWager.markets.values {
487 if market.status == MarketStatus.Active {
488 activeMarkets.append(market)
489 }
490 }
491 return activeMarkets
492 }
493
494 access(all) fun getMarketsByCategory(category: MarketCategory): [Market] {
495 let filteredMarkets: [Market] = []
496 for market in FlowWager.markets.values {
497 if market.category == category {
498 filteredMarkets.append(market)
499 }
500 }
501 return filteredMarkets
502 }
503
504 access(all) fun getUserProfile(address: Address): UserProfile? {
505 return FlowWager.userProfiles[address]
506 }
507
508 access(all) fun getUserStats(address: Address): UserStats? {
509 return FlowWager.userStats[address]
510 }
511
512 access(all) fun getPlatformStats(): PlatformStats {
513 var activeMarkets: UInt64 = 0
514 for market in FlowWager.markets.values {
515 if market.status == MarketStatus.Active {
516 activeMarkets = activeMarkets + 1
517 }
518 }
519
520 return PlatformStats(
521 totalMarkets: UInt64(FlowWager.markets.length),
522 activeMarkets: activeMarkets,
523 totalUsers: UInt64(FlowWager.userProfiles.length),
524 totalVolume: FlowWager.totalVolumeTraded,
525 totalFees: FlowWager.totalPlatformFees
526 )
527 }
528
529 // =====================================
530 // CONTRACT INITIALIZATION
531 // =====================================
532
533 init() {
534 self.nextMarketId = 1
535 self.platformFeePercentage = 3.0
536 self.totalPlatformFees = 0.0
537 self.totalVolumeTraded = 0.0
538
539 self.markets = {}
540 self.userProfiles = {}
541 self.userStats = {}
542
543 self.UserPositionsStoragePath = /storage/FlowWagerUserPositions
544 self.UserPositionsPublicPath = /public/FlowWagerUserPositions
545 self.UserStatsStoragePath = /storage/FlowWagerUserStats
546 self.UserStatsPublicPath = /public/FlowWagerUserStats
547
548 let admin <- create Admin()
549 self.account.storage.save(<-admin, to: /storage/FlowWagerAdmin)
550
551 emit ContractInitialized()
552 }
553}