Smart Contract
GoatedGoatsManager
A.2068315349bdfce5.GoatedGoatsManager
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