Smart Contract
NFTViewUtils
A.15a918087ab12d86.NFTViewUtils
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# NFT List - An on-chain list of Flow Standard Non-Fungible Tokens (NFTs).
5
6This is the Non-Fungible Token view utilties contract of the Token List.
7
8*/
9import MetadataViews from 0x1d7e57aa55817448
10import ViewResolver from 0x1d7e57aa55817448
11import NonFungibleToken from 0x1d7e57aa55817448
12// TokenList Imports
13import FTViewUtils from 0x15a918087ab12d86
14
15access(all) contract NFTViewUtils {
16
17 // ----- Entitlement -----
18
19 // An entitlement for allowing edit the FT View Data
20 access(all) entitlement Editable
21
22 /* ---- Events ---- */
23
24 access(all) event NFTDisplayUpdated(
25 address: Address,
26 contractName: String,
27 name: String?,
28 description: String?,
29 externalURL: String?,
30 squareImage: String?,
31 bannerImage: String?,
32 socials: {String: String}
33 )
34
35 /// The struct for the Fungible Token Identity
36 ///
37 access(all) struct NFTIdentity: FTViewUtils.TokenIdentity {
38 access(all)
39 let address: Address
40 access(all)
41 let contractName: String
42
43 view init(
44 _ address: Address,
45 _ contractName: String
46 ) {
47 self.address = address
48 self.contractName = contractName
49 }
50
51 access(all)
52 view fun buildType(): Type {
53 return self.buildCollectionType()
54 }
55
56 access(all)
57 view fun buildCollectionType(): Type {
58 return NFTViewUtils.buildCollectionType(self.address, self.contractName)
59 ?? panic("Could not build the FT Type")
60 }
61
62 access(all)
63 view fun buildNFTType(): Type {
64 return NFTViewUtils.buildNFTType(self.address, self.contractName)
65 ?? panic("Could not build the FT Type")
66 }
67
68 /// Borrow the Fungible Token Contract
69 ///
70 access(all)
71 fun borrowNFTContract(): &{NonFungibleToken} {
72 return getAccount(self.address)
73 .contracts.borrow<&{NonFungibleToken}>(name: self.contractName)
74 ?? panic("Could not borrow the FungibleToken contract reference")
75 }
76 }
77
78 /// The struct for the Fungible Token Display with Source
79 ///
80 access(all) struct NFTCollectionViewWithSource {
81 access(all)
82 let source: Address?
83 access(all)
84 let display: MetadataViews.NFTCollectionDisplay
85
86 view init(
87 _ source: Address?,
88 _ display: MetadataViews.NFTCollectionDisplay
89 ) {
90 self.source = source
91 self.display = display
92 }
93 }
94
95 /// The struct for the Fungible Token Paths
96 ///
97 access(all) struct StandardNFTPaths {
98 access(all)
99 let storagePath: StoragePath
100 access(all)
101 let publicPath: PublicPath
102
103 view init(
104 _ storagePath: StoragePath,
105 _ publicPath: PublicPath,
106 ) {
107 self.storagePath = storagePath
108 self.publicPath = publicPath
109 }
110 }
111
112 /// The struct for the Fungible Token List View
113 ///
114 access(all) struct StandardTokenView: FTViewUtils.ITokenView {
115 access(all)
116 let identity: NFTIdentity
117 access(all)
118 let tags: [String]
119 access(all)
120 let dataSource: Address?
121 access(all)
122 let paths: StandardNFTPaths?
123 access(all)
124 let display: NFTCollectionViewWithSource?
125
126 view init(
127 identity: NFTIdentity,
128 tags: [String],
129 dataSource: Address?,
130 paths: StandardNFTPaths?,
131 display: NFTCollectionViewWithSource?,
132 ) {
133 self.identity = identity
134 self.tags = tags
135 self.dataSource = dataSource
136 self.paths = paths
137 self.display = display
138 }
139
140 access(all)
141 view fun getIdentity(): {FTViewUtils.TokenIdentity} {
142 return self.identity
143 }
144 }
145
146 /// The struct for the Bridged Token List View
147 ///
148 access(all) struct BridgedTokenView: FTViewUtils.IBridgedTokenView, FTViewUtils.ITokenView {
149 access(all)
150 let identity: NFTIdentity
151 access(all)
152 let evmAddress: String
153 access(all)
154 let tags: [String]
155 access(all)
156 let dataSource: Address?
157 access(all)
158 let paths: StandardNFTPaths?
159 access(all)
160 let display: NFTCollectionViewWithSource?
161
162 view init(
163 identity: NFTIdentity,
164 evmAddress: String,
165 tags: [String],
166 dataSource: Address?,
167 paths: StandardNFTPaths?,
168 display: NFTCollectionViewWithSource?,
169 ) {
170 self.identity = identity
171 self.evmAddress = evmAddress
172 self.tags = tags
173 self.dataSource = dataSource
174 self.paths = paths
175 self.display = display
176 }
177
178 access(all)
179 view fun getIdentity(): {FTViewUtils.TokenIdentity} {
180 return self.identity
181 }
182 }
183
184 /** Editable NFTView */
185
186 /// The interface for the Editable FT View Display
187 ///
188 access(all) resource interface EditableNFTCollectionDisplayInterface: ViewResolver.Resolver {
189 /// Identity of the FT
190 access(all)
191 let identity: NFTIdentity
192 // ----- FT Display -----
193 access(all)
194 view fun getName(): String?
195 access(all)
196 view fun getDescription(): String?
197 access(all)
198 view fun getExternalURL(): MetadataViews.ExternalURL?
199 access(all)
200 view fun getSocials(): {String: MetadataViews.ExternalURL}
201 // Square-sized image to represent this collection.
202 access(all)
203 view fun getSquareImage(): MetadataViews.Media?
204 // Banner-sized image for this collection, recommended to have a size near 1200x630.
205 access(all)
206 view fun getBannerImage(): MetadataViews.Media?
207 // Get all the images
208 access(all)
209 fun getImages(): MetadataViews.Medias
210 // --- default implementation ---
211 // Get the default square image
212 access(all)
213 view fun getDefaultSquareImage(): MetadataViews.Media {
214 return MetadataViews.Media(
215 file: MetadataViews.HTTPFile(url: "https://i.imgur.com/hs3U5CY.png"),
216 mediaType: "image/png"
217 )
218 }
219 // Get the default banner image
220 //
221 access(all)
222 view fun getDefaultBannerImage(): MetadataViews.Media {
223 return MetadataViews.Media(
224 file: MetadataViews.HTTPFile(url: "https://i.imgur.com/4DOuqFf.jpeg"),
225 mediaType: "image/jpeg"
226 )
227 }
228 /// Get the FT Display
229 ///
230 access(all)
231 fun getCollectionDisplay(): MetadataViews.NFTCollectionDisplay {
232 return MetadataViews.NFTCollectionDisplay(
233 name: self.getName() ?? "Unknown Token",
234 description: self.getDescription() ?? "No Description",
235 externalURL: self.getExternalURL() ?? MetadataViews.ExternalURL("https://fixes.world"),
236 // Square-sized image to represent this collection.
237 squareImage: self.getSquareImage() ?? self.getDefaultSquareImage(),
238 // Banner-sized image for this collection, recommended to have a size near 1200x630.
239 bannerImage: self.getBannerImage() ?? self.getDefaultBannerImage(),
240 socials: self.getSocials()
241 )
242 }
243 }
244
245 /// The interface for the FT View Display Editor
246 ///
247 access(all) resource interface NFTCollectionDisplayEditor: EditableNFTCollectionDisplayInterface {
248 /// Set the FT Display
249 access(Editable)
250 fun setDisplay(
251 name: String?,
252 description: String?,
253 externalURL: String?,
254 squareImage: String?,
255 bannerImage: String?,
256 socials: {String: String}
257 )
258 }
259
260 /// The Resource for the FT Display
261 ///
262 access(all) resource EditableNFTCollectionDisplay: NFTCollectionDisplayEditor, EditableNFTCollectionDisplayInterface {
263 access(all)
264 let identity: NFTIdentity
265 access(contract)
266 let metadata: {String: String}
267
268 init(
269 _ address: Address,
270 _ contractName: String,
271 ) {
272 self.identity = NFTIdentity(address, contractName)
273 self.metadata = {}
274 // ensure identity is valid
275 self.identity.borrowNFTContract()
276 }
277
278 /// Set the FT Display
279 ///
280 access(Editable)
281 fun setDisplay(
282 name: String?,
283 description: String?,
284 externalURL: String?,
285 squareImage: String?,
286 bannerImage: String?,
287 socials: {String: String}
288 ) {
289 // set name
290 if name != nil {
291 self.metadata["name"] = name!
292 }
293 // set description
294 if description != nil {
295 self.metadata["description"] = description!
296 }
297 // set external URL
298 if externalURL != nil {
299 self.metadata["externalURL"] = externalURL!
300 }
301 // set squareImage
302 if squareImage != nil {
303 // the last 3 chars are file type, check the type
304 let fileType = squareImage!.slice(from: squareImage!.length - 3, upTo: squareImage!.length)
305 if fileType == "png" {
306 self.metadata["image:square:png"] = squareImage!
307 } else if fileType == "svg" {
308 self.metadata["image:square:svg"] = squareImage!
309 } else if fileType == "jpg" {
310 self.metadata["image:square:jpg"] = squareImage!
311 } else {
312 self.metadata["image:square"] = squareImage!
313 }
314 }
315 // set bannerImage
316 if bannerImage != nil {
317 // the last 3 chars are file type, check the type
318 let fileType = bannerImage!.slice(from: bannerImage!.length - 3, upTo: bannerImage!.length)
319 if fileType == "png" {
320 self.metadata["image:banner:png"] = bannerImage!
321 } else if fileType == "svg" {
322 self.metadata["image:banner:svg"] = bannerImage!
323 } else if fileType == "jpg" {
324 self.metadata["image:banner:jpg"] = bannerImage!
325 } else {
326 self.metadata["image:banner"] = bannerImage!
327 }
328 }
329 // set socials
330 for key in socials.keys {
331 self.metadata["social:".concat(key)] = socials[key]!
332 }
333
334 // Emit the event
335 emit NFTDisplayUpdated(
336 address: self.identity.address,
337 contractName: self.identity.contractName,
338 name: name,
339 description: description,
340 externalURL: externalURL,
341 squareImage: squareImage,
342 bannerImage: bannerImage,
343 socials: socials
344 )
345 }
346
347 /** ---- Implement the EditableFTViewDisplayInterface ---- */
348
349 access(all)
350 view fun getName(): String? {
351 return self.metadata["name"]
352 }
353
354 access(all)
355 view fun getDescription(): String? {
356 return self.metadata["description"]
357 }
358
359 access(all)
360 view fun getExternalURL(): MetadataViews.ExternalURL? {
361 let url = self.metadata["externalURL"]
362 return url != nil ? MetadataViews.ExternalURL(url!) : nil
363 }
364
365 access(self)
366 view fun getImage(_ key: String, _ type: String?): MetadataViews.Media? {
367 var keyName = "image:".concat(key)
368 if type != nil && (type == "svg" || type == "png" || type == "jpg") {
369 keyName = keyName.concat(":").concat(type!)
370 }
371 if self.metadata[keyName] != nil {
372 let mediaType = type == "svg"
373 ? "image/svg+xml"
374 : type == "png"
375 ? "image/png"
376 : type == "jpg"
377 ? "image/jpeg"
378 : "image/*"
379 return MetadataViews.Media(
380 file: MetadataViews.HTTPFile(url: self.metadata[keyName]!),
381 mediaType: mediaType
382 )
383 }
384 return nil
385 }
386
387 access(all)
388 view fun getBannerImage(): MetadataViews.Media? {
389 return self.getImage("banner", "svg") ?? self.getImage("banner", "png") ?? self.getImage("banner", "jpg") ?? self.getImage("banner", nil)
390 }
391
392 access(all)
393 view fun getSquareImage(): MetadataViews.Media? {
394 return self.getImage("square", "svg") ?? self.getImage("square", "png") ?? self.getImage("square", "jpg") ?? self.getImage("square", nil)
395 }
396
397 /// Get all the images
398 ///
399 access(all)
400 fun getImages(): MetadataViews.Medias {
401 let medias: [MetadataViews.Media] = []
402 if let bannerSvg = self.getImage("banner", "svg") {
403 medias.append(bannerSvg)
404 }
405 if let bannerPng = self.getImage("banner", "png") {
406 medias.append(bannerPng)
407 }
408 if let bannerJpg = self.getImage("banner", "jpg") {
409 medias.append(bannerJpg)
410 }
411 if let squareSvg = self.getImage("square", "svg") {
412 medias.append(squareSvg)
413 }
414 if let squarePng = self.getImage("square", "png") {
415 medias.append(squarePng)
416 }
417 if let squareJpg = self.getImage("square", "jpg") {
418 medias.append(squareJpg)
419 }
420 return MetadataViews.Medias(medias)
421 }
422
423 access(all)
424 view fun getSocials(): {String: MetadataViews.ExternalURL} {
425 let ret: {String: MetadataViews.ExternalURL} = {}
426 let socialKey = "social:"
427 let socialKeyLen = socialKey.length
428 for key in self.metadata.keys {
429 if key.length >= socialKeyLen && key.slice(from: 0, upTo: socialKey.length) == socialKey {
430 let socialName = key.slice(from: socialKey.length, upTo: key.length)
431 ret[socialName] = MetadataViews.ExternalURL(self.metadata[key]!)
432 }
433 }
434 return ret
435 }
436
437 /* --- Implement the MetadataViews.Resolver --- */
438
439 access(all)
440 view fun getViews(): [Type] {
441 return [
442 Type<MetadataViews.NFTCollectionDisplay>(),
443 Type<MetadataViews.Medias>()
444 ]
445 }
446
447 access(all)
448 fun resolveView(_ view: Type): AnyStruct? {
449 switch view {
450 case Type<MetadataViews.NFTCollectionDisplay>():
451 return self.getCollectionDisplay()
452 case Type<MetadataViews.Medias>():
453 return self.getImages()
454 }
455 return nil
456 }
457 }
458
459 /// Create the FT View Data
460 ///
461 access(all)
462 fun createEditableCollectionDisplay(
463 _ address: Address,
464 _ contractName: String,
465 ): @EditableNFTCollectionDisplay {
466 return <- create EditableNFTCollectionDisplay(address, contractName)
467 }
468
469 /// Build the NFT Type
470 ///
471 access(all)
472 view fun buildNFTType(_ address: Address, _ contractName: String): Type? {
473 let addrStr = address.toString()
474 let addrStrNo0x = addrStr.slice(from: 2, upTo: addrStr.length)
475 return CompositeType("A.".concat(addrStrNo0x).concat(".").concat(contractName).concat(".NFT"))
476 }
477
478 /// Build the NFT Collection Type
479 ///
480 access(all)
481 view fun buildCollectionType(_ address: Address, _ contractName: String): Type? {
482 let addrStr = address.toString()
483 let addrStrNo0x = addrStr.slice(from: 2, upTo: addrStr.length)
484 return CompositeType("A.".concat(addrStrNo0x).concat(".").concat(contractName).concat(".Collection"))
485 }
486}
487