Smart Contract

FLOAT

A.2d4c3caffbeab845.FLOAT

Deployed

2w ago
Feb 11, 2026, 06:39:16 PM UTC

Dependents

3855 imports
1// MADE BY: Emerald City, Jacob Tucker
2
3// This contract is for FLOAT, a proof of participation platform
4// on Flow. It is similar to POAP, but a lot, lot cooler. ;)
5
6// The main idea is that FLOATs are simply NFTs. They are minted from
7// a FLOATEvent, which is basically an event that a host starts. For example,
8// if I have a Twitter space and want to create an event for it, I can create
9// a new FLOATEvent in my FLOATEvents collection and mint FLOATs to people
10// from this Twitter space event representing that they were there.  
11
12// The complicated part is the FLOATVerifiers contract. That contract 
13// defines a list of "verifiers" that can be tagged onto a FLOATEvent to make
14// the claiming more secure. For example, a host can decide to put a time 
15// constraint on when users can claim a FLOAT. They would do that by passing in
16// a Timelock struct (defined in FLOATVerifiers.cdc) with a time period for which
17// users can claim. 
18
19// For a whole list of verifiers, see FLOATVerifiers.cdc 
20
21import NonFungibleToken from 0x1d7e57aa55817448
22import MetadataViews from 0x1d7e57aa55817448
23import FungibleToken from 0xf233dcee88fe0abe
24import FlowToken from 0x1654653399040a61
25// import "FindViews"
26import ViewResolver from 0x1d7e57aa55817448
27
28access(all) contract FLOAT: NonFungibleToken, ViewResolver {
29
30    access(all) entitlement EventOwner
31    access(all) entitlement EventsOwner
32    access(all) entitlement CollectionOwner
33
34    /***********************************************/
35    /******************** PATHS ********************/
36    /***********************************************/
37
38    access(all) let FLOATCollectionStoragePath: StoragePath
39    access(all) let FLOATCollectionPublicPath: PublicPath
40    access(all) let FLOATEventsStoragePath: StoragePath
41    access(all) let FLOATEventsPublicPath: PublicPath
42    access(all) let FLOATEventsPrivatePath: PrivatePath
43
44    /************************************************/
45    /******************** EVENTS ********************/
46    /************************************************/
47
48    access(all) event ContractInitialized()
49    access(all) event FLOATMinted(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, recipient: Address, serial: UInt64)
50    access(all) event FLOATClaimed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, eventName: String, recipient: Address, serial: UInt64)
51    access(all) event FLOATDestroyed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, serial: UInt64)
52    access(all) event FLOATTransferred(id: UInt64, eventHost: Address, eventId: UInt64, newOwner: Address?, serial: UInt64)
53    access(all) event FLOATPurchased(id: UInt64, eventHost: Address, eventId: UInt64, recipient: Address, serial: UInt64)
54    access(all) event FLOATEventCreated(eventId: UInt64, description: String, host: Address, image: String, name: String, url: String)
55    access(all) event FLOATEventDestroyed(eventId: UInt64, host: Address, name: String)
56
57    access(all) event Deposit(id: UInt64, to: Address?)
58    access(all) event Withdraw(id: UInt64, from: Address?)
59
60    /***********************************************/
61    /******************** STATE ********************/
62    /***********************************************/
63
64    // The total amount of FLOATs that have ever been
65    // created (does not go down when a FLOAT is destroyed)
66    access(all) var totalSupply: UInt64
67    // The total amount of FLOATEvents that have ever been
68    // created (does not go down when a FLOATEvent is destroyed)
69    access(all) var totalFLOATEvents: UInt64
70
71    /***********************************************/
72    /**************** FUNCTIONALITY ****************/
73    /***********************************************/
74
75    // A helpful wrapper to contain an address, 
76    // the id of a FLOAT, and its serial
77    access(all) struct TokenIdentifier {
78        access(all) let id: UInt64
79        access(all) let address: Address
80        access(all) let serial: UInt64
81
82        init(_id: UInt64, _address: Address, _serial: UInt64) {
83            self.id = _id
84            self.address = _address
85            self.serial = _serial
86        }
87    }
88
89    access(all) struct TokenInfo {
90        access(all) let path: PublicPath
91        access(all) let price: UFix64
92
93        init(_path: PublicPath, _price: UFix64) {
94            self.path = _path
95            self.price = _price
96        }
97    }
98
99    // Represents a FLOAT
100    access(all) resource NFT: NonFungibleToken.NFT {
101        // The `uuid` of this resource
102        access(all) let id: UInt64
103
104        // Some of these are also duplicated on the event,
105        // but it's necessary to put them here as well
106        // in case the FLOATEvent host deletes the event
107        access(all) let dateReceived: UFix64
108        access(all) let eventDescription: String
109        access(all) let eventHost: Address
110        access(all) let eventId: UInt64
111        access(all) let eventImage: String
112        access(all) let eventName: String
113        access(all) let originalRecipient: Address
114        access(all) let serial: UInt64
115
116        // A capability that points to the FLOATEvents this FLOAT is from.
117        // There is a chance the event host unlinks their event from
118        // the public, in which case it's impossible to know details
119        // about the event. Which is fine, since we store the
120        // crucial data to know about the FLOAT in the FLOAT itself.
121        access(all) let eventsCap: Capability<&FLOATEvents>
122
123        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
124            return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>())
125        }
126        
127        // Helper function to get the metadata of the event 
128        // this FLOAT is from.
129        access(all) fun getEventRef(): &FLOATEvent? {
130            if let events: &FLOATEvents = self.eventsCap.borrow() {
131                return events.borrowPublicEventRef(eventId: self.eventId)
132            }
133            return nil
134        }
135
136        access(all) fun getExtraMetadata(): {String: AnyStruct} {
137            if let eventRef: &FLOATEvent = self.getEventRef() {
138                return eventRef.getExtraFloatMetadata(serial: self.serial)
139            }
140            return {}
141        }
142
143        access(all) fun getSpecificExtraMetadata(key: String): AnyStruct? {
144            return self.getExtraMetadata()[key]
145        }
146
147        access(all) fun getImage(): String {
148            if let extraEventMetadata: {String: AnyStruct} = self.getEventRef()?.getExtraMetadata() {
149                if FLOAT.extraMetadataToStrOpt(extraEventMetadata, "visibilityMode") == "picture" {
150                    return self.eventImage
151                }
152                if let certificateType: String = FLOAT.extraMetadataToStrOpt(extraEventMetadata, "certificateType") {
153                    if certificateType == "medal" {
154                         // Extra metadata about medal colors
155                        if let medalType: String = FLOAT.extraMetadataToStrOpt(self.getExtraMetadata(), "medalType") {
156                            return FLOAT.extraMetadataToStrOpt(extraEventMetadata, "certificateImage.".concat(medalType)) ?? self.eventImage
157                        }
158                        // if there is no medal type for the FLOAT
159                        return FLOAT.extraMetadataToStrOpt(extraEventMetadata, "certificateImage.participation") ?? self.eventImage
160                    }
161                    return FLOAT.extraMetadataToStrOpt(extraEventMetadata, "certificateImage") ?? self.eventImage
162                }
163            }
164            return self.eventImage
165        }
166
167        // This is for the MetdataStandard
168        access(all) view fun getViews(): [Type] {
169            let supportedViews = [
170                Type<MetadataViews.Display>(),
171                Type<MetadataViews.Royalties>(),
172                Type<MetadataViews.ExternalURL>(),
173                Type<MetadataViews.NFTCollectionData>(),
174                Type<MetadataViews.NFTCollectionDisplay>(),
175                Type<MetadataViews.Traits>(),
176                Type<MetadataViews.Serial>(),
177                Type<TokenIdentifier>()
178            ]
179
180            // if self.getEventRef()?.transferrable == false {
181            //     supportedViews.append(Type<FindViews.SoulBound>())
182            // }
183
184            return supportedViews
185        }
186
187        // This is for the MetdataStandard
188        access(all) fun resolveView(_ view: Type): AnyStruct? {
189            switch view {
190                case Type<MetadataViews.Display>():
191                    return MetadataViews.Display(
192                        name: self.eventName, 
193                        description: self.eventDescription, 
194                        thumbnail: MetadataViews.HTTPFile(url: "https://nftstorage.link/ipfs/".concat(self.getImage()))
195                    )
196                case Type<MetadataViews.Royalties>():
197                    return MetadataViews.Royalties([
198						MetadataViews.Royalty(
199							receiver: getAccount(0x5643fd47a29770e7).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver),
200							cut: 0.05, // 5% royalty on secondary sales
201							description: "Emerald City DAO receives a 5% royalty from secondary sales because this NFT was created using FLOAT (https://floats.city/), a proof of attendance platform created by Emerald City DAO."
202						)
203					])
204                case Type<MetadataViews.ExternalURL>():
205                    return MetadataViews.ExternalURL("https://floats.city/".concat(self.owner!.address.toString()).concat("/float/").concat(self.id.toString()))
206                case Type<MetadataViews.NFTCollectionData>():
207                    return FLOAT.resolveContractView(resourceType: Type<@FLOAT.NFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
208                case Type<MetadataViews.NFTCollectionDisplay>():
209                    return FLOAT.resolveContractView(resourceType: Type<@FLOAT.NFT>(), viewType: Type<MetadataViews.NFTCollectionDisplay>())
210                case Type<MetadataViews.Serial>():
211                    return MetadataViews.Serial(
212                        self.serial
213                    )
214                case Type<TokenIdentifier>():
215                    return TokenIdentifier(
216                        _id: self.id, 
217                        _address: self.owner!.address,
218                        _serial: self.serial
219                    ) 
220                // case Type<FindViews.SoulBound>():
221                //     if self.getEventRef()?.transferrable == false {
222                //         return FindViews.SoulBound(
223                //             "This FLOAT is soulbound because the event host toggled off transferring."
224                //         )
225                //     }
226                //     return nil
227                case Type<MetadataViews.Traits>():
228                    let traitsView: MetadataViews.Traits = MetadataViews.dictToTraits(dict: self.getExtraMetadata(), excludedNames: nil)
229
230                    if let eventRef: &FLOATEvent = self.getEventRef() {
231                        let eventExtraMetadata: {String: AnyStruct} = eventRef.getExtraMetadata()
232                        
233                        // certificate type doesn't apply if it's a picture FLOAT
234                        if FLOAT.extraMetadataToStrOpt(eventExtraMetadata, "visibilityMode") == "certificate" {
235                            let certificateType: MetadataViews.Trait = MetadataViews.Trait(name: "certificateType", value: eventExtraMetadata["certificateType"], displayType: nil, rarity: nil)
236                            traitsView.addTrait(certificateType)
237                        }
238                        
239                        let serial: MetadataViews.Trait = MetadataViews.Trait(name: "serial", value: self.serial, displayType: nil, rarity: nil)
240                        traitsView.addTrait(serial)
241                        let originalRecipient: MetadataViews.Trait = MetadataViews.Trait(name: "originalRecipient", value: self.originalRecipient, displayType: nil, rarity: nil)
242                        traitsView.addTrait(originalRecipient)
243                        let eventCreator: MetadataViews.Trait = MetadataViews.Trait(name: "eventCreator", value: self.eventHost, displayType: nil, rarity: nil)
244                        traitsView.addTrait(eventCreator)
245                        let eventType: MetadataViews.Trait = MetadataViews.Trait(name: "eventType", value: eventExtraMetadata["eventType"], displayType: nil, rarity: nil)
246                        traitsView.addTrait(eventType)
247                        let dateReceived: MetadataViews.Trait = MetadataViews.Trait(name: "dateMinted", value: self.dateReceived, displayType: "Date", rarity: nil)
248                        traitsView.addTrait(dateReceived)
249                        let eventId: MetadataViews.Trait = MetadataViews.Trait(name: "eventId", value: self.eventId, displayType: nil, rarity: nil)
250                        traitsView.addTrait(eventId)
251                    }
252
253                    return traitsView
254            }
255
256            return nil
257        }
258
259        init(_eventDescription: String, _eventHost: Address, _eventId: UInt64, _eventImage: String, _eventName: String, _originalRecipient: Address, _serial: UInt64) {
260            self.id = self.uuid
261            self.dateReceived = getCurrentBlock().timestamp
262            self.eventDescription = _eventDescription
263            self.eventHost = _eventHost
264            self.eventId = _eventId
265            self.eventImage = _eventImage
266            self.eventName = _eventName
267            self.originalRecipient = _originalRecipient
268            self.serial = _serial
269
270            // Stores a capability to the FLOATEvents of its creator
271            self.eventsCap = getAccount(_eventHost).capabilities.get<&FLOATEvents>(FLOAT.FLOATEventsPublicPath)
272            
273            emit FLOATMinted(
274                id: self.id, 
275                eventHost: _eventHost, 
276                eventId: _eventId, 
277                eventImage: _eventImage,
278                recipient: _originalRecipient,
279                serial: _serial
280            )
281
282            FLOAT.totalSupply = FLOAT.totalSupply + 1
283        }
284
285        // destroy() {
286        //     emit FLOATDestroyed(
287        //         id: self.id, 
288        //         eventHost: self.eventHost, 
289        //         eventId: self.eventId, 
290        //         eventImage: self.eventImage,
291        //         serial: self.serial
292        //     )
293        // }
294    }
295
296    access(all) resource interface CollectionPublic {}
297
298    // A Collection that holds all of the users FLOATs.
299    // Withdrawing is not allowed. You can only transfer.
300    access(all) resource Collection: NonFungibleToken.Collection, CollectionPublic {
301        // Maps a FLOAT id to the FLOAT itself
302        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
303        // Maps an eventId to the ids of FLOATs that
304        // this user owns from that event. It's possible
305        // for it to be out of sync until June 2022 spork, 
306        // but it is used merely as a helper, so that's okay.
307        access(self) var events: {UInt64: {UInt64: Bool}}
308
309        // Deposits a FLOAT to the collection
310        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
311            let nft <- token as! @NFT
312            let id = nft.id
313            let eventId = nft.eventId
314        
315            // Update self.events[eventId] to have
316            // this FLOAT's id in it
317            if self.events[eventId] == nil {
318                self.events[eventId] = {id: true}
319            } else {
320                self.events[eventId]!.insert(key: id, true)
321            }
322
323            emit Deposit(id: id, to: self.owner?.address)
324            emit FLOATTransferred(id: id, eventHost: nft.eventHost, eventId: nft.eventId, newOwner: self.owner?.address, serial: nft.serial)
325            self.ownedNFTs[id] <-! nft
326        }
327
328        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
329            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("You do not own this FLOAT in your collection")
330            let nft <- token as! @NFT
331
332            // Update self.events[eventId] to not
333            // have this FLOAT's id in it
334            self.events[nft.eventId]!.remove(key: withdrawID)
335
336            // This checks if the FLOATEvent host wanted this
337            // FLOAT to be transferrable. Secondary marketplaces will use this
338            // withdraw function, so if the FLOAT is not transferrable,
339            // you can't sell it there.
340            if let floatEvent: &FLOATEvent = nft.getEventRef() {
341                assert(
342                    floatEvent.transferrable, 
343                    message: "This FLOAT is not transferrable."
344                )
345            }
346
347            emit Withdraw(id: withdrawID, from: self.owner?.address) 
348            emit FLOATTransferred(id: withdrawID, eventHost: nft.eventHost, eventId: nft.eventId, newOwner: nil, serial: nft.serial)
349            return <- nft
350        }
351
352        access(CollectionOwner) fun delete(id: UInt64) {
353            let token <- self.ownedNFTs.remove(key: id) ?? panic("You do not own this FLOAT in your collection")
354            let nft <- token as! @NFT
355
356            // Update self.events[eventId] to not
357            // have this FLOAT's id in it
358            self.events[nft.eventId]!.remove(key: id)
359
360            destroy nft
361        }
362
363        // Only returns the FLOATs for which we can still
364        // access data about their event.
365        access(all) view fun getIDs(): [UInt64] {
366            return self.ownedNFTs.keys
367        }
368
369        // Returns an array of ids that belong to
370        // the passed in eventId
371        //
372        // It's possible for FLOAT ids to be present that
373        // shouldn't be if people tried to withdraw directly
374        // from `ownedNFTs` (not possible after June 2022 spork), 
375        // but this makes sure the returned
376        // ids are all actually owned by this account.
377        access(all) fun ownedIdsFromEvent(eventId: UInt64): [UInt64] {
378            let answer: [UInt64] = []
379            if let idsInEvent = self.events[eventId]?.keys {
380                for id in idsInEvent {
381                    if self.ownedNFTs[id] != nil {
382                        answer.append(id)
383                    }
384                }
385            }
386            return answer
387        }
388
389        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
390            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
391        }
392
393        access(all) view fun getLength(): Int {
394            return self.ownedNFTs.keys.length
395        }
396
397        /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
398        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
399            let supportedTypes: {Type: Bool} = {}
400            supportedTypes[Type<@FLOAT.NFT>()] = true
401            return supportedTypes
402        }
403
404        /// Returns whether or not the given type is accepted by the collection
405        /// A collection that can accept any type should just return true by default
406        access(all) view fun isSupportedNFTType(type: Type): Bool {
407           if type == Type<@FLOAT.NFT>() {
408            return true
409           } else {
410            return false
411           }
412        }
413
414        access(all) fun borrowFLOAT(id: UInt64): &NFT? {
415            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
416                return nft as! &NFT
417            }
418            return nil
419        }
420
421        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
422            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
423                return nft as &{ViewResolver.Resolver}
424            }
425            return nil
426        }
427
428         access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
429            return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>())
430        }
431
432        init() {
433            self.ownedNFTs <- {}
434            self.events = {}
435        }
436    }
437
438    // An interface that every "verifier" must implement. 
439    // A verifier is one of the options on the FLOAT Event page,
440    // for example, a "time limit," or a "limited" number of 
441    // FLOATs that can be claimed. 
442    // All the current verifiers can be seen inside FLOATVerifiers.cdc
443    access(all) struct interface IVerifier {
444        // A function every verifier must implement. 
445        // Will have `assert`s in it to make sure
446        // the user fits some criteria.
447        access(account) fun verify(_ params: {String: AnyStruct})
448    }
449
450    access(all) resource interface FLOATEventPublic {}
451
452    //
453    // FLOATEvent
454    //
455    access(all) resource FLOATEvent: FLOATEventPublic, ViewResolver.Resolver {
456        // Whether or not users can claim from our event (can be toggled
457        // at any time)
458        access(all) var claimable: Bool
459        access(all) let dateCreated: UFix64
460        access(all) let description: String 
461        // This is equal to this resource's uuid
462        access(all) let eventId: UInt64
463        // Who created this FLOAT Event
464        access(all) let host: Address
465        // The image of the FLOAT Event
466        access(all) let image: String 
467        // The name of the FLOAT Event
468        access(all) let name: String
469        // The total number of FLOATs that have been
470        // minted from this event
471        access(all) var totalSupply: UInt64
472        // Whether or not the FLOATs that users own
473        // from this event can be transferred on the
474        // FLOAT platform itself (transferring allowed
475        // elsewhere)
476        access(all) var transferrable: Bool
477        // A url of where the event took place
478        access(all) let url: String
479        // A list of verifiers this FLOAT Event contains.
480        // Will be used every time someone "claims" a FLOAT
481        // to see if they pass the requirements
482        access(self) let verifiers: {String: [{IVerifier}]}
483        // Used to store extra metadata about the event but
484        // also individual FLOATs, because Jacob forgot to
485        // put a dictionary in the NFT resource :/ Idiot
486        access(self) var extraMetadata: {String: AnyStruct}
487
488        // DEPRECATED, DO NOT USE
489        access(self) var claimed: {Address: TokenIdentifier}
490        // DEPRECATED, DO NOT USE (used for storing claim info now)
491        access(self) var currentHolders: {UInt64: TokenIdentifier}
492        // DEPRECATED, DO NOT USE
493        access(self) var groups: {String: Bool}
494
495        // Type: Admin Toggle
496        access(EventOwner) fun toggleClaimable(): Bool {
497            self.claimable = !self.claimable
498            return self.claimable
499        }
500
501        // Type: Admin Toggle
502        access(EventOwner) fun toggleTransferrable(): Bool {
503            self.transferrable = !self.transferrable
504            return self.transferrable
505        }
506
507        // Type: Admin Toggle
508        access(EventOwner) fun toggleVisibilityMode() {
509            if let currentVisibilityMode: String = FLOAT.extraMetadataToStrOpt(self.getExtraMetadata(), "visibilityMode") {
510                if currentVisibilityMode == "certificate" {
511                    self.extraMetadata["visibilityMode"] = "picture"
512                    return
513                }
514            }
515            self.extraMetadata["visibilityMode"] = "certificate"
516        }
517
518        // Type: Contract Setter
519        access(self) fun setUserClaim(serial: UInt64, address: Address, floatId: UInt64) {
520            self.currentHolders[serial] = TokenIdentifier(_id: floatId, _address: address, _serial: serial)
521
522            if self.extraMetadata["userClaims"] == nil {
523                let userClaims: {Address: [UInt64]} = {}
524                self.extraMetadata["userClaims"] = userClaims
525            }
526            let e = (&self.extraMetadata["userClaims"] as auth(Mutate) &AnyStruct?)!
527            let claims = e as! auth(Mutate) &{Address: [UInt64]}
528            let claimsByAddress = claims[address] as! auth(Mutate) &[UInt64]?
529            
530            if let specificUserClaims: auth(Mutate) &[UInt64] = claimsByAddress {
531                specificUserClaims.append(serial)
532            } else {
533                claims[address] = [serial]
534            }
535        }
536
537        // Type: Contract Setter
538        access(self) fun setExtraFloatMetadata(serial: UInt64, metadata: {String: AnyStruct}) {
539            if self.extraMetadata["extraFloatMetadatas"] == nil {
540                let extraFloatMetadatas: {UInt64: AnyStruct} = {}
541                self.extraMetadata["extraFloatMetadatas"] = extraFloatMetadatas
542            }
543            let e = (&self.extraMetadata["extraFloatMetadatas"] as auth(Mutate)&AnyStruct?)!
544            let extraFloatMetadatas = e as! auth(Mutate) &{UInt64: AnyStruct}
545            extraFloatMetadatas[serial] = metadata
546        }
547
548        // Type: Contract Setter
549        access(self) fun setSpecificExtraFloatMetadata(serial: UInt64, key: String, value: AnyStruct) {
550            if self.extraMetadata["extraFloatMetadatas"] == nil {
551                let extraFloatMetadatas: {UInt64: AnyStruct} = {}
552                self.extraMetadata["extraFloatMetadatas"] = extraFloatMetadatas
553            }
554            let e = (&self.extraMetadata["extraFloatMetadatas"] as auth(Mutate) &AnyStruct?)!
555            let extraFloatMetadatas = e as! auth(Mutate) &{UInt64: AnyStruct}
556
557            if extraFloatMetadatas[serial] == nil {
558                let extraFloatMetadata: {String: AnyStruct} = {}
559                extraFloatMetadatas[serial] = extraFloatMetadata
560            }
561
562            let f = (extraFloatMetadatas[serial] as! auth(Mutate) &AnyStruct?)!
563            let extraFloatMetadata = e as! auth(Mutate) &{String: AnyStruct}
564            extraFloatMetadata[key] = value
565        }
566
567        // Type: Getter
568        // Description: Get extra metadata on a specific FLOAT from this event
569        access(all) view fun getExtraFloatMetadata(serial: UInt64): {String: AnyStruct} {
570            if self.extraMetadata["extraFloatMetadatas"] != nil {
571                if let e: {UInt64: AnyStruct} = self.extraMetadata["extraFloatMetadatas"]! as? {UInt64: AnyStruct} {
572                    if e[serial] != nil {
573                        if let f: {String: AnyStruct} = e[serial]! as? {String: AnyStruct} {
574                            return f
575                        }
576                    }
577                }
578            }
579            return {}
580        }
581
582        // Type: Getter
583        // Description: Get specific extra metadata on a specific FLOAT from this event
584        access(all) view fun getSpecificExtraFloatMetadata(serial: UInt64, key: String): AnyStruct? {
585            return self.getExtraFloatMetadata(serial: serial)[key]
586        }
587
588        // Type: Getter
589        // Description: Returns claim info of all the serials
590        access(all) view fun getClaims(): {UInt64: TokenIdentifier} {
591            return self.currentHolders
592        }
593
594        // Type: Getter
595        // Description: Will return an array of all the serials a user claimed.  
596        // Most of the time this will be a maximum length of 1 because most 
597        // events only allow 1 claim per user.
598        access(all) view fun getSerialsUserClaimed(address: Address): [UInt64] {
599            var serials: [UInt64] = []
600            if let userClaims: {Address: [UInt64]} = self.getSpecificExtraMetadata(key: "userClaims") as! {Address: [UInt64]}? {
601                serials = userClaims[address] ?? []
602            }
603            // take into account claims during FLOATv1
604            if let oldClaim: TokenIdentifier = self.claimed[address] {
605                serials = serials.concat([oldClaim.serial])
606            }
607            return serials
608        }
609
610        // Type: Getter
611        // Description: Returns true if the user has either claimed
612        // or been minted at least one float from this event
613        access(all) view fun userHasClaimed(address: Address): Bool {
614            return self.getSerialsUserClaimed(address: address).length >= 1
615        }
616
617        // Type: Getter
618        // Description: Get extra metadata on this event
619        access(all) view fun getExtraMetadata(): {String: AnyStruct} {
620            return self.extraMetadata
621        }
622
623        // Type: Getter
624        // Description: Get specific extra metadata on this event
625        access(all) view fun getSpecificExtraMetadata(key: String): AnyStruct? {
626            return self.extraMetadata[key]
627        }
628
629        // Type: Getter
630        // Description: Checks if a user can mint a new FLOAT from this event
631        access(all) view fun userCanMint(address: Address): Bool {
632            if let allows: Bool = self.getSpecificExtraMetadata(key: "allowMultipleClaim") as! Bool? {
633                if allows || self.getSerialsUserClaimed(address: address).length == 0 {
634                    return true
635                }
636            }
637            return !self.userHasClaimed(address: address)
638        }
639
640        // Type: Getter
641        // Description: Gets all the verifiers that will be used
642        // for claiming
643        access(all) view fun getVerifiers(): {String: [{IVerifier}]} {
644            return self.verifiers
645        }
646
647        // Type: Getter
648        // Description: Returns a dictionary whose key is a token identifier
649        // and value is the path to that token and price of the FLOAT in that
650        // currency
651        access(all) view fun getPrices(): {String: TokenInfo}? {
652            return self.extraMetadata["prices"] as! {String: TokenInfo}?
653        }
654
655        // Type: Getter
656        // Description: For MetadataViews
657        access(all) view fun getViews(): [Type] {
658             return [
659                Type<MetadataViews.Display>()
660            ]
661        }
662
663        // Type: Getter
664        // Description: For MetadataViews
665        access(all) view fun resolveView(_ view: Type): AnyStruct? {
666            switch view {
667                case Type<MetadataViews.Display>():
668                    return MetadataViews.Display(
669                        name: self.name, 
670                        description: self.description, 
671                        thumbnail: MetadataViews.IPFSFile(cid: self.image, path: nil)
672                    )
673            }
674
675            return nil
676        }
677
678        // Type: Admin / Helper Function for `verifyAndMint`
679        // Description: Used to give a person a FLOAT from this event.
680        // If the event owner directly mints to a user, it does not
681        // run the verifiers on the user. It bypasses all of them.
682        // Return the id of the FLOAT it minted.
683        access(EventOwner) fun mint(recipient: &Collection, optExtraFloatMetadata: {String: AnyStruct}?): UInt64 {
684            pre {
685                self.userCanMint(address: recipient.owner!.address): "Only 1 FLOAT allowed per user, and this user already claimed their FLOAT!"
686            }
687            let recipientAddr: Address = recipient.owner!.address
688            let serial = self.totalSupply
689
690            let token <- create NFT(
691                _eventDescription: self.description,
692                _eventHost: self.host, 
693                _eventId: self.eventId,
694                _eventImage: self.image,
695                _eventName: self.name,
696                _originalRecipient: recipientAddr, 
697                _serial: serial
698            ) 
699
700            if let extraFloatMetadata: {String: AnyStruct} = optExtraFloatMetadata {
701                // ensure 
702                assert(
703                    FLOAT.validateExtraFloatMetadata(data: extraFloatMetadata), 
704                    message: "Extra FLOAT metadata is not proper. Check the `FLOAT.validateExtraFloatMetadata` function."
705                )
706                self.setExtraFloatMetadata(serial: serial, metadata: extraFloatMetadata)
707            }
708
709            let id: UInt64 = token.id
710
711            self.setUserClaim(serial: serial, address: recipientAddr, floatId: id)
712
713            self.totalSupply = self.totalSupply + 1
714            recipient.deposit(token: <- token)
715            return id
716        }
717
718        // Type: Helper Function for `claim` and `purchase`
719        // Description: Will get run by the public, so verifies
720        // the user can mint
721        access(self) fun verifyAndMint(recipient: &Collection, params: {String: AnyStruct}): UInt64 {
722            params["event"] = &self as &FLOATEvent
723            params["claimee"] = recipient.owner!.address
724            
725            // Runs a loop over all the verifiers that this FLOAT Events
726            // implements. For example, "Limited", "Timelock", "Secret", etc.  
727            // All the verifiers are in the FLOATVerifiers.cdc contract
728            for identifier in self.verifiers.keys {
729                let typedModules = (&self.verifiers[identifier] as &[{IVerifier}]?)!
730                var i = 0
731                while i < typedModules.length {
732                    let verifier = typedModules[i]
733                    verifier.verify(params)
734                    i = i + 1
735                }
736            }
737
738            var optExtraFloatMetadata: {String: AnyStruct}? = nil
739            // if this is a medal type float and user is publicly claiming, assign to participation
740            if FLOAT.extraMetadataToStrOpt(self.getExtraMetadata(), "certificateType") == "medal" {
741                optExtraFloatMetadata = {"medalType": "participation"}
742            }
743
744            // You're good to go.
745            let id: UInt64 = self.mint(recipient: recipient, optExtraFloatMetadata: optExtraFloatMetadata)
746
747            emit FLOATClaimed(
748                id: id,
749                eventHost: self.host, 
750                eventId: self.eventId, 
751                eventImage: self.image,
752                eventName: self.name,
753                recipient: recipient.owner!.address,
754                serial: self.totalSupply - 1
755            )
756            return id
757        }
758
759        // For the public to claim FLOATs. Must be claimable to do so.
760        // You can pass in `params` that will be forwarded to the
761        // customized `verify` function of the verifier.  
762        //
763        // For example, the FLOAT platform allows event hosts
764        // to specify a secret phrase. That secret phrase will 
765        // be passed in the `params`.
766        access(all) fun claim(recipient: &Collection, params: {String: AnyStruct}) {
767            pre {
768                self.getPrices() == nil:
769                    "You need to purchase this FLOAT."
770                self.claimable: 
771                    "This FLOAT event is not claimable, and thus not currently active."
772            }
773            
774            self.verifyAndMint(recipient: recipient, params: params)
775        }
776 
777        access(all) fun purchase(recipient: &Collection, params: {String: AnyStruct}, payment: @{FungibleToken.Vault}) {
778            pre {
779                self.getPrices() != nil:
780                    "Don't call this function. The FLOAT is free. Call the claim function instead."
781                self.getPrices()![payment.getType().identifier] != nil:
782                    "This FLOAT does not support purchasing in the passed in token."
783                payment.balance == self.getPrices()![payment.getType().identifier]!.price:
784                    "You did not pass in the correct amount of tokens."
785                self.claimable: 
786                    "This FLOAT event is not claimable, and thus not currently active."
787            }
788            let royalty: UFix64 = 0.05
789            let emeraldCityTreasury: Address = 0x5643fd47a29770e7
790            let paymentType: String = payment.getType().identifier
791            let tokenInfo: TokenInfo = self.getPrices()![paymentType]!
792
793            let EventHostVault = getAccount(self.host).capabilities.borrow<&{FungibleToken.Receiver}>(tokenInfo.path)
794                                    ?? panic("Could not borrow the &{FungibleToken.Receiver} from the event host.")
795
796            assert(
797                EventHostVault.getType().identifier == paymentType,
798                message: "The event host's path is not associated with the intended token."
799            )
800            
801            let EmeraldCityVault = getAccount(emeraldCityTreasury).capabilities.borrow<&{FungibleToken.Receiver}>(tokenInfo.path)
802                                    ?? panic("Could not borrow the &{FungibleToken.Receiver} from Emerald City's Vault.")
803
804            assert(
805                EmeraldCityVault.getType().identifier == paymentType,
806                message: "Emerald City's path is not associated with the intended token."
807            )
808
809            let emeraldCityCut <- payment.withdraw(amount: payment.balance * royalty)
810
811            EmeraldCityVault.deposit(from: <- emeraldCityCut)
812            EventHostVault.deposit(from: <- payment)
813
814            let id = self.verifyAndMint(recipient: recipient, params: params)
815
816            emit FLOATPurchased(id: id, eventHost: self.host, eventId: self.eventId, recipient: recipient.owner!.address, serial: self.totalSupply - 1)
817        }
818
819        init (
820            _claimable: Bool,
821            _description: String, 
822            _extraMetadata: {String: AnyStruct},
823            _host: Address, 
824            _image: String, 
825            _name: String,
826            _transferrable: Bool,
827            _url: String,
828            _verifiers: {String: [{IVerifier}]}
829        ) {
830            self.claimable = _claimable
831            self.claimed = {}
832            self.currentHolders = {}
833            self.dateCreated = getCurrentBlock().timestamp
834            self.description = _description
835            self.eventId = self.uuid
836            self.extraMetadata = _extraMetadata
837            self.host = _host
838            self.image = _image
839            self.name = _name
840            self.transferrable = _transferrable
841            self.totalSupply = 0
842            self.url = _url
843            self.verifiers = _verifiers
844            self.groups = {}
845
846            FLOAT.totalFLOATEvents = FLOAT.totalFLOATEvents + 1
847            emit FLOATEventCreated(eventId: self.eventId, description: self.description, host: self.host, image: self.image, name: self.name, url: self.url)
848        }
849
850        // destroy() {
851        //     emit FLOATEventDestroyed(eventId: self.eventId, host: self.host, name: self.name)
852        // }
853    }
854
855    // DEPRECATED
856    access(all) resource Group {
857        access(all) let id: UInt64
858        access(all) let name: String
859        access(all) let image: String
860        access(all) let description: String
861        access(self) var events: {UInt64: Bool}
862        init() {
863            self.id = 0
864            self.name = ""
865            self.image = ""
866            self.description = ""
867            self.events = {}
868        }
869    }
870 
871    // 
872    // FLOATEvents
873    //
874
875    access(all) resource interface FLOATEventsPublic {}
876
877    // A "Collection" of FLOAT Events
878    access(all) resource FLOATEvents: FLOATEventsPublic, ViewResolver.ResolverCollection {
879        // All the FLOAT Events this collection stores
880        access(self) var events: @{UInt64: FLOATEvent}
881        // DEPRECATED
882        access(self) var groups: @{String: Group}
883
884        // Creates a new FLOAT Event
885        //
886        // Read below for a description on all the values and expectations here
887        //
888        // claimable: Do you want this FLOAT to be publicly claimable by users?
889        // transferrable: Should this FLOAT be transferrable or soulbound?
890        // url: A generic url to your FLOAT Event
891        // verifiers: An array of verifiers from FLOATVerifiers contract
892        // allowMultipleClaim: Should users be able to claim/receive multiple
893        // of this FLOAT?
894        // certificateType: Determines how the FLOAT is displayed on the FLOAT platform. Must be one of the 
895        // following or it will fail: "ticket", "medal", "certificate"
896        // visibilityMode: Determines how the FLOAT is displayed on the FLOAT platform. Must be one of the 
897        // following: "picture", "certificate"
898        // extraMetadata: Any extra metadata for your event. Here are some restrictions on the keys of this dictionary:
899            // userClaims: You cannot provide a userClaims key
900            // extraFloatMetadatas: You cannot provide a extraFloatMetadatas key
901            // certificateImage: Must either be nil or a String type
902            // backImage: The IPFS CID of what will display on the back of your FLOAT. Must either be nil or a String type
903            // eventType: Must either be nil or a String type
904        access(EventsOwner) fun createEvent(
905            claimable: Bool,
906            description: String,
907            image: String, 
908            name: String, 
909            transferrable: Bool,
910            url: String,
911            verifiers: [{IVerifier}],
912            allowMultipleClaim: Bool,
913            certificateType: String,
914            visibilityMode: String,
915            extraMetadata: {String: AnyStruct}
916        ): UInt64 {
917            pre {
918                certificateType == "ticket" || certificateType == "medal" || certificateType == "certificate": "You must either choose 'ticket', 'medal', or 'certificate' for certificateType. This is how your FLOAT will be displayed."
919                visibilityMode == "certificate" || visibilityMode == "picture": "You must either choose 'certificate' or 'picture' for visibilityMode. This is how your FLOAT will be displayed."
920                extraMetadata["userClaims"] == nil: "Cannot use userClaims key in extraMetadata."
921                extraMetadata["extraFloatMetadatas"] == nil: "Cannot use extraFloatMetadatas key in extraMetadata."
922                extraMetadata["certificateImage"] == nil || extraMetadata["certificateImage"]!.getType() == Type<String>(): "certificateImage must be a String or nil type."
923                extraMetadata["backImage"] == nil || extraMetadata["backImage"]!.getType() == Type<String>(): "backImage must be a String or nil type."
924                extraMetadata["eventType"] == nil || extraMetadata["eventType"]!.getType() == Type<String>(): "eventType must be a String or nil type."
925            }
926
927            let typedVerifiers: {String: [{IVerifier}]} = {}
928            for verifier in verifiers {
929                let identifier: String = verifier.getType().identifier
930                if typedVerifiers[identifier] == nil {
931                    typedVerifiers[identifier] = [verifier]
932                } else {
933                    typedVerifiers[identifier]!.append(verifier)
934                }
935            }
936
937            extraMetadata["allowMultipleClaim"] = allowMultipleClaim
938            extraMetadata["certificateType"] = certificateType
939            extraMetadata["visibilityMode"] = visibilityMode
940
941            let FLOATEvent <- create FLOATEvent(
942                _claimable: claimable,
943                _description: description, 
944                _extraMetadata: extraMetadata,
945                _host: self.owner!.address, 
946                _image: image, 
947                _name: name, 
948                _transferrable: transferrable,
949                _url: url,
950                _verifiers: typedVerifiers
951            )
952            let eventId: UInt64 = FLOATEvent.eventId
953            self.events[eventId] <-! FLOATEvent
954
955            return eventId
956        }
957
958        // Deletes an event.
959        access(EventsOwner) fun deleteEvent(eventId: UInt64) {
960            let eventRef = self.borrowEventRef(eventId: eventId) ?? panic("This FLOAT does not exist.")
961            destroy self.events.remove(key: eventId)
962        }
963
964        access(EventsOwner) fun borrowEventRef(eventId: UInt64): auth(EventOwner) &FLOATEvent? {
965            return &self.events[eventId]
966        }
967
968        // Get a public reference to the FLOATEvent
969        // so you can call some helpful getters
970        access(all) fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent? {
971            return &self.events[eventId] as &FLOATEvent?
972        }
973
974        access(all) view fun getIDs(): [UInt64] {
975            return self.events.keys
976        }
977
978        // Maps the eventId to the name of that
979        // event. Just a kind helper.
980        access(all) fun getAllEvents(): {UInt64: String} {
981            let answer: {UInt64: String} = {}
982            for id in self.events.keys {
983                let ref = (&self.events[id] as &FLOATEvent?)!
984                answer[id] = ref.name
985            }
986            return answer
987        }
988
989        init() {
990            self.events <- {}
991            self.groups <- {}
992        }
993    }
994
995    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
996        return <- create Collection()
997    }
998
999    access(all) fun createEmptyFLOATEventCollection(): @FLOATEvents {
1000        return <- create FLOATEvents()
1001    }
1002
1003    // A function to validate expected FLOAT metadata that must be in a 
1004    // certain format as to not cause aborts during expected casting
1005    access(all) fun validateExtraFloatMetadata(data: {String: AnyStruct}): Bool {
1006        if data.containsKey("medalType") {
1007            let medalType: String? = FLOAT.extraMetadataToStrOpt(data, "medalType")
1008            if medalType == nil || (medalType != "gold" && medalType != "silver" && medalType != "bronze" && medalType != "participation") {
1009                return false
1010            }
1011        }
1012        return true
1013    }
1014
1015    // Helper to cast dictionary value to String? type
1016    //
1017    // Note about all the casting going on:
1018    // You might be saying, "Why are you double force unwrapping 
1019    // medalType Jacob?" "Why would an unwrapped type still needed to be unwrapped?" 
1020    // The reason is because in Cadence dictionaries, you can encounter double optionals
1021    // where the actual type that lies in the value of a dictionary is itself
1022    // nil. In other words, it's possible to have `{ "jacob": nil }` in a dictionary.
1023    // So we force unwrap due to the dictionary, then unwrap the value within.
1024    // It will never abort because we have checked for nil above, which checks 
1025    // for both types of nil.
1026    access(all) fun extraMetadataToStrOpt(_ dict: {String: AnyStruct}, _ key: String): String? {
1027        // `dict[key] == nil` means:
1028        //    1. the key doesn't exist
1029        //    2. the value for the key is nil
1030        if dict[key] == nil || dict[key]!!.getType() != Type<String>() {
1031            return nil
1032        }
1033        return dict[key]!! as! String
1034    }
1035
1036    /// Function that returns all the Metadata Views implemented by a Non Fungible Token
1037    ///
1038    /// @return An array of Types defining the implemented views. This value will be used by
1039    ///         developers to know which parameter to pass to the resolveView() method.
1040    ///
1041    access(all) fun getViews(): [Type] {
1042        return [
1043            Type<MetadataViews.NFTCollectionData>(),
1044            Type<MetadataViews.NFTCollectionDisplay>()
1045        ]
1046    }
1047
1048    /// Function that resolves a metadata view for this contract.
1049    ///
1050    /// @param view: The Type of the desired view.
1051    /// @return A structure representing the requested view.
1052    ///
1053    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
1054        switch viewType {
1055            case Type<MetadataViews.NFTCollectionData>():
1056                return MetadataViews.NFTCollectionData(
1057                    storagePath: FLOAT.FLOATCollectionStoragePath,
1058                    publicPath: FLOAT.FLOATCollectionPublicPath,
1059                    publicCollection: Type<&Collection>(),
1060                    publicLinkedType: Type<&Collection>(),
1061                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
1062                        return <- FLOAT.createEmptyCollection(nftType: Type<@FLOAT.NFT>())
1063                    })
1064                )
1065            case Type<MetadataViews.NFTCollectionDisplay>():
1066                let squareMedia: MetadataViews.Media = MetadataViews.Media(
1067                    file: MetadataViews.HTTPFile(
1068                        url: "https://i.imgur.com/v0Njnnk.png"
1069                    ),
1070                    mediaType: "image"
1071                )
1072                let bannerMedia: MetadataViews.Media = MetadataViews.Media(
1073                    file: MetadataViews.HTTPFile(
1074                        url: "https://i.imgur.com/ETeVZZU.jpg"
1075                    ),
1076                    mediaType: "image"
1077                )
1078                return MetadataViews.NFTCollectionDisplay(
1079                    name: "FLOAT",
1080                    description: "FLOAT is a proof of attendance platform on the Flow blockchain.",
1081                    externalURL: MetadataViews.ExternalURL("https://floats.city"),
1082                    squareImage: squareMedia,
1083                    bannerImage: bannerMedia,
1084                    socials: {
1085                        "twitter": MetadataViews.ExternalURL("https://twitter.com/emerald_dao"),
1086                        "discord": MetadataViews.ExternalURL("https://discord.gg/emeraldcity")
1087                    }
1088                )
1089        }
1090        return nil
1091    }
1092
1093    access(all) view fun getContractViews(resourceType: Type?): [Type] {
1094        return [
1095            Type<MetadataViews.NFTCollectionData>(),
1096            Type<MetadataViews.NFTCollectionDisplay>()
1097        ]
1098    }
1099
1100    init() {
1101        self.totalSupply = 0
1102        self.totalFLOATEvents = 0
1103        emit ContractInitialized()
1104
1105        self.FLOATCollectionStoragePath = /storage/FLOATCollectionStoragePath
1106        self.FLOATCollectionPublicPath = /public/FLOATCollectionPublicPath
1107        self.FLOATEventsStoragePath = /storage/FLOATEventsStoragePath
1108        self.FLOATEventsPrivatePath = /private/FLOATEventsPrivatePath
1109        self.FLOATEventsPublicPath = /public/FLOATEventsPublicPath
1110
1111        // delete later
1112
1113        if self.account.storage.borrow<&FLOAT.Collection>(from: FLOAT.FLOATCollectionStoragePath) == nil {
1114            self.account.storage.save(<- create Collection(), to: FLOAT.FLOATCollectionStoragePath)
1115            let collectionCap = self.account.capabilities.storage.issue<&FLOAT.Collection>(FLOAT.FLOATCollectionStoragePath)
1116            self.account.capabilities.publish(collectionCap, at: FLOAT.FLOATCollectionPublicPath)
1117        }
1118
1119        if self.account.storage.borrow<&FLOAT.FLOATEvents>(from: FLOAT.FLOATEventsStoragePath) == nil {
1120            self.account.storage.save(<- FLOAT.createEmptyFLOATEventCollection(), to: FLOAT.FLOATEventsStoragePath)
1121            let eventsCap = self.account.capabilities.storage.issue<&FLOAT.FLOATEvents>(FLOAT.FLOATEventsStoragePath)
1122            self.account.capabilities.publish(eventsCap, at: FLOAT.FLOATEventsPublicPath)
1123        }
1124
1125        let FLOATEvents = self.account.storage.borrow<auth(EventsOwner) &FLOAT.FLOATEvents>(from: FLOAT.FLOATEventsStoragePath)
1126                        ?? panic("Could not borrow the FLOATEvents from the signer.")
1127
1128        var verifiers: [{FLOAT.IVerifier}] = []
1129
1130        let extraMetadata: {String: AnyStruct} = {}
1131
1132        extraMetadata["backImage"] = "bafkreihwra72f2sby4h2bswej4zzrmparb6jy55ygjrymxjk572tjziatu"
1133        extraMetadata["eventType"] = "course"
1134        extraMetadata["certificateImage"] = "bafkreidcwg6jkcsugms2jtv6suwk2cao2ij6y57mopz4p4anpnvwswv2ku"
1135
1136        FLOATEvents.createEvent(claimable: true, description: "Test description for the upcoming Flow Hackathon. This is soooo fun! Woohoo!", image: "bafybeifpsnwb2vkz4p6nxhgsbwgyslmlfd7jyicx5ukbj3tp7qsz7myzrq", name: "Flow Hackathon", transferrable: true, url: "", verifiers: verifiers, allowMultipleClaim: false, certificateType: "medal", visibilityMode: "certificate", extraMetadata: extraMetadata)
1137        
1138        extraMetadata["backImage"] = "bafkreihwra72f2sby4h2bswej4zzrmparb6jy55ygjrymxjk572tjziatu"
1139        extraMetadata["eventType"] = "discordMeeting"
1140        extraMetadata["certificateImage"] = "bafkreidcwg6jkcsugms2jtv6suwk2cao2ij6y57mopz4p4anpnvwswv2ku"
1141
1142        FLOATEvents.createEvent(claimable: true, description: "Test description for a Discord meeting. This is soooo fun! Woohoo!", image: "bafybeifpsnwb2vkz4p6nxhgsbwgyslmlfd7jyicx5ukbj3tp7qsz7myzrq", name: "Discord Meeting", transferrable: true, url: "", verifiers: verifiers, allowMultipleClaim: false, certificateType: "ticket", visibilityMode: "picture", extraMetadata: extraMetadata)
1143    }
1144}
1145