Smart Contract
Pinnacle
A.edf9df96c92f4595.Pinnacle
1/*
2 Contract for Pinnacle NFTs and metadata
3 Author: Loic Lesavre loic.lesavre@dapperlabs.com
4*/
5
6import NonFungibleToken from 0x1d7e57aa55817448
7import FungibleToken from 0xf233dcee88fe0abe
8import MetadataViews from 0x1d7e57aa55817448
9import ViewResolver from 0x1d7e57aa55817448
10
11/*
12The Pinnacle contract introduces five entities that establish the requirements for minting Pinnacle Pin NFTs and
13organizing their metadata: Edition Types, Series, Sets, Shapes, and Editions.
14
15These entities are defined as struct types, created by the Admin, and stored in arrays in the contract state.
16Edition Types and Series are independent from other entities. Sets, Shapes, and Editions are linked to a
17parent entity: a Series, Set, or Shape, respectively. Sets are also linked to a parent Edition Type.
18
19Pinnacle Pin NFTs are minted from Editions. Owners have the option to add an Inscription to their NFT. The NFTs
20adhere to the Flow NFT standard. Any Flow account can create a Collection to store Pinnacle Pin NFTs, which
21includes functionality for Inscriptions and XP balance.
22
23An Admin resource is created in the contract's init function. It is meant to be saved in the Admin's account
24and provides the following key abilities:
25
26- Create new entities (Edition Types, Series, Sets, Shapes, and Editions).
27- Lock Series and Sets, preventing the creation of new child Sets and Shapes, respectively.
28- Close Shapes, preventing the creation of new child Editions, Open/Unlimited Editions, preventing the
29minting of new NFTs from those Editions, and Edition Types, preventing the creation of new child Sets, Shapes,
30and Editions.
31- Unlock or reopen entities within the undo period.
32- Update the name of Series, Sets, and Shapes, as well as the description of Editions.
33- Increment the current printing of Shapes unless they are closed.
34- Mint NFTs, subject to the conditions defined in the contract, and deposit them in any account.
35- Update an Inscription's note with owner co-signing if the owner has added an Inscription to their NFT.
36- Update an NFT's XP balance unless the owner has revoked this ability.
37- Create new Admins.
38
39Notes:
40
41- All functions will fail if an invalid argument is provided or one of the pre- or post-conditions are not
42met. For getter functions, calls to non-existing objects are considered valid and will return nil so that they
43can be handled differently by the caller. The borrowNFT function in the Collection resource is an exception to
44that pattern, the borrowNFTSafe or borrowPinNFT function can be used instead.
45- All dates specified in the contract are Unix timestamps.
46 */
47
48/// The Pinnacle Pin NFTs and metadata contract
49///
50access(all) contract Pinnacle: NonFungibleToken {
51 //------------------------------------------------------------
52 // Events
53 //------------------------------------------------------------
54
55 // Series Events
56 //
57 /// Emitted when a new Series has been created, meaning new Sets can be created with the Series
58 access(all) event SeriesCreated(id: Int, name: String)
59 /// Emitted when a Series is locked, meaning new Sets cannot be created with the Series anymore
60 access(all) event SeriesLocked(id: Int, name: String)
61 /// Emitted when a Series's name is updated
62 access(all) event SeriesNameUpdated(id: Int, name: String)
63
64 // Set Events
65 //
66 /// Emitted when a new Set has been created, meaning new Shapes can be created with the Set
67 access(all) event SetCreated(id: Int, renderID: String, name: String, seriesID: Int, editionType: String)
68 /// Emitted when a Set is locked, meaning new Shapes cannot be created with the Set anymore
69 access(all) event SetLocked(id: Int, renderID: String, name: String, seriesID: Int, editionType: String)
70 /// Emitted when a Set's name is updated
71 access(all) event SetNameUpdated(id: Int, renderID: String, name: String, seriesID: Int, editionType: String)
72
73 // Shape Events
74 //
75 /// Emitted when a new Shape has been created, meaning new Editions can be created with the Shape
76 access(all) event ShapeCreated(
77 id: Int,
78 renderID: String,
79 setID: Int,
80 name: String,
81 editionType: String,
82 metadata: {String: [String]}
83 )
84 /// Emitted when a Shape is closed, meaning new Editions cannot be created with the Shape anymore
85 access(all) event ShapeClosed(
86 id: Int,
87 renderID: String,
88 setID: Int,
89 name: String,
90 currentPrinting: UInt64,
91 editionType: String
92 )
93 /// Emitted when a Shape's name is updated
94 access(all) event ShapeNameUpdated(
95 id: Int,
96 renderID: String,
97 setID: Int,
98 name: String,
99 currentPrinting: UInt64,
100 editionType: String
101 )
102 /// Emitted when a Shape's metadata field is updated
103 access(all) event ShapeMetadataUpdated(
104 id: Int,
105 metadata: {String: [String]}
106 )
107 /// Emitted when a Shape's current printing is incremented
108 access(all) event ShapeCurrentPrintingIncremented(
109 id: Int,
110 renderID: String,
111 setID: Int,
112 name: String,
113 currentPrinting: UInt64,
114 editionType: String
115 )
116
117 // Edition Events
118 //
119 /// Emitted when a new Edition has been created, meaning new NFTs can be minted with the Edition
120 access(all) event EditionCreated(
121 id: Int,
122 renderID: String,
123 seriesID: Int,
124 setID: Int,
125 shapeID: Int,
126 variant: String?,
127 printing: UInt64,
128 editionTypeID: Int,
129 description: String,
130 isChaser: Bool,
131 maxMintSize: UInt64?,
132 maturationPeriod: UInt64?,
133 traits: {String: [String]}
134 )
135 /// Emitted when an Edition is either closed by the Admin or the maximum amount of pins have been minted
136 access(all) event EditionClosed(
137 id: Int,
138 maxMintSize: UInt64
139 )
140 /// Emitted when an Edition's description is updated
141 access(all) event EditionDescriptionUpdated(
142 id: Int,
143 description: String
144 )
145
146 /// Emitted when an Edition's renderID is updated
147 access(all) event EditionRenderIDUpdated(
148 id: Int,
149 renderID: String
150 )
151
152 /// Emitted when an Edition has been removed from the contract by the Admin, this can only be done if the
153 /// Edition is the last one that was created in the contract and no NFTs were minted from it
154 access(all) event EditionRemoved(
155 id: Int
156 )
157
158 // Edition Type Events
159 //
160 /// Emitted when a new Edition Type has been created
161 access(all) event EditionTypeCreated(id: Int, name: String, isLimited: Bool, isMaturing: Bool)
162 /// Emitted when an Edition Type has been closed, meaning new Editions cannot be created with the Edition
163 /// Type anymore
164 access(all) event EditionTypeClosed(id: Int, name: String, isLimited: Bool, isMaturing: Bool)
165
166 // NFT Events
167 //
168 // TODO: Consider removing 'Withdraw' and 'Deposit' events now that similar 'Withdrawn' and 'Deposited' events are emitted in NonFungibleToken contract interface
169 /// Emitted when a Pin NFT is withdrawn from the Collection
170 access(all) event Withdraw(id: UInt64, from: Address?)
171 /// Emitted when a Pin NFT is deposited into the Collection
172 access(all) event Deposit(id: UInt64, to: Address?)
173 /// Emitted when a Pin NFT is minted
174 access(all) event PinNFTMinted(id: UInt64, renderID: String, editionID: Int, serialNumber: UInt64?, maturityDate: UInt64?)
175 /// Emitted when a Pin NFT's XP is updated
176 access(all) event NFTXPUpdated(id: UInt64, editionID: Int, xp: UInt64?)
177 /// Emitted when an Inscription is added to a Pin NFT
178 access(all) event NFTInscriptionAdded(
179 id: Int,
180 owner: Address,
181 note: String?,
182 nftID: UInt64,
183 editionID: Int
184 )
185 /// Emitted when an NFT's Inscription is updated
186 access(all) event NFTInscriptionUpdated(
187 id: Int,
188 owner: Address,
189 note: String?,
190 nftID: UInt64,
191 editionID: Int
192 )
193 /// Emitted when an NFT's Inscription is removed by the owner. This can only be done during the undo
194 /// period and if the Inscription is the last one that was added to the NFT - meaning the Inscription is
195 /// permanent after the undo period has expired or if the NFT is transferred to another owner and the new
196 /// owner adds a new Inscription to the NFT
197 access(all) event NFTInscriptionRemoved(id: Int, owner: Address, nftID: UInt64, editionID: Int)
198
199 // Other Events
200 //
201 /// Emitted when a Series, Set, Shape, or Edition Type is reopened or unlocked by the Admin during the
202 /// undo period. Editions cannot be reopened even during the undo period.
203 access(all) event EntityReactivated(entity: String, id: Int, name: String?)
204 /// Emitted when a new Variant has been inserted
205 access(all) event VariantInserted(name: String)
206 /// Emitted when the wrapper emitPurchasedEvent Admin function is called
207 access(all) event Purchased(
208 purchaseIntentID: String,
209 buyerAddress: Address,
210 countPurchased: UInt64,
211 totalSalePrice: UFix64
212 )
213 /// Emitted when an Open/Unlimited Edition NFT is destroyed by the Admin
214 access(all) event OpenEditionNFTBurned(id: UInt64, editionID: Int)
215
216 //------------------------------------------------------------
217 // Named values
218 //------------------------------------------------------------
219
220 /// Named Paths
221 ///
222 access(all) let CollectionStoragePath: StoragePath
223 access(all) let CollectionPublicPath: PublicPath
224 access(all) let AdminStoragePath: StoragePath
225
226 //------------------------------------------------------------
227 // Publicly readable contract state
228 //------------------------------------------------------------
229
230 /// The period in seconds during which entities can be unlocked or reopened in case they are locked or
231 /// closed by mistake. Entities become permanently locked or closed after the undo period has passed.
232 access(all) let undoPeriod: UInt64
233
234 /// The address returned by the Royalties MetadataView to indicate where royalties should be deposited
235 access(all) var royaltyAddress: Address
236
237 /// The end user license URL and statement, it gets added to every Shape's metadata dictionary
238 access(all) var endUserLicenseURL: String
239
240 //------------------------------------------------------------
241 // Internal contract state
242 //------------------------------------------------------------
243
244 /// The arrays that store the entities in the contract state.
245 ///
246 /// Each array index corresponds to the entity's ID - 1. See how entities are added to the contract arrays
247 /// in the entity creation functions defined in the Admin resource. The entities are stored in arrays
248 /// rather than dictionaries so that they can be returned in slices at scale (no need to call the .keys
249 /// built-in dictionary function that can cause a computation exceed limit error if the dictionary is too
250 /// large).
251 access(self) let series: [Series]
252 access(self) let sets: [Set]
253 access(self) let shapes: [Shape]
254 access(self) let editions: [Edition]
255 access(self) let editionTypes: [EditionType]
256
257 /// The dictionaries that allow entities to be looked up by unique name
258 access(self) let seriesIDsByName: {String: Int}
259 access(self) let setIDsByName: {String: Int}
260 access(self) let editionTypeIDsByName: {String: Int}
261
262 /// The dictionary that stores the Variant strings that are allowed to be used when creating a new Edition
263 ///
264 /// Variants are defined as String dictionary entries rather than structs because they do not have any
265 /// other properties than their name.
266 access(self) let variants: {String: Bool}
267
268 /// The dictionary that stores the maximum number of Inscriptions that can be added to each NFT. By
269 /// default (no entry in the dictionary), an NFT can have up to 100 Inscriptions.
270 access(self) let inscriptionsLimits: {UInt64: Int}
271
272 /// The dictionary that stores any additional data that needs to be stored in the contract. This field
273 /// has been added to accommodate potential future contract updates and facilitate new functionalities.
274 /// It is not in use currently.
275 access(self) let extension: {String: AnyStruct}
276
277 //------------------------------------------------------------
278 // Series
279 //------------------------------------------------------------
280
281 /// Struct that defines a Series
282 ///
283 /// Each Series is created independently of any other entity.
284 ///
285 access(all) struct Series {
286 /// This Series' unique ID
287 access(all) let id: Int
288
289 /// This Series' unique name. It can be updated by the Admin.
290 access(all) var name: String
291
292 /// This field indicates whether the Series is currently unlocked (lockedDate is nil) or locked (has
293 /// actual date).
294 ///
295 /// Initially, when a Series is created, it is in an unlocked state, allowing the creation of Sets.
296 /// Once a Series is locked, it is no longer possible to create Sets linked to that Series. However,
297 /// it is still possible to create Shapes and Editions within those Shapes using the Sets already
298 /// created from the Series. Locking a Series takes immediate effect, but it can be undone during the
299 /// undo period. The lockedDate field indicates the date when the Series is permanently locked,
300 /// including the undo period.
301 access(all) var lockedDate: UInt64?
302
303 /// Struct initializer
304 ///
305 view init(id: Int, name: String) {
306 self.id = id
307 self.name = name
308 self.lockedDate = nil
309 }
310
311 /// Close this Series
312 ///
313 access(contract) fun lock() {
314 pre {
315 self.lockedDate == nil: "Series is already locked"
316 }
317 // Set the locked date to the current block timestamp plus the undo period
318 self.lockedDate = UInt64(getCurrentBlock().timestamp) + Pinnacle.undoPeriod
319 emit SeriesLocked(id: self.id, name: self.name)
320 }
321
322 /// Unlock this Series
323 ///
324 /// This will fail if the undo period has expired.
325 ///
326 access(contract) fun unlock() {
327 pre {
328 self.lockedDate != nil: "Series is already unlocked"
329 self.lockedDate! >= UInt64(getCurrentBlock().timestamp):
330 "Undo period has expired, Series is permanently locked"
331 }
332 self.lockedDate = nil
333 emit EntityReactivated(entity: "Series", id: self.id, name: self.name)
334 }
335
336 /// Update this Series' name
337 ///
338 access(contract) fun updateName(_ name: String) {
339 pre {
340 name != "": "The name of a Series cannot be an empty string"
341 Pinnacle.seriesIDsByName.containsKey(name) == false: "A Series with that name already exists"
342 }
343 Pinnacle.seriesIDsByName.remove(key: self.name)
344 self.name = name
345 Pinnacle.seriesIDsByName[name] = self.id
346 emit SeriesNameUpdated(id: self.id, name: self.name)
347 }
348 }
349
350 /// Return the ID of the latest Series created in the contract
351 ///
352 /// The ID is an incrementing integer equal to the length of the series array.
353 ///
354 access(all) view fun getLatestSeriesID(): Int {
355 return Pinnacle.series.length
356 }
357
358 /// Return a Series struct containing the data of the Series with the given ID, if it exists in the
359 /// contract
360 ///
361 access(all) view fun getSeries(id: Int): Series? {
362 pre {
363 id > 0: "The ID of a Series must be greater than 0"
364 }
365 return Pinnacle.getLatestSeriesID() >= id ? Pinnacle.series[id - 1] : nil
366 }
367
368 /// Return all Series in the contract
369 ///
370 access(all) view fun getAllSeries(): [Series] {
371 return Pinnacle.series
372 }
373
374 /// Return a Series struct containing the data of the Series with the given name, if it exists in the
375 /// contract
376 ///
377 access(all) view fun getSeriesByName(_ name: String): Series? {
378 if let id = Pinnacle.seriesIDsByName[name] {
379 return Pinnacle.getSeries(id: id)
380 }
381 return nil
382 }
383
384 /// Return the ID of the Series with the given name, if it exists in the contract
385 ///
386 access(all) view fun getSeriesIDByName(_ name: String): Int? {
387 return Pinnacle.seriesIDsByName[name]
388 }
389
390 /// Allow iterating over Series names in the contract without allocating an array
391 ///
392 access(all) fun forEachSeriesName(_ function: fun (String): Bool) {
393 Pinnacle.seriesIDsByName.forEachKey(function)
394 }
395
396 /// Return the contract's seriesIDsByName dictionary
397 ///
398 access(all) view fun getAllSeriesIDsByNames(): {String: Int} {
399 return Pinnacle.seriesIDsByName
400 }
401
402 //------------------------------------------------------------
403 // Set
404 //------------------------------------------------------------
405
406 /// Struct that defines a Set
407 ///
408 /// Each Set is linked to a parent Series and Edition Type.
409 ///
410 access(all) struct Set {
411 /// This Set's unique ID
412 access(all) let id: Int
413
414 /// This Set's RenderID. The uniqueness of renderID is NOT required.
415 access(all) var renderID: String
416
417 /// This Set's unique name. It can be updated by the Admin.
418 access(all) var name: String
419
420 /// The ID of the Series that this Set belongs to
421 access(all) let seriesID: Int
422
423 /// This field indicates whether the Set is currently unlocked (lockedDate is nil) or locked (has
424 /// actual date).
425 ///
426 /// Initially, when a Set is created, it is in an unlocked state, allowing the creation of Shapes.
427 /// Once a Set is locked, it is no longer possible to create Shapes linked to that Set. However, it is
428 /// still possible to create Editions using the Shapes already created from the Set. Locking a Set
429 /// takes immediate effect, but it can be undone during the undo period. The lockedDate field
430 /// indicates the date when the Set is permanently locked, including the undo period.
431 access(all) var lockedDate: UInt64?
432
433 /// The type of Editions that can be created from this Set's Shapes
434 access(all) let editionType: String
435
436 /// The dictionary that stores all the Shape names inside the Set to ensure there can be at most one
437 /// Shape with a given name in a Set
438 access(self) let shapeNames: {String: Bool}
439
440 /// Struct initializer
441 ///
442 view init(id: Int, renderID: String, name: String, editionType: String, seriesID: Int) {
443 self.id = id
444 self.renderID = renderID
445 self.name = name
446 self.seriesID = seriesID
447 self.lockedDate = nil
448 self.editionType = editionType
449 self.shapeNames = {}
450 }
451
452 /// Insert a new Shape name to the shapeNames dictionary
453 ///
454 access(contract) fun insertShapeName(_ name: String) {
455 self.shapeNames[name] = true
456 }
457
458 /// Remove a Shape name from the shapeNames dictionary
459 ///
460 access(contract) fun removeShapeName(_ name: String) {
461 self.shapeNames.remove(key: name)
462 }
463
464 /// Check if the Set contains the given Shape name
465 ///
466 access(contract) view fun shapeNameExistsInSet(_ name: String): Bool {
467 return self.shapeNames.containsKey(name)
468 }
469
470 /// Lock the Set so that no more Editions can be created with it
471 ///
472 access(contract) fun lock() {
473 pre {
474 self.lockedDate == nil: "Set is already locked"
475 }
476 // Set the locked date to the current block timestamp plus the undo period
477 self.lockedDate = UInt64(getCurrentBlock().timestamp) + Pinnacle.undoPeriod
478 emit SetLocked(
479 id: self.id,
480 renderID: self.renderID,
481 name: self.name,
482 seriesID: self.seriesID,
483 editionType: self.editionType
484 )
485 }
486
487 /// Unlock this Set
488 ///
489 /// This will fail if the undo period has expired.
490 ///
491 access(contract) fun unlock() {
492 pre {
493 self.lockedDate != nil: "Set is already unlocked"
494 self.lockedDate! >= UInt64(getCurrentBlock().timestamp):
495 "Undo period has expired, Set is permanently locked"
496 }
497 self.lockedDate = nil
498 emit EntityReactivated(entity: "Set", id: self.id, name: self.name)
499 }
500
501 /// Update this Set's name
502 ///
503 access(contract) fun updateName(_ name: String) {
504 pre {
505 name != "": "The name of a Set cannot be an empty string"
506 Pinnacle.setIDsByName.containsKey(name) == false: "A Set with that name already exists"
507 }
508 Pinnacle.setIDsByName.remove(key: self.name)
509 self.name = name
510 Pinnacle.setIDsByName[name] = self.id
511 emit SetNameUpdated(
512 id: self.id,
513 renderID: self.renderID,
514 name: self.name,
515 seriesID: self.seriesID,
516 editionType: self.editionType
517 )
518 }
519
520 /// Return this Set's shapeNames dictionary
521 ///
522 access(all) view fun getShapeNames(): {String: Bool} {
523 return self.shapeNames
524 }
525 }
526
527 /// Return the ID of the latest Set created in the contract
528 ///
529 /// The ID is an incrementing integer equal to the length of the sets array.
530 ///
531 access(all) view fun getLatestSetID(): Int {
532 return Pinnacle.sets.length
533 }
534
535 /// Return a Set struct containing the data of the Set with the given ID, if it exists in the contract
536 ///
537 access(all) view fun getSet(id: Int): Set? {
538 pre {
539 id > 0: "The ID of a Set must be greater than zero"
540 }
541 return Pinnacle.getLatestSetID() >= id ? Pinnacle.sets[id - 1] : nil
542 }
543
544 /// Return all Sets in the contract
545 ///
546 access(all) view fun getAllSets(): [Set] {
547 return Pinnacle.sets
548 }
549
550 /// Return a Set struct containing the data of the Set with the given name, if it exists in the contract
551 ///
552 access(all) view fun getSetByName(_ name: String): Set? {
553 if let id = Pinnacle.setIDsByName[name] {
554 return Pinnacle.getSet(id: id)
555 }
556 return nil
557 }
558
559 /// Return the ID of the Set with the given name, if it exists in the contract
560 ///
561 access(all) view fun getSetIDByName(_ name: String): Int? {
562 return Pinnacle.setIDsByName[name]
563 }
564
565 /// Allow iterating over Set names in the contract without allocating an array
566 ///
567 access(all) fun forEachSetName(_ function: fun (String): Bool) {
568 Pinnacle.setIDsByName.forEachKey(function)
569 }
570
571 /// Return the contract's setIDsByName dictionary
572 ///
573 access(all) view fun getAllSetIDsByNames(): {String: Int} {
574 return Pinnacle.setIDsByName
575 }
576
577 //------------------------------------------------------------
578 // Shape
579 //------------------------------------------------------------
580
581 /// Struct that defines a Shape
582 ///
583 /// Each Shape is linked to a parent Set.
584 ///
585 access(all) struct Shape {
586 /// This Shapes's unique ID
587 access(all) let id: Int
588
589 /// This Shape's renderID. The uniqueness of renderID is NOT required.
590 access(all) let renderID: String
591
592 /// The ID of the Set that this Shape belongs to
593 access(all) let setID: Int
594
595 /// This Shape's name, unique inside a Set. It can be updated by the Admin.
596 access(all) var name: String
597
598 /// This field indicates whether the Shape is currently open (closedDate is nil) or closed (has actual
599 /// date).
600 ///
601 /// Initially, when a Shape is created, it is in an open state, allowing the creation of Editions.
602 /// Once a Shape is closed, it is no longer possible to create Editions linked to that Shape or
603 /// incrementing its current printing. However, it is still possible to mint NFTs using the Editions
604 /// already created from the Shape. Locking a Shape takes immediate effect, but it can be undone
605 /// during the undo period. The closedDate field indicates the date when the Shape is permanently
606 /// closed, including the undo period.
607 access(all) var closedDate: UInt64?
608
609 /// The current printing of the Shape, determining the printing of the Editions linked to the Shape.
610 /// It can be incremented by the Admin.
611 access(all) var currentPrinting: UInt64
612
613 /// The type of Editions that can be created from this Shape, it is determined by that of this Shape's
614 /// parent Set (cached here to avoid repeated lookups when iterating over Shapes)
615 access(all) let editionType: String
616
617 /// This Shape's metadata dictionary, which stores agreed-upon fields and generally any additional
618 /// data that needs to be stored in the Shape
619 access(contract) let metadata: {String: AnyStruct}
620
621 /// The dictionary that stores the Variant-Printing pairs inside Editions to ensure there can be
622 /// at most one Edition with a given Variant-Printing pair
623 access(self) let variantPrintingPairsInEditions: {String: UInt64}
624
625 /// Struct initializer
626 ///
627 view init(
628 id: Int,
629 renderID: String,
630 setID: Int,
631 name: String,
632 metadata: {String: AnyStruct}
633 ) {
634 self.id = id
635 self.renderID = renderID
636 self.setID = setID
637 self.name = name
638 self.closedDate = nil
639 // Initialize the currentPrinting to 1, this can be incremented by the Admin
640 self.currentPrinting = 1
641 // Get the Edition Type from the parent Set
642 self.editionType = Pinnacle.getSet(id: setID)!.editionType
643 self.metadata = metadata
644 self.variantPrintingPairsInEditions = {}
645 }
646
647 /// Insert the given Variant for the current printing
648 ///
649 access(contract) fun insertVariantPrintingPair(_ variant: String) {
650 self.variantPrintingPairsInEditions[variant] = self.currentPrinting
651 }
652
653 /// Remove the given Variant
654 ///
655 access(contract) fun removeVariantPrintingPair(_ variant: String) {
656 self.variantPrintingPairsInEditions.remove(key: variant)
657 }
658
659 /// Check if an Edition exists with the given Variant for this Shape's current printing
660 ///
661 access(contract) view fun variantPrintingPairExistsInEdition(_ variant: String): Bool {
662 return self.variantPrintingPairsInEditions[variant] == self.currentPrinting
663 }
664
665 /// Close this Shape
666 ///
667 access(contract) fun close() {
668 pre {
669 self.closedDate == nil: "Shape is already closed"
670 }
671 // Set the closed date to the current block timestamp plus the undo period
672 self.closedDate = UInt64(getCurrentBlock().timestamp) + Pinnacle.undoPeriod
673 emit ShapeClosed(
674 id: self.id,
675 renderID: self.renderID,
676 setID: self.setID,
677 name: self.name,
678 currentPrinting: self.currentPrinting,
679 editionType: self.editionType
680 )
681 }
682
683 /// Reopen this Shape
684 ///
685 /// This will fail if the undo period has expired.
686 ///
687 access(contract) fun reopen() {
688 pre {
689 self.closedDate != nil: "Shape is already open"
690 self.closedDate! >= UInt64(getCurrentBlock().timestamp):
691 "Undo period has expired, Shape is permanently closed"
692 }
693 self.closedDate = nil
694 emit EntityReactivated(entity: "Shape", id: self.id, name: self.name)
695 }
696
697 /// Update this Shape's name
698 ///
699 access(contract) fun updateName(_ name: String, _ setRef: &Set) {
700 pre {
701 name != "": "The name of a Shape cannot be an empty string"
702 Pinnacle.getSet(id: self.setID)!.shapeNameExistsInSet(name) == false:
703 "A Shape with that name already exists in the Set"
704 }
705 // Remove the old name from the parent Set's shapes dictionary
706 setRef.removeShapeName(self.name)
707 // Update the name
708 self.name = name
709 // Add the new name to the parent Set's shapes dictionary
710 setRef.insertShapeName(self.name)
711 emit ShapeNameUpdated(
712 id: self.id,
713 renderID: self.renderID,
714 setID: self.setID,
715 name: self.name,
716 currentPrinting: self.currentPrinting,
717 editionType: self.editionType
718 )
719 }
720
721 /// Set this Shape's metadata field
722 ///
723 access(contract) fun setMetadataFields(_ metadata: {String: AnyStruct}) {
724 pre {
725 // Check that metadata is valid
726 (metadata.containsKey("Franchises") == false
727 || metadata["Franchises"]!.isInstance(Type<[String]>())):
728 "Franchises must be a string array"
729 (metadata.containsKey("Studios") == false || metadata["Studios"]!.isInstance(Type<[String]>())):
730 "Studios must be a string array"
731 (metadata.containsKey("Categories") == false || metadata["Categories"]!.isInstance(Type<[String]>())):
732 "Categories must be a string array"
733 (metadata.containsKey("RoyaltyCodes") == false ||
734 metadata["RoyaltyCodes"]!.isInstance(Type<[String]>())):
735 "RoyaltyCodes is required and must be a string array"
736 (metadata.containsKey("Characters") == false || metadata["Characters"]!.isInstance(Type<[String]>())):
737 "Characters must be a string array"
738 (metadata.containsKey("Location") == false || metadata["Location"]!.isInstance(Type<String>())):
739 "Location must be a string"
740 (metadata.containsKey("EventName") == false || metadata["EventName"]!.isInstance(Type<String>())):
741 "EventName must be a string"
742 }
743
744 for key in metadata.keys {
745 assert(metadata[key]!.isInstance(Type<String>()) || metadata[key]!.isInstance(Type<[String]>()),
746 message: "Metadata values must be strings or string arrays")
747 self.metadata[key] = metadata[key]!
748 }
749
750 let convertedMetadata: {String: [String]} = {}
751 for key in self.metadata.keys {
752 convertedMetadata[key] = self.metadata[key]!.isInstance(Type<String>()) == true ?
753 [self.metadata[key]! as! String] : self.metadata[key]! as! [String]
754 }
755
756 emit ShapeMetadataUpdated(id: self.id, metadata: convertedMetadata)
757 }
758
759 /// Increment this Shape's current printing and return the new value
760 ///
761 access(contract) fun incrementCurrentPrinting(): UInt64 {
762 pre {
763 self.closedDate == nil: "Cannot increment the current printing of a closed Shape"
764 }
765 self.currentPrinting = self.currentPrinting + 1
766 emit ShapeCurrentPrintingIncremented(
767 id: self.id,
768 renderID: self.renderID,
769 setID: self.setID,
770 name: self.name,
771 currentPrinting: self.currentPrinting,
772 editionType: self.editionType
773 )
774 return self.currentPrinting
775 }
776
777 /// Return this Shape's metadata dictionary
778 ///
779 access(all) view fun getMetadata(): {String: AnyStruct} {
780 return self.metadata
781 }
782
783 /// Return this Shape's variantPrintingPairsInEditions dictionary
784 ///
785 access(all) view fun getVariantPrintingPairsInEditions(): {String: UInt64} {
786 return self.variantPrintingPairsInEditions
787 }
788 }
789
790 /// Return the ID of the latest Shape created in the contract
791 ///
792 /// The ID is an incrementing integer equal to the length of the shapes array.
793 ///
794 access(all) view fun getLatestShapeID(): Int {
795 return Pinnacle.shapes.length
796 }
797
798 /// Return a Shape struct containing the data of the Shape with the given ID, if it exists in the contract
799 ///
800 access(all) view fun getShape(id: Int): Shape? {
801 pre {
802 id > 0: "The ID of a Shape must be greater than zero"
803 }
804 return Pinnacle.getLatestShapeID() >= id ? Pinnacle.shapes[id - 1] : nil
805 }
806
807 /// Return all Shapes in the contract
808 ///
809 access(all) view fun getAllShapes(): [Shape] {
810 return Pinnacle.shapes
811 }
812
813 //------------------------------------------------------------
814 // Edition
815 //------------------------------------------------------------
816
817 /// Struct that defines an Edition
818 ///
819 /// Each Edition is linked to a parent Shape.
820 ///
821 access(all) struct Edition {
822 /// This Edition's unique ID
823 access(all) let id: Int
824
825 /// This Edition's renderID. The uniqueness of renderID is NOT required.
826 access(all) var renderID: String
827
828 /// The ID of the Series that this Edition's is linked to, it is determined by that of this Edition's
829 /// parent Set (cached here to avoid repeated lookups when iterating over Editions)
830 access(all) let seriesID: Int
831
832 /// The ID of the Set that this Edition's is linked to, it is determined by that of this Edition's
833 /// parent Shape's (cached here to avoid repeated lookups when iterating over Editions)
834 access(all) let setID: Int
835
836 /// The ID of the Shape that this Edition belongs to
837 access(all) let shapeID: Int
838
839 /// This Edition's Variant
840 access(all) let variant: String?
841
842 /// This Edition's Printing, determined by the current printing value of the parent Shape when the
843 /// Edition is created
844 access(all) let printing: UInt64
845
846 /// The ID of the Edition Type that this Edition's is linked to, it is determined by that of this
847 /// Edition's parent Shape (cached here to avoid repeated lookups when iterating over Editions)
848 access(all) let editionTypeID: Int
849
850 /// This Edition's description. It can be updated by the Admin.
851 access(all) var description: String
852
853 /// Attribute to denote an alternative class of Editions
854 access(all) let isChaser: Bool
855
856 /// This Edition's traits dictionary, which stores agreed-upon fields and generally any additional
857 /// data that needs to be stored in the Edition
858 access(contract) let traits: {String: AnyStruct}
859
860 /// If the Edition is a Limited Edition, this value is the maximum number of NFTs that can be minted
861 /// in the Edition - otherwise, this value is nil
862 access(all) var maxMintSize: UInt64?
863
864 /// The number of NFTs that have been minted in the Edition, this value is incremented every time a
865 /// new NFT is minted
866 access(all) var numberMinted: UInt64
867
868 /// If the Edition is a Maturing Edition, this value is the time period that must pass starting from
869 /// the Edition's creation date before the NFTs minted in the Edition can be withdrawn from the
870 /// collection - otherwise, this value is nil
871 access(all) let maturationPeriod: UInt64?
872
873 /// This field indicates the Edition's closed date. This value is nil when an Edition is created.
874 ///
875 /// When an Open/Unlimited Edition is closed, it is no longer possible to mint NFTs from that Edition.
876 /// Closing an Open/Unlimited Edition takes immediate effect, but it can be undone during the undo
877 /// period. The closedDate field indicates the date when the Edition is permanently closed.
878 ///
879 /// In contrast, the ability to mint NFTs from a Limited Edition is determined by the Edition's number
880 /// minted being less than the Edition's max mint size. Closing a Limited Edition is only a formality
881 /// allowing the admin to set the Edition's closed date to the end of the primary release sales period,
882 /// once the Edition's max mint size has been reached.
883 access(all) var closedDate: UInt64?
884
885 /// The date that the Edition was created (Unix timestamp)
886 access(all) let creationDate: UInt64
887
888 /// Struct initializer
889 ///
890 view init(
891 id: Int,
892 renderID: String,
893 shapeID: Int,
894 variant: String?,
895 description: String,
896 isChaser: Bool,
897 maxMintSize: UInt64?,
898 maturationPeriod: UInt64?,
899 traits: {String: AnyStruct}
900 ) {
901 self.id = id
902 self.renderID = renderID
903 self.shapeID = shapeID
904 // Get setID from the parent Shape
905 self.setID = Pinnacle.getShape(id: shapeID)!.setID
906 // Get seriesID from the parent Set
907 self.seriesID = Pinnacle.getSet(id: self.setID)!.seriesID
908 self.variant = variant
909 // Get the printing from the parent Shape
910 self.printing = Pinnacle.getShape(id: shapeID)!.currentPrinting
911 // Get editionTypeID from the parent Shape
912 self.editionTypeID = Pinnacle.getEditionTypeByName(Pinnacle.getShape(id: shapeID)!.editionType)!.id
913 self.description = description
914 self.isChaser = isChaser
915 self.traits = traits
916 self.maxMintSize = maxMintSize
917 self.numberMinted = 0
918 self.maturationPeriod = maturationPeriod
919 self.closedDate = nil
920 self.creationDate = UInt64(getCurrentBlock().timestamp)
921 }
922
923 /// Check if this Edition's max mint size has been reached
924 ///
925 access(all) view fun isMaxEditionMintSizeReached(): Bool {
926 return self.numberMinted == self.maxMintSize
927 }
928
929 /// Close this Edition.
930 ///
931 /// For Open/Unlimited Editions, closing an Edition is necessary to set the Edition's max mint size
932 /// to the number minted, so that no more pin NFTs can be minted from it.
933 ///
934 /// For a Limited Edition, closing an Edition is only a formality allowing the admin to set the
935 /// Edition's closed date. A Limited Edition can be closed only once the Edition's max mint size has
936 /// been reached.
937 ///
938 /// This will fail if the Edition is already closed.
939 ///
940 access(contract) fun close() {
941 pre {
942 self.closedDate == nil: "This Edition is already closed, number of pins minted: "
943 .concat(self.numberMinted.toString())
944 (Pinnacle.getEditionType(id: self.editionTypeID)!.isLimited == false ||
945 self.isMaxEditionMintSizeReached()):
946 "The Edition must be an Open/Unlimited Edition or a Limited Edition that has reached its max mint size"
947 }
948 // Set the max mint size to the number minted
949 self.maxMintSize = self.numberMinted
950 // Set the closed date to the current block timestamp plus the undo period
951 self.closedDate = UInt64(getCurrentBlock().timestamp) + Pinnacle.undoPeriod
952 emit EditionClosed(
953 id: self.id,
954 maxMintSize: self.maxMintSize!
955 )
956 }
957
958 /// Reopen this Edition
959 ///
960 /// This will fail if the Edition if a Limited Edition or if the undo period has expired.
961 ///
962 access(contract) fun reopen() {
963 pre {
964 self.closedDate != nil: "Edition is already open"
965 self.closedDate! >= UInt64(getCurrentBlock().timestamp):
966 "Undo period has expired, Edition Type is permanently closed"
967 Pinnacle.getEditionType(id: self.editionTypeID)!.isLimited == false:
968 "The Edition must be an Open/Unlimited Edition"
969 }
970 self.maxMintSize = nil
971 self.closedDate = nil
972 emit EntityReactivated(entity: "Edition", id: self.id, name: nil)
973 }
974
975 /// Increment this Edition's number minted
976 ///
977 /// This is only called when a Pin NFT is minted in the Edition.
978 ///
979 access(contract) fun incrementNumberMinted() {
980 self.numberMinted = self.numberMinted + 1
981 }
982
983 /// Decrement this Edition's number minted
984 ///
985 /// This is only called from the burnOpenEditionNFT Admin function.
986 ///
987 access(contract) fun decrementNumberMinted() {
988 pre {
989 self.numberMinted != self.maxMintSize:
990 "This Edition must not have been closed"
991 Pinnacle.getEditionType(id: self.editionTypeID)!.isLimited == false:
992 "The Edition must be an Open/Unlimited Edition"
993 }
994 self.numberMinted = self.numberMinted - 1
995 }
996
997 /// Return the serial number of the NFT to be minted, the Edition's number minted + 1 if the Edition
998 /// is limited, nil otherwise.
999 ///
1000 access(contract) view fun getNextSerialNumber(): UInt64? {
1001 return Pinnacle.getEditionType(id: self.editionTypeID)!.isLimited ? self.numberMinted + 1 : nil
1002 }
1003
1004 /// Update this Edition's description
1005 ///
1006 access(contract) fun updateDescription(_ description: String) {
1007 self.description = description
1008 emit EditionDescriptionUpdated(
1009 id: self.id,
1010 description: self.description
1011 )
1012 }
1013
1014 /// Update this Edition's Render ID
1015 ///
1016 access(contract) fun updateRenderID(_ renderID: String) {
1017 self.renderID = renderID
1018 emit EditionRenderIDUpdated(
1019 id: self.id,
1020 renderID: self.renderID
1021 )
1022 }
1023
1024 /// Return this Edition's traits dictionary
1025 ///
1026 access(all) view fun getTraits(): {String: AnyStruct} {
1027 return self.traits
1028 }
1029 }
1030
1031 /// Return the ID of the latest Edition created in the contract.
1032 ///
1033 /// The ID is an incrementing integer equal to the length of the editions array.
1034 ///
1035 access(all) view fun getLatestEditionID(): Int {
1036 return Pinnacle.editions.length
1037 }
1038
1039 /// Return an Edition struct containing the data of the Edition with the given ID, if it exists in the
1040 /// contract
1041 ///
1042 access(all) view fun getEdition(id: Int): Edition? {
1043 pre {
1044 id > 0: "The ID of an Edition must be greater than 0"
1045 }
1046 return Pinnacle.getLatestEditionID() >= id ? Pinnacle.editions[id - 1] : nil
1047 }
1048
1049 /// Return all Editions in the contract
1050 ///
1051 access(all) view fun getAllEditions(): [Edition] {
1052 return Pinnacle.editions
1053 }
1054
1055 //------------------------------------------------------------
1056 // Edition Type
1057 //------------------------------------------------------------
1058
1059 /// Struct that defines an Edition Type
1060 ///
1061 /// Each Edition Type is created independently of any other entity.
1062 ///
1063 /// The contract creates the following default Edition Types during initialization: "Genesis Edition",
1064 /// "Unique Edition", "Limited Edition", "Open Edition", "Starter Edition", and "Event Edition".
1065 ///
1066 access(all) struct EditionType {
1067 /// This Edition Type's unique ID
1068 access(all) let id: Int
1069
1070 /// This Edition Type's unique name
1071 access(all) let name: String
1072
1073 /// Indicate if the Edition Type is Limited (true) or Open/Unlimited (false)
1074 access(all) let isLimited: Bool
1075
1076 /// Indicate if the Edition Type is Maturing (true) or Non-Maturing (false)
1077 access(all) let isMaturing: Bool
1078
1079 /// This field indicates whether the Edition Type is currently open (closedDate is nil) or closed (has
1080 /// actual date).
1081 ///
1082 /// Initially, when an Edition Type is created, it is in an open state, allowing the creation of Sets.
1083 /// Once an Edition Type is closed, it is no longer possible to create Sets linked to that Edition
1084 /// Type as well as Shapes linked to those Sets and Editions linked to those Shapes. However, it is
1085 /// still possible to mint NFTs using the Editions already created from any Shapes. Locking an Edition
1086 /// Type takes immediate effect, but it can be undone during the undo period. The closedDate field
1087 /// indicates the date when the Edition Type is permanently closed, including the undo period.
1088 access(all) var closedDate: UInt64?
1089
1090 /// Struct initializer
1091 ///
1092 view init(id: Int, name: String, isLimited: Bool, isMaturing: Bool) {
1093 self.id = id
1094 self.name = name
1095 self.isLimited = isLimited
1096 self.isMaturing = isMaturing
1097 self.closedDate = nil
1098 }
1099
1100 /// Close this Edition Type
1101 ///
1102 access(contract) fun close() {
1103 pre {
1104 self.closedDate == nil: "Edition type is already closed"
1105 }
1106 // Set the closed date to the current block timestamp plus the undo period
1107 self.closedDate = UInt64(getCurrentBlock().timestamp) + Pinnacle.undoPeriod
1108 emit EditionTypeClosed(
1109 id: self.id,
1110 name: self.name,
1111 isLimited: self.isLimited,
1112 isMaturing: self.isMaturing
1113 )
1114 }
1115
1116 /// Reopen this Edition Type
1117 ///
1118 /// This will fail if the undo period has expired.
1119 ///
1120 access(contract) fun reopen() {
1121 pre {
1122 self.closedDate != nil: "Edition Type is already open"
1123 self.closedDate! >= UInt64(getCurrentBlock().timestamp):
1124 "Undo period has expired, Edition Type is permanently closed"
1125 }
1126 self.closedDate = nil
1127 emit EntityReactivated(entity: "EditionType", id: self.id, name: self.name)
1128 }
1129 }
1130
1131 /// Return the ID of the latest Edition Type created in the contract
1132 ///
1133 /// The ID is an incrementing integer equal to the length of the editionTypes array.
1134 ///
1135 access(all) view fun getLatestEditionTypeID(): Int {
1136 return Pinnacle.editionTypes.length
1137 }
1138
1139 /// Return an EditionType struct containing the data of the EditionType with the given ID, if it exists
1140 /// in the contract
1141 ///
1142 access(all) view fun getEditionType(id: Int): EditionType? {
1143 pre {
1144 id > 0: "The ID of an Edition Type must be greater than 0"
1145 }
1146 return Pinnacle.getLatestEditionTypeID() >= id ? Pinnacle.editionTypes[id - 1] : nil
1147 }
1148
1149 /// Return all Edition Types in the contract
1150 ///
1151 access(all) view fun getAllEditionTypes(): [EditionType] {
1152 return Pinnacle.editionTypes
1153 }
1154
1155 /// Return an EditionType struct containing the data of the EditionType with the given name, if it exists
1156 /// in the contract
1157 ///
1158 access(all) view fun getEditionTypeByName(_ name: String): EditionType? {
1159 if let id = Pinnacle.editionTypeIDsByName[name] {
1160 return Pinnacle.getEditionType(id: id)
1161 }
1162 return nil
1163 }
1164
1165 /// Return the ID of the Edition Type with the given name, if it exists in the contract
1166 ///
1167 access(all) view fun getEditionTypeIDByName(_ name: String): Int? {
1168 return Pinnacle.editionTypeIDsByName[name]
1169 }
1170
1171 //------------------------------------------------------------
1172 // Inscription
1173 //------------------------------------------------------------
1174
1175 /// Struct that defines an Inscription
1176 ///
1177 /// Inscriptions are stored in NFTs.
1178 ///
1179 access(all) struct Inscription {
1180 /// This Inscription's ID, unique inside an NFT
1181 access(all) let id: Int
1182
1183 /// The address of the account that added the Inscription, unique inside an NFT
1184 access(all) let thenOwner: Address
1185
1186 /// The note that can be added to the Inscription
1187 access(all) var note: String?
1188
1189 /// The date the Inscription was added to the NFT
1190 access(all) let dateAdded: UInt64
1191
1192 /// This Inscription's extension dictionary, which stores any additional data that needs to be stored
1193 /// in the Inscription. This field has been added to accommodate potential future contract updates and
1194 /// facilitate new functionalities. It is not in use currently.
1195 access(self) let extension: {String: AnyStruct}
1196
1197 /// Struct initializer
1198 ///
1199 view init(
1200 id: Int,
1201 owner: Address,
1202 extension: {String: AnyStruct}?
1203 ) {
1204 self.id = id
1205 self.thenOwner = owner
1206 self.note = nil
1207 self.dateAdded = UInt64(getCurrentBlock().timestamp)
1208 self.extension = extension ?? {}
1209 }
1210
1211 /// Set this Inscription's note
1212 ///
1213 access(contract) fun setNote(_ note: String?) {
1214 self.note = note
1215 }
1216
1217 /// Return this Inscription's extension dictionary
1218 ///
1219 access(all) view fun getExtension(): {String: AnyStruct} {
1220 return self.extension
1221 }
1222 }
1223
1224 //------------------------------------------------------------
1225 // NFT
1226 //------------------------------------------------------------
1227
1228 /// Resource that defines a Pin NFT
1229 ///
1230 access(all) resource NFT: NonFungibleToken.NFT {
1231 /// This NFT's unique ID
1232 access(all) let id: UInt64
1233
1234 /// This NFT's unique renderID. The uniqueness of renderID is not required.
1235 access(all) let renderID: String
1236
1237 /// The ID of the Edition that this NFT belongs to
1238 access(all) let editionID: Int
1239
1240 /// This NFT' serial number - nil if the NFT has not been minted from a Limited Edition
1241 access(all) let serialNumber: UInt64?
1242
1243 /// The date that this NFT was minted (Unix timestamp)
1244 access(all) let mintingDate: UInt64
1245
1246 /// This NFT's experience points balance - nil if the NFT's owner has opted out
1247 access(all) var xp: UInt64?
1248
1249 /// The array where this NFT's Inscriptions are stored
1250 access(self) let inscriptions: [Inscription]
1251
1252 /// The dictionary that allows Inscriptions to be looked up by address
1253 access(self) let inscriptionIDsByAddress: {Address: Int}
1254
1255 /// This NFT's extension dictionary, which stores any additional data that needs to be stored in the
1256 /// NFT. This field has been added to accommodate potential future contract updates and facilitate new
1257 /// functionalities. It is not in use currently.
1258 access(self) let extension: {String: AnyStruct}
1259
1260 /// NFT initializer
1261 ///
1262 init(editionID: Int, extension: {String: AnyStruct}?) {
1263 pre {
1264 // Check that the Edition exists and has not reached its max mint size
1265 Pinnacle.getLatestEditionID() >= editionID: "editionID does not exist"
1266 Pinnacle.getEdition(id: editionID)!.isMaxEditionMintSizeReached() == false:
1267 "Max mint size (".concat(Pinnacle.getEdition(id: editionID)!.maxMintSize!.toString())
1268 .concat(") reached for Edition ID = ").concat(editionID.toString())
1269 }
1270 self.id = self.uuid
1271 self.renderID = Pinnacle.getEdition(id: editionID)!.renderID
1272 self.editionID = editionID
1273 self.serialNumber = Pinnacle.getEdition(id: editionID)!.getNextSerialNumber()
1274 self.mintingDate = UInt64(getCurrentBlock().timestamp)
1275 self.xp = 0
1276 self.inscriptions = []
1277 self.inscriptionIDsByAddress = {}
1278 self.extension = extension ?? {}
1279 emit PinNFTMinted(
1280 id: self.id,
1281 renderID: self.renderID,
1282 editionID: self.editionID,
1283 serialNumber: self.serialNumber,
1284 maturityDate: self.getMaturityDate()
1285 )
1286 }
1287
1288 /// Event emitted when a Pin NFT is destroyed (replaces PinNFTBurned event in Pinnacle contract
1289 /// before Cadence 1.0 update)
1290 ///
1291 access(all) event ResourceDestroyed(
1292 id: UInt64 = self.id,
1293 editionID: Int = self.editionID,
1294 serialNumber: UInt64? = self.serialNumber,
1295 xp: UInt64? = self.xp
1296 )
1297
1298 /// Return this NFT's maturity date if it is a Maturing Edition NFT, nil otherwise
1299 ///
1300 access(all) view fun getMaturityDate(): UInt64? {
1301 let edition = Pinnacle.getEdition(id: self.editionID)!
1302 return edition.maturationPeriod != nil ? edition.creationDate + edition.maturationPeriod! : nil
1303 }
1304
1305 /// Return if this NFT's is locked by maturity date.
1306 /// Return true if the current block timestamp is less than the lock expiry, false otherwise.
1307 ///
1308 access(all) view fun isLocked(): Bool {
1309 if let maturityDate = self.getMaturityDate() {
1310 return maturityDate > UInt64(getCurrentBlock().timestamp)
1311 }
1312 return false
1313 }
1314
1315
1316 /// Return this NFT's inscriptions limit
1317 ///
1318 access(all) view fun getInscriptionsLimit(): Int {
1319 return Pinnacle.inscriptionsLimits[self.id] ?? 100
1320 }
1321
1322 /// Add an Inscription in this NFT tied to the current owner address and return its ID
1323 ///
1324 /// This function can only be called from the Collection resource that contains this NFT. It will
1325 /// fail if the Inscription was already added by the current owner or if the NFT has reached its
1326 /// max inscriptions size, which is 100 by default and can be set to a higher value by the Admin.
1327 ///
1328 access(contract) fun addCurrentOwnerInscription(_ currentOwner: Address): Int {
1329 pre {
1330 self.inscriptionIDsByAddress.containsKey(currentOwner) == false:
1331 "The Inscription was already added by the current owner, date added: "
1332 .concat(self.inscriptions[self.inscriptionIDsByAddress[currentOwner]!].dateAdded.toString())
1333 self.getLatestInscriptionID() < self.getInscriptionsLimit():
1334 "Max Inscriptions size (".concat((self.getInscriptionsLimit()).toString())
1335 .concat(") reached for NFT ID = ").concat(self.id.toString())
1336 }
1337 let inscription = Inscription(
1338 id: self.getLatestInscriptionID() + 1,
1339 owner: currentOwner,
1340 extension: nil
1341 )
1342 self.inscriptions.append(inscription)
1343 self.inscriptionIDsByAddress[currentOwner] = inscription.id
1344 emit NFTInscriptionAdded(
1345 id: inscription.id,
1346 owner: inscription.thenOwner,
1347 note: inscription.note,
1348 nftID: self.id,
1349 editionID: self.editionID
1350 )
1351 return inscription.id
1352 }
1353
1354 /// Return a reference to the Inscription with the given ID, if it exists in the NFT
1355 ///
1356 access(contract) view fun borrowInscription(id: Int): &Inscription? {
1357 pre {
1358 id > 0: "The ID of an Inscription must be greater than 0"
1359 }
1360 return self.getLatestInscriptionID() >= id ? &self.inscriptions[id - 1] as &Inscription : nil
1361 }
1362
1363 /// Remove the current owner's Inscription from this NFT
1364 ///
1365 /// This will fail if the undo period has expired or if another Inscription has been added after the
1366 /// current owner's.
1367 ///
1368 access(contract) fun removeCurrentOwnerInscription(_ currentOwner: Address) {
1369 pre {
1370 self.inscriptionIDsByAddress.containsKey(currentOwner) == true:
1371 "No Inscription added by the current owner"
1372 self.getInscriptionByAddress(currentOwner)!.id == self.getLatestInscriptionID():
1373 "The Inscription to remove must be the last one added"
1374 self.getInscriptionByAddress(currentOwner)!.dateAdded + Pinnacle.undoPeriod >=
1375 UInt64(getCurrentBlock().timestamp):
1376 "Undo period has expired, Inscription is permanently added"
1377 }
1378 let inscriptionID = self.getInscriptionIDsByAddress(currentOwner)!
1379 self.inscriptions.removeLast()
1380 self.inscriptionIDsByAddress.remove(key: currentOwner)
1381 emit NFTInscriptionRemoved(
1382 id: inscriptionID,
1383 owner: currentOwner,
1384 nftID: self.id,
1385 editionID: self.editionID
1386 )
1387 }
1388
1389 /// Update the current owner's Inscription note in this NFT
1390 ///
1391 /// This function can only be called from the Collection resource that contains this NFT and requires
1392 /// Admin co-signing.
1393 ///
1394 access(contract) fun updateCurrentOwnerInscriptionNote(currentOwner: Address, note: String) {
1395 pre {
1396 self.inscriptionIDsByAddress.containsKey(currentOwner) == true:
1397 "Inscription must have been added by the current owner"
1398 }
1399 let inscriptionRef = self.borrowInscription(
1400 id: self.getInscriptionIDsByAddress(currentOwner)!
1401 )!
1402 inscriptionRef.setNote(note)
1403 emit NFTInscriptionUpdated(
1404 id: inscriptionRef.id,
1405 owner: currentOwner,
1406 note: inscriptionRef.note,
1407 nftID: self.id,
1408 editionID: self.editionID
1409 )
1410 Pinnacle.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &NFT)
1411 }
1412
1413 /// Toggle this NFT's ability to hold a XP balance (turned on by default) and return the XP's new value
1414 ///
1415 /// This function can only be called from the Collection resource that contains this NFT.
1416 ///
1417 /// This allows opting in or out of the NFT's ability to hold XP. If XP is nil, this will set it to 0.
1418 /// If XP is 0, this will set it to nil.
1419 ///
1420 access(contract) fun toggleXP(): UInt64? {
1421 self.xp = self.xp == nil ? 0 : nil
1422 emit NFTXPUpdated(id: self.id, editionID: self.editionID, xp: self.xp)
1423 Pinnacle.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &NFT)
1424 return self.xp
1425 }
1426
1427 /// Add experience points to this NFT and return the new XP balance
1428 ///
1429 /// This function can only be called from the Admin resource with the condition that the NFT owner
1430 /// has not opted out of the NFT's ability to hold XP with the toggleXP function.
1431 ///
1432 access(contract) fun addXP(_ value: UInt64): UInt64 {
1433 pre {
1434 self.xp != nil: "XP must have been previously set by the owner"
1435 }
1436 self.xp = self.xp! + value
1437 emit NFTXPUpdated(id: self.id, editionID: self.editionID, xp: self.xp)
1438 Pinnacle.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &NFT)
1439 return self.xp!
1440 }
1441
1442 /// Subtract experience points from this NFT and return the new XP balance
1443 ///
1444 /// This function can only be called from the Admin resource with the condition that the NFT owner
1445 /// has not opted out of the NFT's ability to hold XP with the toggleXP function.
1446 ///
1447 access(contract) fun subtractXP(_ value: UInt64): UInt64 {
1448 pre {
1449 self.xp != nil: "XP must have been previously set by the owner"
1450 self.xp! >= value:
1451 "Cannot subtract below minimum XP of 0, current XP: ".concat(self.xp!.toString())
1452 }
1453 self.xp = self.xp! - value
1454 emit NFTXPUpdated(id: self.id, editionID: self.editionID, xp: self.xp)
1455 Pinnacle.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &NFT)
1456 return self.xp!
1457 }
1458
1459 /// Return the ID of the latest Inscription added in the NFT
1460 ///
1461 /// The ID is an incrementing integer equal to the length of the inscriptions array.
1462 ///
1463 access(all) view fun getLatestInscriptionID(): Int {
1464 return self.inscriptions.length
1465 }
1466
1467 /// Return an Inscription struct containing the data of the Inscription with the given ID, if it
1468 /// exists in the NFT
1469 ///
1470 access(all) view fun getInscription(id: Int): Inscription? {
1471 pre {
1472 id > 0: "The ID of an Inscription must be greater than 0"
1473 }
1474 return self.getLatestInscriptionID() >= id ? self.inscriptions[id - 1] : nil
1475 }
1476
1477 /// Return all Inscriptions in the NFT
1478 ///
1479 access(all) view fun getAllInscriptions(): [Inscription] {
1480 return self.inscriptions
1481 }
1482
1483 /// Return an Inscription struct containing the data of the Inscription with the given address if it
1484 /// exists in the NFT
1485 ///
1486 access(all) view fun getInscriptionByAddress(_ address: Address): Inscription? {
1487 if let id = self.inscriptionIDsByAddress[address] {
1488 return self.getInscription(id: id)
1489 }
1490 return nil
1491 }
1492
1493 /// Return the ID of the Inscription with the given address, if it exists in the NFT
1494 ///
1495 access(all) view fun getInscriptionIDsByAddress(_ address: Address): Int? {
1496 return self.inscriptionIDsByAddress[address]
1497 }
1498
1499 /// Return this NFT's inscriptionIDsByAddress dictionary
1500 ///
1501 access(all) view fun getAllInscriptionIDsByAddresses(): {Address: Int} {
1502 return self.inscriptionIDsByAddress
1503 }
1504
1505 /// Return this NFT's extension dictionary
1506 ///
1507 access(all) view fun getExtension(): {String: AnyStruct} {
1508 return self.extension
1509 }
1510
1511 /// Return the metadata view types available for this NFT
1512 ///
1513 access(all) view fun getViews(): [Type] {
1514 return [
1515 Type<MetadataViews.Display>(),
1516 Type<MetadataViews.ExternalURL>(),
1517 Type<MetadataViews.Traits>(),
1518 Type<MetadataViews.Medias>(),
1519 Type<MetadataViews.Editions>(),
1520 Type<MetadataViews.Serial>(),
1521 Type<MetadataViews.Royalties>(),
1522 Type<MetadataViews.NFTCollectionDisplay>(),
1523 Type<MetadataViews.NFTCollectionData>()
1524 ]
1525 }
1526
1527 /// Resolve this NFT's metadata views
1528 ///
1529 access(all) fun resolveView(_ view: Type): AnyStruct? {
1530 post {
1531 result == nil || result!.getType() == view:
1532 "The returned view must be of the given type or nil"
1533 }
1534 switch view {
1535 case Type<MetadataViews.Display>(): return self.resolveDisplayView()
1536 case Type<MetadataViews.ExternalURL>(): return self.resolveExternalURLView()
1537 case Type<MetadataViews.Traits>(): return self.resolveTraitsView()
1538 case Type<MetadataViews.Medias>(): return self.resolveMediasView()
1539 case Type<MetadataViews.Editions>(): return self.resolveEditionsView()
1540 case Type<MetadataViews.Serial>(): return self.resolveSerialView()
1541 case Type<MetadataViews.Royalties>(): return self.resolveRoyaltiesView()
1542 case Type<MetadataViews.NFTCollectionDisplay>(): return Pinnacle.resolveNFTCollectionDisplayView()
1543 case Type<MetadataViews.NFTCollectionData>(): return Pinnacle.resolveNFTCollectionDataView()
1544 }
1545 return nil
1546 }
1547
1548 /// Resolve this NFT's Display view
1549 ///
1550 access(all) fun resolveDisplayView(): MetadataViews.Display {
1551 return MetadataViews.Display(
1552 name: self.getName(),
1553 description: self.getDescription(),
1554 thumbnail: self.getThumbnailPath()
1555 )
1556 }
1557
1558 /// Resolve this NFT's ExternalURL view
1559 ///
1560 access(all) fun resolveExternalURLView(): MetadataViews.ExternalURL {
1561 return MetadataViews.ExternalURL("https://disneypinnacle.com")
1562 }
1563
1564 /// Resolve this NFT's Traits view
1565 ///
1566 access(all) fun resolveTraitsView(): MetadataViews.Traits {
1567 // Retrieve this NFT's parent Edition, Shape, Set, and Series data
1568 let edition = Pinnacle.getEdition(id: self.editionID)!
1569 let shape = Pinnacle.getShape(id: edition.shapeID)!
1570 let set = Pinnacle.getSet(id: edition.setID)!
1571 let series = Pinnacle.getSeries(id: edition.seriesID)!
1572 // Create a dictionary of this NFT's traits with the default metadata entries
1573 let traits: {String: AnyStruct} = {
1574 "EditionType" : Pinnacle.getEditionType(id: edition.editionTypeID)!.name,
1575 "SeriesName" : series.name,
1576 "SetName" : set.name,
1577 "IsChaser" : edition.isChaser,
1578 "Printing": edition.printing,
1579 "MintingDate": self.mintingDate
1580 }
1581 // If the Edition has a Variant, add the Variant trait
1582 if edition.variant != nil {
1583 traits["Variant"] = edition.variant!
1584 }
1585 // If the NFT is a Limited Edition NFT, add the SerialNumber trait
1586 if self.serialNumber != nil {
1587 traits["SerialNumber"] = self.serialNumber!
1588 }
1589 // If the NFT's Edition is a Maturing Edition, add the MaturityDate trait
1590 if edition.maturationPeriod != nil {
1591 traits["MaturityDate"] = self.getMaturityDate()!
1592 }
1593 // Add the Shape's metadata entries
1594 for key in shape.metadata.keys {
1595 traits[key] = shape.metadata[key]
1596 }
1597 // Add the Edition's traits entries
1598 for key in edition.traits.keys {
1599 traits[key] = edition.traits[key]
1600 }
1601 // Return the traits dictionary
1602 return MetadataViews.dictToTraits(dict: traits, excludedNames: nil)
1603 }
1604
1605 /// Resolve this NFT's Medias view
1606 ///
1607 access(all) fun resolveMediasView(): MetadataViews.Medias {
1608 return MetadataViews.Medias(
1609 [
1610 MetadataViews.Media(
1611 file: MetadataViews.HTTPFile(url: "https://assets.disneypinnacle.com/on-chain/pinnacle.jpg"),
1612 mediaType: "image/jpg"
1613 )
1614 ]
1615 )
1616 }
1617
1618 /// Resolve this NFT's Editions view
1619 ///
1620 access(all) fun resolveEditionsView(): MetadataViews.Editions {
1621 let edition = Pinnacle.getEdition(id: self.editionID)!
1622 let shape = Pinnacle.getShape(id: edition.shapeID)!
1623 let set = Pinnacle.getSet(id: edition.setID)!
1624 // Assemble the name
1625 let editionName = shape.name.concat(" [").concat(set.name)
1626 .concat(edition.variant != nil ? ", ".concat(edition.variant!) : "")
1627 .concat(edition.printing > 1 ? ", Printing #".concat(edition.printing.toString()) : "")
1628 .concat("]")
1629 // Create and return the Editions view
1630 return MetadataViews.Editions(
1631 [MetadataViews.Edition(
1632 name: editionName,
1633 number: self.serialNumber ?? 0,
1634 max: edition.maxMintSize
1635 )
1636 ]
1637 )
1638 }
1639
1640 /// Resolve this NFT's Serial view if it is a Limited Edition NFT - return nil otherwise
1641 ///
1642 access(all) fun resolveSerialView(): MetadataViews.Serial? {
1643 return Pinnacle.getEditionType(id: Pinnacle.getEdition(id: self.editionID)!.editionTypeID)!.isLimited ?
1644 MetadataViews.Serial(self.serialNumber!) : nil
1645 }
1646
1647 /// Resolve this NFT's Royalties view
1648 ///
1649 access(all) fun resolveRoyaltiesView(): MetadataViews.Royalties {
1650 let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
1651 getAccount(Pinnacle.royaltyAddress).capabilities.get<&{FungibleToken.Receiver}>(
1652 MetadataViews.getRoyaltyReceiverPublicPath())!
1653 return MetadataViews.Royalties(
1654 [
1655 MetadataViews.Royalty(
1656 receiver: royaltyReceiver,
1657 cut: 0.05,
1658 description: "placeholder_royalty_description"
1659 )
1660 ]
1661 )
1662 }
1663
1664 /// Return this NFT's name
1665 ///
1666 access(all) view fun getName(): String {
1667 // Retrieve this NFT's parent Edition, Shape, and Set data
1668 let edition = Pinnacle.getEdition(id: self.editionID)!
1669 let shape = Pinnacle.getShape(id: edition.shapeID)!
1670 let set = Pinnacle.getSet(id: edition.setID)!
1671 // Assemble and return the name
1672 return shape.name.concat(self.serialNumber != nil ? " [#".concat(self.serialNumber!.toString())
1673 .concat("/").concat(edition.maxMintSize!.toString()).concat("] [") : " [").concat(set.name)
1674 .concat(edition.variant != nil ? ", ".concat(edition.variant!) : "")
1675 .concat(edition.printing > 1 ? ", Printing #".concat(edition.printing.toString()) : "")
1676 .concat("]")
1677 }
1678
1679 /// Return this NFT's description
1680 ///
1681 /// The description is composed of the end-user license URL, the description of this NFT's Edition,
1682 /// and this NFT's concatenated Inscription notes if there any, ordered by the date they have been
1683 /// added. It is generally intended that the Inscription notes are human-readable and that they are
1684 /// written in a way that makes sense when concatenated, avoiding escape chars and with each
1685 /// Inscription's details included as needed. Inscription notes require both the owner's and the
1686 /// Admin's approval to be updated (see the updateNFTInscriptionNote function in the Collection
1687 /// resource).
1688 ///
1689 access(all) view fun getDescription(): String {
1690 var notes = ""
1691 for inscription in self.inscriptions {
1692 // If the Inscription is permanently added and has a note, add it to the notes string
1693 if inscription.dateAdded + Pinnacle.undoPeriod < UInt64(getCurrentBlock().timestamp) {
1694 if let note = inscription.note {
1695 notes.concat("\n\n").concat(note)
1696 }
1697 }
1698 }
1699 var header = ""
1700 if let caption = Pinnacle.extension["EndUserLicenseCaption"] as! String? {
1701 header = header.concat(caption).concat(": ")
1702 }
1703 header = header.concat(Pinnacle.endUserLicenseURL)
1704 return header.concat("\n\n")
1705 .concat(Pinnacle.getEdition(id: self.editionID)!.description)
1706 .concat(notes != "" ? "\n\n".concat(notes) : "")
1707 }
1708
1709 /// Return this NFT's thumbnail path
1710 ///
1711 access(all) fun getThumbnailPath(): MetadataViews.HTTPFile {
1712 return MetadataViews.HTTPFile(url:"https://assets.disneypinnacle.com/on-chain/pinnacle.jpg")
1713 }
1714
1715 /// Return an asset path
1716 ///
1717 access(all) view fun getAssetPath(): String {
1718 return "placeholder_pinnacle_base_asset_path"
1719 }
1720
1721 /// Return an image path
1722 ///
1723 access(all) view fun getImagePath(): String {
1724 return "placeholder_image_path"
1725 }
1726
1727 /// Return a video path
1728 ///
1729 access(all) view fun getVideoPath(): String {
1730 return "placeholder_video_path"
1731 }
1732
1733 /// Return a Pin path
1734 ///
1735 access(all) view fun getPinPath(): String {
1736 return "placeholder_pinnacle_pin_path"
1737 }
1738
1739 /// Create an empty Collection for Pinnacle NFTs and return it to the caller
1740 ///
1741 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
1742 return <- Pinnacle.createEmptyCollection(nftType: Type<@NFT>())
1743 }
1744 }
1745
1746 //------------------------------------------------------------
1747 // Collection
1748 //------------------------------------------------------------
1749
1750 // Deprecated in favor of using entitlement-based access control as required by Cadence 1.0
1751 access(all) resource interface PinNFTCollectionPublic: NonFungibleToken.Collection {}
1752
1753 /// Resource that defines a Pinnacle NFT Collection
1754 ///
1755 access(all) resource Collection: NonFungibleToken.Collection, PinNFTCollectionPublic {
1756 /// Dictionary of NFT conforming tokens
1757 /// NFT is a resource type with a UInt64 ID field
1758 ///
1759 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
1760
1761 /// Collection initializer
1762 ///
1763 view init() {
1764 self.ownedNFTs <- {}
1765 }
1766
1767 /// Return a list of NFT types that this receiver accepts
1768 ///
1769 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
1770 let supportedTypes: {Type: Bool} = {}
1771 supportedTypes[Type<@NFT>()] = true
1772 return supportedTypes
1773 }
1774
1775 /// Return whether or not the given type is accepted by the collection
1776 /// A collection that can accept any type should just return true by default
1777 ///
1778 access(all) view fun isSupportedNFTType(type: Type): Bool {
1779 if type == Type<@NFT>() {
1780 return true
1781 }
1782 return false
1783 }
1784
1785 /// Remove an NFT from the Collection and move it to the caller
1786 ///
1787 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
1788 let token <- self.ownedNFTs.remove(key: withdrawID)
1789 ?? panic("No NFT with such ID in the Collection")
1790 let nft <- token as! @NFT
1791 // If the NFT was minted from a Maturing Edition, check that the Edition's maturation period has
1792 // expired counting from the Edition's creation date - if not, the NFT cannot be withdrawn yet.
1793 // The Edition's maturation period is a parameter provided when creating the Edition that cannot
1794 // be changed later and the creation date is set to the timestamp of the block where the Edition
1795 // is created.
1796 if let maturityDate = nft.getMaturityDate() {
1797 assert(maturityDate <= UInt64(getCurrentBlock().timestamp),
1798 message: "This is a Maturing Edition NFT for which the maturation period has not expired yet, maturity date: "
1799 .concat(maturityDate.toString()).concat(", current timestamp: ")
1800 .concat(UInt64(getCurrentBlock().timestamp).toString()))
1801 }
1802 // Withdrawn event emitted from NonFungibleToken contract interface
1803 emit Withdraw(id: nft.id, from: self.owner?.address) // TODO: Consider removing
1804 return <- nft
1805 }
1806
1807 /// Withdraw the tokens with given IDs and returns them as a Collection
1808 ///
1809 access(NonFungibleToken.Withdraw) fun batchWithdraw(ids: [UInt64]): @{NonFungibleToken.Collection} {
1810 // Create an empty Collection
1811 var batchCollection <- create Collection()
1812 // Iterate through the ids and withdraw them from the Collection
1813 for id in ids {
1814 batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
1815 }
1816 // Return the withdrawn tokens
1817 return <- batchCollection
1818 }
1819
1820 /// Deposit an NFT into this Collection
1821 ///
1822 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
1823 let token <- token as! @NFT
1824 let id: UInt64 = token.id
1825 // Add the new token to the dictionary which removes the old one
1826 let oldToken <- self.ownedNFTs[id] <- token
1827 // Deposited event emitted from NonFungibleToken contract interface
1828 emit Deposit(id: id, to: self.owner?.address) // TODO: Consider removing
1829 destroy oldToken
1830 }
1831
1832 /// Deposit the NFTs from a Collection into this Collection
1833 ///
1834 access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection}) {
1835 // Iterate through the NFT IDs in the Collection
1836 for id in tokens.getIDs() {
1837 self.deposit(token: <- tokens.withdraw(withdrawID: id))
1838 }
1839 // Destroy the empty Collection
1840 destroy tokens
1841 }
1842
1843 /// Add an Inscription in the NFT with the specified ID for the current owner and return the
1844 /// Inscription ID. The Inscription becomes permanent after the undo period has expired.
1845 ///
1846 access(NonFungibleToken.Update) fun addNFTInscription(id: UInt64): Int {
1847 pre {
1848 self.owner != nil: "Collection must be owned by an account"
1849 }
1850 return self.borrowPinNFT(id: id)!.addCurrentOwnerInscription(self.owner!.address)
1851 }
1852
1853 /// Remove the current owner's Inscription from the NFT with the specified ID
1854 ///
1855 /// This will fail if the undo period has expired or if another Inscription has been added after the
1856 /// current owner's.
1857 ///
1858 access(NonFungibleToken.Update) fun removeNFTInscription(id: UInt64) {
1859 self.borrowPinNFT(id: id)!.removeCurrentOwnerInscription(self.owner!.address)
1860 }
1861
1862 /// Update the note in the current owner's Inscription in the NFT with the specified ID. This requires
1863 /// Admin co-signing in the form of passing a reference to the Admin resource, and is generally
1864 /// intended to be called only when adding a note to the Inscription for the first time or appending
1865 /// to an existing note, with the content of the note being human-readable and sanitized off-chain.
1866 /// The note is part of the NFT's description returned in the Display view (see the getDescription
1867 /// function in the NFT resource).
1868 ///
1869 /// This will fail if the Inscription was not previously added by the current owner.
1870 ///
1871 access(NonFungibleToken.Update) fun updateNFTInscriptionNote(id: UInt64, note: String, adminRef: &Admin) {
1872 self.borrowPinNFT(id: id)!.updateCurrentOwnerInscriptionNote(
1873 currentOwner: self.owner!.address,
1874 note: note
1875 )
1876 }
1877
1878 /// Toggle the XP of the NFT with the specified ID and return the new XP value.
1879 ///
1880 /// If this NFT's XP has been previously activated, this will deactivate it. It will remain possible
1881 /// to reactivate XP but XP will be reinitialized to 0.
1882 ///
1883 access(NonFungibleToken.Update) fun toggleNFTXP(id: UInt64): UInt64? {
1884 return self.borrowPinNFT(id: id)!.toggleXP()
1885 }
1886
1887 /// Activate or deactivate the XP of all the NFTs in this Collection
1888 ///
1889 access(NonFungibleToken.Update) fun batchToggleXP(_ activateAll: Bool) {
1890 // Iterate through the NFT IDs in the Collection
1891 for id in self.ownedNFTs.keys {
1892 let nftRef = self.borrowPinNFT(id: id)!
1893 if activateAll && nftRef.xp == nil || !activateAll && nftRef.xp != nil {
1894 nftRef.toggleXP()
1895 }
1896 }
1897 }
1898
1899 /// Return an array of the NFT IDs that are in the Collection
1900 ///
1901 access(all) view fun getIDs(): [UInt64] {
1902 return self.ownedNFTs.keys
1903 }
1904
1905 /// Return the amount of NFTs stored in the collection
1906 ///
1907 access(all) view fun getLength(): Int {
1908 return self.ownedNFTs.length
1909 }
1910
1911 /// Return a reference to an NFT in the Collection
1912 ///
1913 /// This function returns nil if the NFT does not exist in this Collection.
1914 ///
1915 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
1916 return &self.ownedNFTs[id]
1917 }
1918
1919 /// Return a reference to an NFT in the Collection typed as Pinnacle.NFT
1920 ///
1921 /// This function returns nil if the NFT does not exist in this Collection.
1922 ///
1923 /// This function exposes all the NFT's fields and functions, though there are functions that
1924 /// modify the xp and inscriptions fields and those functions are declared with the access(contract)
1925 /// modifier so that they can only be called in the scope of this contract through the corresponding
1926 /// wrapper functions defined in the Collection and Admin resources.
1927 ///
1928 access(all) view fun borrowPinNFT(id: UInt64): &NFT? {
1929 return self.borrowNFT(id) as! &NFT?
1930 }
1931
1932 /// Return a reference to an NFT in this Collection typed as ViewResolver.Resolver
1933 ///
1934 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
1935 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
1936 return nft as &{ViewResolver.Resolver}
1937 }
1938 return nil
1939 }
1940
1941 /// Create an empty Collection for Pinnacle NFTs and return it to the caller
1942 ///
1943 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
1944 return <- Pinnacle.createEmptyCollection(nftType: Type<@NFT>())
1945 }
1946 }
1947
1948 /// Create an empty Collection for Pinnacle NFTs and return it to the caller
1949 ///
1950 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
1951 if nftType != Type<@NFT>() {
1952 panic("NFT type is not supported")
1953 }
1954 return <- create Collection()
1955 }
1956
1957 /// Return a Collection Public reference to the Collection owned by the account with the specified address
1958 ///
1959 access(all) view fun borrowCollectionPublic(owner: Address, _ publicPathID: String?): &Collection? {
1960 return getAccount(owner).capabilities.borrow<&Collection>(
1961 publicPathID != nil ? PublicPath(identifier: publicPathID!)! : Pinnacle.CollectionPublicPath)
1962 }
1963
1964 //------------------------------------------------------------
1965 // Admin
1966 //------------------------------------------------------------
1967
1968 /// Entitlement that grants the ability to mint Pinnacle NFTs
1969 access(all) entitlement Mint
1970
1971 /// Entitlement that grants the ability to operate admin functions
1972 access(all) entitlement Operate
1973
1974 // Deprecated in favor of using entitlement-based access control as required by Cadence 1.0
1975 access(all) resource interface NFTMinter {}
1976
1977 /// Resource that defines an Admin
1978 ///
1979 /// The Admin allows managing entities in the contract and minting Pinnacle NFTs.
1980 ///
1981 access(all) resource Admin: NFTMinter {
1982 /// Create a Series and return its ID
1983 ///
1984 /// This is irreversible, it will not be possible to remove the Series once it is created
1985 /// (no undo period).
1986 ///
1987 access(Operate) fun createSeries(name: String): Int {
1988 pre {
1989 name != "": "The name of a Series cannot be an empty string"
1990 Pinnacle.seriesIDsByName.containsKey(name) == false: "A Series with that name already exists"
1991 }
1992 let series = Series(id: Pinnacle.getLatestSeriesID() + 1, name: name)
1993 Pinnacle.series.append(series)
1994 Pinnacle.seriesIDsByName[name] = series.id
1995 emit SeriesCreated(id: series.id, name: series.name)
1996 return series.id
1997 }
1998
1999 /// Return a reference to the Series with the given ID, if it exists in the contract
2000 ///
2001 access(self) view fun borrowSeries(id: Int): &Series? {
2002 pre {
2003 id > 0: "The ID of a Series must be greater than 0"
2004 }
2005 return Pinnacle.getLatestSeriesID() >= id ? &Pinnacle.series[id - 1] as &Series : nil
2006 }
2007
2008 /// Lock a Series
2009 ///
2010 /// This is irreversible after the undo period is over. It will not be possible to create new Sets in
2011 /// the Series.
2012 ///
2013 /// @param id: The ID of the Series to lock.
2014 /// @param undo: A boolean to indicate whether to unlock the Series within the undo period.
2015 ///
2016 access(Operate) fun lockSeries(id: Int, undo: Bool) {
2017 undo ? self.borrowSeries(id: id)!.unlock() : self.borrowSeries(id: id)!.lock()
2018 }
2019
2020 /// Update a Series's name
2021 ///
2022 access(Operate) fun updateSeriesName(id: Int, name: String) {
2023 self.borrowSeries(id: id)!.updateName(name)
2024 }
2025
2026 /// Create a Set and return its ID
2027 ///
2028 /// This is irreversible, it will not be possible to remove the Set once it is created
2029 /// (no undo period).
2030 ///
2031 access(Operate) fun createSet(renderID: String, name: String, editionType: String, seriesID: Int): Int {
2032 pre {
2033 // Check that renderID is valid
2034 (renderID != ""): "The renderID of a Set cannot be an empty string"
2035 // Check that name is valid
2036 name != "": "The name of a Set cannot be an empty string"
2037 Pinnacle.setIDsByName.containsKey(name) == false: "A Set with that name already exists"
2038 // Check that editionType is valid
2039 Pinnacle.editionTypeIDsByName.containsKey(editionType): "No such Edition Type"
2040 Pinnacle.getEditionTypeByName(editionType)!.closedDate == nil: "Edition Type is closed"
2041 // Check that seriesID is valid
2042 Pinnacle.getLatestSeriesID() >= seriesID: "seriesID does not exist"
2043 Pinnacle.getSeries(id: seriesID)!.lockedDate == nil:
2044 "Cannot create a Set linked to a locked Series"
2045 }
2046 let set = Set(
2047 id: Pinnacle.getLatestSetID() + 1,
2048 renderID: renderID,
2049 name: name,
2050 editionType: editionType,
2051 seriesID: seriesID
2052 )
2053
2054 emit SetCreated(id: set.id, renderID: set.renderID, name: set.name, seriesID: set.seriesID, editionType: set.editionType)
2055 Pinnacle.sets.append(set)
2056 Pinnacle.setIDsByName[name] = set.id
2057 emit SetCreated(id: set.id, renderID: set.renderID, name: set.name, seriesID: set.seriesID, editionType: set.editionType)
2058 return set.id
2059 }
2060
2061 /// Return a reference to the Set with the given ID, if it exists in the contract
2062 ///
2063 access(self) view fun borrowSet(id: Int): &Set? {
2064 pre {
2065 id > 0: "The ID of a Set must be greater than 0"
2066 }
2067 return Pinnacle.getLatestSetID() >= id ? &Pinnacle.sets[id - 1] as &Set : nil
2068 }
2069
2070 /// Lock a Set
2071 ///
2072 /// This is irreversible after the undo period is over. It will not be possible to create new Shapes
2073 /// in the Set.
2074 ///
2075 /// @param id: The ID of the Set to lock.
2076 /// @param undo: A boolean to indicate whether to unlock the Set within the undo period.
2077 ///
2078 access(Operate) fun lockSet(id: Int, undo: Bool) {
2079 undo ? self.borrowSet(id: id)!.unlock() : self.borrowSet(id: id)!.lock()
2080 }
2081
2082 /// Update a Set's name
2083 ///
2084 access(Operate) fun updateSetName(id: Int, name: String) {
2085 self.borrowSet(id: id)!.updateName(name)
2086 }
2087
2088 /// Create a Shape and return its ID
2089 ///
2090 /// This is irreversible, it will not be possible to remove the Shape once it is created
2091 /// (no undo period).
2092 ///
2093 access(Operate) fun createShape(
2094 renderID: String,
2095 setID: Int,
2096 name: String,
2097 metadata: {String: AnyStruct}): Int {
2098 pre {
2099 // Check that renderID is valid
2100 (renderID != ""): "The renderID of a Shape cannot be an empty string"
2101
2102 // Check that setID is valid
2103 (Pinnacle.getLatestSetID() >= setID): "setID does not exist"
2104 (Pinnacle.getSet(id: setID)!.lockedDate == nil): "Cannot create a Shape with a locked Set"
2105 (Pinnacle.getEditionTypeByName(Pinnacle.getSet(id: setID)!.editionType)!.closedDate == nil):
2106 "Edition Type is closed"
2107 // Check that name is valid
2108 (name != ""): "The name of a Shape cannot be an empty string"
2109 (Pinnacle.getSet(id: setID)!.shapeNameExistsInSet(name) == false):
2110 "A Shape with that name already exists in the Set"
2111 // Check that metadata is valid
2112 (metadata.containsKey("Franchises") == true
2113 && metadata["Franchises"]!.isInstance(Type<[String]>())):
2114 "Franchises is required and must be a string array"
2115 (metadata.containsKey("Studios") == true || metadata.containsKey("Categories") == true):
2116 "Either Studios or Categories must be present"
2117 (metadata.containsKey("Studios") == false || metadata["Studios"]!.isInstance(Type<[String]>())):
2118 "Studios must be a string array"
2119 (metadata.containsKey("Categories") == false || metadata["Categories"]!.isInstance(Type<[String]>())):
2120 "Categories must be a string array"
2121 (metadata.containsKey("RoyaltyCodes") == true &&
2122 metadata["RoyaltyCodes"]!.isInstance(Type<[String]>())):
2123 "RoyaltyCodes is required and must be a string array"
2124 (metadata.containsKey("Characters") == false || metadata["Characters"]!.isInstance(Type<[String]>())):
2125 "Characters must be a string array"
2126 (metadata.containsKey("Location") == false || metadata["Location"]!.isInstance(Type<String>())):
2127 "Location must a string"
2128 (metadata.containsKey("EventName") == false || metadata["EventName"]!.isInstance(Type<String>())):
2129 "EventName must a string"
2130 }
2131 // Check that metadata contains only strings or string arrays, that the keys are valid, and
2132 // convert strings to string arrays for the ShapeCreated event because events don't support
2133 // {String: AnyStruct} parameters
2134 let convertedMetadata: {String: [String]} = {}
2135 let defaultTraits = {"EditionType" : true, "SeriesName" : true, "SetName" : true,
2136 "SerialNumber" : true, "IsChaser" : true, "Variant": true, "Printing": true,
2137 "MintingDate": true, "MaturityDate": true}
2138 for key in metadata.keys {
2139 assert(metadata[key]!.isInstance(Type<String>()) || metadata[key]!.isInstance(Type<[String]>()),
2140 message: "Metadata values must be strings or string arrays")
2141 assert(defaultTraits.containsKey(key) == false,
2142 message: "Metadata key cannot already exist in the default traits dictionary")
2143 convertedMetadata[key] = metadata[key]!.isInstance(Type<String>()) == true ?
2144 [metadata[key]! as! String] : metadata[key]! as! [String]
2145 }
2146 // Create the Shape
2147 let shape = Shape(
2148 id: Pinnacle.getLatestShapeID() + 1,
2149 renderID: renderID,
2150 setID: setID,
2151 name: name,
2152 metadata: metadata
2153 )
2154 Pinnacle.shapes.append(shape)
2155 // Insert the new Shape's name in the parent Set's shapeNames dictionary
2156 self.borrowSet(id: setID)!.insertShapeName(name)
2157 // Emit the ShapeCreated event with the converted metadata {String: [String]} dictionary
2158 emit ShapeCreated(
2159 id: shape.id,
2160 renderID: shape.renderID,
2161 setID: shape.setID,
2162 name: shape.name,
2163 editionType: shape.editionType,
2164 metadata: convertedMetadata
2165 )
2166 return shape.id
2167 }
2168
2169 /// Return a reference to the Shape with the given ID, if it exists in the contract
2170 ///
2171 access(self) view fun borrowShape(id: Int): &Shape? {
2172 pre {
2173 id > 0: "The ID of a Shape must be greater than 0"
2174 }
2175 return Pinnacle.getLatestShapeID() >= id ? &Pinnacle.shapes[id - 1] as &Shape : nil
2176 }
2177
2178 /// Close a Shape
2179 ///
2180 /// This is irreversible after the undo period is over. It will not be possible to create new Editions
2181 /// in the Shape.
2182 ///
2183 /// @param id: The ID of the Shape to close.
2184 /// @param undo: A boolean to indicate whether to reopen the Shape within the undo period.
2185 ///
2186 access(Operate) fun closeShape(id: Int, undo: Bool) {
2187 undo ? self.borrowShape(id: id)!.reopen() : self.borrowShape(id: id)!.close()
2188 }
2189
2190 /// Update a Shape's name
2191 ///
2192 access(Operate) fun updateShapeName(id: Int, name: String) {
2193 let shapeRef = self.borrowShape(id: id)!
2194 shapeRef.updateName(name, self.borrowSet(id: shapeRef.setID)!)
2195 }
2196
2197 /// Set a Shape's metadata fields
2198 ///
2199 access(Operate) fun setShapeMetadataFields(id: Int, metadata: {String: AnyStruct}) {
2200 let shapeRef = self.borrowShape(id: id)!
2201 shapeRef.setMetadataFields(metadata)
2202 }
2203
2204 /// Increment a Shape's current printing
2205 ///
2206 access(Operate) fun incrementShapeCurrentPrinting(id: Int): UInt64 {
2207 return self.borrowShape(id: id)!.incrementCurrentPrinting()
2208 }
2209
2210 /// Create an Edition and return its ID
2211 ///
2212 /// This becomes irreversible once NFTs have been minted from the Edition or another Edition has been
2213 /// created in the contract.
2214 ///
2215 access(Operate) fun createEdition(
2216 renderID: String,
2217 shapeID: Int,
2218 variant: String?,
2219 description: String,
2220 isChaser: Bool,
2221 maxMintSize: UInt64?,
2222 maturationPeriod: UInt64?,
2223 traits: {String: AnyStruct}): Int {
2224 pre {
2225 // Check that renderID is valid
2226 (renderID != ""): "The renderID of a Shape cannot be an empty string"
2227 // Check that shapeID is valid
2228 (Pinnacle.getLatestShapeID() >= shapeID): "shapeID does not exist"
2229 (Pinnacle.getShape(id: shapeID)!.closedDate == nil):
2230 "Cannot create an Edition with a closed Shape"
2231 (Pinnacle.getEditionTypeByName(Pinnacle.getShape(id: shapeID)!.editionType)!.closedDate == nil):
2232 "Edition type is closed"
2233 // Check that description is valid
2234 (description != ""): "The description of an Edition cannot be an empty string"
2235 // Check that variant is valid
2236 (variant == nil || Pinnacle.variants.containsKey(variant!) == true):
2237 "Variant does not exist"
2238 (Pinnacle.getShape(id: shapeID)!.variantPrintingPairExistsInEdition(
2239 variant ?? "Standard") == false):
2240 "Variant - printing pair already exists in an Edition"
2241 // Check that maxMintSize is not zero
2242 (maxMintSize != 0): "Max mint size cannot be equal to zero"
2243 // Check that traits is valid
2244 (traits.containsKey("Materials") == true && traits["Materials"]!.isInstance(Type<[String]>())):
2245 "Materials is required and must be a string array"
2246 (traits.containsKey("Size") == true && traits["Size"]!.isInstance(Type<String>())):
2247 "Size is required and must be a string"
2248 (traits.containsKey("Thickness") == true && traits["Thickness"]!.isInstance(Type<String>())):
2249 "Thickness is required and must be a string"
2250 (traits.containsKey("Effects") == false || traits["Effects"]!.isInstance(Type<[String]>())):
2251 "Effects must be a string array"
2252 (traits.containsKey("Color") == false || traits["Color"]!.isInstance(Type<String>())):
2253 "Color must be a string"
2254 }
2255 let editionType = Pinnacle.getEditionTypeByName(
2256 Pinnacle.getShape(id: shapeID)!.editionType)!
2257 // Check that max mint size is valid
2258 if editionType.isLimited {
2259 assert(maxMintSize != nil,
2260 message: "Only Limited Editions can be created in this Shape, maxMintSize cannot be nil")
2261 } else {
2262 assert(maxMintSize == nil,
2263 message: "Limited Editions cannot be created in this Shape, maxMintSize must be nil")
2264 }
2265 // Check that the maturation period is not nil for Maturing Editions and nil otherwise. Note that
2266 // it may be set to zero, which is beneficial for creating Editions that don't undergo maturation
2267 // but still fall under the Maturing Edition category (for example, this might apply to certain
2268 // Event Editions).
2269 if editionType.isMaturing {
2270 assert(maturationPeriod != nil,
2271 message: "Only Maturing Editions can be created in this Shape, maturationPeriod cannot be nil")
2272 } else {
2273 assert(maturationPeriod == nil,
2274 message: "Maturing Editions cannot be created in this Shape, maturationPeriod must be nil")
2275 }
2276 // Check that traits contains only strings or string arrays, that the keys are valid, and convert
2277 // strings to string arrays for the EditionCreated event because events don't support
2278 // {String: AnyStruct} parameters
2279 let convertedTraits: {String: [String]} = {}
2280 let shapeMetadata = Pinnacle.getShape(id: shapeID)!.metadata
2281 let defaultTraits = {"EditionType" : true, "SeriesName" : true, "SetName" : true,
2282 "SerialNumber" : true, "IsChaser" : true, "Variant": true, "Printing": true,
2283 "MintingDate": true, "MaturityDate": true}
2284 for key in traits.keys {
2285 assert(traits[key]!.isInstance(Type<String>()) || traits[key]!.isInstance(Type<[String]>()),
2286 message: "Trait values must be strings or string arrays")
2287 assert(defaultTraits.containsKey(key) == false,
2288 message: "Trait key cannot already exist in the default traits dictionary")
2289 assert(shapeMetadata.containsKey(key) == false,
2290 message: "Trait key cannot already exist in the Shape's metadata dictionary")
2291 convertedTraits[key] = traits[key]!.isInstance(Type<String>()) == true ?
2292 [traits[key]! as! String] : traits[key]! as! [String]
2293 }
2294 // Create the Edition
2295 let edition = Edition(
2296 id: Pinnacle.getLatestEditionID() + 1,
2297 renderID: renderID,
2298 shapeID: shapeID,
2299 variant: variant,
2300 description: description,
2301 isChaser: isChaser,
2302 maxMintSize: maxMintSize,
2303 maturationPeriod: maturationPeriod,
2304 traits: traits,
2305 )
2306 Pinnacle.editions.append(edition)
2307 // Insert the Variant in the parent Shape for the current printing
2308 self.borrowShape(id: shapeID)!.insertVariantPrintingPair(variant ?? "Standard")
2309 // Emit the EditionCreated event with the converted traits {String: [String]} dictionary
2310 emit EditionCreated(
2311 id: edition.id,
2312 renderID: edition.renderID,
2313 seriesID: edition.seriesID,
2314 setID: edition.setID,
2315 shapeID: edition.shapeID,
2316 variant: edition.variant,
2317 printing: edition.printing,
2318 editionTypeID: edition.editionTypeID,
2319 description: edition.description,
2320 isChaser: edition.isChaser,
2321 maxMintSize: edition.maxMintSize,
2322 maturationPeriod: edition.maturationPeriod,
2323 traits: convertedTraits
2324 )
2325 return edition.id
2326 }
2327
2328 /// Return a reference to the Edition with the given ID, if it exists in the contract
2329 ///
2330 access(self) view fun borrowEdition(id: Int): &Edition? {
2331 pre {
2332 id > 0: "The ID of an Edition must be greater than zero"
2333 }
2334 return Pinnacle.getLatestEditionID() >= id ? &Pinnacle.editions[id - 1] as &Edition : nil
2335 }
2336
2337 /// Close an Edition
2338 ///
2339 /// For Open/Unlimited Editions, this is irreversible after the undo period is over. It will no longer
2340 /// be possible to mint NFTs from the Edition.
2341 ///
2342 /// For Limited Editions, closing an Edition allows setting the Edition's closed date to the end of the
2343 /// primary release sales. The ability to mint NFTs from the Edition is determined by the Edition's
2344 /// number minted being less than the max mint size.
2345 ///
2346 access(Operate) fun closeEdition(id: Int) {
2347 self.borrowEdition(id: id)!.close()
2348 }
2349
2350 /// Remove an Edition
2351 ///
2352 /// This will fail if NFTs have been minted from the Edition or the Edition is not the last one that
2353 /// was created in the contract.
2354 ///
2355 access(Operate) fun removeEdition(id: Int) {
2356 let editionRef = self.borrowEdition(id: id)!
2357 assert(editionRef.numberMinted == 0 && id == Pinnacle.getLatestEditionID(),
2358 message: "Cannot remove an Edition that has minted NFTs and is not the last one created")
2359 self.borrowShape(id: editionRef.shapeID)!
2360 .removeVariantPrintingPair(editionRef.variant ?? "Standard")
2361 emit EditionRemoved(
2362 id: id
2363 )
2364 Pinnacle.editions.removeLast()
2365 }
2366
2367 /// Reopen an Open/Unlimited Edition
2368 ///
2369 /// This will fail if the Edition is a Limited Edition or if the undo period has expired.
2370 ///
2371 access(Operate) fun reopenEdition(id: Int) {
2372 self.borrowEdition(id: id)!.reopen()
2373 }
2374
2375 /// Update an Edition's description
2376 ///
2377 access(Operate) fun updateEditionDescription(id: Int, description: String) {
2378 self.borrowEdition(id: id)!.updateDescription(description)
2379 }
2380
2381 /// Update an Edition's renderID
2382 ///
2383 access(Operate) fun updateEditionRenderID(id: Int, renderID: String) {
2384 self.borrowEdition(id: id)!.updateRenderID(renderID)
2385 }
2386
2387 /// Create an Edition Type and return its ID
2388 ///
2389 /// This is irreversible, it will not be possible to remove the Edition Type once it is created
2390 /// (no undo period).
2391 ///
2392 access(Operate) fun createEditionType(name: String, isLimited: Bool, isMaturing: Bool): Int {
2393 pre {
2394 name != "": "The name of an Edition Type cannot be an empty string"
2395 Pinnacle.editionTypeIDsByName.containsKey(name) == false:
2396 "An Edition Type with that name already exists"
2397 }
2398 let editionType = EditionType(
2399 id: Pinnacle.getLatestEditionTypeID() + 1,
2400 name: name,
2401 isLimited: isLimited,
2402 isMaturing: isMaturing
2403 )
2404 Pinnacle.editionTypes.append(editionType)
2405 Pinnacle.editionTypeIDsByName[name] = editionType.id
2406 emit EditionTypeCreated(
2407 id: editionType.id,
2408 name: name,
2409 isLimited: isLimited,
2410 isMaturing: isMaturing
2411 )
2412 return editionType.id
2413 }
2414
2415 /// Return a reference to the Edition Type with the given ID, if it exists in the contract
2416 ///
2417 access(self) view fun borrowEditionType(id: Int): &EditionType? {
2418 pre {
2419 id > 0: "The ID of an Edition Type must be greater than zero"
2420 }
2421 return Pinnacle.getLatestEditionTypeID() >= id ? &Pinnacle.editionTypes[id - 1] as &EditionType : nil
2422 }
2423
2424 /// Close an Edition Type
2425 ///
2426 /// This is irreversible after the undo period is over. It will not be possible to create new Shapes
2427 /// and Editions with the dependent Sets and Shapes, even if they are unlocked/open. The dependent
2428 /// Sets and Shapes should thus be locked/closed to avoid confusion. This can be done automatically
2429 /// by setting the proper flags to true when calling the closeEditionType function. All the Sets and
2430 /// Shapes stored in the contract are iterated over rather than just the dependent ones. This is
2431 /// because closing an Edition Type is a rare operation and it is anticipated that the decreased
2432 /// contract size and gas consumption of the more frequent create entity operations are preferable to
2433 /// maintaining separate Sets and Shapes arrays in each Edition Type. It is also possible to lock or
2434 /// close the dependent Sets and Shapes individually by calling the lock or close function on each of
2435 /// them after determining the IDs off-chain.
2436 ///
2437 /// @param id: The ID of the Edition Type to close.
2438 /// @param lockDependentSets: A boolean to indicate whether dependent Sets should be closed
2439 /// automatically.
2440 /// @param closeDependentShapes: Same purpose as the lockDependentSets param but for Shapes.
2441 ///
2442 access(Operate) fun closeEditionType(
2443 id: Int,
2444 lockDependentSets: Bool,
2445 closeDependentShapes: Bool
2446 ): {String: [Int]} {
2447 let editionTypeRef = self.borrowEditionType(id: id)!
2448 editionTypeRef.close()
2449 let setsLocked: [Int] = []
2450 if lockDependentSets {
2451 for index, set in Pinnacle.sets {
2452 let setRef = self.borrowSet(id: index + 1)!
2453 if setRef.lockedDate == nil && setRef.editionType == editionTypeRef.name {
2454 setRef.lock()
2455 setsLocked.append(setRef.id)
2456 }
2457 }
2458 }
2459 let shapesClosed: [Int] = []
2460 if closeDependentShapes {
2461 for index, shape in Pinnacle.shapes {
2462 let shapeRef = self.borrowShape(id: index + 1)!
2463 if shapeRef.closedDate == nil && shapeRef.editionType == editionTypeRef.name {
2464 shapeRef.close()
2465 shapesClosed.append(shapeRef.id)
2466 }
2467 }
2468 }
2469 return {"DependentSetsLocked": setsLocked, "DependentShapesClosed": shapesClosed}
2470 }
2471
2472 /// Reopen an Edition Type
2473 ///
2474 /// This will fail if the undo period has expired. This will not unlock dependent Sets or reopen
2475 /// dependent Shapes that may have been locked or closed when closing the Edition Type. This can be
2476 /// done separately using the lockSet and reopenShape functions with the undo flag set to true, if
2477 /// the undo period has not expired.
2478 ///
2479 access(Operate) fun reopenEditionType(id: Int) {
2480 self.borrowEditionType(id: id)!.reopen()
2481 }
2482
2483 /// Insert a Variant in the variants dictionary
2484 ///
2485 /// This is irreversible, it will not be possible to remove the Variant once it is inserted
2486 /// (no undo period). Furthermore, Variants cannot be closed.
2487 ///
2488 access(Operate) fun insertVariant(name: String) {
2489 pre {
2490 name != "": "The name of a Variant cannot be an empty string"
2491 Pinnacle.variants.containsKey(name) == false: "A Variant with that name already exists"
2492 }
2493 Pinnacle.variants[name] = true
2494 emit VariantInserted(name: name)
2495 }
2496
2497 /// Mint a Pin NFT in the Edition with the given ID and return it to the caller
2498 ///
2499 access(Mint) fun mintNFT(editionID: Int, extension: {String: String}?): @NFT {
2500 let pinNFT <- create NFT(editionID: editionID, extension: extension)
2501 self.borrowEdition(id: editionID)!.incrementNumberMinted()
2502 return <- pinNFT
2503 }
2504
2505 /// Burn an Open/Unlimited Edition Pin NFT and decrement the Edition's number minted
2506 ///
2507 /// Any account can burn an NFT it owns with the destroy keyword, the purpose of this function is to
2508 /// allow the Admin to decrement the Edition's number minted while burning an Open Edition NFT in an
2509 /// Edition that has not been closed.
2510 ///
2511 access(Operate) fun burnOpenEditionNFT(_ nft: @{NonFungibleToken.NFT}) {
2512 let nft <- nft as! @NFT
2513 // Decrement the number minted in the Edition. This will fail if the Edition is a Limited Edition.
2514 self.borrowEdition(id: nft.editionID)!.decrementNumberMinted()
2515 emit OpenEditionNFTBurned(id: nft.id, editionID: nft.editionID)
2516 destroy nft
2517 }
2518
2519 /// Set a limit on the number entries that can be added to the inscriptions of the NFT with the given
2520 /// ID (default is 100)
2521 ///
2522 access(Operate) fun setNFTInscriptionsLimit(
2523 nftID: UInt64,
2524 limit: Int,
2525 owner: Address,
2526 collectionPublicPathID: String?
2527 ) {
2528 pre {
2529 limit > 100: "The limit must be greater than the default value of 100"
2530 }
2531 let collectionPublicRef = Pinnacle.borrowCollectionPublic(
2532 owner: owner,
2533 collectionPublicPathID
2534 )
2535 assert(collectionPublicRef!.borrowNFT(nftID) != nil,
2536 message: "No NFT with such ID in the Collection")
2537 Pinnacle.inscriptionsLimits[nftID] = limit
2538 }
2539
2540 /// Add XP to an NFT and return the NFT's new XP balance
2541 ///
2542 access(Operate) fun addXPtoNFT(
2543 nftID: UInt64,
2544 owner: Address,
2545 collectionPublicPathID: String?,
2546 value: UInt64
2547 ): UInt64 {
2548 return Pinnacle.borrowCollectionPublic(owner: owner, collectionPublicPathID)!
2549 .borrowPinNFT(id: nftID)!
2550 .addXP(value)
2551 }
2552
2553 /// Remove XP from an NFT and return the NFT's new XP balance
2554 ///
2555 access(Operate) fun subtractXPfromNFT(
2556 nftID: UInt64,
2557 owner: Address,
2558 collectionPublicPathID: String?,
2559 value: UInt64
2560 ): UInt64 {
2561 return Pinnacle.borrowCollectionPublic(owner: owner, collectionPublicPathID)!
2562 .borrowPinNFT(id: nftID)!
2563 .subtractXP(value)
2564 }
2565
2566 /// When conducting primary release sales, emit a "Purchased" event to facilitate purchase tracking
2567 /// off-chain. The parameters are passed through to the event and are not used by the contract.
2568 ///
2569 access(Operate) view fun emitPurchasedEvent(
2570 purchaseIntentID: String,
2571 buyerAddress: Address,
2572 countPurchased: UInt64,
2573 totalSalePrice: UFix64
2574 ) {
2575 emit Purchased(
2576 purchaseIntentID: purchaseIntentID,
2577 buyerAddress: buyerAddress,
2578 countPurchased: countPurchased,
2579 totalSalePrice: totalSalePrice
2580 )
2581 }
2582
2583 /// Create an Admin resource and return it to the caller
2584 ///
2585 access(Operate) fun createAdmin(): @Admin {
2586 return <- create Admin()
2587 }
2588
2589 /// Set the contract's royalty address
2590 ///
2591 access(Operate) fun setRoyaltyAddress(_ address: Address) {
2592 Pinnacle.royaltyAddress = address
2593 }
2594
2595 /// Set the contract's end user license URL
2596 ///
2597 access(Operate) fun setEndUserLicenseURL(_ url: String) {
2598 Pinnacle.endUserLicenseURL = url
2599 }
2600
2601 /// Set an entry in the contract's extension dictionary
2602 ///
2603 access(Operate) fun setExtensionEntry(_ key: String, _ value: AnyStruct, _ overwrite: Bool) {
2604 pre {
2605 key != "": "The key cannot be an empty string"
2606 overwrite || !Pinnacle.extension.containsKey(key): "Overwrite is false and the key already exists"
2607 key != "EndUserLicenseCaption" || value.isInstance(Type<String>()): "EndUserLicenseCaption must be a string"
2608 }
2609 Pinnacle.extension[key] = value
2610 }
2611 }
2612
2613 //------------------------------------------------------------
2614 // Variants, Path, and Utils Functions
2615 //------------------------------------------------------------
2616
2617 /// Emit generic Updated event in NonFungibleToken contract interface
2618 ///
2619 /// Although the Pinnacle-specific NFTInscriptionUpdated and NFTXPUpdated events allow indexing the details of what is being updated, there is
2620 /// value in also emitting the generic Updated event as listeners can have support for it by default thanks to being defined in NonFungibleToken v2.
2621 ///
2622 access(all) view fun emitNFTUpdated(_ nftRef: auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}) {}
2623
2624 /// Return the contract's variants dictionary
2625 ///
2626 access(all) view fun getAllVariants(): {String: Bool} {
2627 return Pinnacle.variants
2628 }
2629
2630 /// Allow iterating over Variants in the contract without allocating an array
2631 ///
2632 access(all) fun forEachVariant(_ function: fun (String): Bool) {
2633 Pinnacle.variants.forEachKey(function)
2634 }
2635
2636 /// Return this contract's extension dictionary
2637 ///
2638 access(all) view fun getExtension(): {String: AnyStruct} {
2639 return Pinnacle.extension
2640 }
2641
2642 /// Return a public path that is scoped to this contract
2643 ///
2644 access(all) view fun getPublicPath(suffix: String): PublicPath {
2645 return PublicPath(identifier: "Pinnacle".concat(suffix))!
2646 }
2647
2648 /// Return a storage path that is scoped to this contract
2649 ///
2650 access(all) view fun getStoragePath(suffix: String): StoragePath {
2651 return StoragePath(identifier: "Pinnacle".concat(suffix))!
2652 }
2653
2654 /// Return a Collection name with an optional bucket suffix
2655 ///
2656 access(all) view fun makeCollectionName(bucketName maybeBucketName: String?): String {
2657 if let bucketName = maybeBucketName {
2658 return "Collection_".concat(bucketName)
2659 }
2660 return "Collection"
2661 }
2662
2663 /// Return a queue name with an optional bucket suffix
2664 ///
2665 access(all) view fun makeQueueName(bucketName maybeBucketName: String?): String {
2666 if let bucketName = maybeBucketName {
2667 return "Queue_".concat(bucketName)
2668 }
2669 return "Queue"
2670 }
2671
2672 /// Check if the contract is deployed to mainnet
2673 ///
2674 /// The function relies on checking the type of the imported MetadataViews contract.
2675 /// 0x1d7e57aa55817448 is the address of the known MetadataViews contract standard on mainnet.
2676 /// This is a workaround for the fact that there is no way to check the network ID in Cadence yet.
2677 ///
2678 access(all) view fun isContractDeployedToMainnet(): Bool {
2679 return Type<MetadataViews>().identifier == "A.1d7e57aa55817448.MetadataViews"
2680 }
2681
2682 //------------------------------------------------------------
2683 // Contract MetadataViews
2684 //------------------------------------------------------------
2685
2686 /// Return the metadata view types available for this contract
2687 ///
2688 access(all) view fun getContractViews(resourceType: Type?): [Type] {
2689 return [Type<MetadataViews.NFTCollectionData>(), Type<MetadataViews.NFTCollectionDisplay>()]
2690 }
2691
2692 /// Resolve this contract's metadata views
2693 ///
2694 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
2695 post {
2696 result == nil || result!.getType() == viewType: "The returned view must be of the given type or nil"
2697 }
2698 switch viewType {
2699 case Type<MetadataViews.NFTCollectionData>(): return Pinnacle.resolveNFTCollectionDataView()
2700 case Type<MetadataViews.NFTCollectionDisplay>(): return Pinnacle.resolveNFTCollectionDisplayView()
2701 }
2702 return nil
2703 }
2704
2705 /// Resolve this contract's NFTCollectionData view
2706 ///
2707 access(all) fun resolveNFTCollectionDataView(): MetadataViews.NFTCollectionData {
2708 return MetadataViews.NFTCollectionData(
2709 storagePath: Pinnacle.CollectionStoragePath,
2710 publicPath: Pinnacle.CollectionPublicPath,
2711 publicCollection: Type<&Collection>(),
2712 publicLinkedType: Type<&Collection>(),
2713 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {return <- Pinnacle.createEmptyCollection(nftType: Type<@NFT>())})
2714 )
2715 }
2716
2717 /// Resolve this contract's NFTCollectionDisplay view
2718 ///
2719 access(all) fun resolveNFTCollectionDisplayView(): MetadataViews.NFTCollectionDisplay {
2720 let squareImage = MetadataViews.Media(
2721 file: MetadataViews.HTTPFile(
2722 url: "https://assets.disneypinnacle.com/on-chain/pinnacle.jpg"
2723 ),
2724 mediaType: "image/jpg"
2725 )
2726 let bannerImage = MetadataViews.Media(
2727 file: MetadataViews.HTTPFile(
2728 url: "https://assets.disneypinnacle.com/on-chain/pinnacle-banner.jpeg"
2729 ),
2730 mediaType: "image/jpeg"
2731 )
2732 return MetadataViews.NFTCollectionDisplay(
2733 name: "Pinnacle",
2734 description: "placeholder_description",
2735 externalURL: MetadataViews.ExternalURL("https://disneypinnacle.com"),
2736 squareImage: squareImage,
2737 bannerImage: bannerImage,
2738 socials : {
2739 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/disneypinnacle/"),
2740 "twitter": MetadataViews.ExternalURL("https://twitter.com/DisneyPinnacle"),
2741 "discord": MetadataViews.ExternalURL("https://discord.gg/DisneyPinnacle"),
2742 "facebook": MetadataViews.ExternalURL("https://www.facebook.com/groups/disneypinnacle/")
2743 }
2744 )
2745 }
2746
2747 //------------------------------------------------------------
2748 // Contract lifecycle
2749 //------------------------------------------------------------
2750
2751 /// Pinnacle contract initializer
2752 ///
2753 /// The undo period is specified as a parameter to facilitate automated tests.
2754 ///
2755 init(undoPeriod: UInt64) {
2756 pre {
2757 // Check that the contract is properly configured on mainnet as part of the contract's code
2758 Pinnacle.isContractDeployedToMainnet() == false || undoPeriod == 259200:
2759 "The undo period must be set to 259200 (3 days) if the contract is deployed to mainnet"
2760 }
2761
2762 // Set the named paths
2763 self.CollectionStoragePath = Pinnacle.getStoragePath(suffix: "Collection")
2764 self.CollectionPublicPath = Pinnacle.getPublicPath(suffix: "Collection")
2765 self.AdminStoragePath = Pinnacle.getStoragePath(suffix: "Admin")
2766
2767 // Initialize the non-container fields
2768 self.undoPeriod = undoPeriod
2769 self.royaltyAddress = self.account.address
2770 self.endUserLicenseURL = "https://disneypinnacle.com/terms"
2771
2772 // Initialize the entity arrays
2773 self.series = []
2774 self.sets = []
2775 self.shapes = []
2776 self.editions = []
2777 self.editionTypes = []
2778
2779 // Initialize the dictionaries
2780 self.seriesIDsByName = {}
2781 self.setIDsByName = {}
2782 self.editionTypeIDsByName = {}
2783 self.variants = {}
2784 self.inscriptionsLimits = {}
2785 self.extension = {}
2786
2787 // Create an Admin resource
2788 let admin <- create Admin()
2789
2790 // Create the default Edition Types
2791 admin.createEditionType(name: "Genesis Edition", isLimited: true, isMaturing: false)
2792 admin.createEditionType(name: "Unique Edition", isLimited: true, isMaturing: false)
2793 admin.createEditionType(name: "Limited Edition", isLimited: true, isMaturing: false)
2794 admin.createEditionType(name: "Open Edition", isLimited: false, isMaturing: false)
2795 admin.createEditionType(name: "Starter Edition", isLimited: false, isMaturing: true)
2796 admin.createEditionType(name: "Event Edition", isLimited: false, isMaturing: true)
2797
2798 // Save the Admin resource to storage
2799 self.account.storage.save(<- admin, to: self.AdminStoragePath)
2800 }
2801}
2802