Smart Contract
Ordinal
A.9212a87501a8a6a2.Ordinal
1/*
2 Ordinal.cdc
3
4 Author: Brian Min brian@flowverse.co
5*/
6
7import NonFungibleToken from 0x1d7e57aa55817448
8import MetadataViews from 0x1d7e57aa55817448
9import FungibleToken from 0xf233dcee88fe0abe
10import OrdinalVendor from 0x9212a87501a8a6a2
11import ViewResolver from 0x1d7e57aa55817448
12
13access(all) contract Ordinal: NonFungibleToken {
14 // Events
15 access(all) event OrdinalMinted(id: UInt64, nftUUID: UInt64, creator: Address, type: String)
16 access(all) event OrdinalUpdated(id: UInt64, size: Int)
17
18 // Paths
19 access(all) let CollectionStoragePath: StoragePath
20 access(all) let CollectionPublicPath: PublicPath
21 access(all) let AdminStoragePath: StoragePath
22
23 // Incremented each time a new Ordinal is minted
24 access(all) var totalSupply: UInt64
25
26 // Custom metadata view for Ordinal Inscription Data
27 access(all) struct InscriptionMetadataView {
28 access(all) let type: String?
29 access(all) let inscriptionData: String?
30
31 view init(type: String?, inscriptionData: String?) {
32 self.type = type
33 self.inscriptionData = inscriptionData
34 }
35 }
36
37 access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
38 // The inscription number
39 access(all) let id: UInt64
40
41 // The original creator
42 access(all) let creator: Address
43
44 // The type of ordinal (text, domain or image)
45 access(all) let type: String
46
47 // The inscription data
48 access(self) var data: String
49
50 init(creator: Address, type: String, data: String) {
51 self.id = Ordinal.totalSupply
52 self.creator = creator
53 self.type = type
54 self.data = data
55
56 emit OrdinalMinted(id: self.id, nftUUID: self.uuid, creator: self.creator, type: self.type)
57 }
58
59 access(contract) fun updateData(data: String) {
60 assert(self.data.length > 300000, message: "This ordinal inscription cannot be updated as it is less than 300KB")
61 assert(self.type == "image", message: "Inscription data can only be updated for image ordinals")
62 assert(data.length > 0 && data.length <= 300000, message: "Inscription data must be non-empty and less than 300KB")
63 self.data = data
64
65 emit OrdinalUpdated(id: self.id, size: data.length)
66 }
67
68 access(all) view fun getViews(): [Type] {
69 let supportedViews = [
70 Type<MetadataViews.Display>(),
71 Type<MetadataViews.Royalties>(),
72 Type<MetadataViews.Edition>(),
73 Type<MetadataViews.Traits>(),
74 Type<MetadataViews.ExternalURL>(),
75 Type<MetadataViews.Rarity>(),
76 Type<InscriptionMetadataView>()
77 ]
78 return supportedViews
79 }
80
81 access(all) view fun resolveView(_ view: Type): AnyStruct? {
82 var data = self.data
83 let isOrdinalRestricted = OrdinalVendor.checkOrdinalRestricted(id: self.id)
84 if isOrdinalRestricted {
85 data = "Content restricted due to policy violation"
86 }
87 switch view {
88 case Type<MetadataViews.Display>():
89 return MetadataViews.Display(
90 name: "Ordinal".concat(" #").concat(self.id.toString()),
91 description: "Ordinals on the Flow blockchain",
92 thumbnail: MetadataViews.HTTPFile(
93 url: "https://flowverse.myfilebase.com/ipfs/QmQ45TvzGVTmoMCfGqxgbiMmR4rdmSHAhz661bPyUfFrAT"
94 )
95 )
96 case Type<MetadataViews.Royalties>():
97 let royalties : [MetadataViews.Royalty] = [
98 MetadataViews.Royalty(
99 receiver: getAccount(0x604b63bcbef5974f).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!,
100 cut: 0.01,
101 description: "Platform Royalty Fee")
102 ]
103 return MetadataViews.Royalties(royalties)
104 case Type<MetadataViews.Traits>():
105 let traits: [MetadataViews.Trait] = [
106 MetadataViews.Trait(
107 name: "Inscription Number",
108 value: self.id,
109 displayType: "Number",
110 rarity: nil
111 ),
112 MetadataViews.Trait(
113 name: "Type",
114 value: self.type,
115 displayType: nil,
116 rarity: nil
117 ),
118 MetadataViews.Trait(
119 name: "Size",
120 value: self.data.length,
121 displayType: "Number",
122 rarity: nil
123 ),
124 MetadataViews.Trait(
125 name: "Creator",
126 value: self.creator.toString(),
127 displayType: nil,
128 rarity: nil
129 ),
130 MetadataViews.Trait(
131 name: "UUID",
132 value: self.uuid.toString(),
133 displayType: nil,
134 rarity: nil
135 )
136 ]
137 return MetadataViews.Traits(traits)
138 case Type<InscriptionMetadataView>():
139 return InscriptionMetadataView(
140 type: self.type,
141 inscriptionData: data
142 )
143 case Type<MetadataViews.Rarity>():
144 var description = ""
145 if self.id <= 100 {
146 description = "Sub 100"
147 } else if self.id <= 1000 {
148 description = "Sub 1K"
149 } else if self.id <= 5000 {
150 description = "Sub 5K"
151 } else if self.id <= 10000 {
152 description = "Sub 10K"
153 } else if self.id <= 25000 {
154 description = "Sub 25K"
155 } else if self.id <= 50000 {
156 description = "Sub 50K"
157 } else {
158 description = "Sub 100K"
159 }
160 return MetadataViews.Rarity(
161 score: nil,
162 max: nil,
163 description: description
164 )
165 case Type<MetadataViews.ExternalURL>():
166 return MetadataViews.ExternalURL("https://nft.flowverse.co/ordinals/".concat(self.id.toString()))
167 }
168 return nil
169 }
170
171 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
172 return <- Ordinal.createEmptyCollection(nftType: Type<@Ordinal.NFT>())
173 }
174 }
175
176 access(self) fun mint(creator: Address, type: String, data: String): @NFT {
177 pre {
178 type == "image" || type == "text" || type == "domain" : "Invalid type (must be either image, text or domain)"
179 data.length > 0 : "Invalid data (must be non-empty)"
180 }
181
182 if type == "domain" {
183 assert(OrdinalVendor.checkDomainAvailability(domain: data), message: "domain already exists")
184 }
185
186 // Increment the inscription number
187 Ordinal.totalSupply = Ordinal.totalSupply + UInt64(1)
188
189 // Mint the new NFT
190 let nft: @NFT <- create NFT(creator: creator, type: type, data: data)
191 return <-nft
192 }
193
194 access(all) resource Minter: OrdinalVendor.IMinter {
195 init() {}
196 access(all) fun mint(creator: Address, type: String, data: String): @NFT {
197 return <-Ordinal.mint(creator: creator, type: type, data: data)
198 }
199 }
200
201 access(all) resource Admin {
202 access(all) fun createMinter(): @Minter {
203 return <-create Minter()
204 }
205
206 access(all) fun createNewAdmin(): @Admin {
207 return <-create Admin()
208 }
209 }
210
211 //TODO: remove this as it's not used
212 access(all) resource interface CollectionPrivate {
213 access(all) fun borrowUpdateOrdinalNFT(id: UInt64): &Ordinal.NFT?
214 access(all) fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?
215 }
216
217 access(all) resource interface CollectionUpdate {
218 access(all) fun updateData(id: UInt64, data: String)
219 }
220
221 // Public interface for the FlowverseShirt Collection that allows users access to certain functionalities
222 access(all) resource interface CollectionPublic {
223 access(all) fun deposit(token: @{NonFungibleToken.NFT})
224 access(all) view fun getIDs(): [UInt64]
225 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
226 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?
227 }
228
229 // Collection of FlowverseShirt NFTs owned by an account
230 access(all) resource Collection: CollectionPublic, CollectionUpdate, NonFungibleToken.Collection {
231 // Dictionary of entity instances conforming tokens
232 // NFT is a resource type with a UInt64 ID field
233 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
234
235 init () {
236 self.ownedNFTs <- {}
237 }
238
239 /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
240 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
241 let supportedTypes: {Type: Bool} = {}
242 supportedTypes[Type<@Ordinal.NFT>()] = true
243 return supportedTypes
244 }
245
246 /// Returns whether or not the given type is accepted by the collection
247 /// A collection that can accept any type should just return true by default
248 access(all) view fun isSupportedNFTType(type: Type): Bool {
249 if type == Type<@Ordinal.NFT>() {
250 return true
251 } else {
252 return false
253 }
254 }
255
256 /// Gets the amount of NFTs stored in the collection
257 access(all) view fun getLength(): Int {
258 return self.ownedNFTs.keys.length
259 }
260
261 /// withdraw removes an NFT from the collection and moves it to the caller
262 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
263 let token <- self.ownedNFTs.remove(key: withdrawID)
264 ?? panic("Could not withdraw an NFT with the provided ID from the collection")
265
266 return <-token
267 }
268
269 /// deposit takes a NFT and adds it to the collections dictionary
270 /// and adds the ID to the id array
271 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
272 let token <- token as! @Ordinal.NFT
273 let id = token.id
274
275 // add the new token to the dictionary which removes the old one
276 let oldToken <- self.ownedNFTs[token.id] <- token
277
278 destroy oldToken
279 }
280
281 access(all) view fun getIDs(): [UInt64] {
282 return self.ownedNFTs.keys
283 }
284
285 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
286 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
287 }
288
289 /// Borrow the view resolver for the specified NFT ID
290 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
291 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
292 return nft as &{ViewResolver.Resolver}
293 }
294 return nil
295 }
296
297 access(all) fun updateData(id: UInt64, data: String) {
298 if let nft = self.borrowNFT(id) {
299 let ordinalNFT = nft as! &Ordinal.NFT
300 ordinalNFT.updateData(data: data)
301 }
302 }
303
304 /// createEmptyCollection creates an empty Collection of the same type
305 /// and returns it to the caller
306 /// @return A an empty collection of the same type
307 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
308 return <-Ordinal.createEmptyCollection(nftType: Type<@Ordinal.NFT>())
309 }
310 }
311
312 // -----------------------------------------------------------------------
313 // Ordinal contract-level function definitions
314 // -----------------------------------------------------------------------
315
316 /// createEmptyCollection creates an empty Collection for the specified NFT type
317 /// and returns it to the caller so that they can own NFTs
318 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
319 return <- create Collection()
320 }
321
322
323 /// Function that returns all the Metadata Views implemented by a Non Fungible Token
324 ///
325 /// @return An array of Types defining the implemented views. This value will be used by
326 /// developers to know which parameter to pass to the resolveView() method.
327 ///
328 access(all) view fun getContractViews(resourceType: Type?): [Type] {
329 return [
330 Type<MetadataViews.NFTCollectionData>(),
331 Type<MetadataViews.NFTCollectionDisplay>()
332 ]
333 }
334
335 /// Function that resolves a metadata view for this contract.
336 ///
337 /// @param view: The Type of the desired view.
338 /// @return A structure representing the requested view.
339 ///
340 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
341 switch viewType {
342 case Type<MetadataViews.NFTCollectionData>():
343 return MetadataViews.NFTCollectionData(
344 storagePath: self.CollectionStoragePath,
345 publicPath: self.CollectionPublicPath,
346 publicCollection: Type<&Ordinal.Collection>(),
347 publicLinkedType: Type<&Ordinal.Collection>(),
348 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {return <- Ordinal.createEmptyCollection(nftType: Type<@Ordinal.NFT>())}),
349 )
350 case Type<MetadataViews.NFTCollectionDisplay>():
351 return MetadataViews.NFTCollectionDisplay(
352 name: "Ordinals",
353 description: "Ordinals on the Flow blockchain",
354 externalURL: MetadataViews.ExternalURL("https://twitter.com/flowverse_"),
355 squareImage: MetadataViews.Media(
356 file: MetadataViews.HTTPFile(
357 url: "https://flowverse.myfilebase.com/ipfs/QmQ45TvzGVTmoMCfGqxgbiMmR4rdmSHAhz661bPyUfFrAT"
358 ),
359 mediaType: "image/png"
360 ),
361 bannerImage: MetadataViews.Media(
362 file: MetadataViews.HTTPFile(
363 url: "https://flowverse.myfilebase.com/ipfs/QmaTj276rAUFoFiik84xCx1PYZnqZHcGp78vG6xqHLfoXo"
364 ),
365 mediaType: "image/png"
366 ),
367 socials: {
368 "twitter": MetadataViews.ExternalURL("https://twitter.com/flowverse_"),
369 "discord": MetadataViews.ExternalURL("https://discord.gg/flowverse"),
370 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/flowverseofficial")
371 }
372 )
373 }
374 return nil
375 }
376
377 // -----------------------------------------------------------------------
378 // Ordinal initialization function
379 // -----------------------------------------------------------------------
380 //
381 init() {
382 self.CollectionStoragePath = /storage/OrdinalCollection
383 self.CollectionPublicPath = /public/OrdinalCollection
384 self.AdminStoragePath = /storage/OrdinalAdmin
385
386 // Initialize contract fields
387 self.totalSupply = 0
388
389 // Create and save a new Collection in storage
390 let collection <- create Collection()
391 self.account.storage.save(<-collection, to: self.CollectionStoragePath)
392
393 // Issue a public capability for the Collection
394 let collectionCap = self.account.capabilities.storage.issue<&Ordinal.Collection>(self.CollectionStoragePath)
395 self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
396
397 // Create and store Admin resource
398 self.account.storage.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
399 }
400}