Smart Contract
FLOAT
A.2d4c3caffbeab845.FLOAT
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