Smart Contract

GoatedGoatsManager

A.2068315349bdfce5.GoatedGoatsManager

Deployed

3h ago
Mar 02, 2026, 04:20:17 AM UTC

Dependents

0 imports
1/*
2    A contract that manages the creation and sale of Goated Goats, Traits, and Packs.
3
4    A manager resource exists to allow modifications to the parameters of the public
5    sale and have ability to mint editions themself.
6*/
7
8import NonFungibleToken from 0x1d7e57aa55817448
9import FungibleToken from 0xf233dcee88fe0abe
10import FlowToken from 0x1654653399040a61
11import GoatedGoatsTrait from 0x2068315349bdfce5
12import GoatedGoatsVouchers from 0xdfc74d9d561374c0
13import TraitPacksVouchers from 0xdfc74d9d561374c0
14import GoatedGoatsTraitPack from 0x2068315349bdfce5
15import GoatedGoats from 0x2068315349bdfce5
16
17pub contract GoatedGoatsManager {
18    // -----------------------------------------------------------------------
19    //  Events
20    // -----------------------------------------------------------------------
21    // Emitted when the contract is initialized
22    pub event ContractInitialized()
23
24    // -----------------------------------------------------------------------
25    //  Trait
26    // -----------------------------------------------------------------------
27    // Emitted when an admin has initiated a mint of a GoatedGoat NFT
28    pub event AdminMintTrait(id: UInt64)
29    // Emitted when a GoatedGoatsTrait collection has had metadata updated
30    pub event UpdateTraitCollectionMetadata()
31    // Emitted when an edition within a GoatedGoatsTrait has had it's metadata updated
32    pub event UpdateTraitEditionMetadata(id: UInt64)
33
34    // -----------------------------------------------------------------------
35    //  TraitPack
36    // -----------------------------------------------------------------------
37    // Emitted when an admin has initiated a mint of a GoatedGoat NFT
38    pub event AdminMintTraitPack(id: UInt64)
39    // Emitted when someone has redeemed a voucher for a trait pack
40    pub event RedeemTraitPackVoucher(id: UInt64)
41    // Emitted when someone has redeemed a trait pack for traits
42    pub event RedeemTraitPack(id: UInt64, packID: UInt64, packEditionID: UInt64, address: Address)
43    // Emitted when a GoatedGoatsTrait collection has had metadata updated
44    pub event UpdateTraitPackCollectionMetadata()
45    // Emitted when an edition within a GoatedGoatsTrait has had it's metadata updated
46    pub event UpdateTraitPackEditionMetadata(id: UInt64)
47    // Emitted when any info about redeem logistics has been modified
48    pub event UpdateTraitPackRedeemInfo(redeemStartTime: UFix64)
49    // -----------------------------------------------------------------------
50    //  Goat
51    // -----------------------------------------------------------------------
52    // Emitted when someone has redeemed a voucher for a goat
53    pub event RedeemGoatVoucher(id: UInt64, goatID: UInt64, address: Address)
54    // Emitted whenever a goat trait action is done, e.g. equip/unequip
55    pub event UpdateGoatTraits(id: UInt64, goatID: UInt64, address: Address)
56    // Emitted when a GoatedGoats collection has had metadata updated
57    pub event UpdateGoatCollectionMetadata()
58    // Emitted when an edition within a GoatedGoats has had it's metadata updated
59    pub event UpdateGoatEditionMetadata(id: UInt64)
60    // Emitted when any info about redeem logistics has been modified
61    pub event UpdateGoatRedeemInfo(redeemStartTime: UFix64)
62
63    // -----------------------------------------------------------------------
64    // Named Paths
65    // -----------------------------------------------------------------------
66    pub let ManagerStoragePath: StoragePath
67
68    // -----------------------------------------------------------------------
69    // GoatedGoatsManager fields
70    // -----------------------------------------------------------------------
71    // -----------------------------------------------------------------------
72    //  Trait
73    // -----------------------------------------------------------------------
74    access(self) let traitsMintedEditions: {UInt64: Bool}
75    access(self) var traitsSequentialMintMin: UInt64
76    pub var traitTotalSupply: UInt64
77    // -----------------------------------------------------------------------
78    //  TraitPack
79    // -----------------------------------------------------------------------
80    access(self) let traitPacksMintedEditions: {UInt64: Bool}
81    access(self) let traitPacksByPackIdMintedEditions: {UInt64: {UInt64: Bool}}
82    access(self) var traitPacksSequentialMintMin: UInt64
83    access(self) var traitPacksByPackIdSequentialMintMin: {UInt64: UInt64}
84    pub var traitPackTotalSupply: UInt64
85    pub var traitPackRedeemStartTime: UFix64
86    // -----------------------------------------------------------------------
87    //  Goat
88    // -----------------------------------------------------------------------
89    access(self) let goatsMintedEditions: {UInt64: Bool}
90    access(self) let goatsByGoatIdMintedEditions: {UInt64: Bool}
91    access(self) var goatsSequentialMintMin: UInt64
92    pub var goatMaxSupply: UInt64
93    pub var goatTotalSupply: UInt64
94    pub var goatRedeemStartTime: UFix64
95
96    // -----------------------------------------------------------------------
97    // Manager resource for all NFTs
98    // -----------------------------------------------------------------------
99    pub resource Manager {
100        // -----------------------------------------------------------------------
101        //  Trait
102        // -----------------------------------------------------------------------
103        pub fun updateTraitCollectionMetadata(metadata: {String: String}) {
104            GoatedGoatsTrait.setCollectionMetadata(metadata: metadata)
105            emit UpdateTraitCollectionMetadata()
106        }
107        
108        pub fun updateTraitEditionMetadata(editionNumber: UInt64, metadata: {String: String}) {
109            GoatedGoatsTrait.setEditionMetadata(editionNumber: editionNumber, metadata: metadata)
110            emit UpdateTraitEditionMetadata(id: editionNumber)
111        }
112
113        pub fun mintTraitAtEdition(edition: UInt64, packID: UInt64): @NonFungibleToken.NFT {
114            emit AdminMintTrait(id: edition)
115            return <-GoatedGoatsManager.mintTrait(edition: edition, packID: packID)
116        }
117
118        pub fun mintSequentialTrait(packID: UInt64): @NonFungibleToken.NFT {
119            let trait <- GoatedGoatsManager.mintSequentialTrait(packID: packID)
120            emit AdminMintTrait(id: trait.id)
121            return <- trait
122        }
123
124        // -----------------------------------------------------------------------
125        //  TraitPack
126        // -----------------------------------------------------------------------
127        pub fun updateTraitPackCollectionMetadata(metadata: {String: String}) {
128            GoatedGoatsTraitPack.setCollectionMetadata(metadata: metadata)
129            emit UpdateTraitPackCollectionMetadata()
130        }
131        
132        pub fun updateTraitPackEditionMetadata(editionNumber: UInt64, metadata: {String: String}) {
133            GoatedGoatsTraitPack.setEditionMetadata(editionNumber: editionNumber, metadata: metadata)
134            emit UpdateTraitPackEditionMetadata(id: editionNumber)
135        }
136
137        pub fun mintTraitPackAtEdition(edition: UInt64, packID: UInt64, packEditionID: UInt64): @NonFungibleToken.NFT {
138            emit AdminMintTraitPack(id: edition)
139            return <-GoatedGoatsManager.mintTraitPack(edition: edition, packID: packID, packEditionID: packEditionID)
140        }
141
142        pub fun mintSequentialTraitPack(packID: UInt64): @NonFungibleToken.NFT {
143            let trait <- GoatedGoatsManager.mintSequentialTraitPack(packID: packID)
144            emit AdminMintTraitPack(id: trait.id)
145            return <- trait
146        }
147
148        pub fun updateTraitPackRedeemStartTime(_ redeemStartTime: UFix64) {
149            GoatedGoatsManager.traitPackRedeemStartTime = redeemStartTime
150            emit UpdateTraitPackRedeemInfo(
151                redeemStartTime: GoatedGoatsManager.traitPackRedeemStartTime,
152            )
153        }
154
155        // -----------------------------------------------------------------------
156        //  Goat
157        // -----------------------------------------------------------------------
158        pub fun updateGoatCollectionMetadata(metadata: {String: String}) {
159            GoatedGoats.setCollectionMetadata(metadata: metadata)
160            emit UpdateGoatCollectionMetadata()
161        }
162        
163        pub fun updateGoatEditionMetadata(goatID: UInt64, metadata: {String: String}, traitSlots: UInt8) {
164            GoatedGoats.setEditionMetadata(goatID: goatID, metadata: metadata, traitSlots: traitSlots)
165            emit UpdateGoatEditionMetadata(id: goatID)
166        }
167
168        pub fun updateGoatRedeemStartTime(_ redeemStartTime: UFix64) {
169            GoatedGoatsManager.goatRedeemStartTime = redeemStartTime
170            emit UpdateGoatRedeemInfo(
171                redeemStartTime: GoatedGoatsManager.goatRedeemStartTime,
172            )
173        }
174    }
175
176    // -----------------------------------------------------------------------
177    //  Trait
178    // -----------------------------------------------------------------------
179    // Mint a GoatedGoatTrait
180    access(contract) fun mintTrait(edition: UInt64, packID: UInt64): @NonFungibleToken.NFT {
181        pre {
182            edition >= 1: "Requested edition is outside of allowed bounds."
183            self.traitsMintedEditions[edition] == nil : "Requested edition has already been minted"
184        }
185        self.traitsMintedEditions[edition] = true
186        self.traitTotalSupply = self.traitTotalSupply + 1
187        let trait <- GoatedGoatsTrait.mint(nftID: edition, packID: packID)
188        return <-trait
189    }
190
191    // Look for the next available trait, and mint there
192    access(self) fun mintSequentialTrait(packID: UInt64): @NonFungibleToken.NFT {
193        var curEditionNumber = self.traitsSequentialMintMin
194        while (self.traitsMintedEditions.containsKey(UInt64(curEditionNumber))) {
195            curEditionNumber = curEditionNumber + 1
196        }
197        self.traitsSequentialMintMin = curEditionNumber
198        let newTrait <- self.mintTrait(edition: UInt64(curEditionNumber), packID: packID)
199        return <-newTrait
200    }
201
202    // -----------------------------------------------------------------------
203    //  TraitPack
204    // -----------------------------------------------------------------------
205    // Mint a GoatedGoatTraitPack
206    access(contract) fun mintTraitPack(edition: UInt64, packID: UInt64, packEditionID: UInt64): @NonFungibleToken.NFT {
207        pre {
208            edition >= 1: "Requested edition is outside of allowed bounds."
209            self.traitPacksMintedEditions[edition] == nil : "Requested edition has already been minted"
210            self.traitPacksByPackIdMintedEditions[packID] == nil || 
211                self.traitPacksByPackIdMintedEditions[packID]![packEditionID] == nil: "Requested pack edition has already been minted"
212        }
213        self.traitPacksMintedEditions[edition] = true
214        // Setup packID if doesn't exist.
215        if self.traitPacksByPackIdMintedEditions[packID] == nil {
216            self.traitPacksByPackIdMintedEditions[packID] = {}
217        }
218        // Set packEditionID status
219        let ref = self.traitPacksByPackIdMintedEditions[packID]!
220        ref[packEditionID] = true
221        self.traitPacksByPackIdMintedEditions[packID] = ref
222
223        self.traitPackTotalSupply = self.traitPackTotalSupply + 1
224        let trait <- GoatedGoatsTraitPack.mint(nftID: edition, packID: packID, packEditionID: packEditionID)
225        return <-trait
226    }
227
228    // Look for the next available trait pack, and mint there
229    access(self) fun mintSequentialTraitPack(packID: UInt64): @NonFungibleToken.NFT {
230        // Grab the resource ID aka editionID
231        var curEditionNumber = self.traitPacksSequentialMintMin
232        while (self.traitPacksMintedEditions.containsKey(UInt64(curEditionNumber))) {
233            curEditionNumber = curEditionNumber + 1
234        }
235        self.traitPacksSequentialMintMin = curEditionNumber
236        
237        // Setup sequential ID for new packs
238        if self.traitPacksByPackIdSequentialMintMin[packID] == nil {
239            self.traitPacksByPackIdSequentialMintMin[packID] = 1
240        }
241        // Grab the packEditionID
242        var curPackEditionNumber = self.traitPacksByPackIdSequentialMintMin[packID]!
243        while (self.traitPacksByPackIdMintedEditions[packID]!.containsKey(UInt64(curPackEditionNumber))) {
244            curPackEditionNumber = curPackEditionNumber + 1
245        }
246        self.traitPacksByPackIdSequentialMintMin[packID] = curPackEditionNumber
247        let newTrait <- self.mintTraitPack(edition: UInt64(curEditionNumber), packID: packID, packEditionID: UInt64(curPackEditionNumber))
248        return <-newTrait
249    }
250
251    // -----------------------------------------------------------------------
252    //  Goat
253    // -----------------------------------------------------------------------
254    // Mint a GoatedGoat
255    access(contract) fun mintGoat(edition: UInt64, goatID: UInt64, traitActions: UInt64, goatCreationDate: UFix64, lastTraitActionDate: UFix64): @NonFungibleToken.NFT {
256        pre {
257            edition >= 1: "Requested edition is outside of allowed bounds."
258            goatID >= 1 && goatID <= self.goatMaxSupply: "Requested goat ID is outside of allowed bounds."
259            self.goatsMintedEditions[edition] == nil : "Requested edition has already been minted"
260            self.goatsByGoatIdMintedEditions[goatID] == nil : "Requested goat ID has already been minted"
261        }
262        self.goatsMintedEditions[edition] = true
263        self.goatsByGoatIdMintedEditions[goatID] = true
264        self.goatTotalSupply = self.goatTotalSupply + 1
265        let goat <- GoatedGoats.mint(nftID: edition, goatID: goatID, traitActions: traitActions, goatCreationDate: goatCreationDate, lastTraitActionDate: lastTraitActionDate)
266        return <-goat
267    }
268
269    // Look for the next available goat, and mint there
270    access(self) fun mintSequentialGoat(goatID: UInt64, traitActions: UInt64, goatCreationDate: UFix64, lastTraitActionDate: UFix64): @NonFungibleToken.NFT {
271        // Grab the resource ID aka editionID
272        var curEditionNumber = self.goatsSequentialMintMin
273        while (self.goatsMintedEditions.containsKey(UInt64(curEditionNumber))) {
274            curEditionNumber = curEditionNumber + 1
275        }
276        self.goatsSequentialMintMin = curEditionNumber
277        
278        let goat <- self.mintGoat(edition: UInt64(curEditionNumber), goatID: goatID, traitActions: traitActions, goatCreationDate: goatCreationDate, lastTraitActionDate: lastTraitActionDate)
279        return <-goat
280    }
281
282    // -----------------------------------------------------------------------
283    // Public Functions
284    // -----------------------------------------------------------------------
285    // -----------------------------------------------------------------------
286    //  TraitPack
287    // -----------------------------------------------------------------------
288    pub fun publicRedeemTraitPackWithVoucher(traitPackVoucher: @NonFungibleToken.NFT): @NonFungibleToken.Collection {
289        pre {
290            getCurrentBlock().timestamp >= self.traitPackRedeemStartTime: "Redemption has not yet started"
291            traitPackVoucher.isInstance(Type<@TraitPacksVouchers.NFT>()): "Invalid type provided, expected TraitPacksVoucher.NFT"
292        }
293
294        // -- Burn voucher --
295        let id = traitPackVoucher.id
296        destroy traitPackVoucher
297
298        // -- Mint the trait pack --
299        let traitPackCollection <- GoatedGoatsTraitPack.createEmptyCollection()
300        // Default To 1 for all Voucher Based Packs.
301        let traitPack <- self.mintSequentialTraitPack(packID: 1)
302
303        traitPackCollection.deposit(token: <-traitPack)
304
305        emit RedeemTraitPackVoucher(id: id);
306
307        return <-traitPackCollection
308    }
309
310    pub fun publicRedeemTraitPack(traitPack: @NonFungibleToken.NFT, address: Address) {
311        pre {
312            getCurrentBlock().timestamp >= self.traitPackRedeemStartTime: "Redemption has not yet started"
313            traitPack.isInstance(Type<@GoatedGoatsTraitPack.NFT>())
314        }
315        let traitPackInstance <- traitPack as! @GoatedGoatsTraitPack.NFT
316
317        // Emit an event that our backend will read and mint traits to the associating address.
318        emit RedeemTraitPack(id: traitPackInstance.id, packID: traitPackInstance.packID, packEditionID: traitPackInstance.packEditionID, address: address)
319        // Burn trait pack
320        destroy traitPackInstance
321    }
322
323    // -----------------------------------------------------------------------
324    //  Goat
325    // -----------------------------------------------------------------------
326    pub fun publicRedeemGoatWithVoucher(goatVoucher: @NonFungibleToken.NFT, address: Address): @NonFungibleToken.Collection {
327        pre {
328            getCurrentBlock().timestamp >= self.goatRedeemStartTime: "Redemption has not yet started"
329            goatVoucher.isInstance(Type<@GoatedGoatsVouchers.NFT>()): "Invalid type provided, expected GoatedGoatsVouchers.NFT"
330        }
331
332        // -- Burn voucher --
333        let id = goatVoucher.id
334        destroy goatVoucher
335
336        // -- Mint the goat with same Voucher ID --
337        let goatCollection <- GoatedGoats.createEmptyCollection()
338        // Mint a clean goat with no equipped traits or counters
339        let goat <- self.mintSequentialGoat(goatID: id, traitActions: 0, goatCreationDate: getCurrentBlock().timestamp, lastTraitActionDate: 0.0)
340        let editionId = goat.id
341
342        goatCollection.deposit(token: <-goat)
343
344        emit RedeemGoatVoucher(id: editionId, goatID: id, address: address);
345
346        return <-goatCollection
347    }
348
349    pub resource GoatAndTraits {
350        // This is a list with only the goat.
351        // Cannot move nested resource out of it otherwise.
352        pub var goat: @[NonFungibleToken.NFT]
353        pub var unequippedTraits: @[NonFungibleToken.NFT]
354
355        pub fun extractGoat(): @NonFungibleToken.NFT {
356            return <-self.goat.removeFirst()
357        }
358
359        pub fun extractAllTraits(): @[NonFungibleToken.NFT] {
360            var assets: @[NonFungibleToken.NFT] <- []
361            self.unequippedTraits <-> assets
362            assert(self.unequippedTraits.length == 0, message: "Couldn't extract all goats.")
363            return <-assets
364        }
365
366        init(goat: @NonFungibleToken.NFT, unequippedTraits: @[NonFungibleToken.NFT]) {
367            self.goat <- [<-goat]
368            self.unequippedTraits <- unequippedTraits
369        }
370
371        destroy() {
372            assert(self.goat.length == 0, message: "Cannot destroy with goat.")
373            assert(self.unequippedTraits.length == 0, message: "Can notdestroy with traits.")
374            destroy self.goat
375            destroy self.unequippedTraits
376        }
377    }
378
379    pub fun updateGoatTraits(goat: @NonFungibleToken.NFT, traitsToEquip: @[NonFungibleToken.NFT], traitSlotsToUnequip: [String], address: Address): @GoatAndTraits {
380        pre {
381            getCurrentBlock().timestamp >= self.goatRedeemStartTime: "Updating traits on a goat in not enabled."
382            goat != nil: "Goat not provided."
383            goat.isInstance(Type<@GoatedGoats.NFT>()): "Invalid type provided, expected GoatedGoats.NFT"
384            traitsToEquip.length != 0 || traitSlotsToUnequip.length != 0: "Must provide some action to take place."
385        }
386        // Get Goat typed instance
387        let goatInstance <- goat as! @GoatedGoats.NFT
388        // Unset the store of this Goat ID
389        self.goatsByGoatIdMintedEditions[goatInstance.goatID] = nil
390        // Mint a new goat with same Goat ID, traits to store, and counters (updated)
391        let newGoat <- self.mintSequentialGoat(goatID: goatInstance.goatID, traitActions: goatInstance.traitActions + 1, goatCreationDate: goatInstance.goatCreationDate, lastTraitActionDate: getCurrentBlock().timestamp)
392        let newGoatInstance <- newGoat as! @GoatedGoats.NFT
393        let unequippedTraits: @[NonFungibleToken.NFT] <- []
394
395        // Move traits over to the new Goat
396        for traitSlot in goatInstance.traits.keys {
397
398					  let old <- newGoatInstance.setTrait(key: traitSlot, value: <- goatInstance.removeTrait(traitSlot))
399            assert(old == nil, message: "Existing trait exists with this trait slot.")
400            destroy old
401        }
402        // Destroy the old goat
403        destroy goatInstance
404
405        // First unequip anything provided and store in the return
406        if (traitSlotsToUnequip.length > 0) {
407            // For each trait ID, validate they exist in store, and return them.
408            for traitSlot in traitSlotsToUnequip {
409                assert(newGoatInstance.isTraitEquipped(traitSlot: traitSlot), message: "This goat has the provided trait slot empty.")
410                let trait <- newGoatInstance.removeTrait(traitSlot)!
411                assert(trait.getMetadata().containsKey("traitSlot"), message: "Provided trait is missing the trait slot.")
412                unequippedTraits.append(<-trait)
413            }
414            assert(unequippedTraits.length == traitSlotsToUnequip.length, message: "Was not able to unequip all traits.")
415        }
416
417        // Equip the traits provided onto the new goat
418        // If there are still traits on the goat swap them out and return them.
419        if (traitsToEquip.length > 0) {
420            while traitsToEquip.length > 0 {
421                let trait <- traitsToEquip.removeFirst() as! @GoatedGoatsTrait.NFT
422                assert(trait.getMetadata().containsKey("traitSlot"), message: "Provided trait is missing the trait slot.")
423                let traitSlot = trait.getMetadata()["traitSlot"]!
424                // If goat already has this trait equipped, remove it
425                if newGoatInstance.isTraitEquipped(traitSlot: traitSlot) {
426                    let existingTrait <- newGoatInstance.removeTrait(traitSlot)!
427                    unequippedTraits.append(<-existingTrait)
428                }
429                let old <- newGoatInstance.setTrait(key: traitSlot, value:  <- trait)
430                assert(old == nil, message: "Existing trait exists with this trait slot.")
431                destroy old
432            }
433            assert(traitsToEquip.length == 0, message: "Was not able to equip all traits.")
434        }
435        destroy traitsToEquip
436
437        // Validate didn't equip too many traits.
438        assert(newGoatInstance.traits.length <= Int(newGoatInstance.getTraitSlots()!), message: "Equipped more traits than this goat supports.")
439
440        // Send event to the BE that will update the Goats image.
441        emit UpdateGoatTraits(id: newGoatInstance.id, goatID: newGoatInstance.goatID, address: address);
442
443        return <-create GoatAndTraits(goat: <-newGoatInstance, unequippedTraits: <-unequippedTraits)
444    }
445
446    init() {
447        // Non-human modifiable variables
448        // -----------------------------------------------------------------------
449        //  Trait
450        // -----------------------------------------------------------------------
451        self.traitTotalSupply = 0
452        self.traitsSequentialMintMin = 1
453        // Start with no existing editions minted
454        self.traitsMintedEditions = {}
455        // -----------------------------------------------------------------------
456        //  TraitPack
457        // -----------------------------------------------------------------------
458        self.traitPackTotalSupply = 0
459        self.traitPackRedeemStartTime = 4891048813.0
460        self.traitPacksSequentialMintMin = 1
461        // Start with no existing editions minted
462        self.traitPacksMintedEditions = {}
463        // Setup with initial packID.
464        self.traitPacksByPackIdMintedEditions = {1: {}}
465        self.traitPacksByPackIdSequentialMintMin = {1: 1}
466        // -----------------------------------------------------------------------
467        //  Goat
468        // -----------------------------------------------------------------------
469        self.goatTotalSupply = 0
470        self.goatMaxSupply = 10000
471        self.goatRedeemStartTime = 4891048813.0
472        self.goatsSequentialMintMin = 1
473        // Start with no existing editions minted
474        self.goatsMintedEditions = {}
475        self.goatsByGoatIdMintedEditions = {}
476
477        // Manager resource is only saved to the deploying account's storage
478        self.ManagerStoragePath = /storage/GoatedGoatsManager
479        self.account.save(<- create Manager(), to: self.ManagerStoragePath)
480
481        emit ContractInitialized()
482    }
483}
484 
485