Smart Contract

Profile

A.097bafa4e0b48eef.Profile

Valid From

117,549,722

Deployed

3d ago
Feb 24, 2026, 06:34:26 AM UTC

Dependents

31 imports
1/*
2* Inspiration: https://flow-view-source.com/testnet/account/0xba1132bc08f82fe2/contract/Ghost
3*/
4
5import FungibleToken from 0xf233dcee88fe0abe
6import ProfileCache from 0x097bafa4e0b48eef
7import FindUtils from 0x097bafa4e0b48eef
8
9access(all) contract Profile {
10
11    // entitlements
12    access(all) entitlement Admin
13
14    // paths
15    access(all) let publicPath: PublicPath
16    access(all) let publicReceiverPath: PublicPath
17    access(all) let storagePath: StoragePath
18
19    // an event emitted when somebody follows another user
20    access(all) event Follow(follower:Address, following: Address, tags: [String])
21
22    // an event emitted when somebody unfollows somebody
23    access(all) event Unfollow(follower:Address, unfollowing: Address)
24
25    // an event emitted when a user verifies something
26    access(all) event Verification(account:Address, message:String)
27
28    // an event emitted when a user creates a profile
29    access(all) event Created(account:Address, userName:String, findName:String, createdAt:String)
30
31    // an event emitted when a user updates their profile
32    access(all) event Updated(account:Address, userName:String, findName:String, thumbnail:String)
33
34    /*
35    Represents a Fungible token wallet with a name and a supported type.
36    */
37    access(all) struct Wallet {
38        access(all) let name: String
39        access(all) let receiver: Capability<&{FungibleToken.Receiver}>
40        access(all) let balance: Capability<&{FungibleToken.Balance}>
41        access(all) let accept: Type
42        access(all) let tags: [String]
43
44        init(
45            name: String,
46            receiver: Capability<&{FungibleToken.Receiver}>,
47            balance: Capability<&{FungibleToken.Vault}>,
48            accept: Type,
49            tags: [String]
50        ) {
51            self.name=name
52            self.receiver=receiver
53            self.balance=balance
54            self.accept=accept
55            self.tags=tags
56        }
57    }
58
59    /*
60
61    Represent a collection of a Resource that you want to expose
62    Since NFT standard is not so great at just add Type and you have to use instanceOf to check for now
63    */
64    access(all) struct ResourceCollection {
65        access(all) let collection: Capability
66        access(all) let tags: [String]
67        access(all) let type: Type
68        access(all) let name: String
69
70        init(name: String, collection:Capability, type: Type, tags: [String]) {
71            self.name=name
72            self.collection=collection
73            self.tags=tags
74            self.type=type
75        }
76    }
77
78
79    access(all) struct CollectionProfile{
80        access(all) let tags: [String]
81        access(all) let type: String
82        access(all) let name: String
83
84        init(_ collection: ResourceCollection){
85            self.name=collection.name
86            self.type=collection.type.identifier
87            self.tags=collection.tags
88        }
89    }
90
91    /*
92    A link that you could add to your profile
93    */
94    access(all) struct Link {
95        access(all) let url: String
96        access(all) let title: String
97        access(all) let type: String
98
99        init(title: String, type: String, url: String) {
100            self.url=url
101            self.title=title
102            self.type=type
103        }
104    }
105
106    /*
107    Information about a connection between one profile and another.
108    */
109    access(all) struct FriendStatus {
110        access(all) let follower: Address
111        access(all) let following:Address
112        access(all) let tags: [String]
113
114        init(follower: Address, following:Address, tags: [String]) {
115            self.follower=follower
116            self.following=following
117            self.tags= tags
118        }
119    }
120
121    access(all) struct WalletProfile {
122        access(all) let name: String
123        access(all) let balance: UFix64
124        access(all) let accept:  String
125        access(all) let tags: [String]
126
127        init(_ wallet: Wallet) {
128            self.name=wallet.name
129            self.balance=wallet.balance.borrow()?.balance ?? 0.0
130            self.accept=wallet.accept.identifier
131            self.tags=wallet.tags
132        }
133    }
134
135    //This is the new return struct from the profile
136    access(all) struct UserReport {
137        access(all) let findName: String
138        access(all) let createdAt: String
139        access(all) let address: Address
140        access(all) let name: String
141        access(all) let gender: String
142        access(all) let description: String
143        access(all) let tags: [String]
144        access(all) let avatar: String
145        access(all) let links: {String:Link}
146        access(all) let wallets: [WalletProfile]
147        access(all) let following: [FriendStatus]
148        access(all) let followers: [FriendStatus]
149        access(all) let allowStoringFollowers: Bool
150
151        init(
152            findName:String,
153            address: Address,
154            name: String,
155            gender: String,
156            description: String,
157            tags: [String],
158            avatar: String,
159            links: {String:Link},
160            wallets: [WalletProfile],
161            following: [FriendStatus],
162            followers: [FriendStatus],
163            allowStoringFollowers:Bool,
164            createdAt: String
165        ) {
166            self.findName=findName
167            self.address=address
168            self.name=name
169            self.gender=gender
170            self.description=description
171            self.tags=tags
172            self.avatar=avatar
173            self.links=links
174            self.wallets=wallets
175            self.following=following
176            self.followers=followers
177            self.allowStoringFollowers=allowStoringFollowers
178            self.createdAt=createdAt
179        }
180    }
181
182    //This format is deperated
183    access(all) struct UserProfile {
184        access(all) let findName: String
185        access(all) let createdAt: String
186        access(all) let address: Address
187        access(all) let name: String
188        access(all) let gender: String
189        access(all) let description: String
190        access(all) let tags: [String]
191        access(all) let avatar: String
192        access(all) let links: [Link]
193        access(all) let wallets: [WalletProfile]
194        access(all) let collections: [CollectionProfile]
195        access(all) let following: [FriendStatus]
196        access(all) let followers: [FriendStatus]
197        access(all) let allowStoringFollowers: Bool
198
199        init(
200            findName:String,
201            address: Address,
202            name: String,
203            gender: String,
204            description: String,
205            tags: [String],
206            avatar: String,
207            links: [Link],
208            wallets: [WalletProfile],
209            collections: [CollectionProfile],
210            following: [FriendStatus],
211            followers: [FriendStatus],
212            allowStoringFollowers:Bool,
213            createdAt: String
214        ) {
215            self.findName=findName
216            self.address=address
217            self.name=name
218            self.gender=gender
219            self.description=description
220            self.tags=tags
221            self.avatar=avatar
222            self.links=links
223            self.collections=collections
224            self.wallets=wallets
225            self.following=following
226            self.followers=followers
227            self.allowStoringFollowers=allowStoringFollowers
228            self.createdAt=createdAt
229        }
230    }
231
232    access(all) resource interface Public{
233        access(all) fun getAddress() : Address
234        access(all) fun getName(): String
235        access(all) fun getFindName(): String
236        access(all) fun getCreatedAt(): String
237        access(all) fun getGender(): String
238        access(all) fun getDescription(): String
239        access(all) fun getTags(): [String]
240        access(all) fun getAvatar(): String
241        access(all) fun getCollections(): [ResourceCollection]
242        access(all) fun follows(_ address: Address) : Bool
243        access(all) fun getFollowers(): [FriendStatus]
244        access(all) fun getFollowing(): [FriendStatus]
245        access(all) fun getWallets() : [Wallet]
246        access(all) fun hasWallet(_ name: String) : Bool
247        access(all) fun getLinks() : [Link]
248        access(all) fun deposit(from: @{FungibleToken.Vault})
249        access(all) fun supportedFungigleTokenTypes() : [Type]
250        access(all) fun asProfile() : UserProfile
251        access(all) fun asReport() : UserReport
252        access(all) fun isBanned(_ val: Address): Bool
253        access(all) fun isPrivateModeEnabled() : Bool
254        access(contract) fun internal_addFollower(_ val: FriendStatus)
255        access(contract) fun internal_removeFollower(_ address: Address)
256        access(account) fun setFindName(_ val: String)
257    }
258
259    access(all) resource interface Owner {
260        access(Admin) fun setName(_ val: String) {
261            pre {
262                val.length <= 64: "Name must be 64 or less characters"
263            }
264        }
265
266        access(Admin) fun setGender(_ val:String){
267            pre {
268                val.length <= 64: "Gender must be 64 or less characters"
269            }
270        }
271
272        access(Admin) fun setAvatar(_ val: String){
273            pre {
274                val.length <= 1024: "Avatar must be 1024 characters or less"
275            }
276        }
277
278        access(Admin) fun setTags(_ val: [String])  {
279            pre {
280                Profile.verifyTags(tags: val, tagLength:64, tagSize:32) : "cannot have more then 32 tags of length 64"
281            }
282        }
283
284        //validate length of description to be 255 or something?
285        access(Admin) fun setDescription(_ val: String) {
286            pre {
287                val.length <= 1024: "Description must be 1024 characters or less"
288            }
289        }
290
291        access(Admin) fun follow(_ address: Address, tags:[String]) {
292            pre {
293                Profile.verifyTags(tags: tags, tagLength:64, tagSize:32) : "cannot have more then 32 tags of length 64"
294            }
295        }
296        access(Admin) fun unfollow(_ address: Address)
297
298        access(Admin) fun removeCollection(_ val: String)
299        access(Admin) fun addCollection(_ val: ResourceCollection)
300
301        access(Admin) fun addWallet(_ val : Wallet)
302        access(Admin) fun removeWallet(_ val: String)
303        access(Admin) fun setWallets(_ val: [Wallet])
304        access(all) fun hasWallet(_ name: String) : Bool
305        access(Admin) fun addLink(_ val: Link)
306        access(Admin) fun addLinkWithName(name:String, link:Link)
307
308        access(Admin) fun removeLink(_ val: String)
309
310        //Verify that this user has signed something.
311        access(Admin) fun verify(_ val:String)
312
313        //A user must be able to remove a follower since this data in your account is added there by another user
314        access(Admin) fun removeFollower(_ val: Address)
315
316        //manage bans
317        access(Admin) fun addBan(_ val: Address)
318        access(Admin) fun removeBan(_ val: Address)
319        access(Admin) fun getBans(): [Address]
320
321        //Set if user is allowed to store followers or now
322        access(Admin) fun setAllowStoringFollowers(_ val: Bool)
323
324        //set if this user prefers sensitive information about his account to be kept private, no guarantee here but should be honored
325        access(Admin) fun setPrivateMode(_ val: Bool)
326    }
327
328    access(all) resource User: Owner, Public, FungibleToken.Receiver {
329        access(self) var name: String
330        access(self) var findName: String
331        access(self) var createdAt: String
332        access(self) var gender: String
333        access(self) var description: String
334        access(self) var avatar: String
335        access(self) var tags: [String]
336        access(self) var followers: {Address: FriendStatus}
337        access(self) var bans: {Address: Bool}
338        access(self) var following: {Address: FriendStatus}
339        access(self) var collections: {String: ResourceCollection}
340        access(self) var wallets: [Wallet]
341        access(self) var links: {String: Link}
342        access(self) var allowStoringFollowers: Bool
343
344        //this is just a bag of properties if we need more fields here, so that we can do it with contract upgrade
345        access(self) var additionalProperties: {String : String}
346
347        init(name:String, createdAt: String) {
348            let randomNumber = (1 as UInt64) + revertibleRandom(modulo: 25 as UInt64)
349            self.createdAt=createdAt
350            self.name = name
351            self.findName=""
352            self.gender=""
353            self.description=""
354            self.tags=[]
355            self.avatar = "https://find.xyz/assets/img/avatars/avatar".concat(randomNumber.toString()).concat(".png")
356            self.followers = {}
357            self.following = {}
358            self.collections={}
359            self.wallets=[]
360            self.links={}
361            self.allowStoringFollowers=true
362            self.bans={}
363            self.additionalProperties={}
364
365        }
366
367        /// We do not have a seperate field for this so we use the additionalProperties 'bag' to store this in
368        access(Admin) fun setPrivateMode(_ val: Bool) {
369            var private="true"
370            if !val{
371                private="false"
372            }
373            self.additionalProperties["privateMode"]  = private
374        }
375
376        access(Admin) fun emitUpdatedEvent() {
377            emit Updated(account:self.owner!.address, userName:self.name, findName:self.findName, thumbnail:self.avatar)
378        }
379
380        access(Admin) fun emitCreatedEvent() {
381            emit Created(account:self.owner!.address, userName:self.name, findName:self.findName, createdAt:self.createdAt)
382        }
383
384        access(all) fun isPrivateModeEnabled() : Bool {
385            let boolString= self.additionalProperties["privateMode"]
386            if boolString== nil || boolString=="false" {
387                return false
388            }
389            return true
390        }
391
392        access(Admin) fun addBan(_ val: Address) { 
393            self.bans[val]= true
394        }
395
396        access(Admin) fun removeBan(_ val:Address) { 
397            self.bans.remove(key: val) 
398        }
399
400        access(Admin) fun getBans() : [Address] { 
401            return self.bans.keys 
402        }
403
404        access(all) fun isBanned(_ val:Address) : Bool { 
405            return self.bans.containsKey(val)
406        }
407
408        access(Admin) fun setAllowStoringFollowers(_ val: Bool) {
409            self.allowStoringFollowers=val
410        }
411
412        access(Admin) fun verify(_ val:String) {
413            emit Verification(account: self.owner!.address, message:val)
414        }
415
416        access(all) fun asReport() : UserReport {
417            let wallets: [WalletProfile]=[]
418            for w in self.wallets {
419                wallets.append(WalletProfile(w))
420            }
421
422            return UserReport(
423                findName: self.getFindName(),
424                address: self.owner!.address,
425                name: self.getName(),
426                gender: self.getGender(),
427                description: self.getDescription(),
428                tags: self.getTags(),
429                avatar: self.getAvatar(),
430                links: self.getLinksMap(),
431                wallets: wallets,
432                following: self.getFollowing(),
433                followers: self.getFollowers(),
434                allowStoringFollowers: self.allowStoringFollowers,
435                createdAt:self.getCreatedAt()
436            )
437        }
438
439        access(all) fun getAddress() : Address {
440            return self.owner!.address
441        }
442
443        access(all) fun asProfile() : UserProfile {
444            let wallets: [WalletProfile]=[]
445            for w in self.wallets {
446                wallets.append(WalletProfile(w))
447            }
448
449            let collections:[CollectionProfile]=[]
450            for c in self.getCollections() {
451                collections.append(CollectionProfile(c))
452            }
453
454            return UserProfile(
455                findName: self.getFindName(),
456                address: self.owner!.address,
457                name: self.getName(),
458                gender: self.getGender(),
459                description: self.getDescription(),
460                tags: self.getTags(),
461                avatar: self.getAvatar(),
462                links: self.getLinks(),
463                wallets: wallets,
464                collections: collections,
465                following: self.getFollowing(),
466                followers: self.getFollowers(),
467                allowStoringFollowers: self.allowStoringFollowers,
468                createdAt:self.getCreatedAt()
469            )
470        }
471
472        access(all) fun getLinksMap() : {String: Link} {
473            return self.links
474        }
475
476        access(all) fun getLinks() : [Link] {
477            return self.links.values
478        }
479
480        access(Admin) fun addLinkWithName(name:String, link:Link) {
481            self.links[name]=link
482        }
483
484        access(Admin) fun addLink(_ val: Link) {
485            self.links[val.title]=val
486        }
487
488        access(Admin) fun removeLink(_ val: String) {
489            self.links.remove(key: val)
490        }
491
492        access(all) fun supportedFungigleTokenTypes() : [Type] {
493            let types: [Type] =[]
494            for w in self.wallets {
495                if !types.contains(w.accept) {
496                    types.append(w.accept)
497                }
498            }
499            return types
500        }
501
502        access(all) fun deposit(from: @{FungibleToken.Vault}) {
503
504            let walletIndexCache = ProfileCache.getWalletIndex(address: self.owner!.address, walletType: from.getType())
505
506            if walletIndexCache != nil {
507                let ref = self.wallets[walletIndexCache!].receiver.borrow() ?? panic("This vault is not set up. ".concat(from.getType().identifier).concat(self.owner!.address.toString()).concat("  .  ").concat(from.balance.toString()))
508                ref.deposit(from: <- from)
509                return
510            }
511
512            for i , w in self.wallets {
513                if from.isInstance(w.accept) {
514                    ProfileCache.setWalletIndexCache(address: self.owner!.address, walletType: from.getType(), index: i)
515                    let ref = w.receiver.borrow() ?? panic("This vault is not set up. ".concat(from.getType().identifier).concat(self.owner!.address.toString()).concat("  .  ").concat(from.balance.toString()))
516                    ref.deposit(from: <- from)
517                    return
518                }
519            }
520            let identifier=from.getType().identifier
521
522            // Try borrow that in a standard way. Only work for flow, usdc and fusd
523            // Check the vault type
524            var ref : &{FungibleToken.Receiver}? = nil
525            if FindUtils.contains(identifier, element: "FlowToken.Vault") {
526                ref = self.owner!.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
527            } else if FindUtils.contains(identifier, element: "FlowUtilityToken.Vault") {
528                ref = self.owner!.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowUtilityTokenReceiver)
529            } else if FindUtils.contains(identifier, element: "DapperUtilityCoin.Vault") {
530                ref = self.owner!.capabilities.borrow<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver)
531            }
532
533            if ref != nil {
534                ref!.deposit(from: <- from)
535                return
536            }
537
538            //I need to destroy here for this to compile, but WHY?
539            // oh we dont neet this anymore
540            // destroy from
541            panic("could not find a supported wallet for:".concat(identifier).concat(" for address ").concat(self.owner!.address.toString()))
542        }
543
544        access(all) fun hasWallet(_ name: String) : Bool {
545            for wallet in self.wallets {
546                if wallet.name == name || wallet.accept.identifier == name {
547                    return wallet.receiver.check()
548                }
549            }
550            return false
551        }
552
553        access(all) fun getWallets() : [Wallet] { return self.wallets}
554
555        access(Admin) fun addWallet(_ val: Wallet) { 
556            self.wallets.append(val) 
557        }
558
559        access(Admin) fun removeWallet(_ val: String) {
560            let numWallets=self.wallets.length
561            var i=0
562            while(i < numWallets) {
563                if self.wallets[i].name== val {
564                    self.wallets.remove(at: i)
565                    ProfileCache.resetWalletIndexCache(address: self.owner!.address)
566                    return
567                }
568                i=i+1
569            }
570        }
571
572        access(Admin) fun setWallets(_ val: [Wallet]) {
573            self.wallets=val
574            ProfileCache.resetWalletIndexCache(address: self.owner!.address)
575        }
576
577        access(Admin) fun removeFollower(_ val: Address) {
578            self.followers.remove(key:val)
579        }
580
581        access(all) fun follows(_ address: Address) : Bool {
582            return self.following.containsKey(address)
583        }
584
585        access(all) fun getName(): String { return self.name }
586        access(all) fun getFindName(): String { return self.findName }
587        access(all) fun getCreatedAt(): String { return self.createdAt }
588        access(all) fun getGender() : String { return self.gender }
589        access(all) fun getDescription(): String{ return self.description}
590        access(all) fun getTags(): [String] { return self.tags}
591        access(all) fun getAvatar(): String { return self.avatar }
592        access(all) fun getFollowers(): [FriendStatus] { return self.followers.values }
593        access(all) fun getFollowing(): [FriendStatus] { return self.following.values }
594
595        access(Admin) fun setName(_ val: String) { self.name = val }
596        access(account) fun setFindName(_ val: String) {
597            emit Updated(account:self.owner!.address, userName:self.name, findName:val, thumbnail:self.avatar)
598            ProfileCache.resetLeaseCache(address: self.owner!.address, leaseName: self.findName)
599            self.findName = val
600        }
601        access(Admin) fun setMainName(_ val: String) {
602            emit Updated(account:self.owner!.address, userName:self.name, findName:val, thumbnail:self.avatar)
603            ProfileCache.resetLeaseCache(address: self.owner!.address, leaseName: self.findName)
604            self.findName = val
605        }
606        access(Admin) fun setGender(_ val: String) { self.gender = val }
607        access(Admin) fun setAvatar(_ val: String) { self.avatar = val }
608        access(Admin) fun setDescription(_ val: String) { self.description=val}
609        access(Admin) fun setTags(_ val: [String]) { self.tags=val}
610
611        access(Admin) fun removeCollection(_ val: String) { self.collections.remove(key: val)}
612        access(Admin) fun addCollection(_ val: ResourceCollection) { self.collections[val.name]=val}
613        access(all) fun getCollections(): [ResourceCollection] { return self.collections.values}
614
615
616        access(Admin) fun follow(_ address: Address, tags:[String]) {
617            let friendProfile=Profile.find(address)
618            let owner=self.owner!.address
619            let status=FriendStatus(follower:owner, following:address, tags:tags)
620
621            self.following[address] = status
622            friendProfile.internal_addFollower(status)
623            emit Follow(follower:owner, following: address, tags:tags)
624        }
625
626        access(Admin) fun unfollow(_ address: Address) {
627            self.following.remove(key: address)
628            Profile.find(address).internal_removeFollower(self.owner!.address)
629            emit Unfollow(follower: self.owner!.address, unfollowing:address)
630        }
631
632        access(contract) fun internal_addFollower(_ val: FriendStatus) {
633            if self.allowStoringFollowers && !self.bans.containsKey(val.follower) {
634                self.followers[val.follower] = val
635            }
636        }
637
638        access(contract) fun internal_removeFollower(_ address: Address) {
639            if self.followers.containsKey(address) {
640                self.followers.remove(key: address)
641            }
642        }
643
644        /// A getter function that returns the token types supported by this resource,
645        /// which can be deposited using the 'deposit' function.
646        ///
647        /// @return Dictionary of FT types that can be deposited.
648        access(all) view fun getSupportedVaultTypes(): {Type: Bool} { 
649            let supportedVaults: {Type: Bool} = {}
650            for w in self.wallets {
651                if w.receiver.check() {
652                    supportedVaults[w.accept] = true
653                }
654            }
655            return supportedVaults
656        }
657
658        /// Returns whether or not the given type is accepted by the Receiver
659        access(all) view fun isSupportedVaultType(type: Type): Bool {
660            let supportedVaults = self.getSupportedVaultTypes()
661            if let supported = supportedVaults[type] {
662                return supported
663            } else { return false }
664        }
665    }
666
667    access(all) fun findReceiverCapability(address: Address, path: PublicPath, type: Type) : Capability<&{FungibleToken.Receiver}>? {
668        let profileCap = self.findWalletCapability(address)
669        if profileCap.check() {
670            if let profile = getAccount(address).capabilities.borrow<&Profile.User>(Profile.publicPath) {
671                if profile.hasWallet(type.identifier) {
672                    return profileCap
673                }
674            }
675        }
676        let cap = getAccount(address).capabilities.get<&{FungibleToken.Receiver}>(path)
677        return cap
678    }
679
680    access(all) fun findWalletCapability(_ address: Address) : Capability<&{FungibleToken.Receiver}> {
681        return getAccount(address)
682        .capabilities.get<&{FungibleToken.Receiver}>(Profile.publicReceiverPath)!
683    }
684
685    access(all) fun find(_ address: Address) : &{Profile.Public} {
686        return getAccount(address)
687        .capabilities.borrow<&{Profile.Public}>(Profile.publicPath)!
688    }
689
690    access(all) fun createUser(name: String, createdAt:String) : @Profile.User {
691        if name.length > 64 {
692            panic("Name must be 64 or less characters")
693        }
694        if createdAt.length > 32 {
695            panic("createdAt must be 32 or less characters")
696        }
697        return <- create Profile.User(name: name,createdAt: createdAt)
698    }
699
700    access(all) view fun verifyTags(tags : [String], tagLength: Int, tagSize: Int): Bool {
701        if tags.length > tagSize {
702            return false
703        }
704
705        for t in tags {
706            if t.length > tagLength {
707                return false
708            }
709        }
710        return true
711    }
712
713    init() {
714        self.publicPath = /public/findProfile
715        self.publicReceiverPath = /public/findProfileReceiver
716        self.storagePath = /storage/findProfile
717    }
718}
719