Smart Contract
SturdyItems
A.427ceada271aa0b1.SturdyItems
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