Smart Contract
Profile
A.097bafa4e0b48eef.Profile
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