diff --git a/contracts/nft/BaseCollection.cdc b/contracts/nft/BaseNFT.cdc similarity index 55% rename from contracts/nft/BaseCollection.cdc rename to contracts/nft/BaseNFT.cdc index f6e632d..56f620c 100644 --- a/contracts/nft/BaseCollection.cdc +++ b/contracts/nft/BaseNFT.cdc @@ -5,7 +5,7 @@ import "ViewResolver" import "MetadataViews" import "BaseNFTVars" import "FlowtyDrops" - +import "NFTMetadata" // A few primary challenges that have come up in thinking about how to define base-level interfaces // for collections and NFTs: @@ -21,7 +21,86 @@ import "FlowtyDrops" // pieces to and modify the code to their liking. This could achieve the best of both worlds where there is minimal work to get something // off the ground, but doesn't close the door to customization in the future. This could come at the cost of duplicated resource definitions, // or could have the risk of circular imports depending on how we resolve certain pieces of information about a collection. -access(all) contract interface BaseCollection: ViewResolver { +access(all) contract interface BaseNFT: ViewResolver { + + + access(all) resource interface NFT: NonFungibleToken.NFT { + // This is the id entry that corresponds to an NFTs NFTMetadata.Container entry. + // Some NFTs might share the same data, so we want to permit reusing storage where possible + access(all) metadataID: UInt64 + + access(all) view fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + if view == Type() { + return self.id + } + + let rt = self.getType() + let segments = rt.identifier.split(separator: ".") + let addr = AddressUtils.parseAddress(rt)! + let tmp = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2]) + if tmp == nil { + return nil + } + + let c = tmp! + let tmpMd = c.MetadataCap.borrow() + if tmpMd == nil { + return nil + } + + let md = tmpMd! + switch view { + case Type(): + let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_") + return MetadataViews.NFTCollectionData( + storagePath: StoragePath(identifier: pathIdentifier)!, + publicPath: PublicPath(identifier: pathIdentifier)!, + publicCollection: Type<&{NonFungibleToken.Collection}>(), + publicLinkedType: Type<&{NonFungibleToken.Collection}>(), + createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} { + let addr = AddressUtils.parseAddress(rt)! + let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])! + return <- c.createEmptyCollection(nftType: rt) + } + ) + case Type(): + return md.collectionInfo.collectionDisplay + } + + if let entry = md.borrowMetadata(id: self.metadataID) { + switch view { + case Type(): + return entry.traits + case Type(): + return entry.editions + case Type(): + let num = (entry.editions?.infoList?.length ?? 0) > 0 ? entry.editions!.infoList[0].number : self.id + + return MetadataViews.Display( + name: entry.name.concat(" #").concat(num.toString()), + description: entry.description, + thumbnail: NFTMetadata.UriFile(entry.thumbnail.uri()) + ) + case Type(): + return entry.externalURL + } + } + + return nil + } + } // The base collection is an interface that attmepts to take more boilerplate // off of NFT-standard compliant definitions. @@ -93,7 +172,12 @@ access(all) contract interface BaseCollection: ViewResolver { ) case Type(): let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])! - return c.collectionDisplay + let md = c.MetadataCap.borrow() + if md == nil { + return nil + } + + return md!.collectionInfo.collectionDisplay case Type(): return FlowtyDrops.DropResolver(cap: acct.capabilities.get<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath)) } diff --git a/contracts/nft/BaseNFTMetadata.cdc b/contracts/nft/BaseNFTMetadata.cdc deleted file mode 100644 index cd32039..0000000 --- a/contracts/nft/BaseNFTMetadata.cdc +++ /dev/null @@ -1,12 +0,0 @@ -import "NonFungibleToken" -import "MetadataViews" - -access(all) contract BaseNFTMetadata { - access(all) struct NFTMetadata { - - } - - access(all) resource MetadataContainer { - access(all) let metadata: {UInt64: NFTMetadata} - } -} \ No newline at end of file diff --git a/contracts/nft/BaseNFTVars.cdc b/contracts/nft/BaseNFTVars.cdc index 357c66b..14bfe2f 100644 --- a/contracts/nft/BaseNFTVars.cdc +++ b/contracts/nft/BaseNFTVars.cdc @@ -1,9 +1,9 @@ import "MetadataViews" import "NonFungibleToken" +import "NFTMetadata" access(all) contract interface BaseNFTVars { - access(all) let collectionDisplay: MetadataViews.NFTCollectionDisplay - access(all) var totalMinted: UInt64 - + access(all) var MetadataCap: Capability<&NFTMetadata.Container> + access(all) var totalSupply: UInt64 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} } \ No newline at end of file diff --git a/contracts/nft/NFTMetadata.cdc b/contracts/nft/NFTMetadata.cdc new file mode 100644 index 0000000..7511440 --- /dev/null +++ b/contracts/nft/NFTMetadata.cdc @@ -0,0 +1,122 @@ +import "NonFungibleToken" +import "MetadataViews" + +access(all) contract NFTMetadata { + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + + access(all) entitlement Owner + + access(all) event MetadataFrozen(uuid: UInt64, owner: Address?) + + access(all) struct CollectionInfo { + access(all) var collectionDisplay: MetadataViews.NFTCollectionDisplay + + init(collectionDisplay: MetadataViews.NFTCollectionDisplay) { + self.collectionDisplay = collectionDisplay + } + } + + access(all) struct Metadata { + // these are used to create the display metadata view so that we can concatenate + // the id onto it. + access(all) let name: String + access(all) let description: String + access(all) let thumbnail: {MetadataViews.File} + + access(all) let traits: MetadataViews.Traits? + access(all) let editions: MetadataViews.Editions? + access(all) let externalURL: MetadataViews.ExternalURL? + + access(all) let data: {String: AnyStruct} // general-purpose data bucket + + init( + name: String, + description: String, + thumbnail: {MetadataViews.File}, + traits: MetadataViews.Traits?, + editions: MetadataViews.Editions?, + externalURL: MetadataViews.ExternalURL?, + data: {String: AnyStruct} + ) { + self.name = name + self.description = description + self.thumbnail = thumbnail + + self.traits = traits + self.editions = editions + self.externalURL = externalURL + + self.data = {} + } + } + + access(all) resource Container { + access(all) var collectionInfo: CollectionInfo + access(all) let metadata: {UInt64: Metadata} + access(all) var frozen: Bool + + access(all) fun borrowMetadata(id: UInt64): &Metadata? { + return &self.metadata[id] + } + + access(Owner) fun addMetadata(id: UInt64, data: Metadata) { + pre { + self.metadata[id] == nil: "id already has metadata assigned" + } + + self.metadata[id] = data + } + + access(Owner) fun freeze() { + self.frozen = true + emit MetadataFrozen(uuid: self.uuid, owner: self.owner?.address) + } + + init(collectionInfo: CollectionInfo) { + self.collectionInfo = collectionInfo + self.metadata = {} + self.frozen = false + } + } + + access(all) struct InitializeCaps { + access(all) let pubCap: Capability<&Container> + access(all) let ownerCap: Capability + + init(pubCap: Capability<&Container>, ownerCap: Capability) { + self.pubCap = pubCap + self.ownerCap = ownerCap + } + } + + access(all) fun createContainer(collectionInfo: CollectionInfo): @Container { + return <- create Container(collectionInfo: collectionInfo) + } + + access(all) fun initialize(acct: auth(SaveValue, IssueStorageCapabilityController, PublishCapability) &Account, collectionInfo: CollectionInfo): InitializeCaps { + let container <- self.createContainer(collectionInfo: collectionInfo) + acct.storage.save(<-container, to: self.StoragePath) + let pubCap = acct.capabilities.storage.issue<&Container>(self.StoragePath) + let ownerCap = acct.capabilities.storage.issue(self.StoragePath) + return InitializeCaps(pubCap: pubCap, ownerCap: ownerCap) + } + + access(all) struct UriFile: MetadataViews.File { + access(self) let url: String + + access(all) view fun uri(): String { + return self.url + } + + init(_ url: String) { + self.url = url + } + } + + init() { + let identifier = "NFTMetadata_".concat(self.account.address.toString()) + self.StoragePath = StoragePath(identifier: identifier)! + self.PublicPath = PublicPath(identifier: identifier)! + } +} \ No newline at end of file diff --git a/contracts/nft/OpenEditionNFT.cdc b/contracts/nft/OpenEditionNFT.cdc index c335da6..f90d066 100644 --- a/contracts/nft/OpenEditionNFT.cdc +++ b/contracts/nft/OpenEditionNFT.cdc @@ -19,55 +19,22 @@ import "FlowtySwitchers" import "FlowtyAddressVerifiers" import "FlowtyPricers" import "DropFactory" -import "BaseCollection" +import "BaseNFT" import "BaseNFTVars" +import "NFTMetadata" -access(all) contract OpenEditionNFT: NonFungibleToken, BaseCollection, BaseNFTVars { - access(all) let collectionDisplay: MetadataViews.NFTCollectionDisplay - access(all) var totalMinted: UInt64 +access(all) contract OpenEditionNFT: NonFungibleToken, BaseNFT, BaseNFTVars { + access(all) var MetadataCap: Capability<&NFTMetadata.Container> + access(all) var totalSupply: UInt64 - access(all) resource NFT: NonFungibleToken.NFT { + access(all) resource NFT: BaseNFT.NFT { access(all) let id: UInt64 - access(all) let display: MetadataViews.Display + access(all) let metadataID: UInt64 init() { - OpenEditionNFT.totalMinted = OpenEditionNFT.totalMinted + 1 - self.id = OpenEditionNFT.totalMinted - - self.display = MetadataViews.Display( - name: "Fluid #".concat(self.id.toString()), - description: "This is a sample open-edition NFT utilizing flowty drops for minting", - thumbnail: MetadataViews.IPFSFile(cid: "QmWWLhnkPR3ejavNtzeJcdG9fwcBHKwBVEP4pZ9rGbdHEM", path: nil) - ) - } - - access(all) view fun getViews(): [Type] { - return [ - Type(), - Type(), - Type(), - Type(), - Type() - ] - } - - access(all) fun resolveView(_ view: Type): AnyStruct? { - switch view { - case Type(): - return self.display - case Type(): - return MetadataViews.Serial( - self.id - ) - case Type(): - return MetadataViews.ExternalURL("https://flowty.io/asset/".concat(OpenEditionNFT.account.address.toString()).concat("/OpenEditionNFT/").concat(self.id.toString())) - case Type(): - return OpenEditionNFT.resolveContractView(resourceType: self.getType(), viewType: view) - case Type(): - return OpenEditionNFT.resolveContractView(resourceType: self.getType(), viewType: view) - } - - return nil + OpenEditionNFT.totalSupply = OpenEditionNFT.totalSupply + 1 + self.id = OpenEditionNFT.totalSupply + self.metadataID = 0 } access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { @@ -75,25 +42,6 @@ access(all) contract OpenEditionNFT: NonFungibleToken, BaseCollection, BaseNFTVa } } - // DONE - access(all) resource Collection: BaseCollection.Collection { - access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} - access(all) var nftType: Type - - access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { - return <- create Collection() - } - - init () { - self.ownedNFTs <- {} - self.nftType = Type<@NFT>() - } - } - - access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { - return <- create Collection() - } - access(all) resource NFTMinter: FlowtyDrops.Minter { access(all) fun mint(payment: @{FungibleToken.Vault}, amount: Int, phase: &FlowtyDrops.Phase, data: {String: AnyStruct}): @[{NonFungibleToken.NFT}] { switch(payment.getType()) { @@ -116,7 +64,27 @@ access(all) contract OpenEditionNFT: NonFungibleToken, BaseCollection, BaseNFTVa } } + access(all) resource Collection: BaseNFT.Collection { + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} + access(all) var nftType: Type + + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <- create Collection() + } + + init () { + self.ownedNFTs <- {} + self.nftType = Type<@NFT>() + } + } + + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + return <- create Collection() + } + init() { + self.totalSupply = 0 + let square = MetadataViews.Media( file: MetadataViews.IPFSFile( cid: "QmWWLhnkPR3ejavNtzeJcdG9fwcBHKwBVEP4pZ9rGbdHEM", @@ -133,7 +101,7 @@ access(all) contract OpenEditionNFT: NonFungibleToken, BaseCollection, BaseNFTVa mediaType: "image/png" ) - self.collectionDisplay = MetadataViews.NFTCollectionDisplay( + let collectionDisplay = MetadataViews.NFTCollectionDisplay( name: "The Open Edition Collection", description: "This collection is used as an example to help you develop your next Open Edition Flow NFT", externalURL: MetadataViews.ExternalURL("https://flowty.io"), @@ -143,19 +111,23 @@ access(all) contract OpenEditionNFT: NonFungibleToken, BaseCollection, BaseNFTVa "twitter": MetadataViews.ExternalURL("https://twitter.com/flowty_io") } ) - - self.totalMinted = 0 - let cd: MetadataViews.NFTCollectionData = self.resolveContractView(resourceType: Type<@NFT>(), viewType: Type())! as! MetadataViews.NFTCollectionData - - // Create a Collection resource and save it to storage - let collection <- create Collection() - self.account.storage.save(<-collection, to: cd.storagePath) - - // create a public capability for the collection - self.account.capabilities.publish( - self.account.capabilities.storage.issue<&{NonFungibleToken.Collection}>(cd.storagePath), - at: cd.publicPath - ) + let collectionInfo = NFTMetadata.CollectionInfo(collectionDisplay: collectionDisplay) + + let acct: auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account = Account(payer: self.account) + let cap = acct.capabilities.account.issue() + self.account.storage.save(cap, to: /storage/metadataAuthAccount) + + let caps = NFTMetadata.initialize(acct: acct.capabilities.account.issue().borrow()!, collectionInfo: collectionInfo) + self.MetadataCap = caps.pubCap + caps.ownerCap.borrow()!.addMetadata(id: 0, data: NFTMetadata.Metadata( + name: "Fluid", + description: "This is a sample open-edition NFT utilizing flowty drops for minting", + thumbnail: MetadataViews.IPFSFile(cid: "QmWWLhnkPR3ejavNtzeJcdG9fwcBHKwBVEP4pZ9rGbdHEM", path: nil), + traits: nil, + editions: nil, + externalURL: nil, + data: {} + )) // Create a Minter resource and save it to storage let minter <- create NFTMinter() diff --git a/flow.json b/flow.json index 78846d3..072abf9 100644 --- a/flow.json +++ b/flow.json @@ -59,8 +59,8 @@ } }, "contracts": { - "BaseCollection": { - "source": "./contracts/nft/BaseCollection.cdc", + "BaseNFT": { + "source": "./contracts/nft/BaseNFT.cdc", "aliases": { "testing": "0x0000000000000006" } @@ -71,6 +71,12 @@ "testing": "0x0000000000000006" } }, + "NFTMetadata": { + "source": "./contracts/nft/NFTMetadata.cdc", + "aliases": { + "testing": "0x0000000000000006" + } + }, "FlowtyDrops": { "source": "./contracts/FlowtyDrops.cdc", "aliases": { @@ -206,8 +212,9 @@ "deployments": { "emulator": { "emulator-account": [ + "NFTMetadata", "BaseNFTVars", - "BaseCollection", + "BaseNFT", "FlowtyDrops", "FlowtySwitchers", "FlowtyAddressVerifiers", diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index 4455f38..6403ba2 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -93,8 +93,9 @@ access(all) fun deployAll() { deploy("FlowtyDrops", "../contracts/FlowtyDrops.cdc", []) + deploy("NFTMetadata", "../contracts/nft/NFTMetadata.cdc", []) deploy("BaseNFTVars", "../contracts/nft/BaseNFTVars.cdc", []) - deploy("BaseCollection", "../contracts/nft/BaseCollection.cdc", []) + deploy("BaseNFT", "../contracts/nft/BaseNFT.cdc", []) deploy("FlowtySwitchers", "../contracts/FlowtySwitchers.cdc", []) deploy("FlowtyPricers", "../contracts/FlowtyPricers.cdc", []) deploy("FlowtyAddressVerifiers", "../contracts/FlowtyAddressVerifiers.cdc", [])