Smart Contract
MusicPeaksMembershipToken
A.a02c28dc0aa50c18.MusicPeaksMembershipToken
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 MusicPeaksMembershipToken. 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
21// Lastly, we implemented GrantedAccountAccess.cdc, which allows you to specify
22// someone else can control your account (in the context of FLOAT). This
23// is specifically designed for teams to be able to handle one "host" on the
24// FLOAT platform so all the company's events are under one account.
25// This is mainly used to give other people access to your FLOATEvents resource,
26// and allow them to mint for you and control Admin operations on your events.
27
28// For more info on GrantedAccountAccess, see GrantedAccountAccess.cdc
29
30import NonFungibleToken from 0x1d7e57aa55817448
31import MetadataViews from 0x1d7e57aa55817448
32import FindViews from 0x097bafa4e0b48eef
33import GrantedAccountAccess from 0xa02c28dc0aa50c18
34
35pub contract MusicPeaksMembershipToken: NonFungibleToken {
36
37 /***********************************************/
38 /******************** PATHS ********************/
39 /***********************************************/
40
41 pub let FLOATCollectionStoragePath: StoragePath
42 pub let FLOATCollectionPublicPath: PublicPath
43 pub let FLOATEventsStoragePath: StoragePath
44 pub let FLOATEventsPublicPath: PublicPath
45 pub let FLOATEventsPrivatePath: PrivatePath
46
47 /************************************************/
48 /******************** EVENTS ********************/
49 /************************************************/
50
51 pub event ContractInitialized()
52 pub event FLOATMinted(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, recipient: Address, serial: UInt64)
53 pub event FLOATClaimed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, eventName: String, recipient: Address, serial: UInt64)
54 pub event FLOATDestroyed(id: UInt64, eventHost: Address, eventId: UInt64, eventImage: String, serial: UInt64)
55 pub event FLOATTransferred(id: UInt64, eventHost: Address, eventId: UInt64, newOwner: Address?, serial: UInt64)
56 pub event FLOATEventCreated(eventId: UInt64, description: String, host: Address, image: String, name: String, url: String)
57 pub event FLOATEventDestroyed(eventId: UInt64, host: Address, name: String)
58
59 pub event Deposit(id: UInt64, to: Address?)
60 pub event Withdraw(id: UInt64, from: Address?)
61
62 /***********************************************/
63 /******************** STATE ********************/
64 /***********************************************/
65
66 // The total amount of FLOATs that have ever been
67 // created (does not go down when a FLOAT is destroyed)
68 pub var totalSupply: UInt64
69 // The total amount of FLOATEvents that have ever been
70 // created (does not go down when a FLOATEvent is destroyed)
71 pub var totalFLOATEvents: UInt64
72
73 /***********************************************/
74 /**************** FUNCTIONALITY ****************/
75 /***********************************************/
76
77 // A helpful wrapper to contain an address,
78 // the id of a FLOAT, and its serial
79 pub struct TokenIdentifier {
80 pub let id: UInt64
81 pub let address: Address
82 pub let serial: UInt64
83
84 init(_id: UInt64, _address: Address, _serial: UInt64) {
85 self.id = _id
86 self.address = _address
87 self.serial = _serial
88 }
89 }
90
91 pub struct TokenInfo {
92 pub let path: PublicPath
93 pub let price: UFix64
94
95 init(_path: PublicPath, _price: UFix64) {
96 self.path = _path
97 self.price = _price
98 }
99 }
100
101 // Represents a FLOAT
102 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
103 // The `uuid` of this resource
104 pub let id: UInt64
105
106 // Some of these are also duplicated on the event,
107 // but it's necessary to put them here as well
108 // in case the FLOATEvent host deletes the event
109 pub let dateReceived: UFix64
110 pub let eventDescription: String
111 pub let eventHost: Address
112 pub let eventId: UInt64
113 pub let eventImage: String
114 pub let eventName: String
115 pub let originalRecipient: Address
116 pub let serial: UInt64
117
118 // A capability that points to the FLOATEvents this FLOAT is from.
119 // There is a chance the event host unlinks their event from
120 // the public, in which case it's impossible to know details
121 // about the event. Which is fine, since we store the
122 // crucial data to know about the FLOAT in the FLOAT itself.
123 pub let eventsCap: Capability<&FLOATEvents{FLOATEventsPublic, MetadataViews.ResolverCollection}>
124
125 // Helper function to get the metadata of the event
126 // this MusicPeaksMembershipToken is from.
127 pub fun getEventMetadata(): &FLOATEvent{FLOATEventPublic}? {
128 if let events = self.eventsCap.borrow() {
129 return events.borrowPublicEventRef(eventId: self.eventId)
130 }
131 return nil
132 }
133
134 // This is for the MetdataStandard
135 pub fun getViews(): [Type] {
136 let supportedViews = [
137 Type<MetadataViews.Display>(),
138 Type<MetadataViews.Royalties>(),
139 Type<MetadataViews.ExternalURL>(),
140 Type<MetadataViews.NFTCollectionData>(),
141 Type<MetadataViews.NFTCollectionDisplay>(),
142 Type<MetadataViews.Serial>(),
143 Type<TokenIdentifier>()
144 ]
145
146 if self.getEventMetadata()?.transferrable == false {
147 supportedViews.append(Type<FindViews.SoulBound>())
148 }
149
150 return supportedViews
151 }
152
153 // This is for the MetdataStandard
154 pub fun resolveView(_ view: Type): AnyStruct? {
155 switch view {
156 case Type<MetadataViews.Display>():
157 return MetadataViews.Display(
158 name: self.eventName,
159 description: self.eventDescription,
160 thumbnail: MetadataViews.HTTPFile(url: "https://musicpeaks.mypinata.cloud/ipfs/".concat(self.eventImage))
161 )
162 case Type<MetadataViews.Royalties>():
163 return MetadataViews.Royalties([])
164 case Type<MetadataViews.ExternalURL>():
165 return MetadataViews.ExternalURL("https://musicpeaks.com/".concat(self.owner!.address.toString()).concat("/float/").concat(self.id.toString()))
166 case Type<MetadataViews.NFTCollectionData>():
167 return MetadataViews.NFTCollectionData(
168 storagePath: MusicPeaksMembershipToken.FLOATCollectionStoragePath,
169 publicPath: MusicPeaksMembershipToken.FLOATCollectionPublicPath,
170 providerPath: /private/MusicPeaksMembershipTokenCollectionPrivatePath,
171 publicCollection: Type<&Collection{CollectionPublic}>(),
172 publicLinkedType: Type<&Collection{CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(),
173 providerLinkedType: Type<&Collection{CollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Provider, MetadataViews.ResolverCollection}>(),
174 createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
175 return <- MusicPeaksMembershipToken.createEmptyCollection()
176 })
177 )
178 case Type<MetadataViews.NFTCollectionDisplay>():
179 let squareMedia = MetadataViews.Media(
180 file: MetadataViews.HTTPFile(
181 url: "https://rockpeaksassets.s3.amazonaws.com/musicpeaks/MP-Square-Logo-Purple-White.png"
182 ),
183 mediaType: "image"
184 )
185 let bannerMedia = MetadataViews.Media(
186 file: MetadataViews.HTTPFile(
187 url: "https://rockpeaksassets.s3.amazonaws.com/musicpeaks/MP-Banner-Logo.png"
188 ),
189 mediaType: "image"
190 )
191 return MetadataViews.NFTCollectionDisplay(
192 name: "MusicPeaksMembershipToken",
193 description: "MusicPeaksMembershipToken is a proof of attendance platform on the Flow blockchain.",
194 externalURL: MetadataViews.ExternalURL("https://musicpeaks.com/membership/".concat(self.eventHost.toString()).concat("/").concat(self.eventId.toString())),
195 squareImage: squareMedia,
196 bannerImage: bannerMedia,
197 socials: {
198 "twitter": MetadataViews.ExternalURL("https://twitter.com/MusicPeaks")
199 }
200 )
201 case Type<MetadataViews.Serial>():
202 return MetadataViews.Serial(
203 self.serial
204 )
205 case Type<TokenIdentifier>():
206 return TokenIdentifier(
207 _id: self.id,
208 _address: self.owner!.address,
209 _serial: self.serial
210 )
211 case Type<FindViews.SoulBound>():
212 if self.getEventMetadata()?.transferrable == false {
213 return FindViews.SoulBound(
214 "This FLOAT is soulbound because the event host toggled off transferring."
215 )
216 }
217 return nil
218 }
219
220 return nil
221 }
222
223 init(_eventDescription: String, _eventHost: Address, _eventId: UInt64, _eventImage: String, _eventName: String, _originalRecipient: Address, _serial: UInt64) {
224 self.id = self.uuid
225 self.dateReceived = getCurrentBlock().timestamp
226 self.eventDescription = _eventDescription
227 self.eventHost = _eventHost
228 self.eventId = _eventId
229 self.eventImage = _eventImage
230 self.eventName = _eventName
231 self.originalRecipient = _originalRecipient
232 self.serial = _serial
233
234 // Stores a capability to the FLOATEvents of its creator
235 self.eventsCap = getAccount(_eventHost)
236 .getCapability<&FLOATEvents{FLOATEventsPublic, MetadataViews.ResolverCollection}>(MusicPeaksMembershipToken.FLOATEventsPublicPath)
237
238 emit FLOATMinted(
239 id: self.id,
240 eventHost: _eventHost,
241 eventId: _eventId,
242 eventImage: _eventImage,
243 recipient: _originalRecipient,
244 serial: _serial
245 )
246
247 MusicPeaksMembershipToken.totalSupply = MusicPeaksMembershipToken.totalSupply + 1
248 }
249
250 destroy() {
251 // If the FLOATEvent owner decided to unlink their public reference
252 // for some reason (heavily recommend against it), their records
253 // of who owns the MusicPeaksMembershipToken is going to be messed up. But that is their
254 // fault. We shouldn't let that prevent the user from deleting the MusicPeaksMembershipToken.
255 if let floatEvent: &FLOATEvent{FLOATEventPublic} = self.getEventMetadata() {
256 floatEvent.updateFLOATHome(id: self.id, serial: self.serial, owner: nil)
257 }
258 emit FLOATDestroyed(
259 id: self.id,
260 eventHost: self.eventHost,
261 eventId: self.eventId,
262 eventImage: self.eventImage,
263 serial: self.serial
264 )
265 }
266 }
267
268 // A public interface for people to call into our Collection
269 pub resource interface CollectionPublic {
270 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
271 pub fun borrowFLOAT(id: UInt64): &NFT?
272 pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver}
273 pub fun deposit(token: @NonFungibleToken.NFT)
274 pub fun getIDs(): [UInt64]
275 pub fun getAllIDs(): [UInt64]
276 pub fun ownedIdsFromEvent(eventId: UInt64): [UInt64]
277 }
278
279 // A Collection that holds all of the users FLOATs.
280 // Withdrawing is not allowed. You can only transfer.
281 pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection, CollectionPublic {
282 // Maps a FLOAT id to the FLOAT itself
283 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
284 // Maps an eventId to the ids of FLOATs that
285 // this user owns from that event. It's possible
286 // for it to be out of sync until June 2022 spork,
287 // but it is used merely as a helper, so that's okay.
288 access(account) var events: {UInt64: {UInt64: Bool}}
289
290 // Deposits a FLOAT to the collection
291 pub fun deposit(token: @NonFungibleToken.NFT) {
292 let nft <- token as! @NFT
293 let id = nft.id
294 let eventId = nft.eventId
295
296 // Update self.events[eventId] to have
297 // this FLOAT's id in it
298 if self.events[eventId] == nil {
299 self.events[eventId] = {id: true}
300 } else {
301 self.events[eventId]!.insert(key: id, true)
302 }
303
304 // Try to update the FLOATEvent's current holders. This will
305 // not work if they unlinked their FLOATEvent to the public,
306 // and the data will go out of sync. But that is their fault.
307 if let floatEvent: &FLOATEvent{FLOATEventPublic} = nft.getEventMetadata() {
308 floatEvent.updateFLOATHome(id: id, serial: nft.serial, owner: self.owner!.address)
309 }
310
311 emit Deposit(id: id, to: self.owner!.address)
312 self.ownedNFTs[id] <-! nft
313 }
314
315 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
316 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("You do not own this FLOAT in your collection")
317 let nft <- token as! @NFT
318
319 // Update self.events[eventId] to not
320 // have this FLOAT's id in it
321 self.events[nft.eventId]!.remove(key: withdrawID)
322
323 // Try to update the FLOATEvent's current holders. This will
324 // not work if they unlinked their FLOATEvent to the public,
325 // and the data will go out of sync. But that is their fault.
326 //
327 // Additionally, this checks if the FLOATEvent host wanted this
328 // FLOAT to be transferrable. Secondary marketplaces will use this
329 // withdraw function, so if the FLOAT is not transferrable,
330 // you can't sell it there.
331 if let floatEvent: &FLOATEvent{FLOATEventPublic} = nft.getEventMetadata() {
332 assert(
333 floatEvent.transferrable,
334 message: "This FLOAT is not transferrable."
335 )
336 floatEvent.updateFLOATHome(id: withdrawID, serial: nft.serial, owner: nil)
337 }
338
339 emit Withdraw(id: withdrawID, from: self.owner!.address)
340 return <- nft
341 }
342
343 pub fun delete(id: UInt64) {
344 let token <- self.ownedNFTs.remove(key: id) ?? panic("You do not own this FLOAT in your collection")
345 let nft <- token as! @NFT
346
347 // Update self.events[eventId] to not
348 // have this FLOAT's id in it
349 self.events[nft.eventId]!.remove(key: id)
350
351 destroy nft
352 }
353
354 // Only returns the FLOATs for which we can still
355 // access data about their event.
356 pub fun getIDs(): [UInt64] {
357 let ids: [UInt64] = []
358 for key in self.ownedNFTs.keys {
359 let nftRef = self.borrowFLOAT(id: key)!
360 if nftRef.eventsCap.check() {
361 ids.append(key)
362 }
363 }
364 return ids
365 }
366
367 // Returns all the FLOATs ids
368 pub fun getAllIDs(): [UInt64] {
369 return self.ownedNFTs.keys
370 }
371
372 // Returns an array of ids that belong to
373 // the passed in eventId
374 //
375 // It's possible for FLOAT ids to be present that
376 // shouldn't be if people tried to withdraw directly
377 // from `ownedNFTs` (not possible after June 2022 spork),
378 // but this makes sure the returned
379 // ids are all actually owned by this account.
380 pub fun ownedIdsFromEvent(eventId: UInt64): [UInt64] {
381 let answer: [UInt64] = []
382 if let idsInEvent = self.events[eventId]?.keys {
383 for id in idsInEvent {
384 if self.ownedNFTs[id] != nil {
385 answer.append(id)
386 }
387 }
388 }
389 return answer
390 }
391
392 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
393 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
394 }
395
396 pub fun borrowFLOAT(id: UInt64): &NFT? {
397 if self.ownedNFTs[id] != nil {
398 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
399 return ref as! &NFT
400 }
401 return nil
402 }
403
404 pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} {
405 let tokenRef = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
406 let nftRef = tokenRef as! &NFT
407 return nftRef as &{MetadataViews.Resolver}
408 }
409
410 init() {
411 self.ownedNFTs <- {}
412 self.events = {}
413 }
414
415 destroy() {
416 destroy self.ownedNFTs
417 }
418 }
419
420 // An interface that every "verifier" must implement.
421 // A verifier is one of the options on the FLOAT Event page,
422 // for example, a "time limit," or a "limited" number of
423 // FLOATs that can be claimed.
424 // All the current verifiers can be seen inside FLOATVerifiers.cdc
425 pub struct interface IVerifier {
426 // A function every verifier must implement.
427 // Will have `assert`s in it to make sure
428 // the user fits some criteria.
429 access(account) fun verify(_ params: {String: AnyStruct})
430 }
431
432 // A public interface to read the FLOATEvent
433 pub resource interface FLOATEventPublic {
434 pub var claimable: Bool
435 pub let dateCreated: UFix64
436 pub let description: String
437 pub let eventId: UInt64
438 pub let host: Address
439 pub let image: String
440 pub let name: String
441 pub var totalSupply: UInt64
442 pub var transferrable: Bool
443 pub let url: String
444 pub fun claim(recipient: &Collection, params: {String: AnyStruct})
445 pub fun getClaimed(): {Address: TokenIdentifier}
446 pub fun getCurrentHolders(): {UInt64: TokenIdentifier}
447 pub fun getCurrentHolder(serial: UInt64): TokenIdentifier?
448 pub fun getExtraMetadata(): {String: AnyStruct}
449 pub fun getVerifiers(): {String: [{IVerifier}]}
450 pub fun getGroups(): [String]
451 pub fun getPrices(): {String: TokenInfo}?
452 pub fun hasClaimed(account: Address): TokenIdentifier?
453
454 access(account) fun updateFLOATHome(id: UInt64, serial: UInt64, owner: Address?)
455 }
456
457 //
458 // FLOATEvent
459 //
460 pub resource FLOATEvent: FLOATEventPublic, MetadataViews.Resolver {
461 // Whether or not users can claim from our event (can be toggled
462 // at any time)
463 pub var claimable: Bool
464 // Maps an address to the FLOAT they claimed
465 access(account) var claimed: {Address: TokenIdentifier}
466 // Maps a serial to the person who theoretically owns
467 // that MusicPeaksMembershipToken. Must be serial --> TokenIdentifier because
468 // it's possible someone has multiple FLOATs from this event.
469 access(account) var currentHolders: {UInt64: TokenIdentifier}
470 pub let dateCreated: UFix64
471 pub let description: String
472 // This is equal to this resource's uuid
473 pub let eventId: UInt64
474 access(account) var extraMetadata: {String: AnyStruct}
475 // The groups that this FLOAT Event belongs to (groups
476 // are within the FLOATEvents resource)
477 access(account) var groups: {String: Bool}
478 // Who created this FLOAT Event
479 pub let host: Address
480 // The image of the FLOAT Event
481 pub let image: String
482 // The name of the FLOAT Event
483 pub let name: String
484 // The total number of FLOATs that have been
485 // minted from this event
486 pub var totalSupply: UInt64
487 // Whether or not the FLOATs that users own
488 // from this event can be transferred on the
489 // FLOAT platform itself (transferring allowed
490 // elsewhere)
491 pub var transferrable: Bool
492 // A url of where the event took place
493 pub let url: String
494 // A list of verifiers this FLOAT Event contains.
495 // Will be used every time someone "claims" a FLOAT
496 // to see if they pass the requirements
497 access(account) let verifiers: {String: [{IVerifier}]}
498
499 /***************** Setters for the Event Owner *****************/
500
501 // Toggles claiming on/off
502 pub fun toggleClaimable(): Bool {
503 self.claimable = !self.claimable
504 return self.claimable
505 }
506
507 // Toggles transferring on/off
508 pub fun toggleTransferrable(): Bool {
509 self.transferrable = !self.transferrable
510 return self.transferrable
511 }
512
513 // Updates the metadata in case you want
514 // to add something.
515 pub fun updateMetadata(newExtraMetadata: {String: AnyStruct}) {
516 for key in newExtraMetadata.keys {
517 self.extraMetadata[key] = newExtraMetadata[key]
518 }
519 }
520
521 /***************** Setters for the Contract Only *****************/
522
523 // Called if a user moves their FLOAT to another location.
524 // Needed so we can keep track of who currently has it.
525 access(account) fun updateFLOATHome(id: UInt64, serial: UInt64, owner: Address?) {
526 if owner == nil {
527 self.currentHolders.remove(key: serial)
528 } else {
529 self.currentHolders[serial] = TokenIdentifier(
530 _id: id,
531 _address: owner!,
532 _serial: serial
533 )
534 }
535 emit FLOATTransferred(id: id, eventHost: self.host, eventId: self.eventId, newOwner: owner, serial: serial)
536 }
537
538 // Adds this FLOAT Event to a group
539 access(account) fun addToGroup(groupName: String) {
540 self.groups[groupName] = true
541 }
542
543 // Removes this FLOAT Event to a group
544 access(account) fun removeFromGroup(groupName: String) {
545 self.groups.remove(key: groupName)
546 }
547
548 /***************** Getters (all exposed to the public) *****************/
549
550 // Returns info about the FLOAT that this account claimed
551 // (if any)
552 pub fun hasClaimed(account: Address): TokenIdentifier? {
553 return self.claimed[account]
554 }
555
556 // This is a guarantee that the person owns the FLOAT
557 // with the passed in serial
558 pub fun getCurrentHolder(serial: UInt64): TokenIdentifier? {
559 pre {
560 self.currentHolders[serial] != nil:
561 "This serial has not been created yet."
562 }
563 let data = self.currentHolders[serial]!
564 let collection = getAccount(data.address).getCapability(MusicPeaksMembershipToken.FLOATCollectionPublicPath).borrow<&Collection{CollectionPublic}>()
565 if collection?.borrowFLOAT(id: data.id) != nil {
566 return data
567 }
568
569 return nil
570 }
571
572 // Returns an accurate dictionary of all the
573 // claimers
574 pub fun getClaimed(): {Address: TokenIdentifier} {
575 return self.claimed
576 }
577
578 // This dictionary may be slightly off if for some
579 // reason the FLOATEvents owner ever unlinked their
580 // resource from the public.
581 // Use `getCurrentHolder(serial: UInt64)` to truly
582 // verify if someone holds that serial.
583 pub fun getCurrentHolders(): {UInt64: TokenIdentifier} {
584 return self.currentHolders
585 }
586
587 pub fun getExtraMetadata(): {String: AnyStruct} {
588 return self.extraMetadata
589 }
590
591 // Gets all the verifiers that will be used
592 // for claiming
593 pub fun getVerifiers(): {String: [{IVerifier}]} {
594 return self.verifiers
595 }
596
597 pub fun getGroups(): [String] {
598 return self.groups.keys
599 }
600
601 pub fun getViews(): [Type] {
602 return [
603 Type<MetadataViews.Display>()
604 ]
605 }
606
607 pub fun getPrices(): {String: TokenInfo}? {
608 if let prices = self.extraMetadata["prices"] {
609 return prices as! {String: TokenInfo}
610 }
611 return nil
612 }
613
614 pub fun resolveView(_ view: Type): AnyStruct? {
615 switch view {
616 case Type<MetadataViews.Display>():
617 return MetadataViews.Display(
618 name: self.name,
619 description: self.description,
620 thumbnail: MetadataViews.IPFSFile(cid: self.image, path: nil)
621 )
622 }
623
624 return nil
625 }
626
627 /****************** Getting a FLOAT ******************/
628
629 // Will not panic if one of the recipients has already claimed.
630 // It will just skip them.
631 pub fun batchMint(recipients: [&Collection{NonFungibleToken.CollectionPublic}]) {
632 for recipient in recipients {
633 if self.claimed[recipient.owner!.address] == nil {
634 self.mint(recipient: recipient, params: {})
635 }
636 }
637 }
638
639 // Used to give a person a FLOAT from this event.
640 // Used as a helper function for `claim`, but can also be
641 // used by the event owner and shared accounts to
642 // mint directly to a user.
643 //
644 // If the event owner directly mints to a user, it does not
645 // run the verifiers on the user. It bypasses all of them.
646 //
647 // Return the id of the FLOAT it minted
648 pub fun mint(recipient: &Collection{NonFungibleToken.CollectionPublic}, params: {String: AnyStruct}): UInt64 {
649 pre {
650 self.claimed[recipient.owner!.address] == nil:
651 "This person already claimed their FLOAT!"
652 }
653 let recipientAddr: Address = recipient.owner!.address
654 let serial = self.totalSupply
655
656 let token <- create NFT(
657 _eventDescription: self.description,
658 _eventHost: self.host,
659 _eventId: self.eventId,
660 _eventImage: self.image,
661 _eventName: self.name,
662 _originalRecipient: recipientAddr,
663 _serial: serial
664 )
665 let id = token.id
666 // Saves the claimer
667 self.claimed[recipientAddr] = TokenIdentifier(
668 _id: id,
669 _address: recipientAddr,
670 _serial: serial
671 )
672 // Saves the claimer as the current holder
673 // of the newly minted FLOAT
674 self.currentHolders[serial] = TokenIdentifier(
675 _id: id,
676 _address: recipientAddr,
677 _serial: serial
678 )
679
680 let metadata: {String: AnyStruct} = {}
681 let currentBlock = getCurrentBlock()
682 metadata["mintedTime"] = currentBlock.timestamp
683 if params.containsKey("ipfsUrl") {
684 let ipfsUrl = params!["ipfsUrl"]! as! String
685 metadata["ipfsUrl"] = ipfsUrl
686 }
687 self.extraMetadata[id.toString()] = metadata
688
689 self.totalSupply = self.totalSupply + 1
690 recipient.deposit(token: <- token)
691 return id
692 }
693
694 access(account) fun verifyAndMint(recipient: &Collection, params: {String: AnyStruct}): UInt64 {
695 params["event"] = &self as &FLOATEvent{FLOATEventPublic}
696 params["claimee"] = recipient.owner!.address
697
698 // Runs a loop over all the verifiers that this FLOAT Events
699 // implements. For example, "Limited", "Timelock", "Secret", etc.
700 // All the verifiers are in the FLOATVerifiers.cdc contract
701 for identifier in self.verifiers.keys {
702 let typedModules = (&self.verifiers[identifier] as &[{IVerifier}]?)!
703 var i = 0
704 while i < typedModules.length {
705 let verifier = &typedModules[i] as &{IVerifier}
706 verifier.verify(params)
707 i = i + 1
708 }
709 }
710
711 // You're good to go.
712 let id = self.mint(recipient: recipient, params: params)
713
714 emit FLOATClaimed(
715 id: id,
716 eventHost: self.host,
717 eventId: self.eventId,
718 eventImage: self.image,
719 eventName: self.name,
720 recipient: recipient.owner!.address,
721 serial: self.totalSupply - 1
722 )
723 return id
724 }
725
726 // For the public to claim FLOATs. Must be claimable to do so.
727 // You can pass in `params` that will be forwarded to the
728 // customized `verify` function of the verifier.
729 //
730 // For example, the FLOAT platform allows event hosts
731 // to specify a secret phrase. That secret phrase will
732 // be passed in the `params`.
733 pub fun claim(recipient: &Collection, params: {String: AnyStruct}) {
734 pre {
735 self.getPrices() == nil:
736 "You need to purchase this MusicPeaksMembershipToken."
737 self.claimed[recipient.owner!.address] == nil:
738 "This person already claimed their FLOAT!"
739 self.claimable:
740 "This FLOATEvent is not claimable, and thus not currently active."
741 }
742
743 self.verifyAndMint(recipient: recipient, params: params)
744 }
745
746 init (
747 _claimable: Bool,
748 _description: String,
749 _extraMetadata: {String: AnyStruct},
750 _host: Address,
751 _image: String,
752 _name: String,
753 _transferrable: Bool,
754 _url: String,
755 _verifiers: {String: [{IVerifier}]},
756 ) {
757 self.claimable = _claimable
758 self.claimed = {}
759 self.currentHolders = {}
760 self.dateCreated = getCurrentBlock().timestamp
761 self.description = _description
762 self.eventId = self.uuid
763 self.extraMetadata = _extraMetadata
764 self.groups = {}
765 self.host = _host
766 self.image = _image
767 self.name = _name
768 self.transferrable = _transferrable
769 self.totalSupply = 0
770 self.url = _url
771 self.verifiers = _verifiers
772
773 MusicPeaksMembershipToken.totalFLOATEvents = MusicPeaksMembershipToken.totalFLOATEvents + 1
774 emit FLOATEventCreated(eventId: self.eventId, description: self.description, host: self.host, image: self.image, name: self.name, url: self.url)
775 }
776
777 destroy() {
778 emit FLOATEventDestroyed(eventId: self.eventId, host: self.host, name: self.name)
779 }
780 }
781
782 // A container of FLOAT Events (maybe because they're similar to
783 // one another, or an event host wants to list all their AMAs together, etc).
784 pub resource Group {
785 pub let id: UInt64
786 pub let name: String
787 pub let image: String
788 pub let description: String
789 // All the FLOAT Events that belong
790 // to this group.
791 access(account) var events: {UInt64: Bool}
792
793 access(account) fun addEvent(eventId: UInt64) {
794 self.events[eventId] = true
795 }
796
797 access(account) fun removeEvent(eventId: UInt64) {
798 self.events.remove(key: eventId)
799 }
800
801 pub fun getEvents(): [UInt64] {
802 return self.events.keys
803 }
804
805 init(_name: String, _image: String, _description: String) {
806 self.id = self.uuid
807 self.name = _name
808 self.image = _image
809 self.description = _description
810 self.events = {}
811 }
812 }
813
814 //
815 // FLOATEvents
816 //
817 pub resource interface FLOATEventsPublic {
818 // Public Getters
819 pub fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent{FLOATEventPublic}?
820 pub fun getAllEvents(): {UInt64: String}
821 pub fun getIDs(): [UInt64]
822 pub fun getGroup(groupName: String): &Group?
823 pub fun getGroups(): [String]
824 // Account Getters
825 access(account) fun borrowEventsRef(): &FLOATEvents
826 }
827
828 // A "Collection" of FLOAT Events
829 pub resource FLOATEvents: FLOATEventsPublic, MetadataViews.ResolverCollection {
830 // All the FLOAT Events this collection stores
831 access(account) var events: @{UInt64: FLOATEvent}
832 // All the Groups this collection stores
833 access(account) var groups: @{String: Group}
834
835 // Creates a new FLOAT Event by passing in some basic parameters
836 // and a list of all the verifiers this event must abide by
837 pub fun createEvent(
838 claimable: Bool,
839 description: String,
840 image: String,
841 name: String,
842 transferrable: Bool,
843 url: String,
844 verifiers: [{IVerifier}],
845 _ extraMetadata: {String: AnyStruct},
846 initialGroups: [String]
847 ): UInt64 {
848 let typedVerifiers: {String: [{IVerifier}]} = {}
849 for verifier in verifiers {
850 let identifier = verifier.getType().identifier
851 if typedVerifiers[identifier] == nil {
852 typedVerifiers[identifier] = [verifier]
853 } else {
854 typedVerifiers[identifier]!.append(verifier)
855 }
856 }
857
858 let FLOATEvent <- create FLOATEvent(
859 _claimable: claimable,
860 _description: description,
861 _extraMetadata: extraMetadata,
862 _host: self.owner!.address,
863 _image: image,
864 _name: name,
865 _transferrable: transferrable,
866 _url: url,
867 _verifiers: typedVerifiers
868 )
869 let eventId = FLOATEvent.eventId
870 self.events[eventId] <-! FLOATEvent
871
872 for groupName in initialGroups {
873 self.addEventToGroup(groupName: groupName, eventId: eventId)
874 }
875 return eventId
876 }
877
878 // Deletes an event. Also makes sure to remove
879 // the event from all the groups its in.
880 pub fun deleteEvent(eventId: UInt64) {
881 let eventRef = self.borrowEventRef(eventId: eventId) ?? panic("This FLOAT does not exist.")
882 for groupName in eventRef.getGroups() {
883 let groupRef = (&self.groups[groupName] as &Group?)!
884 groupRef.removeEvent(eventId: eventId)
885 }
886 destroy self.events.remove(key: eventId)
887 }
888
889 pub fun createGroup(groupName: String, image: String, description: String) {
890 pre {
891 self.groups[groupName] == nil: "A group with this name already exists."
892 }
893 self.groups[groupName] <-! create Group(_name: groupName, _image: image, _description: description)
894 }
895
896 // Deletes a group. Also makes sure to remove
897 // the group from all the events that use it.
898 pub fun deleteGroup(groupName: String) {
899 let eventsInGroup = self.groups[groupName]?.getEvents()
900 ?? panic("This Group does not exist.")
901 for eventId in eventsInGroup {
902 let ref = (&self.events[eventId] as &FLOATEvent?)!
903 ref.removeFromGroup(groupName: groupName)
904 }
905 destroy self.groups.remove(key: groupName)
906 }
907
908 // Adds an event to a group. Also adds the group
909 // to the event.
910 pub fun addEventToGroup(groupName: String, eventId: UInt64) {
911 pre {
912 self.groups[groupName] != nil: "This group does not exist."
913 self.events[eventId] != nil: "This event does not exist."
914 }
915 let groupRef = (&self.groups[groupName] as &Group?)!
916 groupRef.addEvent(eventId: eventId)
917
918 let eventRef = self.borrowEventRef(eventId: eventId)!
919 eventRef.addToGroup(groupName: groupName)
920 }
921
922 // Simply takes the event away from the group
923 pub fun removeEventFromGroup(groupName: String, eventId: UInt64) {
924 pre {
925 self.groups[groupName] != nil: "This group does not exist."
926 self.events[eventId] != nil: "This event does not exist."
927 }
928 let groupRef = (&self.groups[groupName] as &Group?)!
929 groupRef.removeEvent(eventId: eventId)
930
931 let eventRef = self.borrowEventRef(eventId: eventId)!
932 eventRef.removeFromGroup(groupName: groupName)
933 }
934
935 pub fun getGroup(groupName: String): &Group? {
936 return &self.groups[groupName] as &Group?
937 }
938
939 pub fun getGroups(): [String] {
940 return self.groups.keys
941 }
942
943 // Only accessible to people who share your account.
944 // If `fromHost` has allowed you to share your account
945 // in the GrantedAccountAccess.cdc contract, you can get a reference
946 // to their FLOATEvents here and do pretty much whatever you want.
947 pub fun borrowSharedRef(fromHost: Address): &FLOATEvents {
948 let sharedInfo = getAccount(fromHost).getCapability(GrantedAccountAccess.InfoPublicPath)
949 .borrow<&GrantedAccountAccess.Info{GrantedAccountAccess.InfoPublic}>()
950 ?? panic("Cannot borrow the InfoPublic from the host")
951 assert(
952 sharedInfo.isAllowed(account: self.owner!.address),
953 message: "This account owner does not share their account with you."
954 )
955 let otherFLOATEvents = getAccount(fromHost).getCapability(MusicPeaksMembershipToken.FLOATEventsPublicPath)
956 .borrow<&FLOATEvents{FLOATEventsPublic}>()
957 ?? panic("Could not borrow the public FLOATEvents.")
958 return otherFLOATEvents.borrowEventsRef()
959 }
960
961 // Only used for the above function.
962 access(account) fun borrowEventsRef(): &FLOATEvents {
963 return &self as &FLOATEvents
964 }
965
966 pub fun borrowEventRef(eventId: UInt64): &FLOATEvent? {
967 return &self.events[eventId] as &FLOATEvent?
968 }
969
970 /************* Getters (for anyone) *************/
971
972 // Get a public reference to the FLOATEvent
973 // so you can call some helpful getters
974 pub fun borrowPublicEventRef(eventId: UInt64): &FLOATEvent{FLOATEventPublic}? {
975 return &self.events[eventId] as &FLOATEvent{FLOATEventPublic}?
976 }
977
978 pub fun getIDs(): [UInt64] {
979 return self.events.keys
980 }
981
982 // Maps the eventId to the name of that
983 // event. Just a kind helper.
984 pub fun getAllEvents(): {UInt64: String} {
985 let answer: {UInt64: String} = {}
986 for id in self.events.keys {
987 let ref = (&self.events[id] as &FLOATEvent?)!
988 answer[id] = ref.name
989 }
990 return answer
991 }
992
993 pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} {
994 return (&self.events[id] as &{MetadataViews.Resolver}?)!
995 }
996
997 init() {
998 self.events <- {}
999 self.groups <- {}
1000 }
1001
1002 destroy() {
1003 destroy self.events
1004 destroy self.groups
1005 }
1006 }
1007
1008 pub fun createEmptyCollection(): @Collection {
1009 return <- create Collection()
1010 }
1011
1012 pub fun createEmptyFLOATEventCollection(): @FLOATEvents {
1013 return <- create FLOATEvents()
1014 }
1015
1016 init() {
1017 self.totalSupply = 0
1018 self.totalFLOATEvents = 0
1019 emit ContractInitialized()
1020
1021 self.FLOATCollectionStoragePath = /storage/MusicPeaksMembershipTokenCollectionStoragePath
1022 self.FLOATCollectionPublicPath = /public/MusicPeaksMembershipTokenCollectionPublicPath
1023 self.FLOATEventsStoragePath = /storage/MusicPeaksMembershipTokenEventsStoragePath
1024 self.FLOATEventsPrivatePath = /private/MusicPeaksMembershipTokenEventsPrivatePath
1025 self.FLOATEventsPublicPath = /public/MusicPeaksMembershipTokenEventsPublicPath
1026 }
1027}
1028