diff --git a/contracts/IPackNFT.cdc b/contracts/IPackNFT.cdc new file mode 100644 index 0000000..129248b --- /dev/null +++ b/contracts/IPackNFT.cdc @@ -0,0 +1,116 @@ +import Crypto +import NonFungibleToken from "./NonFungibleToken.cdc" + + +pub contract interface IPackNFT{ + /// StoragePath for Collection Resource + /// + pub let collectionStoragePath: StoragePath + /// PublicPath expected for deposit + /// + pub let collectionPublicPath: PublicPath + /// PublicPath for receiving PackNFT + /// + pub let collectionIPackNFTPublicPath: PublicPath + /// StoragePath for the PackNFT Operator Resource (issuer owns this) + /// + pub let operatorStoragePath: StoragePath + /// PrivatePath to share IOperator interfaces with Operator (typically with PDS account) + /// + pub let operatorPrivPath: PrivatePath + /// Request for Reveal + /// + pub event RevealRequest(id: UInt64, openRequest: Bool) + /// Request for Open + /// + /// This is emitted when owner of a PackNFT request for the entitled NFT to be + /// deposited to its account + pub event OpenRequest(id: UInt64) + /// New Pack NFT + /// + /// Emitted when a new PackNFT has been minted + pub event Mint(id: UInt64, commitHash: String, distId: UInt64 ) + /// Revealed + /// + /// Emitted when a packNFT has been revealed + pub event Revealed(id: UInt64, salt: String, nfts: String) + /// Opened + /// + /// Emitted when a packNFT has been opened + pub event Opened(id: UInt64) + + pub enum Status: UInt8 { + pub case Sealed + pub case Revealed + pub case Opened + } + + pub struct interface Collectible { + pub let address: Address + pub let contractName: String + pub let id: UInt64 + pub fun hashString(): String + init(address: Address, contractName: String, id: UInt64) + } + + pub resource interface IPack { + pub let commitHash: String + pub let issuer: Address + pub var status: Status + pub var salt: String? + + pub fun verify(nftString: String): Bool + + access(contract) fun reveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) + access(contract) fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) + init(commitHash: String, issuer: Address) + } + + pub resource interface IOperator { + pub fun mint(distId: UInt64, commitHash: String, issuer: Address): @NFT + pub fun reveal(id: UInt64, nfts: [{Collectible}], salt: String) + pub fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) + } + pub resource PackNFTOperator: IOperator { + pub fun mint(distId: UInt64, commitHash: String, issuer: Address): @NFT + pub fun reveal(id: UInt64, nfts: [{Collectible}], salt: String) + pub fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) + } + + pub resource interface IPackNFTToken { + pub let id: UInt64 + pub let commitHash: String + pub let issuer: Address + } + + pub resource NFT: NonFungibleToken.INFT, IPackNFTToken, IPackNFTOwnerOperator{ + pub let id: UInt64 + pub let commitHash: String + pub let issuer: Address + pub fun reveal(openRequest: Bool) + pub fun open() + } + + pub resource interface IPackNFTOwnerOperator{ + pub fun reveal(openRequest: Bool) + pub fun open() + } + + pub resource interface IPackNFTCollectionPublic { + pub fun deposit(token: @NonFungibleToken.NFT) + pub fun getIDs(): [UInt64] + pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT + pub fun borrowPackNFT(id: UInt64): &IPackNFT.NFT? { + // If the result isn't nil, the id of the returned reference + // should be the same as the argument to the function + post { + (result == nil) || (result!.id == id): + "Cannot borrow PackNFT reference: The ID of the returned reference is incorrect" + } + } + } + + access(contract) fun revealRequest(id: UInt64, openRequest: Bool) + access(contract) fun openRequest(id: UInt64) + pub fun publicReveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) +} diff --git a/contracts/NFLPack.cdc b/contracts/NFLPack.cdc new file mode 100644 index 0000000..c46baf1 --- /dev/null +++ b/contracts/NFLPack.cdc @@ -0,0 +1,248 @@ +import Crypto +import NonFungibleToken from 0x{{.NonFungibleToken}} +import IPackNFT from 0x{{.IPackNFT}} + +pub contract PackNFT: NonFungibleToken, IPackNFT { + + pub var totalSupply: UInt64 + pub let version: String + pub let collectionStoragePath: StoragePath + pub let collectionPublicPath: PublicPath + pub let collectionIPackNFTPublicPath: PublicPath + pub let operatorStoragePath: StoragePath + pub let operatorPrivPath: PrivatePath + + // representation of the NFT in this contract to keep track of states + access(contract) let packs: @{UInt64: Pack} + + pub event RevealRequest(id: UInt64, openRequest: Bool) + pub event OpenRequest(id: UInt64) + pub event Revealed(id: UInt64, salt: String, nfts: String) + pub event Opened(id: UInt64) + pub event Mint(id: UInt64, commitHash: String, distId: UInt64) + pub event ContractInitialized() + pub event Withdraw(id: UInt64, from: Address?) + pub event Deposit(id: UInt64, to: Address?) + + pub enum Status: UInt8 { + pub case Sealed + pub case Revealed + pub case Opened + } + + pub resource PackNFTOperator: IPackNFT.IOperator { + + pub fun mint(distId: UInt64, commitHash: String, issuer: Address): @NFT{ + let id = PackNFT.totalSupply + let nft <- create NFT(initID: id, commitHash: commitHash, issuer: issuer) + PackNFT.totalSupply = id + 1 + let p <-create Pack(commitHash: commitHash, issuer: issuer) + PackNFT.packs[id] <-! p + emit Mint(id: id, commitHash: commitHash, distId: distId) + return <- nft + } + + pub fun reveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) { + let p <- PackNFT.packs.remove(key: id) ?? panic("no such pack") + p.reveal(id: id, nfts: nfts, salt: salt) + PackNFT.packs[id] <-! p + } + + pub fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) { + let p <- PackNFT.packs.remove(key: id) ?? panic("no such pack") + p.open(id: id, nfts: nfts) + PackNFT.packs[id] <-! p + } + + init(){} + } + + pub resource Pack { + pub let commitHash: String + pub let issuer: Address + pub var status: PackNFT.Status + pub var salt: String? + + pub fun verify(nftString: String): Bool { + assert(self.status != PackNFT.Status.Sealed, message: "Pack not revealed yet") + var hashString = self.salt! + hashString = hashString.concat(",").concat(nftString) + let hash = HashAlgorithm.SHA2_256.hash(hashString.utf8) + assert(self.commitHash == String.encodeHex(hash), message: "CommitHash was not verified") + return true + } + + access(self) fun _verify(nfts: [{IPackNFT.Collectible}], salt: String, commitHash: String): String { + var hashString = salt + var nftString = nfts[0].hashString() + var i = 1 + while i < nfts.length { + let s = nfts[i].hashString() + nftString = nftString.concat(",").concat(s) + i = i + 1 + } + hashString = hashString.concat(",").concat(nftString) + let hash = HashAlgorithm.SHA2_256.hash(hashString.utf8) + assert(self.commitHash == String.encodeHex(hash), message: "CommitHash was not verified") + return nftString + } + + access(contract) fun reveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) { + assert(self.status == PackNFT.Status.Sealed, message: "Pack status is not Sealed") + let v = self._verify(nfts: nfts, salt: salt, commitHash: self.commitHash) + self.salt = salt + self.status = PackNFT.Status.Revealed + emit Revealed(id: id, salt: salt, nfts: v) + } + + access(contract) fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) { + assert(self.status == PackNFT.Status.Revealed, message: "Pack status is not Revealed") + self._verify(nfts: nfts, salt: self.salt!, commitHash: self.commitHash) + self.status = PackNFT.Status.Opened + emit Opened(id: id) + } + + init(commitHash: String, issuer: Address) { + self.commitHash = commitHash + self.issuer = issuer + self.status = PackNFT.Status.Sealed + self.salt = nil + } + } + + pub resource NFT: NonFungibleToken.INFT, IPackNFT.IPackNFTToken, IPackNFT.IPackNFTOwnerOperator { + pub let id: UInt64 + pub let commitHash: String + pub let issuer: Address + + pub fun reveal(openRequest: Bool){ + PackNFT.revealRequest(id: self.id, openRequest: openRequest) + } + + pub fun open(){ + PackNFT.openRequest(id: self.id) + } + + init(initID: UInt64, commitHash: String, issuer: Address ) { + self.id = initID + self.commitHash = commitHash + self.issuer = issuer + } + + } + + pub resource Collection: + NonFungibleToken.Provider, + NonFungibleToken.Receiver, + NonFungibleToken.CollectionPublic, + IPackNFT.IPackNFTCollectionPublic + { + // dictionary of NFT conforming tokens + // NFT is a resource type with an `UInt64` ID field + pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + + init () { + self.ownedNFTs <- {} + } + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") + emit Withdraw(id: token.id, from: self.owner?.address) + return <- token + } + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NonFungibleToken.NFT) { + let token <- token as! @PackNFT.NFT + + let id: UInt64 = token.id + + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[id] <- token + emit Deposit(id: id, to: self.owner?.address) + + destroy oldToken + } + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] { + return self.ownedNFTs.keys + } + + // borrowNFT gets a reference to an NFT in the collection + // so that the caller can read its metadata and call its methods + pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { + return &self.ownedNFTs[id] as &NonFungibleToken.NFT + } + + pub fun borrowPackNFT(id: UInt64): &IPackNFT.NFT? { + let nft<- self.ownedNFTs.remove(key: id) ?? panic("missing NFT") + let token <- nft as! @PackNFT.NFT + let ref = &token as &IPackNFT.NFT + self.ownedNFTs[id] <-! token as! @PackNFT.NFT + return ref + } + + destroy() { + destroy self.ownedNFTs + } + } + + access(contract) fun revealRequest(id: UInt64, openRequest: Bool ) { + let p = PackNFT.borrowPackRepresentation(id: id) ?? panic ("No such pack") + assert(p.status == PackNFT.Status.Sealed, message: "Pack status must be Sealed for reveal request") + emit RevealRequest(id: id, openRequest: openRequest) + } + + access(contract) fun openRequest(id: UInt64) { + let p = PackNFT.borrowPackRepresentation(id: id) ?? panic ("No such pack") + assert(p.status == PackNFT.Status.Revealed, message: "Pack status must be Revealed for open request") + emit OpenRequest(id: id) + } + + pub fun publicReveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) { + let p = PackNFT.borrowPackRepresentation(id: id) ?? panic ("No such pack") + p.reveal(id: id, nfts: nfts, salt: salt) + } + + pub fun borrowPackRepresentation(id: UInt64): &Pack? { + return &self.packs[id] as &Pack + } + + pub fun createEmptyCollection(): @NonFungibleToken.Collection { + return <- create Collection() + } + + init( + collectionStoragePath: StoragePath, + collectionPublicPath: PublicPath, + collectionIPackNFTPublicPath: PublicPath, + operatorStoragePath: StoragePath, + operatorPrivPath: PrivatePath, + version: String + ){ + self.totalSupply = 0 + self.packs <- {} + self.collectionStoragePath = collectionStoragePath + self.collectionPublicPath = collectionPublicPath + self.collectionIPackNFTPublicPath = collectionIPackNFTPublicPath + self.operatorStoragePath = operatorStoragePath + self.operatorPrivPath = operatorPrivPath + self.version = version + + // Create a collection to receive Pack NFTs + let collection <- create Collection() + self.account.save(<-collection, to: self.collectionStoragePath) + self.account.link<&Collection{NonFungibleToken.CollectionPublic}>(self.collectionPublicPath, target: self.collectionStoragePath) + self.account.link<&Collection{IPackNFT.IPackNFTCollectionPublic}>(self.collectionIPackNFTPublicPath, target: self.collectionStoragePath) + + // Create a operator to share mint capability with proxy + let operator <- create PackNFTOperator() + self.account.save(<-operator, to: self.operatorStoragePath) + self.account.link<&PackNFTOperator{IPackNFT.IOperator}>(self.operatorPrivPath, target: self.operatorStoragePath) + } + +} +