Smart Contract

GoatedGoats

A.2068315349bdfce5.GoatedGoats

Deployed

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

Dependents

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