Smart Contract
MelodyTicket
A.32a6af84f2f54476.MelodyTicket
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