Smart Contract
SomePlaceCollectible
A.667a16294a089ef8.SomePlaceCollectible
1/*
2 Description: Central Smart Contract for Some Place NFT Collectibles
3
4 Some Place Collectibles are available as part of "sets", each with
5 a fixed edition count.
6*/
7
8import NonFungibleToken from 0x1d7e57aa55817448
9import SomePlaceCounter from 0x667a16294a089ef8
10import MetadataViews from 0x1d7e57aa55817448
11import FungibleToken from 0xf233dcee88fe0abe
12
13pub contract SomePlaceCollectible : NonFungibleToken {
14
15 // -----------------------------------------------------------------------
16 // NonFungibleToken Standard Events
17 // -----------------------------------------------------------------------
18 pub event ContractInitialized()
19 pub event Withdraw(id: UInt64, from: Address?)
20 pub event Deposit(id: UInt64, to: Address?)
21
22 // -----------------------------------------------------------------------
23 // SomePlace Events
24 // -----------------------------------------------------------------------
25 pub event Mint(id: UInt64)
26 pub event Burn(id: UInt64)
27 pub event SetCreated(setID: UInt64)
28
29 // -----------------------------------------------------------------------
30 // Named Paths
31 // -----------------------------------------------------------------------
32 pub let CollectionStoragePath: StoragePath
33 pub let CollectionPublicPath: PublicPath
34 pub let CollectionPrivatePath: PrivatePath
35
36 // -----------------------------------------------------------------------
37 // NonFungibleToken Standard Fields
38 // -----------------------------------------------------------------------
39 pub var totalSupply: UInt64
40
41 // -----------------------------------------------------------------------
42 // SomePlace Fields
43 // -----------------------------------------------------------------------
44
45 // Maintains state of what sets and editions have been minted to ensure
46 // there are never 2 of the same set + edition combination
47 // Provides a Set ID -> Edition ID -> Data mapping
48 access(contract) let collectibleData: {UInt64: {UInt64: CollectibleMetadata}}
49
50 // Allows easy access to information for a set
51 // Provides access from a set's setID to the information for that set
52 access(contract) let setData: {UInt64: SomePlaceCollectible.SetMetadata}
53
54 // Allows easy access to pointers from an NFT to its metadata keys
55 // Provides CollectibleID -> (SetID + EditionID) mapping
56 access(contract) let allCollectibleIDs: {UInt64: CollectibleEditionData}
57
58 // -----------------------------------------------------------------------
59 // SomePlace Structs
60 // -----------------------------------------------------------------------
61 pub struct CollectibleMetadata {
62 // The NFT Id is optional so a collectible may have associated metadata prior to being minted
63 // This is useful for porting existing unique collections over to Flow (I.E. ETH NFTs)
64 access(self) var nftID: UInt64?
65 access(self) var metadata: {String: String}
66 access(self) var traits: {String : String}
67
68 init() {
69 self.metadata = {}
70 self.traits = {}
71 self.nftID = nil
72 }
73
74 pub fun getNftID(): UInt64? {
75 return self.nftID
76 }
77 // Returns all metadata for this collectible
78 pub fun getMetadata(): { String: String } {
79 return self.metadata
80 }
81
82 pub fun getTraits(): { String: String } {
83 return self.traits
84 }
85
86 access(account) fun updateNftID(_ nftID: UInt64) {
87 pre {
88 self.nftID == nil : "An NFT already exists for this collectible."
89 }
90 self.nftID = nftID
91 }
92
93 access(account) fun updateMetadata(_ metadata: { String: String }) {
94 self.metadata = metadata
95 }
96
97 access(account) fun updateTraits(_ traits: { String: String }) {
98 self.traits = traits
99 }
100 }
101
102 pub struct CollectibleEditionData {
103 access(self) let editionNumber: UInt64
104 access(self) let setID: UInt64
105
106 init(editionNumber: UInt64, setID: UInt64) {
107 self.editionNumber = editionNumber
108 self.setID = setID
109 }
110 pub fun getEditionNumber(): UInt64 {
111 return self.editionNumber
112 }
113 pub fun getSetID(): UInt64 {
114 return self.setID
115 }
116 }
117
118 pub struct SetMetadata {
119 access(self) let setID: UInt64
120 access(self) var metadata: {String : String}
121 access(self) var maxNumberOfEditions: UInt64
122 access(self) var editionCount: UInt64
123 access(self) var sequentialMintMin: UInt64
124 access(self) var publicFUSDSalePrice: UFix64?
125 access(self) var publicFLOWSalePrice: UFix64?
126 access(self) var publicSaleStartTime: UFix64?
127 access(self) var publicSaleEndTime: UFix64?
128
129 init(
130 setID: UInt64,
131 maxNumberOfEditions: UInt64,
132 metadata: {String: String}
133 ){
134 self.setID = setID
135 self.metadata = metadata
136 self.maxNumberOfEditions = maxNumberOfEditions
137 self.editionCount = 0
138 self.sequentialMintMin = 1
139
140 self.publicFUSDSalePrice = nil
141 self.publicFLOWSalePrice = nil
142 self.publicSaleStartTime = nil
143 self.publicSaleEndTime = nil
144 }
145
146 /*
147 Readonly functions
148 */
149 pub fun getSetID(): UInt64 {
150 return self.setID
151 }
152
153 pub fun getMetadata(): {String : String} {
154 return self.metadata
155 }
156
157 pub fun getMaxNumberOfEditions(): UInt64 {
158 return self.maxNumberOfEditions
159 }
160
161 pub fun getEditionCount(): UInt64 {
162 return self.editionCount
163 }
164
165 pub fun getSequentialMintMin(): UInt64 {
166 return self.sequentialMintMin
167 }
168
169 pub fun getFUSDPublicSalePrice(): UFix64? {
170 return self.publicFUSDSalePrice
171 }
172
173 pub fun getFLOWPublicSalePrice(): UFix64? {
174 return self.publicFLOWSalePrice
175 }
176
177 pub fun getPublicSaleStartTime(): UFix64? {
178 return self.publicSaleStartTime
179 }
180
181 pub fun getPublicSaleEndTime(): UFix64? {
182 return self.publicSaleEndTime
183 }
184
185 pub fun getEditionMetadata(editionNumber: UInt64): { String: String } {
186 pre {
187 editionNumber >= 1 && editionNumber <= self.maxNumberOfEditions : "Invalid edition number provided"
188 SomePlaceCollectible.collectibleData[self.setID]![editionNumber] != nil : "Requested edition has not yet been minted"
189 }
190 return SomePlaceCollectible.collectibleData[self.setID]![editionNumber]!.getMetadata()
191 }
192
193 // A public sale allowing for direct minting from the contract is considered active if we have a valid public
194 // sale price listing, current time is after start time, and current time is before end time
195 pub fun isPublicSaleActive(): Bool {
196 let curBlockTime = getCurrentBlock().timestamp
197 return (self.publicFUSDSalePrice != nil || self.publicFLOWSalePrice != nil) &&
198 (self.publicSaleStartTime != nil && curBlockTime >= self.publicSaleStartTime!) &&
199 (self.publicSaleEndTime == nil || curBlockTime < self.publicSaleEndTime!)
200 }
201
202 /*
203 Mutating functions
204 */
205 access(contract) fun incrementEditionCount(): UInt64 {
206 post {
207 self.editionCount <= self.maxNumberOfEditions : "Number of editions is larger than max allowed editions"
208 }
209 self.editionCount = self.editionCount + 1
210 return self.editionCount
211 }
212
213 access(contract) fun setSequentialMintMin(newMintMin: UInt64) {
214 self.sequentialMintMin = newMintMin
215 }
216
217 access(contract) fun updateSetMetadata(_ newMetadata: {String: String}) {
218 self.metadata = newMetadata
219 }
220
221 access(contract) fun updateFLOWPublicSalePrice(_ publicFLOWSalePrice: UFix64?) {
222 self.publicFLOWSalePrice = publicFLOWSalePrice
223 }
224
225 access(contract) fun updateFUSDPublicSalePrice(_ publicFUSDSalePrice: UFix64?) {
226 self.publicFUSDSalePrice = publicFUSDSalePrice
227 }
228
229 access(contract) fun updatePublicSaleStartTime(_ startTime: UFix64?) {
230 self.publicSaleStartTime = startTime
231 }
232
233 access(contract) fun updatePublicSaleEndTime(_ endTime: UFix64?) {
234 self.publicSaleEndTime = endTime
235 }
236 }
237
238 // -----------------------------------------------------------------------
239 // SomePlace Interfaces
240 // -----------------------------------------------------------------------
241 pub resource interface CollectibleCollectionPublic {
242 pub fun deposit(token: @NonFungibleToken.NFT)
243 pub fun batchDeposit(collectibleCollection: @NonFungibleToken.Collection)
244 pub fun getIDs(): [UInt64]
245 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
246 pub fun borrowCollectible(id: UInt64): &SomePlaceCollectible.NFT? {
247 // If the result isn't nil, the id of the returned reference
248 // should be the same as the argument to the function
249 post {
250 (result == nil) || (result?.id == id):
251 "Cannot borrow SomePlace reference: The ID of the returned reference is incorrect"
252 }
253 }
254 }
255
256 // -----------------------------------------------------------------------
257 // NonFungibleToken Standard Resources
258 // -----------------------------------------------------------------------
259 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
260 pub let id: UInt64
261
262 pub let setID: UInt64
263
264 pub let editionNumber: UInt64
265
266 pub fun getViews(): [Type] {
267 return [
268 Type<MetadataViews.Display>(),
269 Type<MetadataViews.Royalties>(),
270 Type<MetadataViews.ExternalURL>(),
271 Type<MetadataViews.NFTCollectionData>(),
272 Type<MetadataViews.NFTCollectionDisplay>(),
273 Type<MetadataViews.Traits>()
274 ]
275 }
276
277 pub fun resolveView(_ view: Type): AnyStruct? {
278 let metadata: {String: String} = SomePlaceCollectible.getMetadataByEditionID(setID: self.setID, editionNumber: self.editionNumber)!.getMetadata()
279 let traits: {String: String} = SomePlaceCollectible.getMetadataByEditionID(setID: self.setID, editionNumber: self.editionNumber)!.getTraits()
280 switch view {
281 case Type<MetadataViews.Display>():
282 return MetadataViews.Display(
283 name: (metadata["NFTName"] as? String) ?? "The Potion",
284 description: "Time to drink up.",
285 thumbnail: MetadataViews.HTTPFile(
286 url: (metadata["IPFS_Image"] as? String) ?? "https://gateway.pinata.cloud/ipfs/QmWEDqCnHPgRtPvLPFq5jDnRc4mXHGchomYBmzjXdJJpQq"
287 )
288 )
289 case Type<MetadataViews.Royalties>():
290 return MetadataViews.Royalties(
291 [
292 MetadataViews.Royalty(
293 receiver: getAccount(0x8e2e0ebf3c03aa88).getCapability<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath()),
294 cut: 0.10,
295 description: "This is a royalty cut for some.place."
296 )
297 ]
298 )
299 case Type<MetadataViews.ExternalURL>():
300 return MetadataViews.ExternalURL("https://some.place/")
301 case Type<MetadataViews.NFTCollectionData>():
302 return MetadataViews.NFTCollectionData(
303 storagePath: SomePlaceCollectible.CollectionStoragePath,
304 publicPath: SomePlaceCollectible.CollectionPublicPath,
305 providerPath: SomePlaceCollectible.CollectionPrivatePath,
306 publicCollection: Type<&Collection{CollectibleCollectionPublic}>(),
307 publicLinkedType: Type<&Collection{CollectibleCollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(),
308 providerLinkedType: Type<&Collection{CollectibleCollectionPublic, NonFungibleToken.CollectionPublic, NonFungibleToken.Provider, MetadataViews.ResolverCollection}>(),
309 createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection {
310 return <- SomePlaceCollectible.createEmptyCollection()
311 })
312 )
313 case Type<MetadataViews.NFTCollectionDisplay>():
314 let squareImage = MetadataViews.Media(
315 file: MetadataViews.HTTPFile(
316 url: "https://pbs.twimg.com/profile_images/1502703455747534850/O7LzbfKw_400x400.jpg"
317 ),
318 mediaType: "image"
319 )
320 let bannerImage = MetadataViews.Media(
321 file: MetadataViews.HTTPFile(
322 url: "https://pbs.twimg.com/profile_banners/1329160722786336768/1647107895/1500x500"
323 ),
324 mediaType: "image"
325 )
326 return MetadataViews.NFTCollectionDisplay(
327 name: "The Potion",
328 description: "Time to drink up.",
329 externalURL: MetadataViews.ExternalURL("https://some.place/"),
330 squareImage: squareImage,
331 bannerImage: bannerImage,
332 socials: {
333 "twitter": MetadataViews.ExternalURL("https://twitter.com/some_place")
334 }
335 )
336 case Type<MetadataViews.Traits>():
337 let traitsView = MetadataViews.dictToTraits(dict: traits, excludedNames: nil)
338 return traitsView
339
340 }
341 return nil
342 }
343
344 init(setID: UInt64, editionNumber: UInt64) {
345 pre {
346 SomePlaceCollectible.setData.containsKey(setID) : "Invalid Set ID"
347 SomePlaceCollectible.collectibleData[setID]![editionNumber] == nil || SomePlaceCollectible.collectibleData[setID]![editionNumber]!.getNftID() == nil : "This edition already exists"
348 editionNumber > 0 && editionNumber <= SomePlaceCollectible.setData[setID]!.getMaxNumberOfEditions() : "Edition number is too high"
349 }
350 // Update unique set
351 self.id = self.uuid
352 self.setID = setID
353 self.editionNumber = editionNumber
354
355 // If this edition number does not have a metadata object, create one
356 if (SomePlaceCollectible.collectibleData[setID]![editionNumber] == nil) {
357 let ref = SomePlaceCollectible.collectibleData[setID]!
358 ref[editionNumber] = CollectibleMetadata()
359 SomePlaceCollectible.collectibleData[setID] = ref
360 }
361 // Update the metadata object to have a reference to this newly created NFT
362 SomePlaceCollectible.collectibleData[setID]![editionNumber]!.updateNftID(self.id)
363
364 // Create mapping of new nft id to its newly created set and edition data
365 SomePlaceCollectible.allCollectibleIDs[self.uuid] = CollectibleEditionData(editionNumber: editionNumber, setID: setID)
366
367 // Increase total supply of entire someplace collection
368 SomePlaceCollectible.totalSupply = SomePlaceCollectible.totalSupply + (1 as UInt64)
369
370 emit Mint(id: self.uuid)
371 }
372
373 destroy() {
374 emit Burn(id: self.uuid)
375 }
376 }
377
378 pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, CollectibleCollectionPublic, MetadataViews.ResolverCollection {
379 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
380
381 init() {
382 self.ownedNFTs <- {}
383 }
384
385 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
386 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Cannot withdraw: NFT does not exist in the collection")
387
388 emit Withdraw(id: token.uuid, from: self.owner?.address)
389
390 return <-token
391 }
392
393 pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection {
394 var batchCollection <- create Collection()
395
396 for id in ids {
397 batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
398 }
399
400 return <-batchCollection
401 }
402
403 pub fun deposit(token: @NonFungibleToken.NFT) {
404 let token <- token as! @SomePlaceCollectible.NFT
405
406 let id: UInt64 = token.id
407
408 let oldToken <- self.ownedNFTs[id] <- token
409
410 emit Deposit(id: id, to: self.owner?.address)
411
412 destroy oldToken
413 }
414
415 pub fun batchDeposit(collectibleCollection: @NonFungibleToken.Collection) {
416 let keys = collectibleCollection.getIDs()
417
418 for key in keys {
419 self.deposit(token: <-collectibleCollection.withdraw(withdrawID: key))
420 }
421
422 destroy collectibleCollection
423 }
424
425 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
426 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
427 }
428
429 pub fun borrowCollectible(id: UInt64) : &SomePlaceCollectible.NFT? {
430 if self.ownedNFTs[id] == nil {
431 return nil
432 } else {
433 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
434 return ref as! &SomePlaceCollectible.NFT
435 }
436 }
437
438 pub fun getIDs(): [UInt64] {
439 return self.ownedNFTs.keys
440 }
441
442 pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
443 let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
444 let potion = nft as! &NFT
445 return potion as &AnyResource{MetadataViews.Resolver}
446 }
447
448 destroy() {
449 destroy self.ownedNFTs
450 }
451 }
452
453 // -----------------------------------------------------------------------
454 // SomePlace Admin Functionality
455 // -----------------------------------------------------------------------
456 /*
457 Creation of a new NFT set under the SomePlace umbrella of collectibles
458 */
459 access(account) fun addNFTSet(
460 maxNumberOfEditions: UInt64,
461 metadata: {String:String},
462 ): UInt64 {
463 let id = SomePlaceCounter.nextSetID
464
465 let newSet = SomePlaceCollectible.SetMetadata(setID: id, maxNumberOfEditions: maxNumberOfEditions, metadata: metadata)
466
467 self.collectibleData[id] = {}
468 self.setData[id] = newSet
469
470 SomePlaceCounter.incrementSetCounter()
471
472 emit SetCreated(setID: id)
473
474 return id
475 }
476
477 /*
478 Update existing set and edition data
479 */
480 access(account) fun updateEditionMetadata(setID: UInt64, editionNumber: UInt64, metadata: {String: String}) {
481 if (SomePlaceCollectible.collectibleData[setID]![editionNumber] == nil) {
482 let ref = SomePlaceCollectible.collectibleData[setID]!
483 ref[editionNumber] = CollectibleMetadata()
484 SomePlaceCollectible.collectibleData[setID] = ref
485 }
486 SomePlaceCollectible.collectibleData[setID]![editionNumber]!.updateMetadata(metadata)
487 }
488
489 access(account) fun updateEditionTraits(setID: UInt64, editionNumber: UInt64, traits: {String: String}) {
490 if (SomePlaceCollectible.collectibleData[setID]![editionNumber] == nil) {
491 let ref = SomePlaceCollectible.collectibleData[setID]!
492 ref[editionNumber] = CollectibleMetadata()
493 SomePlaceCollectible.collectibleData[setID] = ref
494 }
495 SomePlaceCollectible.collectibleData[setID]![editionNumber]!.updateTraits(traits)
496 }
497
498 access(account) fun updateSetMetadata(setID: UInt64, metadata: {String: String}) {
499 SomePlaceCollectible.setData[setID]!.updateSetMetadata(metadata)
500 }
501
502 access(account) fun updateFLOWPublicSalePrice(setID: UInt64, price: UFix64?) {
503 SomePlaceCollectible.setData[setID]!.updateFLOWPublicSalePrice(price)
504 }
505
506 access(account) fun updateFUSDPublicSalePrice(setID: UInt64, price: UFix64?) {
507 SomePlaceCollectible.setData[setID]!.updateFUSDPublicSalePrice(price)
508 }
509
510 access(account) fun updatePublicSaleStartTime(setID: UInt64, startTime: UFix64?) {
511 SomePlaceCollectible.setData[setID]!.updatePublicSaleStartTime(startTime)
512 }
513
514 access(account) fun updatePublicSaleEndTime(setID: UInt64, endTime: UFix64?) {
515 SomePlaceCollectible.setData[setID]!.updatePublicSaleEndTime(endTime)
516 }
517
518 /*
519 Minting functions to create editions within a set
520 */
521 // This mint is intended for sequential mints (for a normal in-order drop style)
522 access(account) fun mintSequentialEditionNFT(setID: UInt64): @SomePlaceCollectible.NFT {
523 // Find first valid edition number
524 var curEditionNumber = self.setData[setID]!.getSequentialMintMin()
525 while (SomePlaceCollectible.collectibleData[setID]![curEditionNumber] != nil &&
526 SomePlaceCollectible.collectibleData[setID]![curEditionNumber]!.getNftID() != nil) {
527 curEditionNumber = curEditionNumber + 1
528 }
529 self.setData[setID]!.setSequentialMintMin(newMintMin: curEditionNumber)
530 let editionCount = self.setData[setID]!.incrementEditionCount()
531
532 let newCollectible <-create SomePlaceCollectible.NFT(setID: setID, editionNumber: curEditionNumber)
533 return <- newCollectible
534 }
535
536 // This mint is intended for settling auctions or manually minting editions,
537 // where we mint specific editions to specific recipients when settling
538 // SetID + editionID to mint is normally decided off-chain
539 access(account) fun mintNFT(setID: UInt64, editionNumber: UInt64): @SomePlaceCollectible.NFT {
540 self.setData[setID]!.incrementEditionCount()
541 return <-create SomePlaceCollectible.NFT(setID: setID, editionNumber: editionNumber)
542 }
543
544 // -----------------------------------------------------------------------
545 // NonFungibleToken Standard Functions
546 // -----------------------------------------------------------------------
547 pub fun createEmptyCollection(): @NonFungibleToken.Collection {
548 return <- create Collection()
549 }
550
551 // -----------------------------------------------------------------------
552 // SomePlace Functions
553 // -----------------------------------------------------------------------
554
555 // Retrieves all sets (This can be expensive)
556 pub fun getMetadatas(): {UInt64: SomePlaceCollectible.SetMetadata} {
557 return self.setData
558 }
559
560 // Retrieves all collectibles (This can be expensive)
561 pub fun getCollectibleMetadatas(): {UInt64: {UInt64: CollectibleMetadata}} {
562 return self.collectibleData
563 }
564
565 // Retrieves how many NFT sets exist
566 pub fun getMetadatasCount(): UInt64 {
567 return UInt64(self.setData.length)
568 }
569
570 pub fun getMetadataForSetID(setID: UInt64): SomePlaceCollectible.SetMetadata? {
571 return self.setData[setID]
572 }
573
574 pub fun getSetMetadataForNFT(nft: &SomePlaceCollectible.NFT): SomePlaceCollectible.SetMetadata? {
575 return self.setData[nft.setID]
576 }
577
578 pub fun getSetMetadataForNFTByUUID(uuid: UInt64): SomePlaceCollectible.SetMetadata? {
579 let collectibleEditionData = self.allCollectibleIDs[uuid]!
580 return self.setData[collectibleEditionData.getSetID()]!
581 }
582
583 pub fun getMetadataForNFTByUUID(uuid: UInt64): SomePlaceCollectible.CollectibleMetadata? {
584 let collectibleEditionData = self.allCollectibleIDs[uuid]!
585 return self.collectibleData[collectibleEditionData.getSetID()]![collectibleEditionData.getEditionNumber()]
586 }
587
588 pub fun getMetadataByEditionID(setID: UInt64, editionNumber: UInt64): SomePlaceCollectible.CollectibleMetadata? {
589 return self.collectibleData[setID]![editionNumber]
590 }
591
592 pub fun getCollectibleDataForNftByUUID(uuid: UInt64): SomePlaceCollectible.CollectibleEditionData? {
593 return self.allCollectibleIDs[uuid]!
594 }
595
596 init() {
597 self.CollectionStoragePath = /storage/somePlaceCollectibleCollection
598 self.CollectionPublicPath = /public/somePlaceCollectibleCollection
599 self.CollectionPrivatePath = /private/somePlaceCollectibleCollection
600
601 self.totalSupply = 0
602 self.collectibleData = {}
603 self.setData = {}
604 self.allCollectibleIDs = {}
605
606 emit ContractInitialized()
607 }
608
609}
610