Smart Contract
GoatedGoats
A.2068315349bdfce5.GoatedGoats
1/*
2 A NFT contract for the Goated Goats NFT.
3
4 Key Callouts:
5 * Limit of 10,000 NFTs (Id 1-1,000)
6 * Equip functionality to hold traits. When `equipped`, the old NFT is destroyed, and a new one created (new ID as well) - but with the same main Goat ID which is a separate metadata.
7 * Unequip allows removing traits. When 'unequipped', the old NFT is destroyed, and a new one created (new ID as well) - but with the same main Goat ID which is a separate metadata
8 * Redeemable by public function that accepts in a GoatedGoatsVouchers
9 * Collection-level metadata (Name of collection, total supply, royalty information, etc)
10 * Edition-level metadata (Base goat ipfs link, Base Goat color)
11 * Edition-level traits metadata (Link of Trait slot (String) to GoatedGoatsTraits resource)
12 * When equipped, or unequipped - keep a tally of how many actions have happened
13 * When equip or unequip, allow for switching traits of 2,3,4 with 3,5,6 in one transaction (counting as a single action)
14 * Hold timestamp of when last ChangeEquip action has occurred
15*/
16
17import NonFungibleToken from 0x1d7e57aa55817448
18import FungibleToken from 0xf233dcee88fe0abe
19import GoatedGoatsTrait from 0x2068315349bdfce5
20import MetadataViews from 0x1d7e57aa55817448
21
22pub contract GoatedGoats: NonFungibleToken {
23 // -----------------------------------------------------------------------
24 // NonFungibleToken Standard Events
25 // -----------------------------------------------------------------------
26
27 pub event ContractInitialized()
28 pub event Withdraw(id: UInt64, from: Address?)
29 pub event Deposit(id: UInt64, to: Address?)
30
31 // -----------------------------------------------------------------------
32 // GoatedGoats Events
33 // -----------------------------------------------------------------------
34
35 pub event Mint(id: UInt64)
36 pub event Burn(id: UInt64)
37
38 // -----------------------------------------------------------------------
39 // Named Paths
40 // -----------------------------------------------------------------------
41
42 pub let CollectionStoragePath: StoragePath
43 pub let CollectionPublicPath: PublicPath
44
45 // -----------------------------------------------------------------------
46 // NonFungibleToken Standard Fields
47 // -----------------------------------------------------------------------
48
49 pub var totalSupply: UInt64
50
51 // -----------------------------------------------------------------------
52 // GoatedGoats Fields
53 // -----------------------------------------------------------------------
54 pub var name: String
55
56 access(self) var collectionMetadata: { String: String }
57 // NOTE: This is a map of goatID to metadata, unlike other contracts here that map with editionID
58 access(self) let idToGoatMetadata: { UInt64: GoatMetadata }
59 access(self) let editionIDToGoatID: { UInt64: UInt64 }
60
61 pub struct GoatMetadata {
62 pub let metadata: { String: String }
63 pub let traitSlots: UInt8
64
65 init(metadata: { String: String }, traitSlots: UInt8) {
66 self.metadata = metadata
67 self.traitSlots = traitSlots
68 }
69 }
70
71 pub resource NFT : NonFungibleToken.INFT, MetadataViews.Resolver {
72 pub let id: UInt64
73 pub let goatID: UInt64
74 // The following 4 can be constants as they are only updated on burn/mints
75 // Keeps count of how many trait actions have taken place on this goat, e.g. equip/unequip
76 pub let traitActions: UInt64
77 // Last time a traitAction took place.
78 pub let lastTraitActionDate: UFix64
79 // Time the goat was created independent of equip/unequip.
80 pub let goatCreationDate: UFix64
81 // Map of traits to GoatedGoatsTrait NFTs.
82 // There can only be one Trait per slot.
83 access(account) let traits: @{String: GoatedGoatsTrait.NFT}
84
85 pub fun getViews(): [Type] {
86 return [
87 Type<MetadataViews.Display>(),
88 Type<MetadataViews.NFTCollectionData>(),
89 Type<MetadataViews.NFTCollectionDisplay>(),
90 Type<MetadataViews.Traits>(),
91 Type<MetadataViews.ExternalURL>(),
92 Type<MetadataViews.Royalties>()
93 ]
94 }
95
96 access(account) fun removeTrait(_ key: String) : @GoatedGoatsTrait.NFT? {
97 return <- self.traits.remove(key: key)
98 }
99
100 access(account) fun setTrait( key: String, value: @GoatedGoatsTrait.NFT?) : @GoatedGoatsTrait.NFT? {
101 let old <- self.traits[key] <- value
102 return <- old
103 }
104
105 pub fun resolveView(_ view: Type): AnyStruct? {
106 let metadata= self.getMetadata()
107 switch view {
108 //init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) {
109 case Type<MetadataViews.Traits>():
110 let traits : [MetadataViews.Trait] =[]
111 if let slots = self.getTraitSlots() {
112 traits.append(MetadataViews.Trait(name:"TraitSlots", value: slots , displayType:"Number", rarity: nil))
113 }
114 if let skinFileName = metadata["skinFileName"] {
115 let skin = GoatedGoats.formatFileName(value: skinFileName, prefix:"skin")
116 let skinRarity = metadata["skinRarity"]!
117 traits.append(MetadataViews.Trait(name:"Skin", value: skin, displayType:"String", rarity: MetadataViews.Rarity(score:nil, max:nil, description: skinRarity)))
118 }
119
120 for traitSlot in self.traits.keys {
121 let ref = (&self.traits[traitSlot] as &GoatedGoatsTrait.NFT?)!
122 let metadata = ref.getMetadata()
123 let traitSlotName = metadata["traitSlot"]!
124 let traitName = GoatedGoats.formatFileName(value: metadata["fileName"]!, prefix: traitSlotName)
125
126 traits.append(MetadataViews.Trait(name:traitSlot, value: traitName, displayType:"String", rarity: MetadataViews.Rarity(score:nil, max:nil, description: metadata["rarity"]!)))
127 }
128
129 return MetadataViews.Traits(traits)
130
131
132 case Type<MetadataViews.NFTCollectionData>():
133 return MetadataViews.NFTCollectionData(
134 storagePath: GoatedGoats.CollectionStoragePath,
135 publicPath: GoatedGoats.CollectionPublicPath,
136 providerPath: /private/GoatCollection,
137 publicCollection: Type<&GoatedGoats.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, GoatedGoats.GoatCollectionPublic, MetadataViews.ResolverCollection}>(),
138 publicLinkedType: Type<&GoatedGoats.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, GoatedGoats.GoatCollectionPublic, MetadataViews.ResolverCollection}>(),
139 providerLinkedType: Type<&GoatedGoats.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, GoatedGoats.GoatCollectionPublic, MetadataViews.ResolverCollection}>(), createEmptyCollectionFunction: fun(): @NonFungibleToken.Collection {return <- GoatedGoats.createEmptyCollection()})
140
141 case Type<MetadataViews.NFTCollectionDisplay>():
142 let externalURL = MetadataViews.ExternalURL("https://goatedgoats.com")
143 let squareImage = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://goatedgoats.com/_ipx/w_32,q_75/%2FLogo.png?url=%2FLogo.png&w=32&q=75"), mediaType: "image/png")
144
145 let bannerImage = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://goatedgoats.com/_ipx/w_32,q_75/%2FLogo.png?url=%2FLogo.png&w=32&q=75"), mediaType: "image/png")
146
147 let socialMap : {String : MetadataViews.ExternalURL} = {
148 "twitter" : MetadataViews.ExternalURL("https://twitter.com/goatedgoats")
149 }
150 return MetadataViews.NFTCollectionDisplay(name: "GoatedGoats", description: "GoatedGoats", externalURL: externalURL, squareImage: squareImage, bannerImage: bannerImage, socials: socialMap)
151
152
153 case Type<MetadataViews.Display>():
154 var ipfsImage = MetadataViews.IPFSFile(cid: "No thumbnail cid set", path: "No thumbnail pat set")
155 if (metadata.containsKey("thumbnailCID")) {
156 ipfsImage = MetadataViews.IPFSFile(cid: metadata["thumbnailCID"]!, path: metadata["thumbnailPath"])
157 }
158 return MetadataViews.Display(
159 name: metadata["name"] ?? "Goated Goat ".concat(self.goatID.toString()),
160 description: metadata["description"] ?? "No description set",
161 thumbnail: ipfsImage
162 )
163
164 case Type<MetadataViews.ExternalURL>():
165 return MetadataViews.ExternalURL(
166 "https://GoatedGoats.com"
167 )
168
169 case Type<MetadataViews.Royalties>():
170 let royaltyReceiver = getAccount(0xd7081a5c66dc3e7f).getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
171
172 return MetadataViews.Royalties(
173 [MetadataViews.Royalty(recepient: royaltyReceiver, cut: 0.05, description: "This is the royalty receiver for goats")]
174 )
175 }
176
177 return nil
178 }
179
180 pub fun getMetadata(): {String: String} {
181 if (GoatedGoats.idToGoatMetadata[self.goatID] != nil) {
182 return GoatedGoats.idToGoatMetadata[self.goatID]!.metadata
183 } else {
184 return {}
185 }
186 }
187
188 pub fun getTraitSlots(): UInt8? {
189 if (GoatedGoats.idToGoatMetadata[self.goatID] != nil) {
190 return GoatedGoats.idToGoatMetadata[self.goatID]!.traitSlots
191 } else {
192 return nil
193 }
194 }
195
196 // Check if a trait name is currently equipped on this goat.
197 pub fun isTraitEquipped(traitSlot: String): Bool {
198 return self.traits.containsKey(traitSlot)
199 }
200
201 // Get metadata for all traits currently equipped on the NFT.
202 pub fun getEquippedTraits(): [{String: AnyStruct}] {
203 let traitsData: [{String: AnyStruct}] = []
204 for traitSlot in self.traits.keys {
205 let ref = (&self.traits[traitSlot] as &GoatedGoatsTrait.NFT?)!
206 let map: {String: AnyStruct} = {}
207 map["traitID"] = ref.id
208 map["traitPackID"] = ref.packID
209 map["traitEditionMetadata"] = ref.getMetadata()
210 traitsData.append(map)
211 }
212 return traitsData
213 }
214
215 init(id: UInt64, goatID: UInt64, traitActions: UInt64, goatCreationDate: UFix64, lastTraitActionDate: UFix64) {
216 self.id = id
217 self.goatID = goatID
218 self.traitActions = traitActions
219 self.goatCreationDate = goatCreationDate
220 self.lastTraitActionDate = lastTraitActionDate
221 self.traits <- {}
222 // Map the edition ID to goat ID
223 GoatedGoats.editionIDToGoatID.insert(key: id, goatID)
224 emit Mint(id: self.id)
225 }
226
227 destroy() {
228 assert(self.traits.length == 0, message: "Can not destroy Goat that has equipped traits.")
229 destroy self.traits
230 emit Burn(id: self.id)
231 }
232 }
233
234 pub resource interface GoatCollectionPublic {
235 pub fun deposit(token: @NonFungibleToken.NFT)
236 pub fun getIDs(): [UInt64]
237 pub fun getGoatIDs(): [UInt64]
238 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
239 pub fun borrowGoat(id: UInt64): &GoatedGoats.NFT? {
240 post {
241 (result == nil) || (result?.id == id):
242 "Cannot borrow GoatedGoats reference: The ID of the returned reference is incorrect"
243 }
244 }
245 }
246
247 pub resource Collection: GoatCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
248 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
249
250 init () {
251 self.ownedNFTs <- {}
252 }
253
254 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
255 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
256 emit Withdraw(id: token.id, from: self.owner?.address)
257 return <-token
258 }
259
260 pub fun deposit(token: @NonFungibleToken.NFT) {
261 let token <- token as! @GoatedGoats.NFT
262 let id: UInt64 = token.id
263 let oldToken <- self.ownedNFTs[id] <- token
264 emit Deposit(id: id, to: self.owner?.address)
265 destroy oldToken
266 }
267
268 pub fun getIDs(): [UInt64] {
269 return self.ownedNFTs.keys
270 }
271
272 pub fun getGoatIDs(): [UInt64] {
273 let goatIDs: [UInt64] = []
274 for id in self.getIDs() {
275 goatIDs.append(self.borrowGoat(id: id)!.goatID)
276 }
277 return goatIDs
278 }
279
280 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
281 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
282 }
283
284 pub fun borrowGoat(id: UInt64): &GoatedGoats.NFT? {
285 if self.ownedNFTs[id] != nil {
286 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
287 return ref as! &GoatedGoats.NFT
288 } else {
289 return nil
290 }
291 }
292
293 pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
294 let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
295 let goat = nft as! &GoatedGoats.NFT
296 return goat as &AnyResource{MetadataViews.Resolver}
297 }
298
299 destroy() {
300 destroy self.ownedNFTs
301 }
302 }
303
304 // -----------------------------------------------------------------------
305 // Admin Functions
306 // -----------------------------------------------------------------------
307 access(account) fun setEditionMetadata(goatID: UInt64, metadata: {String: String}, traitSlots: UInt8) {
308 self.idToGoatMetadata[goatID] = GoatMetadata(metadata: metadata, traitSlots: traitSlots)
309 }
310
311 access(account) fun setCollectionMetadata(metadata: {String: String}) {
312 self.collectionMetadata = metadata
313 }
314
315 access(account) fun mint(nftID: UInt64, goatID: UInt64, traitActions: UInt64, goatCreationDate: UFix64, lastTraitActionDate: UFix64) : @NonFungibleToken.NFT {
316 self.totalSupply = self.totalSupply + 1
317 return <-create NFT(id: nftID, goatID: goatID, traitActions: traitActions, goatCreationDate: goatCreationDate, lastTraitActionDate: lastTraitActionDate)
318 }
319
320 // -----------------------------------------------------------------------
321 // Public Functions
322 // -----------------------------------------------------------------------
323 pub fun getTotalSupply(): UInt64 {
324 return self.totalSupply
325 }
326
327 pub fun getName(): String {
328 return self.name
329 }
330
331 pub fun getCollectionMetadata(): {String: String} {
332 return self.collectionMetadata
333 }
334
335 pub fun getEditionMetadata(_ goatID: UInt64): {String: String} {
336 if (self.idToGoatMetadata[goatID] != nil) {
337 return self.idToGoatMetadata[goatID]!.metadata
338 } else {
339 return {}
340 }
341 }
342
343 pub fun getEditionTraitSlots(_ goatID: UInt64): UInt8? {
344 if (self.idToGoatMetadata[goatID] != nil) {
345 return self.idToGoatMetadata[goatID]!.traitSlots
346 } else {
347 return nil
348 }
349 }
350
351 access(contract) fun formatFileName(value:String, prefix:String):String {
352 let length= value.length
353 let start=prefix.length+1
354 let trimmed = value.slice(from:start, upTo: length-4)
355 return trimmed
356 }
357
358 // -----------------------------------------------------------------------
359 // NonFungibleToken Standard Functions
360 // -----------------------------------------------------------------------
361 pub fun createEmptyCollection(): @NonFungibleToken.Collection {
362 return <- create Collection()
363 }
364
365 init() {
366 self.name = "Goated Goats"
367 self.totalSupply = 0
368
369 self.collectionMetadata = {}
370 self.idToGoatMetadata = {}
371 self.editionIDToGoatID = {}
372
373 self.CollectionStoragePath = /storage/GoatCollection
374 self.CollectionPublicPath = /public/GoatCollection
375
376 emit ContractInitialized()
377 }
378}
379
380