Smart Contract

SturdyItems

A.427ceada271aa0b1.SturdyItems

Deployed

1w ago
Feb 15, 2026, 11:01:09 PM UTC

Dependents

69 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import HoodlumsMetadata from 0x427ceada271aa0b1
5import ViewResolver from 0x1d7e57aa55817448
6
7// SturdyItems
8// NFT items for Sturdy!
9//
10access(all) contract SturdyItems: ViewResolver, NonFungibleToken {
11
12    // Events
13    //
14    access(all) event ContractInitialized()
15    access(all) event AccountInitialized()
16    access(all) event Withdraw(id: UInt64, from: Address?)
17    access(all) event Deposit(id: UInt64, to: Address?)
18    access(all) event Minted(id: UInt64, 
19    	typeID: UInt64, 
20		tokenURI: String, 
21		tokenTitle: String, 
22		tokenDescription: String,
23		artist: String, 
24		secondaryRoyalty: String, 
25		platformMintedOn: String)
26    access(all) event Purchased(buyer: Address, id: UInt64, price: UInt64)
27
28    // Named Paths
29    //
30    access(all) let CollectionStoragePath: StoragePath
31    access(all) let CollectionPublicPath: PublicPath
32    access(all) let MinterStoragePath: StoragePath
33
34    // totalSupply
35    // The total number of SturdyItems that have been minted
36    //
37    access(all) var totalSupply: UInt64
38
39    // Entitlements
40    //
41    access(all) entitlement Owner
42
43    // NFT
44    // A Sturdy Item as an NFT
45    //
46    access(all) resource NFT: NonFungibleToken.NFT {
47        // The token's ID
48        access(all) let id: UInt64
49        // The token's type, e.g. 3 == Hat
50        access(all) let typeID: UInt64
51        // Token URI
52        access(all) let tokenURI: String
53        // Token Title
54        access(all) let tokenTitle: String
55        // Token Description
56        access(all) let tokenDescription: String
57        // Artist info
58        access(all) let artist: String
59        // Secondary Royalty
60        access(all) let secondaryRoyalty: String
61        // Platform Minted On
62        access(all) let platformMintedOn: String
63        // Token Price
64        // access(all) let price: UInt64
65
66        access(all) view fun getViews(): [Type] {
67            return [
68                Type<MetadataViews.NFTView>(),
69                Type<MetadataViews.Display>(),
70                Type<MetadataViews.ExternalURL>(),
71                Type<MetadataViews.NFTCollectionData>(),
72                Type<MetadataViews.NFTCollectionDisplay>(),
73                Type<MetadataViews.Traits>(),
74                Type<MetadataViews.Medias>(),
75                Type<MetadataViews.Royalties>()
76            ]
77        }
78
79// Helper function to extract digits from a string
80access(all) fun getLumNum(_ str: String): String {
81    var digits: String = ""
82    for char in str {
83        if char >= "0" && char <= "9" {  // Compare character directly
84            digits = digits.concat(char.toString())  // Convert character to string before concatenation
85        }
86    }
87    return digits
88}
89
90
91
92
93
94
95
96
97
98        access(all) fun resolveView(_ view: Type): AnyStruct? {
99
100            switch view {
101                case Type<MetadataViews.NFTView>():
102                    let viewResolver = &self as &{ViewResolver.Resolver}
103                    return MetadataViews.NFTView(
104                        id: self.id,
105                        uuid: self.uuid,
106                        display: MetadataViews.getDisplay(viewResolver),
107                        externalURL: MetadataViews.getExternalURL(viewResolver),
108                        collectionData: MetadataViews.getNFTCollectionData(viewResolver),
109                        collectionDisplay: MetadataViews.getNFTCollectionDisplay(viewResolver),
110                        royalties: MetadataViews.getRoyalties(viewResolver),
111                        traits: MetadataViews.getTraits(viewResolver)
112                    )
113                case Type<MetadataViews.Display>():
114                	let hoodlumNumber = self.getLumNum(self.tokenTitle)
115                    return MetadataViews.Display(
116                        name: self.tokenTitle,
117                        description: self.tokenDescription,
118                        thumbnail: MetadataViews.IPFSFile(cid: "QmTPGjR5TN2QLMm6VN2Ux81NK955qqgvrjQkCwNDqW73fs", path: "someHoodlum_".concat(hoodlumNumber).concat(".png")),
119                    )
120                case Type<MetadataViews.ExternalURL>():
121                    let url = "https://flowty.io/collection/".concat(SturdyItems.account.address.toString()).concat("/SturdyItems/").concat(self.id.toString())
122
123                    return MetadataViews.ExternalURL(url)
124                case Type<MetadataViews.NFTCollectionData>():
125                    return MetadataViews.NFTCollectionData(
126                        storagePath: SturdyItems.CollectionStoragePath,
127                        publicPath: SturdyItems.CollectionPublicPath,
128                        publicCollection: Type<&SturdyItems.Collection>(),
129                        publicLinkedType: Type<&SturdyItems.Collection>(),
130                        createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
131                            return <-SturdyItems.createEmptyCollection(nftType: Type<@NFT>())
132                        })
133                    )
134                case Type<MetadataViews.NFTCollectionDisplay>():
135                    let thumbnail = MetadataViews.Media(
136                        file: MetadataViews.IPFSFile(cid: "QmYQPsikmJxRAtCFGTa3coUoG6bZqduyckAwodUQ35T8p9", path: nil),
137                        mediaType: "image/jpeg"
138                    )
139
140                    let banner = MetadataViews.Media(
141                        file: MetadataViews.IPFSFile(cid: "QmPqVFuM2d4bSqFCjTddajaSb7AVYpDrRJuw3BeE8s1cRJ", path: nil),
142                        mediaType: "image/jpeg"
143                    )
144                    return MetadataViews.NFTCollectionDisplay(
145                        name: "Hoodlums",
146                        description: "Hoodlums NFT is a generative art project featuring 5,000 unique Hoodlum PFPs, crafted from hand-drawn traits by renowned memelord Somehoodlum. Created for creatives, by creatives, the project is owned and operated by Hoodlums holders through Hoodlums DAO. Hoodlums is the first PFP on the Flow Blockchain, minted in September 2021.",
147                        externalURL: MetadataViews.ExternalURL("https://www.hoodlums.io/"),
148                        squareImage: thumbnail,
149                        bannerImage: banner,
150                        socials: {
151                            "twitter": MetadataViews.ExternalURL("https://x.com/HoodlumsNFT"),
152                            "discord": MetadataViews.ExternalURL("https://discord.gg/ah2jynWk")
153                        }
154                    )
155                case Type<MetadataViews.Traits>():
156                    var metadata = HoodlumsMetadata.getMetadata(tokenID: self.id)
157                    return metadata
158                case Type<MetadataViews.Medias>():
159                    let medias: [MetadataViews.Media] = [];
160                    let hoodlumNumber = self.getLumNum(self.tokenTitle)
161                        medias.append(
162                            MetadataViews.Media(
163                                file: MetadataViews.IPFSFile(cid: "QmTPGjR5TN2QLMm6VN2Ux81NK955qqgvrjQkCwNDqW73fs", path: "someHoodlum_".concat(hoodlumNumber).concat(".png")),
164                                mediaType: "image/png"
165                            )
166                        )
167                    return MetadataViews.Medias(medias)
168                case Type<MetadataViews.Royalties>():
169                    return MetadataViews.Royalties(
170                        [
171                            MetadataViews.Royalty(
172                            receiver: getAccount(HoodlumsMetadata.sturdyRoyaltyAddress)
173                                    .capabilities.get<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver),
174                                cut: HoodlumsMetadata.sturdyRoyaltyCut,
175                                description: "Hoodlums DAO Royalty"
176                            ),
177                            MetadataViews.Royalty(
178                                receiver: getAccount(HoodlumsMetadata.artistRoyaltyAddress)
179                                    .capabilities.get<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver),
180                                cut: HoodlumsMetadata.artistRoyaltyCut,
181                                description: "Artist Royalty"
182                            )
183                        ]
184                    )
185            }
186            return nil
187        }
188
189        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
190            return <- SturdyItems.createEmptyCollection(nftType: Type<@NFT>())
191        }
192
193        // initializer
194        //
195        init(initID: UInt64, 
196        	initTypeID: UInt64, 
197        	initTokenURI: String, 
198        	initTokenTitle: String, 
199        	initTokenDescription: String, 
200        	initArtist: String, 
201        	initSecondaryRoyalty: String,
202        	initPlatformMintedOn: String
203        ) {
204	   			self.id = initID
205	            self.typeID = initTypeID
206	            self.tokenURI = initTokenURI
207	            self.tokenTitle = initTokenTitle
208	            self.tokenDescription = initTokenDescription
209	            self.artist = initArtist
210	            self.secondaryRoyalty = initSecondaryRoyalty
211	            self.platformMintedOn = initPlatformMintedOn
212        }
213    }
214
215    // This is the interface that users can cast their SturdyItems Collection as
216    // to allow others to deposit SturdyItems into their Collection. It also allows for reading
217    // the details of SturdyItems in the Collection.
218    access(all) resource interface SturdyItemsCollectionPublic: NonFungibleToken.Collection {
219        access(all) fun deposit(token: @{NonFungibleToken.NFT})
220        access(all) view fun borrowSturdyItem(id: UInt64): &SturdyItems.NFT? {
221            // If the result isn't nil, the id of the returned reference
222            // should be the same as the argument to the function
223            post {
224                (result == nil) || (result?.id == id):
225                    "Cannot borrow SturdyItem reference: The ID of the returned reference is incorrect"
226            }
227        }
228    }
229
230    // Collection
231    // A collection of SturdyItem NFTs owned by an account
232    //
233    access(all) resource Collection: SturdyItemsCollectionPublic {
234        // dictionary of NFT conforming tokens
235        // NFT is a resource type with an `UInt64` ID field
236        //
237        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
238
239        // withdraw
240        // Removes an NFT from the collection and moves it to the caller
241        //
242        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
243            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
244
245            emit Withdraw(id: token.id, from: self.owner?.address)
246
247            return <-token
248        }
249
250        // deposit
251        // Takes a NFT and adds it to the collections dictionary
252        // and adds the ID to the id array
253        //
254        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
255            let token <- token as! @SturdyItems.NFT
256
257            let id: UInt64 = token.id
258
259            // add the new token to the dictionary which removes the old one
260            let oldToken <- self.ownedNFTs[id] <- token
261
262            emit Deposit(id: id, to: self.owner?.address)
263
264            destroy oldToken
265        }
266
267        // getIDs
268        // Returns an array of the IDs that are in the collection
269        //
270        access(all) view fun getIDs(): [UInt64] {
271            return self.ownedNFTs.keys
272        }
273
274        // borrowNFT
275        // Gets a reference to an NFT in the collection
276        // so that the caller can read its metadata and call its methods
277        //
278        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
279            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
280        }
281
282        // borrowSturdyItem
283        // Gets a reference to an NFT in the collection as a SturdyItem,
284        // exposing all of its fields (including the typeID).
285        // This is safe as there are no functions that can be called on the SturdyItem.
286        //
287        access(all) view fun borrowSturdyItem(id: UInt64): &SturdyItems.NFT? {
288            if self.ownedNFTs[id] != nil {
289                let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
290                return ref as! &SturdyItems.NFT
291            } else {
292                return nil
293            }
294        }
295
296
297        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver} {
298            let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
299            return nft
300        }
301
302        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
303            return {
304                Type<@SturdyItems.NFT>(): true
305            }
306        }
307
308        access(all) view fun isSupportedNFTType(type: Type): Bool {
309            return type == Type<@SturdyItems.NFT>()
310        }
311
312        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
313            return <- create Collection()
314        }
315
316        init () {
317            self.ownedNFTs <- {}
318        }
319    }
320
321    // NFTMinter
322    // Resource that an admin or something similar would own to be
323    // able to mint new NFTs
324    //
325	access(all) resource NFTMinter {
326
327		// mintNFT
328        // Mints a new NFT with a new ID
329		// and deposit it in the recipients collection using their collection reference
330        //
331        // price: UInt64
332        // price: price
333        // initPrice: price
334		access(Owner) fun mintNFT(recipient: &{NonFungibleToken.CollectionPublic}, 
335			typeID: UInt64, 
336			tokenURI: String, 
337			tokenTitle: String, 
338			tokenDescription: String, 
339		 	artist: String, 
340		 	secondaryRoyalty: String,  
341		 	platformMintedOn: String
342        ) {
343            SturdyItems.totalSupply = SturdyItems.totalSupply + 1
344            emit Minted(id: SturdyItems.totalSupply, 
345            	typeID: typeID, 
346            	tokenURI: tokenURI, 
347            	tokenTitle: tokenTitle, 
348            	tokenDescription: tokenDescription,
349            	artist: artist, 
350            	secondaryRoyalty: secondaryRoyalty, 
351            	platformMintedOn: platformMintedOn
352            )
353
354			// deposit it in the recipient's account using their reference
355			recipient.deposit(token: <-create NFT(
356				initID: SturdyItems.totalSupply, 
357				initTypeID: typeID, 
358				initTokenURI: tokenURI,
359				initTokenTitle: tokenTitle,
360				initTokenDescription: tokenDescription,
361				initArtist: artist,
362				initSecondaryRoyalty: secondaryRoyalty,
363				initPlatformMintedOn: platformMintedOn
364            ))
365		}
366	}
367
368    // fetch
369    // Get a reference to a SturdyItem from an account's Collection, if available.
370    // If an account does not have a SturdyItems.Collection, panic.
371    // If it has a collection but does not contain the itemId, return nil.
372    // If it has a collection and that collection contains the itemId, return a reference to that.
373    //
374    access(all) fun fetch(_ from: Address, itemID: UInt64): &SturdyItems.NFT? {
375        let collection = getAccount(from).capabilities
376            .get<&{NonFungibleToken.CollectionPublic}>(SturdyItems.CollectionPublicPath)
377            .borrow()
378            ?? panic("Couldn't get collection")
379        let sturdyCollection = collection as! &SturdyItems.Collection
380        // We trust SturdyItems.Collection.borowSturdyItem to get the correct itemID
381        // (it checks it before returning it).
382        return sturdyCollection.borrowSturdyItem(id: itemID)
383    }
384
385    /// Function that resolves a metadata view for this contract.
386    ///
387    /// @param view: The Type of the desired view.
388    /// @return A structure representing the requested view.
389    ///
390    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
391        switch viewType {
392            case Type<MetadataViews.NFTCollectionData>():
393                return MetadataViews.NFTCollectionData(
394                        storagePath: SturdyItems.CollectionStoragePath,
395                        publicPath: SturdyItems.CollectionPublicPath,
396                        publicCollection: Type<&SturdyItems.Collection>(),
397                        publicLinkedType: Type<&SturdyItems.Collection>(),
398                        createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
399                            return <-SturdyItems.createEmptyCollection(nftType: Type<@NFT>())
400                        })
401                )
402            case Type<MetadataViews.NFTCollectionDisplay>():
403                    let thumbnail = MetadataViews.Media(
404                        file: MetadataViews.IPFSFile(cid: "QmYQPsikmJxRAtCFGTa3coUoG6bZqduyckAwodUQ35T8p9", path: nil),
405                        mediaType: "image/jpeg"
406                    )
407
408                    let banner = MetadataViews.Media(
409                        file: MetadataViews.IPFSFile(cid: "QmPqVFuM2d4bSqFCjTddajaSb7AVYpDrRJuw3BeE8s1cRJ", path: nil),
410                        mediaType: "image/jpeg"
411                    )
412                return MetadataViews.NFTCollectionDisplay(
413                        name: "Hoodlums",
414                        description: "Hoodlums NFT is a generative art project featuring 5,000 unique Hoodlum PFPs, crafted from hand-drawn traits by renowned memelord Somehoodlum. Created for creatives, by creatives, the project is owned and operated by Hoodlums holders through Hoodlums DAO. Hoodlums is the first PFP on the Flow Blockchain, minted in September 2021.",
415                        externalURL: MetadataViews.ExternalURL("https://www.hoodlums.io/"),
416                        squareImage: thumbnail,
417                        bannerImage: banner,
418                        socials: {
419                            "twitter": MetadataViews.ExternalURL("https://x.com/HoodlumsNFT"),
420                            "discord": MetadataViews.ExternalURL("https://discord.gg/ah2jynWk")
421                        }
422                    )
423        }
424        return nil
425    }
426
427    /// Function that returns all the Metadata Views implemented by a Non Fungible Token
428    ///
429    /// @return An array of Types defining the implemented views. This value will be used by
430    ///         developers to know which parameter to pass to the resolveView() method.
431    ///
432    access(all) view fun getContractViews(resourceType: Type?): [Type] {
433        return [
434            Type<MetadataViews.NFTCollectionData>(),
435            Type<MetadataViews.NFTCollectionDisplay>(),
436            Type<MetadataViews.ExternalURL>()
437        ]
438    }
439
440    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
441        pre {
442            nftType == Type<@NFT>(): "incorrect nft type given"
443        }
444
445        return <- create Collection()
446    }
447
448    // initializer
449    //
450	init() {
451        // Set our named paths
452        self.CollectionStoragePath = /storage/SturdyItemsCollection
453        self.CollectionPublicPath = /public/SturdyItemsCollection
454        self.MinterStoragePath = /storage/SturdyItemsMinter
455
456        // Initialize the total supply
457        self.totalSupply = 0
458
459        // Create a Minter resource and save it to storage
460        let minter <- create NFTMinter()
461        self.account.storage.save(<-minter, to: self.MinterStoragePath)
462
463        emit ContractInitialized()
464	}
465}
466
467//DG4L
468