Smart Contract
UserRegistry
A.6c1b12e35dca8863.UserRegistry
1// cadence/contracts/UserRegistry.cdc
2// Registry contract for managing user profiles and social features - Fixed for Cadence 1.0
3
4import FlowWager from 0x6c1b12e35dca8863
5
6access(all) contract UserRegistry {
7
8 // ========================================
9 // EVENTS
10 // ========================================
11
12 access(all) event UserRegistered(address: Address, username: String?)
13 access(all) event UsernameUpdated(address: Address, oldUsername: String?, newUsername: String)
14 access(all) event UserFollowed(follower: Address, following: Address)
15 access(all) event UserUnfollowed(follower: Address, following: Address)
16 access(all) event AchievementUnlocked(address: Address, achievementId: String, achievementName: String)
17 access(all) event UserProfileUpdated(address: Address, field: String)
18 access(all) event UserRegistryInitialized()
19
20 // ========================================
21 // STRUCTS
22 // ========================================
23
24 access(all) struct UserProfile {
25 access(all) let address: Address
26 access(all) var username: String?
27 access(all) var displayName: String?
28 access(all) var bio: String?
29 access(all) var avatarURL: String?
30 access(all) var website: String?
31 access(all) var twitterHandle: String?
32 access(all) var discordHandle: String?
33 access(all) let registrationDate: UFix64
34 access(all) var lastActiveDate: UFix64
35 access(all) var isVerified: Bool
36 access(all) var privacySettings: PrivacySettings
37 access(all) var notificationSettings: NotificationSettings
38 access(all) var achievements: [String] // Achievement IDs
39 access(all) var customMetadata: {String: String}
40
41 init(address: Address) {
42 self.address = address
43 self.username = nil
44 self.displayName = nil
45 self.bio = nil
46 self.avatarURL = nil
47 self.website = nil
48 self.twitterHandle = nil
49 self.discordHandle = nil
50 self.registrationDate = getCurrentBlock().timestamp
51 self.lastActiveDate = getCurrentBlock().timestamp
52 self.isVerified = false
53 self.privacySettings = PrivacySettings()
54 self.notificationSettings = NotificationSettings()
55 self.achievements = []
56 self.customMetadata = {}
57 }
58
59 // Setter methods for controlled mutation
60 access(contract) fun setUsername(_ newUsername: String?) {
61 self.username = newUsername
62 }
63
64 access(contract) fun setDisplayName(_ newDisplayName: String?) {
65 self.displayName = newDisplayName
66 }
67
68 access(contract) fun setBio(_ newBio: String?) {
69 self.bio = newBio
70 }
71
72 access(contract) fun setAvatarURL(_ newAvatarURL: String?) {
73 self.avatarURL = newAvatarURL
74 }
75
76 access(contract) fun setWebsite(_ newWebsite: String?) {
77 self.website = newWebsite
78 }
79
80 access(contract) fun setTwitterHandle(_ newTwitterHandle: String?) {
81 self.twitterHandle = newTwitterHandle
82 }
83
84 access(contract) fun setDiscordHandle(_ newDiscordHandle: String?) {
85 self.discordHandle = newDiscordHandle
86 }
87
88 access(contract) fun setVerified(_ verified: Bool) {
89 self.isVerified = verified
90 }
91
92 access(contract) fun setPrivacySettings(_ settings: PrivacySettings) {
93 self.privacySettings = settings
94 }
95
96 access(contract) fun setNotificationSettings(_ settings: NotificationSettings) {
97 self.notificationSettings = settings
98 }
99
100 access(contract) fun updateLastActive() {
101 self.lastActiveDate = getCurrentBlock().timestamp
102 }
103
104 access(contract) fun addAchievement(_ achievementId: String) {
105 if !self.achievements.contains(achievementId) {
106 self.achievements.append(achievementId)
107 }
108 }
109
110 access(contract) fun setCustomMetadata(_ key: String, _ value: String) {
111 self.customMetadata[key] = value
112 }
113
114 // Bulk update method
115 access(contract) fun updateProfile(
116 displayName: String?,
117 bio: String?,
118 avatarURL: String?,
119 website: String?,
120 twitterHandle: String?,
121 discordHandle: String?
122 ) {
123 self.displayName = displayName
124 self.bio = bio
125 self.avatarURL = avatarURL
126 self.website = website
127 self.twitterHandle = twitterHandle
128 self.discordHandle = discordHandle
129 self.updateLastActive()
130 }
131 }
132
133 access(all) struct PrivacySettings {
134 access(all) var profilePublic: Bool
135 access(all) var statsPublic: Bool
136 access(all) var positionsPublic: Bool
137 access(all) var allowFollowers: Bool
138 access(all) var allowDirectMessages: Bool
139 access(all) var showOnLeaderboard: Bool
140
141 init() {
142 self.profilePublic = true
143 self.statsPublic = true
144 self.positionsPublic = false
145 self.allowFollowers = true
146 self.allowDirectMessages = true
147 self.showOnLeaderboard = true
148 }
149 }
150
151 access(all) struct NotificationSettings {
152 access(all) var emailNotifications: Bool
153 access(all) var pushNotifications: Bool
154 access(all) var marketResolutionNotifications: Bool
155 access(all) var winningsNotifications: Bool
156 access(all) var followerNotifications: Bool
157 access(all) var newMarketNotifications: Bool
158 access(all) var breakingNewsNotifications: Bool
159 access(all) var weeklyDigest: Bool
160
161 init() {
162 self.emailNotifications = true
163 self.pushNotifications = true
164 self.marketResolutionNotifications = true
165 self.winningsNotifications = true
166 self.followerNotifications = true
167 self.newMarketNotifications = false
168 self.breakingNewsNotifications = true
169 self.weeklyDigest = true
170 }
171 }
172
173 access(all) struct FollowRelationship {
174 access(all) let follower: Address
175 access(all) let following: Address
176 access(all) let timestamp: UFix64
177
178 init(follower: Address, following: Address) {
179 self.follower = follower
180 self.following = following
181 self.timestamp = getCurrentBlock().timestamp
182 }
183 }
184
185 access(all) struct UserAchievement {
186 access(all) let id: String
187 access(all) let name: String
188 access(all) let description: String
189 access(all) let category: String
190 access(all) let rarity: String // "common", "rare", "epic", "legendary"
191 access(all) let iconURL: String
192 access(all) let points: UInt64
193 access(all) let requirements: {String: AnyStruct}
194 access(all) let isActive: Bool
195
196 init(
197 id: String,
198 name: String,
199 description: String,
200 category: String,
201 rarity: String,
202 iconURL: String,
203 points: UInt64,
204 requirements: {String: AnyStruct}
205 ) {
206 self.id = id
207 self.name = name
208 self.description = description
209 self.category = category
210 self.rarity = rarity
211 self.iconURL = iconURL
212 self.points = points
213 self.requirements = requirements
214 self.isActive = true
215 }
216 }
217
218 access(all) struct UserSocialStats {
219 access(all) let address: Address
220 access(all) let followerCount: UInt64
221 access(all) let followingCount: UInt64
222 access(all) let achievementCount: UInt64
223 access(all) let achievementPoints: UInt64
224 access(all) let registrationDate: UFix64
225 access(all) let lastActiveDate: UFix64
226 access(all) let daysSinceRegistration: UInt64
227 access(all) let isVerified: Bool
228
229 init(
230 address: Address,
231 followerCount: UInt64,
232 followingCount: UInt64,
233 achievementCount: UInt64,
234 achievementPoints: UInt64,
235 registrationDate: UFix64,
236 lastActiveDate: UFix64,
237 isVerified: Bool
238 ) {
239 self.address = address
240 self.followerCount = followerCount
241 self.followingCount = followingCount
242 self.achievementCount = achievementCount
243 self.achievementPoints = achievementPoints
244 self.registrationDate = registrationDate
245 self.lastActiveDate = lastActiveDate
246 self.daysSinceRegistration = UInt64((getCurrentBlock().timestamp - registrationDate) / 86400.0)
247 self.isVerified = isVerified
248 }
249 }
250
251 // ========================================
252 // RESOURCES
253 // ========================================
254
255 access(all) resource Admin {
256 access(all) fun createAchievement(
257 id: String,
258 name: String,
259 description: String,
260 category: String,
261 rarity: String,
262 iconURL: String,
263 points: UInt64,
264 requirements: {String: AnyStruct}
265 ) {
266 pre {
267 !UserRegistry.achievements.containsKey(id): "Achievement already exists"
268 name.length > 0: "Achievement name cannot be empty"
269 description.length > 0: "Achievement description cannot be empty"
270 }
271
272 let achievement = UserAchievement(
273 id: id,
274 name: name,
275 description: description,
276 category: category,
277 rarity: rarity,
278 iconURL: iconURL,
279 points: points,
280 requirements: requirements
281 )
282
283 UserRegistry.achievements[id] = achievement
284 }
285
286 access(all) fun verifyUser(address: Address) {
287 pre {
288 UserRegistry.userProfiles.containsKey(address): "User profile does not exist"
289 }
290
291 if let profile = UserRegistry.userProfiles[address] {
292 profile.setVerified(true)
293 UserRegistry.userProfiles[address] = profile
294 emit UserProfileUpdated(address: address, field: "verified")
295 }
296 }
297
298 access(all) fun unverifyUser(address: Address) {
299 pre {
300 UserRegistry.userProfiles.containsKey(address): "User profile does not exist"
301 }
302
303 if let profile = UserRegistry.userProfiles[address] {
304 profile.setVerified(false)
305 UserRegistry.userProfiles[address] = profile
306 emit UserProfileUpdated(address: address, field: "verified")
307 }
308 }
309
310 access(all) fun grantAchievement(address: Address, achievementId: String) {
311 pre {
312 UserRegistry.userProfiles.containsKey(address): "User profile does not exist"
313 UserRegistry.achievements.containsKey(achievementId): "Achievement does not exist"
314 }
315
316 if let profile = UserRegistry.userProfiles[address] {
317 let achievement = UserRegistry.achievements[achievementId]!
318
319 if !profile.achievements.contains(achievementId) {
320 profile.addAchievement(achievementId)
321 UserRegistry.userProfiles[address] = profile
322
323 emit AchievementUnlocked(
324 address: address,
325 achievementId: achievementId,
326 achievementName: achievement.name
327 )
328 }
329 }
330 }
331
332 access(all) fun bulkGrantAchievements(addresses: [Address], achievementId: String) {
333 for address in addresses {
334 self.grantAchievement(address: address, achievementId: achievementId)
335 }
336 }
337
338 access(all) fun getRegistryStats(): {String: AnyStruct} {
339 var verifiedUsers: UInt64 = 0
340 var usersWithUsernames: UInt64 = 0
341 let totalFollowRelationships = UInt64(UserRegistry.followRelationships.length)
342
343 for profile in UserRegistry.userProfiles.values {
344 if profile.isVerified {
345 verifiedUsers = verifiedUsers + 1
346 }
347 if profile.username != nil {
348 usersWithUsernames = usersWithUsernames + 1
349 }
350 }
351
352 return {
353 "totalUsers": UInt64(UserRegistry.userProfiles.length),
354 "verifiedUsers": verifiedUsers,
355 "usersWithUsernames": usersWithUsernames,
356 "totalFollowRelationships": totalFollowRelationships,
357 "totalAchievements": UInt64(UserRegistry.achievements.length)
358 }
359 }
360 }
361
362 // ========================================
363 // CONTRACT STATE
364 // ========================================
365
366 access(contract) var userProfiles: {Address: UserProfile}
367 access(contract) var usernameToAddress: {String: Address}
368 access(contract) var followRelationships: [FollowRelationship]
369 access(contract) var userFollowers: {Address: [Address]}
370 access(contract) var userFollowing: {Address: [Address]}
371 access(contract) var achievements: {String: UserAchievement}
372 access(all) var totalUsers: UInt64
373
374 // Storage Paths
375 access(all) let AdminStoragePath: StoragePath
376
377 // ========================================
378 // PUBLIC FUNCTIONS
379 // ========================================
380
381 access(all) fun registerUser(address: Address, username: String?): Bool {
382 if self.userProfiles.containsKey(address) {
383 return false // User already registered
384 }
385
386 // Check username availability if provided
387 if username != nil {
388 let usernameStr = username!
389 if self.isUsernameValid(username: usernameStr) && !self.usernameToAddress.containsKey(usernameStr) {
390 self.usernameToAddress[usernameStr] = address
391 } else {
392 panic("Username is invalid or already taken")
393 }
394 }
395
396 // Create user profile
397 let profile = UserProfile(address: address)
398 if username != nil {
399 profile.setUsername(username)
400 }
401
402 self.userProfiles[address] = profile
403 self.userFollowers[address] = []
404 self.userFollowing[address] = []
405 self.totalUsers = self.totalUsers + 1
406
407 emit UserRegistered(address: address, username: username)
408
409 // Check for first-time achievements
410 self.checkAndGrantAchievements(address: address)
411
412 return true
413 }
414
415 access(all) fun updateUserProfile(
416 address: Address,
417 displayName: String?,
418 bio: String?,
419 avatarURL: String?,
420 website: String?,
421 twitterHandle: String?,
422 discordHandle: String?
423 ) {
424 pre {
425 self.userProfiles.containsKey(address): "User profile does not exist"
426 }
427
428 if let profile = self.userProfiles[address] {
429 profile.updateProfile(
430 displayName: displayName,
431 bio: bio,
432 avatarURL: avatarURL,
433 website: website,
434 twitterHandle: twitterHandle,
435 discordHandle: discordHandle
436 )
437
438 self.userProfiles[address] = profile
439 emit UserProfileUpdated(address: address, field: "profile")
440 }
441 }
442
443 access(all) fun updateUsername(address: Address, newUsername: String) {
444 pre {
445 self.userProfiles.containsKey(address): "User profile does not exist"
446 self.isUsernameValid(username: newUsername): "Username is invalid"
447 !self.usernameToAddress.containsKey(newUsername): "Username is already taken"
448 }
449
450 if let profile = self.userProfiles[address] {
451 let oldUsername = profile.username
452
453 // Remove old username mapping
454 if oldUsername != nil {
455 let _ = self.usernameToAddress.remove(key: oldUsername!)
456 }
457
458 // Update username
459 profile.setUsername(newUsername)
460 profile.updateLastActive()
461
462 self.usernameToAddress[newUsername] = address
463 self.userProfiles[address] = profile
464
465 emit UsernameUpdated(address: address, oldUsername: oldUsername, newUsername: newUsername)
466 }
467 }
468
469 access(all) fun updateLastActive(address: Address) {
470 if let profile = self.userProfiles[address] {
471 profile.updateLastActive()
472 self.userProfiles[address] = profile
473 }
474 }
475
476 access(all) fun followUser(followerAddress: Address, followingAddress: Address) {
477 pre {
478 self.userProfiles.containsKey(followerAddress): "Follower profile does not exist"
479 self.userProfiles.containsKey(followingAddress): "Following profile does not exist"
480 followerAddress != followingAddress: "Cannot follow yourself"
481 }
482
483 // Check if already following
484 let currentFollowing = self.userFollowing[followerAddress] ?? []
485 if currentFollowing.contains(followingAddress) {
486 return // Already following
487 }
488
489 // Check privacy settings
490 let followingProfile = self.userProfiles[followingAddress]!
491 if !followingProfile.privacySettings.allowFollowers {
492 panic("User does not allow followers")
493 }
494
495 // Add to following/followers lists
496 if self.userFollowing[followerAddress] == nil {
497 self.userFollowing[followerAddress] = []
498 }
499 if self.userFollowers[followingAddress] == nil {
500 self.userFollowers[followingAddress] = []
501 }
502
503 self.userFollowing[followerAddress]!.append(followingAddress)
504 self.userFollowers[followingAddress]!.append(followerAddress)
505
506 // Create relationship record
507 let relationship = FollowRelationship(follower: followerAddress, following: followingAddress)
508 self.followRelationships.append(relationship)
509
510 // Update last active for follower
511 self.updateLastActive(address: followerAddress)
512
513 emit UserFollowed(follower: followerAddress, following: followingAddress)
514
515 // Check for social achievements
516 self.checkAndGrantAchievements(address: followingAddress)
517 }
518
519 access(all) fun unfollowUser(followerAddress: Address, followingAddress: Address) {
520 pre {
521 self.userProfiles.containsKey(followerAddress): "Follower profile does not exist"
522 self.userProfiles.containsKey(followingAddress): "Following profile does not exist"
523 }
524
525 // Remove from following/followers lists
526 if let followingList = self.userFollowing[followerAddress] {
527 var i = 0
528 while i < followingList.length {
529 if followingList[i] == followingAddress {
530 let _ = followingList.remove(at: i)
531 break
532 }
533 i = i + 1
534 }
535 self.userFollowing[followerAddress] = followingList
536 }
537
538 if let followersList = self.userFollowers[followingAddress] {
539 var i = 0
540 while i < followersList.length {
541 if followersList[i] == followerAddress {
542 let _ = followersList.remove(at: i)
543 break
544 }
545 i = i + 1
546 }
547 self.userFollowers[followingAddress] = followersList
548 }
549
550 // Update last active for follower
551 self.updateLastActive(address: followerAddress)
552
553 emit UserUnfollowed(follower: followerAddress, following: followingAddress)
554 }
555
556 // ========================================
557 // VIEW FUNCTIONS (Pure - No State Modification)
558 // ========================================
559
560 access(all) view fun getUserProfile(address: Address): UserProfile? {
561 return self.userProfiles[address]
562 }
563
564 access(all) view fun getUserByUsername(username: String): UserProfile? {
565 if let address = self.usernameToAddress[username] {
566 return self.userProfiles[address]
567 }
568 return nil
569 }
570
571 access(all) view fun isUsernameAvailable(username: String): Bool {
572 return self.isUsernameValid(username: username) && !self.usernameToAddress.containsKey(username)
573 }
574
575access(all) view fun getUserSocialStats(address: Address): {String: AnyStruct}? {
576 if let profile = self.userProfiles[address] {
577 return {
578 "address": address,
579 "followerCount": UInt64, // Default since field doesn't exist
580 "followingCount": UInt64, // Default since field doesn't exist
581 "achievementCount": UInt64(profile.achievements.length), // ✅ This works
582 "achievementPoints": UInt64(profile.achievements.length * 10), // Calculate: 10 points per achievement
583 "registrationDate": profile.registrationDate,
584 "lastActiveDate": profile.lastActiveDate,
585 "isVerified": profile.isVerified
586 }
587 }
588 return nil
589}
590
591 access(all) view fun getUserFollowers(address: Address): [Address] {
592 return self.userFollowers[address] ?? []
593 }
594
595 access(all) view fun getUserFollowing(address: Address): [Address] {
596 return self.userFollowing[address] ?? []
597 }
598
599 access(all) view fun isFollowing(follower: Address, following: Address): Bool {
600 let followingList = self.userFollowing[follower] ?? []
601 return followingList.contains(following)
602 }
603
604 access(all) view fun getAchievement(achievementId: String): UserAchievement? {
605 return self.achievements[achievementId]
606 }
607
608 access(all) view fun getAllAchievements(): [UserAchievement] {
609 return self.achievements.values
610 }
611
612 access(all) view fun getUserAchievements(address: Address): [UserAchievement] {
613 if let profile = self.userProfiles[address] {
614 // Fixed: Create array using functional approach to avoid impure operations
615 var userAchievements: [UserAchievement] = []
616 var i = 0
617 while i < profile.achievements.length {
618 let achievementId = profile.achievements[i]
619 if let achievement = self.achievements[achievementId] {
620 userAchievements = userAchievements.concat([achievement])
621 }
622 i = i + 1
623 }
624 return userAchievements
625 }
626 return []
627 }
628
629 access(all) view fun searchUsers(query: String, limit: UInt64): [UserProfile] {
630 // Fixed: Create results using functional approach to avoid impure operations
631 var results: [UserProfile] = []
632 var count: UInt64 = 0
633
634 for profile in self.userProfiles.values {
635 if count >= limit {
636 break
637 }
638
639 // Search in username and display name
640 let matchUsername = profile.username != nil && profile.username!.toLower().contains(query.toLower())
641 let matchDisplayName = profile.displayName != nil && profile.displayName!.toLower().contains(query.toLower())
642
643 if matchUsername || matchDisplayName {
644 if profile.privacySettings.profilePublic {
645 results = results.concat([profile])
646 count = count + 1
647 }
648 }
649 }
650
651 return results
652 }
653
654 // ========================================
655 // HELPER FUNCTIONS
656 // ========================================
657
658 access(contract) view fun isUsernameValid(username: String): Bool {
659 // Username validation rules
660 if username.length < 3 || username.length > 20 {
661 return false
662 }
663
664 // Check for valid characters (simplified - alphanumeric and underscores)
665 // In a real implementation, you'd use proper regex
666 return true
667 }
668
669 access(contract) fun checkAndGrantAchievements(address: Address) {
670 // Check social achievements
671 let followerCount = UInt64(self.userFollowers[address]?.length ?? 0)
672 if followerCount >= 10 {
673 if let profile = self.userProfiles[address] {
674 if !profile.achievements.contains("social_10_followers") {
675 profile.addAchievement("social_10_followers")
676 self.userProfiles[address] = profile
677
678 emit AchievementUnlocked(
679 address: address,
680 achievementId: "social_10_followers",
681 achievementName: "Influencer"
682 )
683 }
684 }
685 }
686 }
687
688 access(contract) fun createDefaultAchievements() {
689 // First Bet Achievement
690 self.achievements["first_bet"] = UserAchievement(
691 id: "first_bet",
692 name: "First Prediction",
693 description: "Made your first prediction on FlowWager",
694 category: "getting_started",
695 rarity: "common",
696 iconURL: "https://example.com/achievements/first_bet.png",
697 points: 10,
698 requirements: {"bets": 1}
699 )
700
701 // First Win Achievement
702 self.achievements["first_win"] = UserAchievement(
703 id: "first_win",
704 name: "Lucky Beginner",
705 description: "Won your first prediction",
706 category: "winning",
707 rarity: "common",
708 iconURL: "https://example.com/achievements/first_win.png",
709 points: 25,
710 requirements: {"wins": 1}
711 )
712
713 // Streak Achievements
714 self.achievements["streak_5"] = UserAchievement(
715 id: "streak_5",
716 name: "Hot Streak",
717 description: "Achieved a 5-win streak",
718 category: "streaks",
719 rarity: "rare",
720 iconURL: "https://example.com/achievements/streak_5.png",
721 points: 100,
722 requirements: {"streak": 5}
723 )
724
725 // Volume Achievements
726 self.achievements["volume_1000"] = UserAchievement(
727 id: "volume_1000",
728 name: "High Roller",
729 description: "Wagered over 1,000 FLOW tokens",
730 category: "volume",
731 rarity: "epic",
732 iconURL: "https://example.com/achievements/volume_1000.png",
733 points: 250,
734 requirements: {"totalInvested": 1000.0}
735 )
736
737 // Social Achievements
738 self.achievements["social_10_followers"] = UserAchievement(
739 id: "social_10_followers",
740 name: "Influencer",
741 description: "Gained 10 followers",
742 category: "social",
743 rarity: "rare",
744 iconURL: "https://example.com/achievements/influencer.png",
745 points: 150,
746 requirements: {"followers": 10}
747 )
748 }
749
750 // ========================================
751 // INITIALIZATION
752 // ========================================
753
754 init() {
755 self.userProfiles = {}
756 self.usernameToAddress = {}
757 self.followRelationships = []
758 self.userFollowers = {}
759 self.userFollowing = {}
760 self.achievements = {}
761 self.totalUsers = 0
762
763 self.AdminStoragePath = /storage/UserRegistryAdmin
764
765 // Create and store Admin resource
766 let admin <- create Admin()
767 self.account.storage.save(<-admin, to: self.AdminStoragePath)
768
769 // Create default achievements
770 self.createDefaultAchievements()
771
772 emit UserRegistryInitialized()
773 }
774}