Smart Contract

MelodyTicket

A.32a6af84f2f54476.MelodyTicket

Deployed

2h ago
Feb 28, 2026, 11:36:14 PM UTC

Dependents

0 imports
1
2
3import NonFungibleToken from 0x1d7e57aa55817448
4import FungibleToken from 0xf233dcee88fe0abe
5import MetadataViews from 0x1d7e57aa55817448
6import MelodyError from 0x32a6af84f2f54476
7
8
9
10pub contract MelodyTicket: NonFungibleToken {
11
12
13    /**    ___  ____ ___ _  _ ____
14       *   |__] |__|  |  |__| [__
15        *  |    |  |  |  |  | ___]
16         *************************/
17
18
19    pub let CollectionStoragePath: StoragePath
20    pub let CollectionPublicPath: PublicPath
21    pub let CollectionPrivatePath: PrivatePath
22    pub let MinterStoragePath: StoragePath
23
24
25    /**    ____ _  _ ____ _  _ ___ ____
26       *   |___ |  | |___ |\ |  |  [__
27        *  |___  \/  |___ | \|  |  ___]
28         ******************************/
29
30    pub event ContractInitialized()
31    pub event Withdraw(id: UInt64, from: Address?)
32    pub event Deposit(id: UInt64, to: Address?)
33
34    pub event TicketCreated(id: UInt64, creator: Address?)
35    pub event TicketDestoryed(id: UInt64, owner: Address?)
36    pub event TicketTransfered(paymentId: UInt64, from: Address?, to: Address?)
37    pub event MetadataUpdated(id: UInt64, key: String)
38    pub event MetadataInited(id: UInt64)
39    pub event BaseURIUpdated(before: String, after: String)
40   
41    /**    ____ ___ ____ ___ ____
42       *   [__   |  |__|  |  |___
43        *  ___]  |  |  |  |  |___
44         ************************/
45
46    pub var totalSupply: UInt64
47    pub var baseURI: String
48
49    // metadata 
50    access(contract) var predefinedMetadata: {UInt64: {String: AnyStruct}}
51
52    // Reserved parameter fields: {ParamName: Value}
53    access(self) let _reservedFields: {String: AnyStruct}
54
55
56    /**    ____ _  _ _  _ ____ ___ _ ____ _  _ ____ _    _ ___ _   _
57       *   |___ |  | |\ | |     |  | |  | |\ | |__| |    |  |   \_/
58        *  |    |__| | \| |___  |  | |__| | \| |  | |___ |  |    |
59         ***********************************************************/
60    
61
62    pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
63        pub let id: UInt64
64
65        pub let name: String
66        pub let description: String
67        pub let thumbnail: String
68
69        access(self) let royalties: [MetadataViews.Royalty]
70        access(self) let metadata: {String: AnyStruct}
71
72
73    
74        init(
75            id: UInt64,
76            name: String,
77            description: String,
78            metadata: {String: AnyStruct},
79        ) {
80            self.id = id
81            self.name = name
82            self.description = description
83            if MelodyTicket.baseURI != "" {
84                self.thumbnail = MelodyTicket.baseURI.concat(id.toString())
85            } else {
86                self.thumbnail = "https://testnet.melody.im/api/data/payment/".concat(self.id.toString()).concat("&width=600&height=400")
87            }
88            self.royalties = []
89            self.metadata = metadata
90        }
91
92        destroy (){
93            let metadata = self.getMetadata()
94            let status = (metadata["status"] as? UInt8?)!
95            let owner = (metadata["owner"] as? Address?)!
96            assert(status! > 1, message: MelodyError.errorEncode(msg: "Cannot destory ticket while it is activing", err: MelodyError.ErrorCode.WRONG_LIFE_CYCLE_STATE))
97            emit TicketDestoryed(id: self.id, owner: owner)
98        }
99
100
101        pub fun getMetadata(): {String: AnyStruct} {
102
103            let metadata = MelodyTicket.predefinedMetadata[self.id] ?? {}
104            metadata["metadata"] = self.metadata
105            return metadata
106        }
107    
108        pub fun getViews(): [Type] {
109            return [
110                Type<MetadataViews.Display>(),
111                Type<MetadataViews.Royalties>(),
112                Type<MetadataViews.Editions>(),
113                Type<MetadataViews.ExternalURL>(),
114                Type<MetadataViews.NFTCollectionData>(),
115                Type<MetadataViews.NFTCollectionDisplay>(),
116                Type<MetadataViews.Serial>(),
117                Type<MetadataViews.Traits>()
118            ]
119        }
120
121        pub fun resolveView(_ view: Type): AnyStruct? {
122            let metadata = MelodyTicket.predefinedMetadata[self.id] ?? {}
123            let transferable = (metadata["transferable"] as? Bool?)! ?? true
124            let paymentType = (metadata["paymentType"] as? UInt8?)!
125            let revocable = paymentType == 0 || paymentType == 0
126
127            switch view {
128                case Type<MetadataViews.Display>():
129                    var desc = "\n"
130                    if transferable {
131                        desc = desc.concat("Transferable \n")
132                    } else {
133                        desc = desc.concat("Cannot transfer \n")
134                    }
135                    if revocable {
136                        desc = desc.concat("Revocable \n")
137                    } else {
138                        desc = desc.concat("Cannot revoke \n")
139                    }
140
141                    return MetadataViews.Display(
142                        name: self.name,
143                        description: self.description.concat(desc),
144                        thumbnail: MetadataViews.HTTPFile(
145                            url: "https://testnet.melody.im/api/data/payment/".concat(self.id.toString()).concat("?width=600&height=400")
146                        )
147                    )
148                case Type<MetadataViews.Editions>():
149                    // There is no max number of NFTs that can be minted from this contract
150                    // so the max edition field value is set to nil
151                    let editionInfo = MetadataViews.Edition(name: "Melody ticket NFT", number: self.id, max: nil)
152                    let editionList: [MetadataViews.Edition] = [editionInfo]
153                    return MetadataViews.Editions(
154                        editionList
155                    )
156                case Type<MetadataViews.Serial>():
157                    return MetadataViews.Serial(
158                        self.id
159                    )
160                case Type<MetadataViews.Royalties>():
161                    let receieverCap = MelodyTicket.account.getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
162                    let royalty= MetadataViews.Royalty(receiver: receieverCap, cut: 0.03, description: "LyricLabs will take 3% as second trade royalty fee")
163                    return MetadataViews.Royalties([royalty])
164
165                case Type<MetadataViews.ExternalURL>(): // todo
166                    return MetadataViews.ExternalURL("https://melody.im/payment/".concat(self.id.toString()))
167                case Type<MetadataViews.NFTCollectionData>():
168                    return MetadataViews.NFTCollectionData(
169                        storagePath: MelodyTicket.CollectionStoragePath,
170                        publicPath: MelodyTicket.CollectionPublicPath,
171                        providerPath: /private/MelodyTicketCollection,
172                        publicCollection: Type<&MelodyTicket.Collection{MelodyTicket.CollectionPublic}>(),
173                        publicLinkedType: Type<&MelodyTicket.Collection{MelodyTicket.CollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
174                        providerLinkedType: Type<&MelodyTicket.Collection{MelodyTicket.CollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
175                        createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
176                            return <-MelodyTicket.createEmptyCollection()
177                        })
178                    )
179                case Type<MetadataViews.NFTCollectionDisplay>():
180                    
181                    return MetadataViews.NFTCollectionDisplay(
182                        name: "The Melody ticket NFT",
183                        description: "This collection is Melody ticket NFT.",
184                        externalURL: MetadataViews.ExternalURL(""), // todo
185                        squareImage: MetadataViews.Media(
186                            file: MetadataViews.HTTPFile(
187                                url:"https://trello.com/1/cards/62dd12a167854020143ccd01/attachments/631422356f0fe60111e1ed3c/previews/631422366f0fe60111e1ed43/download/image.png"
188                            ),
189                            mediaType: "image/png"
190                        ),
191                        bannerImage: MetadataViews.Media(
192                            file: MetadataViews.HTTPFile(
193                                url: "https://trello.com/1/cards/62dd12a167854020143ccd01/attachments/631423e7c7e6b800d710f2a1/download/image.png" // todo
194                            ),
195                            mediaType: "image/png"
196                        ),
197                        socials: {
198                            "twitter": MetadataViews.ExternalURL("https://twitter.com/lyric_labs"),
199                            "website": MetadataViews.ExternalURL("https://lyriclabs.xyz")
200                        }
201                    )
202                case Type<MetadataViews.Traits>():
203
204                    let metadata = MelodyTicket.predefinedMetadata[self.id]!
205
206                    let traitsView = MetadataViews.dictToTraits(dict: metadata, excludedNames: [])
207
208                    // mintedTime is a unix timestamp, we should mark it with a displayType so platforms know how to show it.
209                    let mintedTimeTrait = MetadataViews.Trait(name: "mintedTime", value: self.metadata["mintedTime"]!, displayType: "Date", rarity: nil)
210                    traitsView.addTrait(mintedTimeTrait)
211                    
212                    return traitsView
213
214            }
215            return nil
216        }
217    }
218
219    pub resource interface CollectionPublic {
220        pub fun deposit(token: @NonFungibleToken.NFT)
221        pub fun getIDs(): [UInt64]
222        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
223        pub fun borrowNFTResolver(id: UInt64): &{MetadataViews.Resolver}?
224    }
225
226    pub resource interface CollectionPrivate {
227        pub fun borrowMelodyTicket(id: UInt64): &MelodyTicket.NFT? {
228            post {
229                (result == nil) || (result?.id == id):
230                    "Cannot borrow MelodyTicket reference: the ID of the returned reference is incorrect"
231            }
232        }
233    }
234
235    pub resource Collection: CollectionPublic, CollectionPrivate, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
236        // dictionary of NFT conforming tokens
237        // NFT is a resource type with an `UInt64` ID field
238        pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
239
240        init () {
241            self.ownedNFTs <- {}
242        }
243
244        // withdraw removes an NFT from the collection and moves it to the caller
245        pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
246            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
247
248            emit Withdraw(id: token.id, from: self.owner?.address)
249
250            return <- token
251        }
252
253        // deposit takes a NFT and adds it to the collections dictionary
254        // and adds the ID to the id array
255        pub fun deposit(token: @NonFungibleToken.NFT) {
256            pre {
257                self.checkTransferable(token.id, address: self.owner?.address) == true : MelodyError.errorEncode(msg: "Ticket is not transferable", err: MelodyError.ErrorCode.NOT_TRANSFERABLE)
258            }
259            let id: UInt64 = token.id
260
261            let token <- token as! @MelodyTicket.NFT
262
263            // add the new token to the dictionary which removes the old one
264            let oldToken <- self.ownedNFTs[id] <- token
265
266            // update owner
267            let owner = self.owner?.address
268            let metadata = MelodyTicket.getMetadata(id)!
269            let currentOwner = (metadata["owner"] as? Address?)!
270
271            emit TicketTransfered(paymentId: id, from: currentOwner, to: owner)
272
273            MelodyTicket.updateMetadata(id: id, key: "owner", value: owner)
274            
275            emit Deposit(id: id, to: owner)
276
277            destroy oldToken
278        }
279
280        pub fun checkTransferable(_ id: UInt64, address: Address?): Bool {
281            let metadata = MelodyTicket.getMetadata(id)!
282            let receievr = (metadata["receiver"] as? Address?)!
283            if address != nil && receievr == address {
284                return true
285            }
286            let transferable = (metadata["transferable"] as? Bool?)! ?? true
287
288            return transferable
289        }
290
291        // getIDs returns an array of the IDs that are in the collection
292        pub fun getIDs(): [UInt64] {
293            return self.ownedNFTs.keys
294        }
295
296        // borrowNFT gets a reference to an NFT in the collection
297        // so that the caller can read its metadata and call its methods
298        pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
299            return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
300        }
301 
302        pub fun borrowNFTResolver(id: UInt64): &{MetadataViews.Resolver}? {
303            if self.ownedNFTs[id] != nil {
304                // Create an authorized reference to allow downcasting
305                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
306                return ref as! &MelodyTicket.NFT
307            }
308            return nil
309        }
310        pub fun borrowMelodyTicket(id: UInt64): &MelodyTicket.NFT? {
311            if self.ownedNFTs[id] != nil {
312                // Create an authorized reference to allow downcasting
313                let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
314                return ref as! &MelodyTicket.NFT
315            }
316            return nil
317        }
318
319        pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
320            let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
321            let MelodyTicket = nft as! &MelodyTicket.NFT
322            return MelodyTicket as &AnyResource{MetadataViews.Resolver}
323        }
324
325        destroy() {
326            destroy self.ownedNFTs
327        }
328    }
329
330    // public function that anyone can call to create a new empty collection
331    pub fun createEmptyCollection(): @NonFungibleToken.Collection {
332        return <- create Collection()
333    }
334
335    // Resource that an admin or something similar would own to be
336    // able to mint new NFTs
337    //
338    pub resource NFTMinter {
339
340        // mintNFT mints a new NFT with a new ID
341        access(account) fun mintNFT(
342            name: String,
343            description: String,
344            metadata: {String: AnyStruct}
345        ): @MelodyTicket.NFT {
346            let currentBlock = getCurrentBlock()
347            metadata["mintedBlock"] = currentBlock.height
348            metadata["mintedTime"] = currentBlock.timestamp
349
350            let nftId = MelodyTicket.totalSupply + UInt64(1)
351            // create a new NFT
352            var newNFT <- create NFT(
353                id: nftId,
354                name: name,
355                description: description,
356                metadata: metadata,
357            )
358            // deposit it in the recipient's account using their reference
359            // recipient.deposit(token: <- newNFT)
360            let creator = (metadata["creator"] as? Address?)!
361            emit TicketCreated(id: nftId, creator: creator)
362            MelodyTicket.totalSupply = nftId
363            return <- newNFT
364        }
365        
366        pub fun setBaseURI(_ uri: String) {
367            emit BaseURIUpdated(before: MelodyTicket.baseURI, after: uri )
368            MelodyTicket.baseURI = uri
369        }
370    }
371
372
373    access(account) fun setMetadata(id: UInt64, metadata: {String: AnyStruct}) {
374        MelodyTicket.predefinedMetadata[id] = metadata
375        // emit
376        emit MetadataInited(id: id)
377    }
378
379    access(account) fun updateMetadata(id: UInt64, key: String, value: AnyStruct) {
380        pre {
381            MelodyTicket.predefinedMetadata[id] != nil : MelodyError.errorEncode(msg: "Metadata not found", err: MelodyError.ErrorCode.NOT_EXIST)
382        }
383        let metadata = MelodyTicket.predefinedMetadata[id]!
384
385        emit MetadataUpdated(id: id, key: key)
386        metadata[key] = value
387        MelodyTicket.predefinedMetadata[id] = metadata
388    }
389
390    // public funcs
391
392    pub fun getTotalSupply(): UInt64 {
393        return MelodyTicket.totalSupply
394    }
395
396    pub fun getMetadata(_ id: UInt64): {String: AnyStruct}? {
397        return MelodyTicket.predefinedMetadata[id]
398    }
399
400
401
402
403    init() {
404        // Initialize the total supply
405        self.totalSupply = 0
406
407        // Set the named paths
408        self.CollectionStoragePath = /storage/MelodyTicketCollection
409        self.CollectionPublicPath = /public/MelodyTicketCollection
410        self.CollectionPrivatePath = /private/MelodyTicketCollection
411        self.MinterStoragePath = /storage/MelodyTicketMinter
412        self._reservedFields = {}
413
414        self.predefinedMetadata = {}
415        self.baseURI = ""
416
417        // Create a Collection resource and save it to storage
418        let collection <- create Collection()
419        self.account.save(<-collection, to: self.CollectionStoragePath)
420
421        // create a public capability for the collection
422        self.account.link<&MelodyTicket.Collection{NonFungibleToken.CollectionPublic, MelodyTicket.CollectionPublic, MetadataViews.ResolverCollection}>(
423            self.CollectionPublicPath,
424            target: self.CollectionStoragePath
425        )
426        // create a public capability for the collection
427        self.account.link<&MelodyTicket.Collection{MelodyTicket.CollectionPrivate}>(
428            self.CollectionPrivatePath,
429            target: self.CollectionStoragePath
430        )
431
432
433
434        // Create a Minter resource and save it to storage
435        let minter <- create NFTMinter()
436        self.account.save(<- minter, to: self.MinterStoragePath)
437
438        emit ContractInitialized()
439    }
440}
441