Smart Contract

Pinnacle

A.edf9df96c92f4595.Pinnacle

Valid From

122,720,002

Deployed

2w ago
Feb 11, 2026, 05:25:51 PM UTC

Dependents

408068 imports
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