diff --git a/contracts/AllDay.cdc b/contracts/AllDay.cdc index 26e6fde..789e5ea 100644 --- a/contracts/AllDay.cdc +++ b/contracts/AllDay.cdc @@ -4,8 +4,8 @@ Author: Sadie Freeman sadie.freeman@dapperlabs.com */ - import NonFungibleToken from "./NonFungibleToken.cdc" +import MetadataViews from "./MetadataViews.cdc" /* AllDay is structured similarly to Genies and TopShot. @@ -285,6 +285,13 @@ pub contract AllDay: NonFungibleToken { return AllDay.SetData(id: id) } + // Get the publicly available data for a Set by the provided Edition ID + // + pub fun getSetDataByEditionID(id: UInt64): AllDay.SetData { + let setID = AllDay.getEditionData(id: id).setID + return AllDay.getSetData(id: setID) + } + // Get the publicly available data for a Set by name // pub fun getSetDataByName(name: String): AllDay.SetData { @@ -359,6 +366,13 @@ pub contract AllDay: NonFungibleToken { return AllDay.PlayData(id: id) } + + // Get the publicly available data for a Play by the provided Edition ID + // + pub fun getPlayDataByEditionID(id: UInt64): AllDay.PlayData { + let playID = AllDay.getEditionData(id: id).playID + return AllDay.getPlayData(id: playID) + } //------------------------------------------------------------ // Edition @@ -506,7 +520,7 @@ pub contract AllDay: NonFungibleToken { // A Moment NFT // - pub resource NFT: NonFungibleToken.INFT { + pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver { pub let id: UInt64 pub let editionID: UInt64 pub let serialNumber: UInt64 @@ -537,8 +551,178 @@ pub contract AllDay: NonFungibleToken { emit MomentNFTMinted(id: self.id, editionID: self.editionID, serialNumber: self.serialNumber) } + + //------------------------------------------------------------ + // MetadataViews + //------------------------------------------------------------ + + pub fun name(): String { + let playMetadata = AllDay.getPlayDataByEditionID(id: self.id).metadata + let firstName: String = playMetadata["playerFirstName"] ?? "" + let lastName: String = playMetadata["playerLastName"] ?? "" + let playType: String = playMetadata["playType"] ?? "" + return firstName + .concat(" ") + .concat(lastName) + .concat(" ") + .concat(playType) + } + + pub fun description(): String { + let setName: String = AllDay.getSetDataByEditionID(id: self.id).name + let serialNumber: String = self.serialNumber.toString() + let seriesNumber: String = AllDay.getEditionData(id: self.id).seriesID.toString() + return "A series " + .concat(seriesNumber) + .concat(" ") + .concat(setName) + .concat(" moment with serial number ") + .concat(serialNumber) + } + + pub fun assetPath(): String { + // TODO: Ensure this route is available before deploying contract update + return "https://assets.nflallday.com/flow-asset/" + } + + // returns a url to display an medium sized image + pub fun mediumimage(): String { + let playID = AllDay.getEditionData(id: self.id).playID + return self.assetPath().concat(playID.toString()).concat("_512_512.jpg") + } + + // returns a url to display a thumbnail associated with the moment + pub fun thumbnail(): String { + let playID = AllDay.getEditionData(id: self.id).playID + return self.assetPath().concat(playID.toString()).concat("_256_256.jpg") + } + + // getMomentURL + // Returns: The computed external url of the moment + pub fun getMomentURL(): String { + // TODO: Ensure this route is available before deploying contract update + return "https://nflallday.com/moment/".concat(self.id.toString()) + } + + // getEditionName Moment's edition name is a combination of the Moment's setName and playID + // `setName: #playID` + pub fun getEditionName() : String { + let setName: String = AllDay.getSetDataByEditionID(id: self.id).name + let playID = AllDay.getEditionData(id: self.id).playID + let editionName = setName.concat(": #").concat(playID.toString()) + return editionName + } + + // All support views + // + pub fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + pub fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return MetadataViews.Display( + name: self.name(), + description: self.description(), + thumbnail: MetadataViews.HTTPFile( + url: self.thumbnail() + ) + ) + case Type(): + let name = self.getEditionName() + let max = AllDay.getSetDataByEditionID(id: self.id).setPlaysInEditions.length + let editionInfo = MetadataViews.Edition(name: name, number: UInt64(self.serialNumber), max: max > 0 ? UInt64(max) : nil) + let editionList: [MetadataViews.Edition] = [editionInfo] + return MetadataViews.Editions( + editionList + ) + case Type(): + return MetadataViews.Serial( + UInt64(self.serialNumber) + ) + case Type(): + return MetadataViews.Royalties( + [ + // Reserved for 3rd party marketplace royalty + ] + ) + case Type(): + return MetadataViews.ExternalURL(self.getMomentURL()) + case Type(): + return MetadataViews.NFTCollectionData( + storagePath: /storage/AllDayNFTCollection, + publicPath: /public/AllDayNFTCollection, + providerPath: /private/AllDayNFTCollection, + publicCollection: Type<&AllDay.Collection{AllDay.MomentNFTCollectionPublic}>(), + publicLinkedType: Type<&AllDay.Collection{AllDay.MomentNFTCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(), + providerLinkedType: Type<&AllDay.Collection{NonFungibleToken.Provider,AllDay.MomentNFTCollectionPublic,NonFungibleToken.Receiver,NonFungibleToken.CollectionPublic,MetadataViews.ResolverCollection}>(), + createEmptyCollectionFunction: (fun (): @NonFungibleToken.Collection { + return <-AllDay.createEmptyCollection() + }) + ) + case Type(): + let bannerImage = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://dka575ofm4ao0.cloudfront.net/pages-transactional_logos/retina/273214/NFL_ALLDAY_Horizontal_white_%281%29.png" + ), + mediaType: "image/png" + ) + let squareImage = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.nflallday.com/static/images/drops/blank-pack-black.png" + ), + mediaType: "image/png" + ) + return MetadataViews.NFTCollectionDisplay( + name: "NFL-All-Day", + description: "NFL All Day is your chance to own, sell, and trade official digital collectibles of the NFL's greatest plays and players", + externalURL: MetadataViews.ExternalURL("https://nflallday.com"), + squareImage: squareImage, + bannerImage: bannerImage, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/NFLAllDay"), + "discord": MetadataViews.ExternalURL("https://discord.com/invite/5K6qyTzj2k"), + "instagram": MetadataViews.ExternalURL("https://www.instagram.com/NFLALLDAY") + } + ) + case Type(): + let playMetadata = AllDay.getPlayDataByEditionID(id: self.id).metadata + let traitDictionary: {String: AnyStruct} = { + "Date Of Moment": playMetadata["gameDate"], + "Play Type": playMetadata["playType"], + "Series Number": AllDay.getEditionData(id: self.id).seriesID.toString(), + "Set Name": AllDay.getSetDataByEditionID(id: self.id).name, + "Serial Number": self.serialNumber.toString() + } + return MetadataViews.dictToTraits(dict: traitDictionary, excludedNames: []) + case Type(): + return MetadataViews.Medias( + [ + MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: self.mediumimage() + ), + mediaType: "image/jpeg" + ) + ] + ) + } + return nil + } } + //------------------------------------------------------------ // Collection //------------------------------------------------------------ @@ -566,7 +750,8 @@ pub contract AllDay: NonFungibleToken { NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, - MomentNFTCollectionPublic + MomentNFTCollectionPublic, + MetadataViews.ResolverCollection { // dictionary of NFT conforming tokens // NFT is a resource type with an UInt64 ID field @@ -643,6 +828,12 @@ pub contract AllDay: NonFungibleToken { } } + pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} { + let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)! + let allDayNFT = nft as! &AllDay.NFT + return allDayNFT as &AnyResource{MetadataViews.Resolver} + } + // Collection destructor // destroy() { diff --git a/contracts/FungibleToken.cdc b/contracts/FungibleToken.cdc new file mode 100644 index 0000000..5028cb6 --- /dev/null +++ b/contracts/FungibleToken.cdc @@ -0,0 +1,205 @@ +/** + +# The Flow Fungible Token standard + +## `FungibleToken` contract interface + +The interface that all fungible token contracts would have to conform to. +If a users wants to deploy a new token contract, their contract +would need to implement the FungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## `Vault` resource + +Each account that owns tokens would need to have an instance +of the Vault resource stored in their account storage. + +The Vault resource has methods that the owner and other users can call. + +## `Provider`, `Receiver`, and `Balance` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields functions +in one or more of the interfaces. + +It also gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +By using resources and interfaces, users of FungibleToken contracts +can send and receive tokens peer-to-peer, without having to interact +with a central ledger smart contract. To send tokens to another user, +a user would simply withdraw the tokens from their Vault, then call +the deposit function on another user's Vault to complete the transfer. + +*/ + +/// FungibleToken +/// +/// The interface that fungible token contracts implement. +/// +pub contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + /// + pub var totalSupply: UFix64 + + /// TokensInitialized + /// + /// The event that is emitted when the contract is created + /// + pub event TokensInitialized(initialSupply: UFix64) + + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + /// + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited into a Vault + /// + pub event TokensDeposited(amount: UFix64, to: Address?) + + /// Provider + /// + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on `balance` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// withdraw subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + pub fun withdraw(amount: UFix64): @Vault { + post { + // `result` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// Receiver + /// + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// deposit takes a Vault and deposits it into the implementing resource type + /// + pub fun deposit(from: @Vault) + } + + /// Balance + /// + /// The interface that contains the `balance` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + pub var balance: UFix64 + + init(balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + } + + /// Vault + /// + /// The resource that contains the functions to send and receive tokens. + /// + pub resource Vault: Provider, Receiver, Balance { + + // The declaration of a concrete type in a contract interface means that + // every Fungible Token contract that implements the FungibleToken interface + // must define a concrete `Vault` resource that conforms to the `Provider`, `Receiver`, + // and `Balance` interfaces, and declares their required fields and functions + + /// The total balance of the vault + /// + pub var balance: UFix64 + + // The conforming type must declare an initializer + // that allows prioviding the initial balance of the Vault + // + init(balance: UFix64) + + /// withdraw subtracts `amount` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + pub fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function `before` to get the value of the `balance` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// deposit takes a Vault and adds its balance to the balance of this Vault + /// + pub fun deposit(from: @Vault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + pub fun createEmptyVault(): @Vault { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } +} \ No newline at end of file diff --git a/contracts/MetadataViews.cdc b/contracts/MetadataViews.cdc new file mode 100644 index 0000000..98f00b7 --- /dev/null +++ b/contracts/MetadataViews.cdc @@ -0,0 +1,740 @@ +import FungibleToken from "./FungibleToken.cdc" +import NonFungibleToken from "./NonFungibleToken.cdc" + +/// This contract implements the metadata standard proposed +/// in FLIP-0636. +/// +/// Ref: https://github.com/onflow/flow/blob/master/flips/20210916-nft-metadata.md +/// +/// Structs and resources can implement one or more +/// metadata types, called views. Each view type represents +/// a different kind of metadata, such as a creator biography +/// or a JPEG image file. +/// +pub contract MetadataViews { + + /// Provides access to a set of metadata views. A struct or + /// resource (e.g. an NFT) can implement this interface to provide access to + /// the views that it supports. + /// + pub resource interface Resolver { + pub fun getViews(): [Type] + pub fun resolveView(_ view: Type): AnyStruct? + } + + /// A group of view resolvers indexed by ID. + /// + pub resource interface ResolverCollection { + pub fun borrowViewResolver(id: UInt64): &{Resolver} + pub fun getIDs(): [UInt64] + } + + /// NFTView wraps all Core views along `id` and `uuid` fields, and is used + /// to give a complete picture of an NFT. Most NFTs should implement this + /// view. + /// + pub struct NFTView { + pub let id: UInt64 + pub let uuid: UInt64 + pub let display: Display? + pub let externalURL: ExternalURL? + pub let collectionData: NFTCollectionData? + pub let collectionDisplay: NFTCollectionDisplay? + pub let royalties: Royalties? + pub let traits: Traits? + + init( + id : UInt64, + uuid : UInt64, + display : Display?, + externalURL : ExternalURL?, + collectionData : NFTCollectionData?, + collectionDisplay : NFTCollectionDisplay?, + royalties : Royalties?, + traits: Traits? + ) { + self.id = id + self.uuid = uuid + self.display = display + self.externalURL = externalURL + self.collectionData = collectionData + self.collectionDisplay = collectionDisplay + self.royalties = royalties + self.traits = traits + } + } + + /// Helper to get an NFT view + /// + /// @param id: The NFT id + /// @param viewResolver: A reference to the resolver resource + /// @return A NFTView struct + /// + pub fun getNFTView(id: UInt64, viewResolver: &{Resolver}) : NFTView { + let nftView = viewResolver.resolveView(Type()) + if nftView != nil { + return nftView! as! NFTView + } + + return NFTView( + id : id, + uuid: viewResolver.uuid, + display: self.getDisplay(viewResolver), + externalURL : self.getExternalURL(viewResolver), + collectionData : self.getNFTCollectionData(viewResolver), + collectionDisplay : self.getNFTCollectionDisplay(viewResolver), + royalties : self.getRoyalties(viewResolver), + traits : self.getTraits(viewResolver) + ) + } + + /// Display is a basic view that includes the name, description and + /// thumbnail for an object. Most objects should implement this view. + /// + pub struct Display { + + /// The name of the object. + /// + /// This field will be displayed in lists and therefore should + /// be short an concise. + /// + pub let name: String + + /// A written description of the object. + /// + /// This field will be displayed in a detailed view of the object, + /// so can be more verbose (e.g. a paragraph instead of a single line). + /// + pub let description: String + + /// A small thumbnail representation of the object. + /// + /// This field should be a web-friendly file (i.e JPEG, PNG) + /// that can be displayed in lists, link previews, etc. + /// + pub let thumbnail: AnyStruct{File} + + init( + name: String, + description: String, + thumbnail: AnyStruct{File} + ) { + self.name = name + self.description = description + self.thumbnail = thumbnail + } + } + + /// Helper to get Display in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Display struct + /// + pub fun getDisplay(_ viewResolver: &{Resolver}) : Display? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Display { + return v + } + } + return nil + } + + /// Generic interface that represents a file stored on or off chain. Files + /// can be used to references images, videos and other media. + /// + pub struct interface File { + pub fun uri(): String + } + + /// View to expose a file that is accessible at an HTTP (or HTTPS) URL. + /// + pub struct HTTPFile: File { + pub let url: String + + init(url: String) { + self.url = url + } + + pub fun uri(): String { + return self.url + } + } + + /// View to expose a file stored on IPFS. + /// IPFS images are referenced by their content identifier (CID) + /// rather than a direct URI. A client application can use this CID + /// to find and load the image via an IPFS gateway. + /// + pub struct IPFSFile: File { + + /// CID is the content identifier for this IPFS file. + /// + /// Ref: https://docs.ipfs.io/concepts/content-addressing/ + /// + pub let cid: String + + /// Path is an optional path to the file resource in an IPFS directory. + /// + /// This field is only needed if the file is inside a directory. + /// + /// Ref: https://docs.ipfs.io/concepts/file-systems/ + /// + pub let path: String? + + init(cid: String, path: String?) { + self.cid = cid + self.path = path + } + + /// This function returns the IPFS native URL for this file. + /// Ref: https://docs.ipfs.io/how-to/address-ipfs-on-web/#native-urls + /// + /// @return The string containing the file uri + /// + pub fun uri(): String { + if let path = self.path { + return "ipfs://".concat(self.cid).concat("/").concat(path) + } + + return "ipfs://".concat(self.cid) + } + } + + /// Optional view for collections that issue multiple objects + /// with the same or similar metadata, for example an X of 100 set. This + /// information is useful for wallets and marketplaces. + /// An NFT might be part of multiple editions, which is why the edition + /// information is returned as an arbitrary sized array + /// + pub struct Edition { + + /// The name of the edition + /// For example, this could be Set, Play, Series, + /// or any other way a project could classify its editions + pub let name: String? + + /// The edition number of the object. + /// For an "24 of 100 (#24/100)" item, the number is 24. + pub let number: UInt64 + + /// The max edition number of this type of objects. + /// This field should only be provided for limited-editioned objects. + /// For an "24 of 100 (#24/100)" item, max is 100. + /// For an item with unlimited edition, max should be set to nil. + /// + pub let max: UInt64? + + init(name: String?, number: UInt64, max: UInt64?) { + if max != nil { + assert(number <= max!, message: "The number cannot be greater than the max number!") + } + self.name = name + self.number = number + self.max = max + } + } + + /// Wrapper view for multiple Edition views + /// + pub struct Editions { + + /// An arbitrary-sized list for any number of editions + /// that the NFT might be a part of + pub let infoList: [Edition] + + init(_ infoList: [Edition]) { + self.infoList = infoList + } + } + + /// Helper to get Editions in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Editions struct + /// + pub fun getEditions(_ viewResolver: &{Resolver}) : Editions? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Editions { + return v + } + } + return nil + } + + /// View representing a project-defined serial number for a specific NFT + /// Projects have different definitions for what a serial number should be + /// Some may use the NFTs regular ID and some may use a different + /// classification system. The serial number is expected to be unique among + /// other NFTs within that project + /// + pub struct Serial { + pub let number: UInt64 + + init(_ number: UInt64) { + self.number = number + } + } + + /// Helper to get Serial in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional Serial struct + /// + pub fun getSerial(_ viewResolver: &{Resolver}) : Serial? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Serial { + return v + } + } + return nil + } + + /// View that defines the composable royalty standard that gives marketplaces a + /// unified interface to support NFT royalties. + /// + pub struct Royalty { + + /// Generic FungibleToken Receiver for the beneficiary of the royalty + /// Can get the concrete type of the receiver with receiver.getType() + /// Recommendation - Users should create a new link for a FlowToken + /// receiver for this using `getRoyaltyReceiverPublicPath()`, and not + /// use the default FlowToken receiver. This will allow users to update + /// the capability in the future to use a more generic capability + pub let receiver: Capability<&AnyResource{FungibleToken.Receiver}> + + /// Multiplier used to calculate the amount of sale value transferred to + /// royalty receiver. Note - It should be between 0.0 and 1.0 + /// Ex - If the sale value is x and multiplier is 0.56 then the royalty + /// value would be 0.56 * x. + /// Generally percentage get represented in terms of basis points + /// in solidity based smart contracts while cadence offers `UFix64` + /// that already supports the basis points use case because its + /// operations are entirely deterministic integer operations and support + /// up to 8 points of precision. + pub let cut: UFix64 + + /// Optional description: This can be the cause of paying the royalty, + /// the relationship between the `wallet` and the NFT, or anything else + /// that the owner might want to specify. + pub let description: String + + init(receiver: Capability<&AnyResource{FungibleToken.Receiver}>, cut: UFix64, description: String) { + pre { + cut >= 0.0 && cut <= 1.0 : "Cut value should be in valid range i.e [0,1]" + } + self.receiver = receiver + self.cut = cut + self.description = description + } + } + + /// Wrapper view for multiple Royalty views. + /// Marketplaces can query this `Royalties` struct from NFTs + /// and are expected to pay royalties based on these specifications. + /// + pub struct Royalties { + + /// Array that tracks the individual royalties + access(self) let cutInfos: [Royalty] + + pub init(_ cutInfos: [Royalty]) { + // Validate that sum of all cut multipliers should not be greater than 1.0 + var totalCut = 0.0 + for royalty in cutInfos { + totalCut = totalCut + royalty.cut + } + assert(totalCut <= 1.0, message: "Sum of cutInfos multipliers should not be greater than 1.0") + // Assign the cutInfos + self.cutInfos = cutInfos + } + + /// Return the cutInfos list + /// + /// @return An array containing all the royalties structs + /// + pub fun getRoyalties(): [Royalty] { + return self.cutInfos + } + } + + /// Helper to get Royalties in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Royalties struct + /// + pub fun getRoyalties(_ viewResolver: &{Resolver}) : Royalties? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Royalties { + return v + } + } + return nil + } + + /// Get the path that should be used for receiving royalties + /// This is a path that will eventually be used for a generic switchboard receiver, + /// hence the name but will only be used for royalties for now. + /// + /// @return The PublicPath for the generic FT receiver + /// + pub fun getRoyaltyReceiverPublicPath(): PublicPath { + return /public/GenericFTReceiver + } + + /// View to represent, a file with an correspoiding mediaType. + /// + pub struct Media { + + /// File for the media + /// + pub let file: AnyStruct{File} + + /// media-type comes on the form of type/subtype as described here + /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + /// + pub let mediaType: String + + init(file: AnyStruct{File}, mediaType: String) { + self.file=file + self.mediaType=mediaType + } + } + + /// Wrapper view for multiple media views + /// + pub struct Medias { + + /// An arbitrary-sized list for any number of Media items + pub let items: [Media] + + init(_ items: [Media]) { + self.items = items + } + } + + /// Helper to get Medias in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Medias struct + /// + pub fun getMedias(_ viewResolver: &{Resolver}) : Medias? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Medias { + return v + } + } + return nil + } + + /// View to represent a license according to https://spdx.org/licenses/ + /// This view can be used if the content of an NFT is licensed. + /// + pub struct License { + pub let spdxIdentifier: String + + init(_ identifier: String) { + self.spdxIdentifier = identifier + } + } + + /// Helper to get License in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional License struct + /// + pub fun getLicense(_ viewResolver: &{Resolver}) : License? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? License { + return v + } + } + return nil + } + + /// View to expose a URL to this item on an external site. + /// This can be used by applications like .find and Blocto to direct users + /// to the original link for an NFT. + /// + pub struct ExternalURL { + pub let url: String + + init(_ url: String) { + self.url=url + } + } + + /// Helper to get ExternalURL in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional ExternalURL struct + /// + pub fun getExternalURL(_ viewResolver: &{Resolver}) : ExternalURL? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? ExternalURL { + return v + } + } + return nil + } + + /// View to expose the information needed store and retrieve an NFT. + /// This can be used by applications to setup a NFT collection with proper + /// storage and public capabilities. + /// + pub struct NFTCollectionData { + /// Path in storage where this NFT is recommended to be stored. + pub let storagePath: StoragePath + + /// Public path which must be linked to expose public capabilities of this NFT + /// including standard NFT interfaces and metadataviews interfaces + pub let publicPath: PublicPath + + /// Private path which should be linked to expose the provider + /// capability to withdraw NFTs from the collection holding NFTs + pub let providerPath: PrivatePath + + /// Public collection type that is expected to provide sufficient read-only access to standard + /// functions (deposit + getIDs + borrowNFT) + /// This field is for backwards compatibility with collections that have not used the standard + /// NonFungibleToken.CollectionPublic interface when setting up collections. For new + /// collections, this may be set to be equal to the type specified in `publicLinkedType`. + pub let publicCollection: Type + + /// Type that should be linked at the aforementioned public path. This is normally a + /// restricted type with many interfaces. Notably the `NFT.CollectionPublic`, + /// `NFT.Receiver`, and `MetadataViews.ResolverCollection` interfaces are required. + pub let publicLinkedType: Type + + /// Type that should be linked at the aforementioned private path. This is normally + /// a restricted type with at a minimum the `NFT.Provider` interface + pub let providerLinkedType: Type + + /// Function that allows creation of an empty NFT collection that is intended to store + /// this NFT. + pub let createEmptyCollection: ((): @NonFungibleToken.Collection) + + init( + storagePath: StoragePath, + publicPath: PublicPath, + providerPath: PrivatePath, + publicCollection: Type, + publicLinkedType: Type, + providerLinkedType: Type, + createEmptyCollectionFunction: ((): @NonFungibleToken.Collection) + ) { + pre { + publicLinkedType.isSubtype(of: Type<&{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>()): "Public type must include NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, and MetadataViews.ResolverCollection interfaces." + providerLinkedType.isSubtype(of: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>()): "Provider type must include NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, and MetadataViews.ResolverCollection interface." + } + self.storagePath=storagePath + self.publicPath=publicPath + self.providerPath = providerPath + self.publicCollection=publicCollection + self.publicLinkedType=publicLinkedType + self.providerLinkedType = providerLinkedType + self.createEmptyCollection=createEmptyCollectionFunction + } + } + + /// Helper to get NFTCollectionData in a way that will return an typed Optional + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional NFTCollectionData struct + /// + pub fun getNFTCollectionData(_ viewResolver: &{Resolver}) : NFTCollectionData? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? NFTCollectionData { + return v + } + } + return nil + } + + /// View to expose the information needed to showcase this NFT's + /// collection. This can be used by applications to give an overview and + /// graphics of the NFT collection this NFT belongs to. + /// + pub struct NFTCollectionDisplay { + // Name that should be used when displaying this NFT collection. + pub let name: String + + // Description that should be used to give an overview of this collection. + pub let description: String + + // External link to a URL to view more information about this collection. + pub let externalURL: ExternalURL + + // Square-sized image to represent this collection. + pub let squareImage: Media + + // Banner-sized image for this collection, recommended to have a size near 1200x630. + pub let bannerImage: Media + + // Social links to reach this collection's social homepages. + // Possible keys may be "instagram", "twitter", "discord", etc. + pub let socials: {String: ExternalURL} + + init( + name: String, + description: String, + externalURL: ExternalURL, + squareImage: Media, + bannerImage: Media, + socials: {String: ExternalURL} + ) { + self.name = name + self.description = description + self.externalURL = externalURL + self.squareImage = squareImage + self.bannerImage = bannerImage + self.socials = socials + } + } + + /// Helper to get NFTCollectionDisplay in a way that will return a typed + /// Optional + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional NFTCollection struct + /// + pub fun getNFTCollectionDisplay(_ viewResolver: &{Resolver}) : NFTCollectionDisplay? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? NFTCollectionDisplay { + return v + } + } + return nil + } + + /// View to expose rarity information for a single rarity + /// Note that a rarity needs to have either score or description but it can + /// have both + /// + pub struct Rarity { + /// The score of the rarity as a number + pub let score: UFix64? + + /// The maximum value of score + pub let max: UFix64? + + /// The description of the rarity as a string. + /// + /// This could be Legendary, Epic, Rare, Uncommon, Common or any other string value + pub let description: String? + + init(score: UFix64?, max: UFix64?, description: String?) { + if score == nil && description == nil { + panic("A Rarity needs to set score, description or both") + } + + self.score = score + self.max = max + self.description = description + } + } + + /// Helper to get Rarity view in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Rarity struct + /// + pub fun getRarity(_ viewResolver: &{Resolver}) : Rarity? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Rarity { + return v + } + } + return nil + } + + /// View to represent a single field of metadata on an NFT. + /// This is used to get traits of individual key/value pairs along with some + /// contextualized data about the trait + /// + pub struct Trait { + // The name of the trait. Like Background, Eyes, Hair, etc. + pub let name: String + + // The underlying value of the trait, the rest of the fields of a trait provide context to the value. + pub let value: AnyStruct + + // displayType is used to show some context about what this name and value represent + // for instance, you could set value to a unix timestamp, and specify displayType as "Date" to tell + // platforms to consume this trait as a date and not a number + pub let displayType: String? + + // Rarity can also be used directly on an attribute. + // + // This is optional because not all attributes need to contribute to the NFT's rarity. + pub let rarity: Rarity? + + init(name: String, value: AnyStruct, displayType: String?, rarity: Rarity?) { + self.name = name + self.value = value + self.displayType = displayType + self.rarity = rarity + } + } + + /// Wrapper view to return all the traits on an NFT. + /// This is used to return traits as individual key/value pairs along with + /// some contextualized data about each trait. + pub struct Traits { + pub let traits: [Trait] + + init(_ traits: [Trait]) { + self.traits = traits + } + + /// Adds a single Trait to the Traits view + /// + /// @param Trait: The trait struct to be added + /// + pub fun addTrait(_ t: Trait) { + self.traits.append(t) + } + } + + /// Helper to get Traits view in a typesafe way + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional Traits struct + /// + pub fun getTraits(_ viewResolver: &{Resolver}) : Traits? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? Traits { + return v + } + } + return nil + } + + /// Helper function to easily convert a dictionary to traits. For NFT + /// collections that do not need either of the optional values of a Trait, + /// this method should suffice to give them an array of valid traits. + /// + /// @param dict: The dictionary to be converted to Traits + /// @param excludedNames: An optional String array specifying the `dict` + /// keys that are not wanted to become `Traits` + /// @return The generated Traits view + /// + pub fun dictToTraits(dict: {String: AnyStruct}, excludedNames: [String]?): Traits { + // Collection owners might not want all the fields in their metadata included. + // They might want to handle some specially, or they might just not want them included at all. + if excludedNames != nil { + for k in excludedNames! { + dict.remove(key: k) + } + } + + let traits: [Trait] = [] + for k in dict.keys { + let trait = Trait(name: k, value: dict[k]!, displayType: nil, rarity: nil) + traits.append(trait) + } + + return Traits(traits) + } + +} + \ No newline at end of file diff --git a/lib/go/test/go.mod b/lib/go/test/go.mod index 3071b75..c7988ca 100644 --- a/lib/go/test/go.mod +++ b/lib/go/test/go.mod @@ -9,8 +9,8 @@ require ( github.com/onflow/flow-ft/lib/go/templates v0.2.0 github.com/onflow/flow-go v0.25.13-0.20220422145107-5a9458db1f1d // indirect github.com/onflow/flow-go-sdk v0.24.1-0.20220421152843-9ce4d554036e - github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20210915191154-12ee8c507a0e - github.com/onflow/flow-nft/lib/go/templates v0.0.0-20210603230546-76c44712d829 + github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20220809160615-f7b3c6b2e43f + github.com/onflow/flow-nft/lib/go/templates v0.0.0-20220809160615-f7b3c6b2e43f github.com/stretchr/testify v1.7.1-0.20210824115523-ab6dc3262822 gotest.tools v2.2.0+incompatible // indirect -) \ No newline at end of file +) diff --git a/lib/go/test/go.sum b/lib/go/test/go.sum index 9a0b3a4..cb7b696 100644 --- a/lib/go/test/go.sum +++ b/lib/go/test/go.sum @@ -48,6 +48,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/kms v1.0.0/go.mod h1:nhUehi+w7zht2XrUfvTRNpxrfayBHqP4lu2NSywui/0= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= @@ -1220,20 +1221,25 @@ github.com/onflow/flow-go-sdk v0.20.0 h1:0xcSC7OGO8DWZ7GWk/TUorVNcaPRfudH67RTzc7 github.com/onflow/flow-go-sdk v0.20.0/go.mod h1:52QZyLwU3p3UZ2FXOy+sRl4JPdtvJoae1spIUBOFxA8= github.com/onflow/flow-go-sdk v0.20.1-0.20210623043139-533a95abf071 h1:Ytzw/EZl32II4Y9dwbYvUeT19Tl/a2bNR6AfNGwfvbw= github.com/onflow/flow-go-sdk v0.20.1-0.20210623043139-533a95abf071/go.mod h1:eTbMbxgmC5CAc4sSFsD2RfpVnt0vOLwWtGfnMppjvNM= +github.com/onflow/flow-go-sdk v0.24.0/go.mod h1:IoptMLPyFXWvyd9yYA6/4EmSeeozl6nJoIv4FaEMg74= github.com/onflow/flow-go-sdk v0.24.1-0.20220419204316-4f7efebeb2e9/go.mod h1:8LExIqsYMJHkC/h1OKd7XkRmPR6jA7bFv5eFtFXpv14= github.com/onflow/flow-go-sdk v0.24.1-0.20220421152843-9ce4d554036e h1:7XUu6YIETX5fFmQThCv9GDCWrergcymw941ynma5keM= github.com/onflow/flow-go-sdk v0.24.1-0.20220421152843-9ce4d554036e/go.mod h1:d0tO4l/bpLhISgYw1or2p5ee5xeJbnWgnU9CGUkvLO8= github.com/onflow/flow-go/crypto v0.12.0/go.mod h1:oXuvU0Dr4lHKgye6nHEFbBXIWNv+dBQUzoVW5Go38+o= github.com/onflow/flow-go/crypto v0.18.0 h1:2tucSdxmld/08LfIfLxGfw+8QuSNFiHAUojX5AFvq6k= github.com/onflow/flow-go/crypto v0.18.0/go.mod h1:3Ah843iPyjIL+7nH9EillV3OWNptrL+DrQUbVKgg2E4= +github.com/onflow/flow-go/crypto v0.21.3/go.mod h1:vI6V4CY3R6c4JKBxdcRiR/AnjBfL8OSD97bJc60cLuQ= github.com/onflow/flow-go/crypto v0.24.3 h1:5puosmiy853m1GPmBLJr4PiLVcCzE4n5o60hRPo9kYA= github.com/onflow/flow-go/crypto v0.24.3/go.mod h1:dkVL98P6GHR48iD9zCB6XlnkJX8IQd00FKgt1reV90w= github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20210603230546-76c44712d829 h1:3gT5XdTDW9TfYtL19DcoMo0YwTtRM/fkAqW9jr2sEeI= github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20210603230546-76c44712d829/go.mod h1:wdPPIybC/E8hFmfSJ9XaHbuwTZ0/Q/346pbsJ57NoZo= github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20210915191154-12ee8c507a0e h1:svZJ1NydwvNGfkJfJXIeECcKecnYC1CtC/BMzdfEI0U= github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20210915191154-12ee8c507a0e/go.mod h1:epgW8P53PDpHaqBQCmMgJqdet4h7ONaoIL3kVD/nnzU= +github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20220809160615-f7b3c6b2e43f h1:QbbDs6LDXeAFi5bvFi8gky2QLHsuYUmDvWfh5vUwkwI= +github.com/onflow/flow-nft/lib/go/contracts v0.0.0-20220809160615-f7b3c6b2e43f/go.mod h1:YsvzYng4htDgRB9sa9jxdwoTuuhjK8WYWXTyLkIigZY= github.com/onflow/flow-nft/lib/go/templates v0.0.0-20210603230546-76c44712d829 h1:vzsa41FL4WFabtyGUlh6xNb3ML6HVCh4/ik5Lx7YSqg= github.com/onflow/flow-nft/lib/go/templates v0.0.0-20210603230546-76c44712d829/go.mod h1:z0+4Sk2EpCAAuykqYB1/awwcYDfBoM2xCQ3SjsMjs00= +github.com/onflow/flow-nft/lib/go/templates v0.0.0-20220809160615-f7b3c6b2e43f/go.mod h1:lne28Vnwpi3ZVJRm6BaDkNGzAdvBhs9jKvZiQguhTX8= github.com/onflow/flow/protobuf/go/flow v0.1.8/go.mod h1:kRugbzZjwQqvevJhrnnCFMJZNmoSJmxlKt6hTGXZojM= github.com/onflow/flow/protobuf/go/flow v0.1.9/go.mod h1:kRugbzZjwQqvevJhrnnCFMJZNmoSJmxlKt6hTGXZojM= github.com/onflow/flow/protobuf/go/flow v0.2.0 h1:a4Cg0ekoqb76zeOEo1wtSWtlnhGXwcxebp0itFwGtlE= @@ -1921,6 +1927,7 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025112917-711f33c9992c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2075,6 +2082,7 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= @@ -2158,7 +2166,10 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211007155348-82e027067bd4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= diff --git a/lib/go/test/templates.go b/lib/go/test/templates.go index 842cbb8..ec8480f 100644 --- a/lib/go/test/templates.go +++ b/lib/go/test/templates.go @@ -10,6 +10,7 @@ import ( const ( nftAddressPlaceholder = "\"[^\"]*NonFungibleToken.cdc\"" + viewsAddressPlaceholder = "\"[^\"]*MetadataViews.cdc\"" AllDayAddressPlaceholder = "\"[^\"]*AllDay.cdc\"" ) @@ -65,18 +66,24 @@ func replaceAddresses(code []byte, contracts Contracts) []byte { nftRe := regexp.MustCompile(nftAddressPlaceholder) code = nftRe.ReplaceAll(code, []byte("0x"+contracts.NFTAddress.String())) + viewsRe := regexp.MustCompile(viewsAddressPlaceholder) + code = viewsRe.ReplaceAll(code, []byte("0x"+contracts.MetadataViewsAddress.String())) + AllDayRe := regexp.MustCompile(AllDayAddressPlaceholder) code = AllDayRe.ReplaceAll(code, []byte("0x"+contracts.AllDayAddress.String())) return code } -func LoadAllDay(nftAddress flow.Address) []byte { +func LoadAllDay(nftAddress flow.Address, metadataAddress flow.Address) []byte { code := readFile(AllDayPath) nftRe := regexp.MustCompile(nftAddressPlaceholder) code = nftRe.ReplaceAll(code, []byte("0x"+nftAddress.String())) + viewsRe := regexp.MustCompile(viewsAddressPlaceholder) + code = viewsRe.ReplaceAll(code, []byte("0x"+metadataAddress.String())) + return code } diff --git a/lib/go/test/test.go b/lib/go/test/test.go index 6693897..2ddfc15 100644 --- a/lib/go/test/test.go +++ b/lib/go/test/test.go @@ -20,6 +20,7 @@ import ( const ( flowTokenName = "FlowToken" nonFungibleTokenName = "NonFungibleToken" + metadataViewsName = "MetadataViews" defaultAccountFunding = "1000.0" ) @@ -29,12 +30,13 @@ var ( ) type Contracts struct { - NFTAddress flow.Address - AllDayAddress flow.Address - AllDaySigner crypto.Signer + NFTAddress flow.Address + MetadataViewsAddress flow.Address + AllDayAddress flow.Address + AllDaySigner crypto.Signer } -func deployNFTContract(t *testing.T, b *emulator.Blockchain) flow.Address { +func deployNFTContract(t *testing.T, b *emulator.Blockchain) (flow.Address, flow.Address) { nftCode := nftcontracts.NonFungibleToken() nftAddress, err := b.CreateAccount(nil, []sdktemplates.Contract{ @@ -46,19 +48,29 @@ func deployNFTContract(t *testing.T, b *emulator.Blockchain) flow.Address { ) require.NoError(t, err) + metadataAddress, err := b.CreateAccount(nil, + []sdktemplates.Contract{ + { + Name: metadataViewsName, + Source: string(nftcontracts.MetadataViews(ftAddress, nftAddress)), + }, + }, + ) + require.NoError(t, err) + _, err = b.CommitBlock() require.NoError(t, err) - return nftAddress + return nftAddress, metadataAddress } func AllDayDeployContracts(t *testing.T, b *emulator.Blockchain) Contracts { accountKeys := test.AccountKeyGenerator() - nftAddress := deployNFTContract(t, b) + nftAddress, metadataAddress := deployNFTContract(t, b) AllDayAccountKey, AllDaySigner := accountKeys.NewWithSigner() - AllDayCode := LoadAllDay(nftAddress) + AllDayCode := LoadAllDay(nftAddress, metadataAddress) AllDayAddress, err := b.CreateAccount( []*flow.AccountKey{AllDayAccountKey}, @@ -93,6 +105,7 @@ func AllDayDeployContracts(t *testing.T, b *emulator.Blockchain) Contracts { return Contracts{ nftAddress, + metadataAddress, AllDayAddress, AllDaySigner, } diff --git a/transactions/user/setup_allday_account.cdc b/transactions/user/setup_allday_account.cdc index 9544e9e..3c7e4fb 100644 --- a/transactions/user/setup_allday_account.cdc +++ b/transactions/user/setup_allday_account.cdc @@ -1,5 +1,6 @@ import NonFungibleToken from "../../contracts/NonFungibleToken.cdc" import AllDay from "../../contracts/AllDay.cdc" +import MetadataViews from "../../contracts/MetadataViews.cdc" // This transaction configures an account to hold AllDay NFTs. @@ -15,7 +16,7 @@ transaction { signer.save(<-collection, to: AllDay.CollectionStoragePath) // create a public capability for the collection - signer.link<&AllDay.Collection{NonFungibleToken.CollectionPublic, AllDay.MomentNFTCollectionPublic}>( + signer.link<&AllDay.Collection{NonFungibleToken.CollectionPublic, AllDay.MomentNFTCollectionPublic, MetadataViews.ResolverCollection}>( AllDay.CollectionPublicPath, target: AllDay.CollectionStoragePath )