diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..02da711 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://very:4000", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..04ebd33 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.wordWrap": "on" +} \ No newline at end of file diff --git a/cadence/contracts/DappyContract.cdc b/cadence/contracts/DappyContract.cdc index 5418548..350e8a2 100644 --- a/cadence/contracts/DappyContract.cdc +++ b/cadence/contracts/DappyContract.cdc @@ -39,10 +39,10 @@ pub contract DappyContract { pub resource Dappy { pub let id: UInt64 - pub let data: Template + pub var data: Template init(templateID: UInt32) { - pre { + pre { DappyContract.templates[templateID] != nil : "Could not create dappy: template does not exist." } let dappy = DappyContract.templates[templateID]! @@ -50,6 +50,11 @@ pub contract DappyContract { self.id = DappyContract.totalDappies self.data = Template(templateID: templateID, dna: dappy.dna, name: dappy.name) } + + pub fun setData(data: Template) { + self.data = data + } + } pub resource Family { @@ -307,6 +312,16 @@ pub contract DappyContract { return el.templates.contains(templateID) } + pub fun mintWithData(data: Template, paymentVault: @FungibleToken.Vault): @Dappy { + pre { + paymentVault.balance >= data.price : "Could not mint dappy: payment balance insufficient." + } + destroy paymentVault + let dappy <- create Dappy(templateID: self.nextTemplateID - 1) + dappy.setData(data: data) + return <- dappy + } + init() { self.templates = {} self.totalDappies = 0 diff --git a/cadence/contracts/DappyContract.original.cdc b/cadence/contracts/DappyContract.original.cdc new file mode 100644 index 0000000..5418548 --- /dev/null +++ b/cadence/contracts/DappyContract.original.cdc @@ -0,0 +1,322 @@ + +import FungibleToken from "./FungibleToken.cdc" + +pub contract DappyContract { + access(self) var templates: {UInt32: Template} + access(self) var families: @{UInt32: Family} + + pub var nextTemplateID: UInt32 + pub var nextFamilyID: UInt32 + pub var totalDappies: UInt64 + + pub let CollectionStoragePath: StoragePath + pub let CollectionPublicPath: PublicPath + pub let AdminStoragePath: StoragePath + + pub struct Template { + pub let templateID: UInt32 + pub let dna: String + pub let name: String + pub let price: UFix64 + + init(templateID: UInt32, dna: String, name: String) { + self.templateID = templateID + self.dna = dna + self.name = name + self.price = self._calculatePrice(dna: dna.length) + } + + access(self) fun _calculatePrice(dna: Int): UFix64 { + if dna >= 31 { + return 21.0 + } else if dna >= 25 { + return 14.0 + } else { + return 7.0 + } + } + } + + pub resource Dappy { + pub let id: UInt64 + pub let data: Template + + init(templateID: UInt32) { + pre { + DappyContract.templates[templateID] != nil : "Could not create dappy: template does not exist." + } + let dappy = DappyContract.templates[templateID]! + DappyContract.totalDappies = DappyContract.totalDappies + 1 + self.id = DappyContract.totalDappies + self.data = Template(templateID: templateID, dna: dappy.dna, name: dappy.name) + } + } + + pub resource Family { + pub let name: String + pub let familyID: UInt32 + pub var templates: [UInt32] + pub var lazy: {UInt32: Bool} + pub var price: UFix64 + + init(name: String, price: UFix64) { + pre { + name.length > 0: "Could not create family: name is required." + price > 0.00 : "Could not create family: price is required to be higher than 0." + } + self.name = name + self.price = price + self.familyID = DappyContract.nextFamilyID + self.templates = [] + self.lazy = {} + DappyContract.nextFamilyID = DappyContract.nextFamilyID + 1 + } + + pub fun addTemplate(templateID: UInt32) { + pre { + DappyContract.templates[templateID] != nil : "Could not add dappy to pack: template does not exist." + } + self.templates.append(templateID) + self.lazy[templateID] = false + } + + pub fun mintDappy(templateID: UInt32): @Dappy { + pre { + self.templates.contains(templateID): "Could not mint dappy: template does not exist." + !self.lazy[templateID]!: "Could not mint Dappy: template has been retired." + } + return <- create Dappy(templateID: templateID) + } + } + + pub struct FamilyReport { + pub let name: String + pub let familyID: UInt32 + pub var templates: [UInt32] + pub var lazy: {UInt32: Bool} + pub var price: UFix64 + + init(name: String, familyID: UInt32, templates: [UInt32], lazy: {UInt32: Bool}, price: UFix64) { + self.name = name + self.familyID = familyID + self.templates = [] + self.lazy = {} + self.price = price + } + } + + pub resource Admin { + pub fun createTemplate(dna: String, name: String): UInt32 { + pre { + dna.length > 0 : "Could not create template: dna is required." + name.length > 0 : "Could not create template: name is required." + } + let newDappyID = DappyContract.nextTemplateID + DappyContract.templates[newDappyID] = Template(templateID: newDappyID, dna: dna, name: name) + DappyContract.nextTemplateID = DappyContract.nextTemplateID + 1 + return newDappyID + } + + pub fun destroyTemplate(dappyID: UInt32) { + pre { + DappyContract.templates[dappyID] != nil : "Could not delete template: template does not exist." + } + DappyContract.templates.remove(key: dappyID) + } + + pub fun createFamily(name: String, price: UFix64) { + let newFamily <- create Family(name: name, price: price) + DappyContract.families[newFamily.familyID] <-! newFamily + } + + pub fun borrowFamily(familyID: UInt32): &Family { + pre { + DappyContract.families[familyID] != nil : "Could not borrow family: family does not exist." + } + return &DappyContract.families[familyID] as &Family + } + + pub fun destroyFamily(familyID: UInt32) { + pre { + DappyContract.families[familyID] != nil : "Could not borrow family: family does not exist." + } + let familyToDelete <- DappyContract.families.remove(key: familyID)! + destroy familyToDelete + } + } + + pub resource interface CollectionPublic { + pub fun deposit(token: @Dappy) + pub fun getIDs(): [UInt64] + pub fun listDappies(): {UInt64: Template} + } + + pub resource interface Provider { + pub fun withdraw(withdrawID: UInt64): @Dappy + } + + pub resource interface Receiver{ + pub fun deposit(token: @Dappy) + pub fun batchDeposit(collection: @Collection) + } + + pub resource Collection: CollectionPublic, Provider, Receiver { + pub var ownedDappies: @{UInt64: Dappy} + + pub fun withdraw(withdrawID: UInt64): @Dappy { + let token <- self.ownedDappies.remove(key: withdrawID) + ?? panic("Could not withdraw dappy: dappy does not exist in collection") + return <-token + } + + pub fun deposit(token: @Dappy) { + let oldToken <- self.ownedDappies[token.id] <- token + destroy oldToken + } + + pub fun batchDeposit(collection: @Collection) { + let keys = collection.getIDs() + for key in keys { + self.deposit(token: <-collection.withdraw(withdrawID: key)) + } + destroy collection + } + + pub fun getIDs(): [UInt64] { + return self.ownedDappies.keys + } + + pub fun listDappies(): {UInt64: Template} { + var dappyTemplates: {UInt64:Template} = {} + for key in self.ownedDappies.keys { + let el = &self.ownedDappies[key] as &Dappy + dappyTemplates.insert(key: el.id, el.data) + } + return dappyTemplates + } + + destroy() { + destroy self.ownedDappies + } + + init() { + self.ownedDappies <- {} + } + } + + pub fun createEmptyCollection(): @Collection { + return <-create self.Collection() + } + + pub fun mintDappy(templateID: UInt32, paymentVault: @FungibleToken.Vault): @Dappy { + pre { + self.templates[templateID] != nil : "Could not mint dappy: dappy with given ID does not exist." + paymentVault.balance >= self.templates[templateID]!.price : "Could not mint dappy: payment balance insufficient." + } + destroy paymentVault + return <- create Dappy(templateID: templateID) + } + + pub fun mintDappyFromFamily(familyID: UInt32, templateID: UInt32, paymentVault: @FungibleToken.Vault): @Dappy { + pre { + self.families[familyID] != nil : "Could not mint dappy from family: family does not exist." + self.templates[templateID] != nil : "Could not mint dappy from family: template does not exist." + } + let familyRef = &self.families[familyID] as! &Family + if familyRef.price > paymentVault.balance { + panic("Could not mint dappy from family: payment balance is not sufficient.") + } + destroy paymentVault + return <- familyRef.mintDappy(templateID: templateID) + } + + pub fun batchMintDappiesFromFamily(familyID: UInt32, templateIDs: [UInt32], paymentVault: @FungibleToken.Vault): @Collection { + pre { + templateIDs.length > 0 : "Could not batch mint dappies from family: at least one templateID is required." + templateIDs.length <= 5 : "Could not batch mint dappies from family: batch mint limit of 5 dappies exceeded." + self.families[familyID] != nil : "Could not batch mint dappies from family: family does not exist." + } + + let familyRef = &self.families[familyID] as! &Family + if familyRef.price > paymentVault.balance { + panic("Could not batch mint dappy from family: payment balance is not sufficient.") + } + let collection <- create Collection() + + for ID in templateIDs { + if !self.familyContainsTemplate(familyID: familyID, templateID: ID) { + continue + } + collection.deposit(token: <- create Dappy(templateID: ID)) + } + destroy paymentVault + return <-collection + } + + pub fun listTemplates(): {UInt32: Template} { + return self.templates + } + + pub fun listFamilies(): [FamilyReport] { + var families: [FamilyReport] = [] + for key in self.families.keys { + let el = &self.families[key] as &Family + families.append(FamilyReport( + name: el.name, + familyID: el.familyID, + templates: el.templates, + lazy: el.lazy, + price: el.price + )) + } + return families + } + + pub fun listFamilyTemplates(familyID: UInt32): [UInt32] { + pre { + self.families[familyID] != nil : "Could not list family templates: family does not exist." + } + var report: [UInt32] = [] + let el = &self.families[familyID] as! &Family + for temp in el.templates { + report.append(temp) + } + return report + } + + pub fun getFamily(familyID: UInt32): FamilyReport { + pre { + self.families[familyID] != nil : "Could not get family: family does not exist." + } + let el = &self.families[familyID] as! &Family + let report = FamilyReport( + name: el.name, + familyID: el.familyID, + templates: el.templates, + lazy: el.lazy, + price: el.price + ) + return report + } + + pub fun familyContainsTemplate(familyID: UInt32, templateID: UInt32): Bool { + pre { + self.families[familyID] != nil : "Family does not exist" + } + let el = &self.families[familyID] as! &Family + return el.templates.contains(templateID) + } + + init() { + self.templates = {} + self.totalDappies = 0 + self.nextTemplateID = 1 + self.nextFamilyID = 1 + self.CollectionStoragePath = /storage/DappyCollection + self.CollectionPublicPath = /public/DappyCollectionPublic + self.AdminStoragePath = /storage/DappyAdmin + self.account.save<@Admin>(<- create Admin(), to: self.AdminStoragePath) + self.families <- {} + } + +} \ No newline at end of file diff --git a/cadence/contracts/DappyNFT.cdc b/cadence/contracts/DappyNFT.cdc new file mode 100644 index 0000000..359ac63 --- /dev/null +++ b/cadence/contracts/DappyNFT.cdc @@ -0,0 +1,142 @@ +import NonFungibleToken from "./NonFungibleToken.cdc" +import DappyContract from "./DappyContract.cdc" + +pub contract DappyNFT: NonFungibleToken { + + pub let CollectionStoragePath: StoragePath + pub let CollectionPublicPath: PublicPath + pub let CollectionPrivatePath: PrivatePath + + pub var totalSupply: UInt64 + + pub event ContractInitialized() + + pub event Withdraw(id: UInt64, from: Address?) + + pub event Deposit(id: UInt64, to: Address?) + + pub resource NFT: NonFungibleToken.INFT { + + pub let id: UInt64 + access(self) let nft: @{UInt64: DappyContract.Dappy} + // access(self) var nft: @DappyContract.Dappy? + access(self) var data: {UInt64: DappyContract.Template} + access(self) var nftUUID: UInt64? + + pub init(nft: @DappyContract.Dappy) { + self.id = nft.id + self.data = {self.id: nft.data} + self.nftUUID = nft.uuid + self.nft <-{nft.uuid: <-nft} + DappyNFT.totalSupply = DappyNFT.totalSupply + (1 as UInt64) + + } + + pub fun withdrawDappy(): @DappyContract.Dappy? { + let ret <- self.nft[self.nftUUID!] <-nil + self.data = {} + self.nftUUID = nil + return <- ret + } + + pub fun getData(): {UInt64: DappyContract.Template} { + return self.data + } + + pub fun borrowDappy(): &DappyContract.Dappy? { + if self.nftUUID == nil {return nil} + return &self.nft[self.nftUUID!] as &DappyContract.Dappy + // let d <- self.withdrawDappy() + // if d == nil { + // destroy d + // return nil + // } + // let r <- d! + // let ret = &r as &DappyContract.Dappy + // self.nft <-! r + // return ret + } + + destroy () { + destroy self.nft + } + } + + pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { + + pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + + let token <- self.ownedNFTs.remove(key: withdrawID) + ?? panic("Cannot withdraw: NFT does not exist in the collection") + emit Withdraw(id: token.id, from:self.owner?.address) + return <-token + + } + + pub fun deposit(token: @NonFungibleToken.NFT) { + + let token <- token + let id = token.id + let oldToken <- self.ownedNFTs[id] <- token + + if self.owner?.address != nil { + emit Deposit(id: id, to: self.owner?.address) + } + + destroy oldToken + + } + + pub fun getIDs(): [UInt64] { + + return self.ownedNFTs.keys + + } + + pub fun borrowNFT(id: UInt64): auth &NonFungibleToken.NFT { + + return &self.ownedNFTs[id] as auth &NonFungibleToken.NFT + + } + + init () { + + self.ownedNFTs <- {} + + } + + destroy() { + + destroy self.ownedNFTs + + } + + } + + pub fun createEmptyCollection(): @Collection { + + return <-create Collection() + + } + + pub fun createFromDappy(dappy: @DappyContract.Dappy): @NFT { + + return <- create NFT( + nft: <- dappy + ) + + } + + init() { + + self.CollectionStoragePath = /storage/DappyNFTCollection + self.CollectionPublicPath = /public/DappyNFTCollection + self.CollectionPrivatePath = /private/DappyNFTCollection + + self.totalSupply = 0 + + } +} + \ No newline at end of file diff --git a/cadence/contracts/GalleryContract.cdc b/cadence/contracts/GalleryContract.cdc new file mode 100644 index 0000000..fc65af3 --- /dev/null +++ b/cadence/contracts/GalleryContract.cdc @@ -0,0 +1,162 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import PackNFT from "../contracts/PackNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +pub contract GalleryContract { + + pub let GalleryStoragePath: StoragePath + pub let GalleryPublicPath: PublicPath + + pub event GalleryListingAdded( + listingResourceID: UInt64, + sellerAddress: Address + ) + + pub event GalleryListingRemoved( + listingResourceID: UInt64, + sellerAddress: Address + ) + + pub struct GalleryData { + + pub let listingDetails: NFTStorefront.ListingDetails + pub let sellerAddress: Address + pub let dappyCollection: {UInt64: DappyContract.Template} + pub let packName: String + + + init ( + listingDetails: NFTStorefront.ListingDetails, + sellerAddress: Address, + dappyCollection: {UInt64: DappyContract.Template}, + packName: String + ) { + pre { + + dappyCollection.length > 0 : + "Gallery data should include some Dappy data" + + dappyCollection.length > 1 || packName.length == 0 : + "Individual Dappy should not have any pack name" + + dappyCollection.length == 1 || packName.length > 0 : + "Pack should have some name" + + dappyCollection.length == 1 || packName.length >= 3 : + "Pack name should be at least 3 characters" + + } + + self.listingDetails = listingDetails + self.sellerAddress = sellerAddress + self.dappyCollection = dappyCollection + self.packName = packName + + } + + } + + pub resource interface GalleryPublic { + + pub fun addListing ( + listingPublic: &{NFTStorefront.ListingPublic}, + sellerAddress: Address + ) + + pub fun removeListing ( + listingResourceID: UInt64, + sellerAddress: Address + ) : GalleryData? + + pub fun getGalleryCollection (): {UInt64: GalleryData} + + } + + pub resource Gallery: GalleryPublic { + + priv let galleryCollection: {UInt64: GalleryData} + + init() { + self.galleryCollection = {} + } + + pub fun getGalleryCollection (): {UInt64: GalleryData} { + return self.galleryCollection + } + + // Add a listing to gallery + pub fun addListing ( + listingPublic: &{NFTStorefront.ListingPublic}, + sellerAddress: Address + ) { + + pre { + // 1. naively check if the address hold this listing + } + + let details = listingPublic.getDetails() + let nftType = details.nftType + let nftID = details.nftID + var dappyCollection: {UInt64: DappyContract.Template} = {} + + // TODO: Check + let listingResourceID = listingPublic.getListingResourceID() + + var packName = "" + + switch nftType { + + case Type<@DappyNFT.NFT>(): + let nftRef = listingPublic.borrowNFT() as! &DappyNFT.NFT + dappyCollection = nftRef.getData() + + case Type<@PackNFT.NFT>(): + let nftRef = listingPublic.borrowNFT() as! &PackNFT.NFT + dappyCollection = nftRef.getData() + packName = dappyCollection.keys.length.toString().concat("-Pack") + + default: + panic("nftType is not supported: ".concat(nftType.identifier) ) + + } + + let galleryData = GalleryData( + listingDetails: details, + sellerAddress: sellerAddress, + dappyCollection: dappyCollection, + packName: packName + ) + + self.galleryCollection[listingResourceID] = galleryData + + } + + // Add a listing to gallery + pub fun removeListing ( + listingResourceID: UInt64, + sellerAddress: Address + ): GalleryData? { + + pre { + // 1. naively check if the address hold this listing no more + } + + let ret = self.galleryCollection.remove(key: listingResourceID) + + return ret + + } + + } + + pub fun createEmptyGallery(): @Gallery { + return <- create Gallery() + } + + init() { + self.GalleryStoragePath = /storage/DappyGallery + self.GalleryPublicPath = /public/DappyGallery + } + +} diff --git a/cadence/contracts/NFTStorefront.cdc b/cadence/contracts/NFTStorefront.cdc new file mode 100644 index 0000000..bfb94b8 --- /dev/null +++ b/cadence/contracts/NFTStorefront.cdc @@ -0,0 +1,519 @@ +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" + +// NFTStorefront +// +// A general purpose sale support contract for Flow NonFungibleTokens. +// +// Each account that wants to list NFTs for sale installs a Storefront, +// and lists individual sales within that Storefront as Listings. +// There is one Storefront per account, it handles sales of all NFT types +// for that account. +// +// Each Listing can have one or more "cut"s of the sale price that +// goes to one or more addresses. Cuts can be used to pay listing fees +// or other considerations. +// Each NFT may be listed in one or more Listings, the validity of each +// Listing can easily be checked. +// +// Purchasers can watch for Listing events and check the NFT type and +// ID to see if they wish to buy the listed item. +// Marketplaces and other aggregators can watch for Listing events +// and list items of interest. +// +pub contract NFTStorefront { + // NFTStorefrontInitialized + // This contract has been deployed. + // Event consumers can now expect events from this contract. + // + pub event NFTStorefrontInitialized() + + // StorefrontInitialized + // A Storefront resource has been created. + // Event consumers can now expect events from this Storefront. + // Note that we do not specify an address: we cannot and should not. + // Created resources do not have an owner address, and may be moved + // after creation in ways we cannot check. + // ListingAvailable events can be used to determine the address + // of the owner of the Storefront (...its location) at the time of + // the listing but only at that precise moment in that precise transaction. + // If the seller moves the Storefront while the listing is valid, + // that is on them. + // + pub event StorefrontInitialized(storefrontResourceID: UInt64) + + // StorefrontDestroyed + // A Storefront has been destroyed. + // Event consumers can now stop processing events from this Storefront. + // Note that we do not specify an address. + // + pub event StorefrontDestroyed(storefrontResourceID: UInt64) + + // ListingAvailable + // A listing has been created and added to a Storefront resource. + // The Address values here are valid when the event is emitted, but + // the state of the accounts they refer to may be changed outside of the + // NFTStorefront workflow, so be careful to check when using them. + // + pub event ListingAvailable( + storefrontAddress: Address, + listingResourceID: UInt64, + nftType: Type, + nftID: UInt64, + ftVaultType: Type, + price: UFix64 + ) + + // ListingCompleted + // The listing has been resolved. It has either been purchased, or removed and destroyed. + // + pub event ListingCompleted(listingResourceID: UInt64, storefrontResourceID: UInt64, purchased: Bool) + + // StorefrontStoragePath + // The location in storage that a Storefront resource should be located. + pub let StorefrontStoragePath: StoragePath + + // StorefrontPublicPath + // The public location for a Storefront link. + pub let StorefrontPublicPath: PublicPath + + + // SaleCut + // A struct representing a recipient that must be sent a certain amount + // of the payment when a token is sold. + // + pub struct SaleCut { + // The receiver for the payment. + // Note that we do not store an address to find the Vault that this represents, + // as the link or resource that we fetch in this way may be manipulated, + // so to find the address that a cut goes to you must get this struct and then + // call receiver.borrow()!.owner.address on it. + // This can be done efficiently in a script. + pub let receiver: Capability<&{FungibleToken.Receiver}> + + // The amount of the payment FungibleToken that will be paid to the receiver. + pub let amount: UFix64 + + // initializer + // + init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) { + self.receiver = receiver + self.amount = amount + } + } + + + // ListingDetails + // A struct containing a Listing's data. + // + pub struct ListingDetails { + // The Storefront that the Listing is stored in. + // Note that this resource cannot be moved to a different Storefront, + // so this is OK. If we ever make it so that it *can* be moved, + // this should be revisited. + pub var storefrontID: UInt64 + // Whether this listing has been purchased or not. + pub var purchased: Bool + // The Type of the NonFungibleToken.NFT that is being listed. + pub let nftType: Type + // The ID of the NFT within that type. + pub let nftID: UInt64 + // The Type of the FungibleToken that payments must be made in. + pub let salePaymentVaultType: Type + // The amount that must be paid in the specified FungibleToken. + pub let salePrice: UFix64 + // This specifies the division of payment between recipients. + pub let saleCuts: [SaleCut] + + // setToPurchased + // Irreversibly set this listing as purchased. + // + access(contract) fun setToPurchased() { + self.purchased = true + } + + // initializer + // + init ( + nftType: Type, + nftID: UInt64, + salePaymentVaultType: Type, + saleCuts: [SaleCut], + storefrontID: UInt64 + ) { + self.storefrontID = storefrontID + self.purchased = false + self.nftType = nftType + self.nftID = nftID + self.salePaymentVaultType = salePaymentVaultType + + // Store the cuts + assert(saleCuts.length > 0, message: "Listing must have at least one payment cut recipient") + self.saleCuts = saleCuts + + // Calculate the total price from the cuts + var salePrice = 0.0 + // Perform initial check on capabilities, and calculate sale price from cut amounts. + for cut in self.saleCuts { + // Make sure we can borrow the receiver. + // We will check this again when the token is sold. + cut.receiver.borrow() + ?? panic("Cannot borrow receiver") + // Add the cut amount to the total price + salePrice = salePrice + cut.amount + } + assert(salePrice > 0.0, message: "Listing must have non-zero price") + + // Store the calculated sale price + self.salePrice = salePrice + } + } + + + // ListingPublic + // An interface providing a useful public interface to a Listing. + // + pub resource interface ListingPublic { + // borrowNFT + // This will assert in the same way as the NFT standard borrowNFT() + // if the NFT is absent, for example if it has been sold via another listing. + // + pub fun borrowNFT(): auth &NonFungibleToken.NFT + + + // purchase + // Purchase the listing, buying the token. + // This pays the beneficiaries and returns the token to the buyer. + // + pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT + + // getDetails + // + pub fun getDetails(): ListingDetails + + pub fun getListingResourceID(): UInt64 + } + + + // Listing + // A resource that allows an NFT to be sold for an amount of a given FungibleToken, + // and for the proceeds of that sale to be split between several recipients. + // + pub resource Listing: ListingPublic { + // The simple (non-Capability, non-complex) details of the sale + access(self) let details: ListingDetails + + // A capability allowing this resource to withdraw the NFT with the given ID from its collection. + // This capability allows the resource to withdraw *any* NFT, so you should be careful when giving + // such a capability to a resource and always check its code to make sure it will use it in the + // way that it claims. + access(contract) let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + + // borrowNFT + // This will assert in the same way as the NFT standard borrowNFT() + // if the NFT is absent, for example if it has been sold via another listing. + // + pub fun borrowNFT(): auth &NonFungibleToken.NFT { + let ref = self.nftProviderCapability.borrow()!.borrowNFT(id: self.getDetails().nftID) + // let ref = .borrowNFT(id: self.getDetails().nftID) + //- CANNOT DO THIS IN PRECONDITION: "member of restricted type is not accessible: isInstance" + // result.isInstance(self.getDetails().nftType): "token has wrong type" + assert(ref.isInstance(self.getDetails().nftType), message: "token has wrong type") + assert(ref.id == self.getDetails().nftID, message: "token has wrong ID") + + return ref + } + + // getDetails + // Get the details of the current state of the Listing as a struct. + // This avoids having more public variables and getter methods for them, and plays + // nicely with scripts (which cannot return resources). + // + pub fun getDetails(): ListingDetails { + return self.details + } + + pub fun getListingResourceID(): UInt64 { + return self.uuid + } + + + // purchase + // Purchase the listing, buying the token. + // This pays the beneficiaries and returns the token to the buyer. + // + pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT { + pre { + self.details.purchased == false: "listing has already been purchased" + payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token" + payment.balance == self.details.salePrice: "payment vault does not contain requested price" + } + + // Make sure the listing cannot be purchased again. + self.details.setToPurchased() + + // Fetch the token to return to the purchaser. + let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID) + // Neither receivers nor providers are trustworthy, they must implement the correct + // interface but beyond complying with its pre/post conditions they are not gauranteed + // to implement the functionality behind the interface in any given way. + // Therefore we cannot trust the Collection resource behind the interface, + // and we must check the NFT resource it gives us to make sure that it is the correct one. + assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type") + assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID") + + // Rather than aborting the transaction if any receiver is absent when we try to pay it, + // we send the cut to the first valid receiver. + // The first receiver should therefore either be the seller, or an agreed recipient for + // any unpaid cuts. + var residualReceiver: &{FungibleToken.Receiver}? = nil + + // Pay each beneficiary their amount of the payment. + for cut in self.details.saleCuts { + if let receiver = cut.receiver.borrow() { + let paymentCut <- payment.withdraw(amount: cut.amount) + receiver.deposit(from: <-paymentCut) + if (residualReceiver == nil) { + residualReceiver = receiver + } + } + } + + assert(residualReceiver != nil, message: "No valid payment receivers") + + // At this point, if all recievers were active and availabile, then the payment Vault will have + // zero tokens left, and this will functionally be a no-op that consumes the empty vault + residualReceiver!.deposit(from: <-payment) + + // If the listing is purchased, we regard it as completed here. + // Otherwise we regard it as completed in the destructor. + emit ListingCompleted( + listingResourceID: self.uuid, + storefrontResourceID: self.details.storefrontID, + purchased: self.details.purchased + ) + + return <-nft + } + + // destructor + // + destroy () { + // If the listing has not been purchased, we regard it as completed here. + // Otherwise we regard it as completed in purchase(). + // This is because we destroy the listing in Storefront.removeListing() + // or Storefront.cleanup() . + // If we change this destructor, revisit those functions. + if !self.details.purchased { + emit ListingCompleted( + listingResourceID: self.uuid, + storefrontResourceID: self.details.storefrontID, + purchased: self.details.purchased + ) + } + } + + // initializer + // + init ( + nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + nftType: Type, + nftID: UInt64, + salePaymentVaultType: Type, + saleCuts: [SaleCut], + storefrontID: UInt64 + ) { + // Store the sale information + self.details = ListingDetails( + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: saleCuts, + storefrontID: storefrontID + ) + + // Store the NFT provider + self.nftProviderCapability = nftProviderCapability + + // Check that the provider contains the NFT. + // We will check it again when the token is sold. + // We cannot move this into a function because initializers cannot call member functions. + let provider = self.nftProviderCapability.borrow() + assert(provider != nil, message: "cannot borrow nftProviderCapability") + + // This will precondition assert if the token is not available. + let nft = provider!.borrowNFT(id: self.details.nftID) + assert(nft.isInstance(self.details.nftType), message: "token is not of specified type") + assert(nft.id == self.details.nftID, message: "token does not have specified ID") + } + } + + // StorefrontManager + // An interface for adding and removing Listings within a Storefront, + // intended for use by the Storefront's own + // + pub resource interface StorefrontManager { + // createListing + // Allows the Storefront owner to create and insert Listings. + // + pub fun createListing( + nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + nftType: Type, + nftID: UInt64, + salePaymentVaultType: Type, + saleCuts: [SaleCut] + ): UInt64 + // removeListing + // Allows the Storefront owner to remove any sale listing, acepted or not. + // + pub fun removeListing(listingResourceID: UInt64) + } + + // StorefrontPublic + // An interface to allow listing and borrowing Listings, and purchasing items via Listings + // in a Storefront. + // + pub resource interface StorefrontPublic { + pub fun getListingIDs(): [UInt64] + pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? + pub fun cleanup(listingResourceID: UInt64) + pub fun getAllListingDetails(): {UInt64: NFTStorefront.ListingDetails} + } + + // Storefront + // A resource that allows its owner to manage a list of Listings, and anyone to interact with them + // in order to query their details and purchase the NFTs that they represent. + // + pub resource Storefront : StorefrontManager, StorefrontPublic { + // The dictionary of Listing uuids to Listing resources. + access(self) var listings: @{UInt64: Listing} + + // insert + // Create and publish a Listing for an NFT. + // + pub fun createListing( + nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + nftType: Type, + nftID: UInt64, + salePaymentVaultType: Type, + saleCuts: [SaleCut] + ): UInt64 { + let listing <- create Listing( + nftProviderCapability: nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: saleCuts, + storefrontID: self.uuid + ) + + let listingResourceID = listing.uuid + let listingPrice = listing.getDetails().salePrice + + // Add the new listing to the dictionary. + let oldListing <- self.listings[listingResourceID] <- listing + // Note that oldListing will always be nil, but we have to handle it. + destroy oldListing + + emit ListingAvailable( + storefrontAddress: self.owner?.address!, + listingResourceID: listingResourceID, + nftType: nftType, + nftID: nftID, + ftVaultType: salePaymentVaultType, + price: listingPrice + ) + + return listingResourceID + } + + // removeListing + // Remove a Listing that has not yet been purchased from the collection and destroy it. + // + pub fun removeListing(listingResourceID: UInt64) { + let listing <- self.listings.remove(key: listingResourceID) + ?? panic("missing Listing") + + // This will emit a ListingCompleted event. + destroy listing + } + + // getListingIDs + // Returns an array of the Listing resource IDs that are in the collection + // + pub fun getListingIDs(): [UInt64] { + return self.listings.keys + } + + pub fun getAllListingDetails(): {UInt64: NFTStorefront.ListingDetails} { + var ret: {UInt64: NFTStorefront.ListingDetails} = {} + + for key in self.listings.keys { + let listingRef = self.borrowListing(listingResourceID: key) as &Listing{ListingPublic}? + if listingRef != nil { + ret[key] = listingRef!.getDetails() + } + } + return ret + } + + // borrowSaleItem + // Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection. + // + pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? { + if self.listings[listingResourceID] != nil { + return &self.listings[listingResourceID] as! &Listing{ListingPublic} + } else { + return nil + } + } + + // cleanup + // Remove an listing *if* it has been purchased. + // Anyone can call, but at present it only benefits the account owner to do so. + // Kind purchasers can however call it if they like. + // + pub fun cleanup(listingResourceID: UInt64) { + pre { + self.listings[listingResourceID] != nil: "could not find listing with given id" + } + + let listing <- self.listings.remove(key: listingResourceID)! + assert(listing.getDetails().purchased == true, message: "listing is not purchased, only admin can remove") + destroy listing + } + + // destructor + // + destroy () { + destroy self.listings + + // Let event consumers know that this storefront will no longer exist + emit StorefrontDestroyed(storefrontResourceID: self.uuid) + } + + // constructor + // + init () { + self.listings <- {} + + // Let event consumers know that this storefront exists + emit StorefrontInitialized(storefrontResourceID: self.uuid) + } + } + + // createStorefront + // Make creating a Storefront publicly accessible. + // + pub fun createStorefront(): @Storefront { + return <-create Storefront() + } + + init () { + self.StorefrontStoragePath = /storage/NFTStorefront + self.StorefrontPublicPath = /public/NFTStorefront + + emit NFTStorefrontInitialized() + } +} + \ No newline at end of file diff --git a/cadence/contracts/NonFungibleToken.cdc b/cadence/contracts/NonFungibleToken.cdc new file mode 100644 index 0000000..656529b --- /dev/null +++ b/cadence/contracts/NonFungibleToken.cdc @@ -0,0 +1,98 @@ +pub contract interface NonFungibleToken { + + // The total number of tokens of this type in existence + pub var totalSupply: UInt64 + + // Event that emitted when the NFT contract is initialized + // + pub event ContractInitialized() + + // Event that is emitted when a token is withdrawn, + // indicating the owner of the collection that it was withdrawn from. + // + // If the collection is not in an account's storage, `from` will be `nil`. + // + pub event Withdraw(id: UInt64, from: Address?) + + // Event that emitted when a token is deposited to a collection. + // + // It indicates the owner of the collection that it was deposited to. + // + pub event Deposit(id: UInt64, to: Address?) + + // Interface that the NFTs have to conform to + // + pub resource interface INFT { + // The unique ID that each NFT has + pub let id: UInt64 + } + + // Requirement that all conforming NFT smart contracts have + // to define a resource called NFT that conforms to INFT + pub resource NFT: INFT { + pub let id: UInt64 + } + + // Interface to mediate withdraws from the Collection + // + pub resource interface Provider { + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT { + post { + result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" + } + } + } + + // Interface to mediate deposits to the Collection + // + pub resource interface Receiver { + + // deposit takes an NFT as an argument and adds it to the Collection + // + pub fun deposit(token: @NFT) + } + + // Interface that an account would commonly + // publish for their collection + pub resource interface CollectionPublic { + pub fun deposit(token: @NFT) + pub fun getIDs(): [UInt64] + pub fun borrowNFT(id: UInt64): auth &NFT + } + + // Requirement for the the concrete resource type + // to be declared in the implementing contract + // + pub resource Collection: Provider, Receiver, CollectionPublic { + + // Dictionary to hold the NFTs in the Collection + pub var ownedNFTs: @{UInt64: NFT} + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NFT) + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] + + // Returns a borrowed reference to an NFT in the collection + // so that the caller can read data and call methods from it + pub fun borrowNFT(id: UInt64): auth &NFT { + pre { + self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" + } + } + } + + // createEmptyCollection creates an empty Collection + // and returns it to the caller so that they can own NFTs + pub fun createEmptyCollection(): @Collection { + post { + result.getIDs().length == 0: "The created collection must be empty!" + } + } +} \ No newline at end of file diff --git a/cadence/contracts/PackNFT.cdc b/cadence/contracts/PackNFT.cdc new file mode 100644 index 0000000..b9369c6 --- /dev/null +++ b/cadence/contracts/PackNFT.cdc @@ -0,0 +1,151 @@ +import NonFungibleToken from "./NonFungibleToken.cdc" +import DappyContract from "./DappyContract.cdc" + +pub contract PackNFT: NonFungibleToken { + + pub let CollectionStoragePath: StoragePath + pub let CollectionPublicPath: PublicPath + pub let CollectionPrivatePath: PrivatePath + + pub var totalSupply: UInt64 + + pub event ContractInitialized() + + pub event Withdraw(id: UInt64, from: Address?) + + pub event Deposit(id: UInt64, to: Address?) + + pub resource NFT: NonFungibleToken.INFT { + + pub let id: UInt64 + access(self) var nft: @{UInt64: DappyContract.Dappy} + access(self) var dappyIDs: [UInt64] + access(self) var data: {UInt64: DappyContract.Template} + + pub init(nft: @{UInt64: DappyContract.Dappy}) { + + self.dappyIDs = nft.keys + + let temp: @{UInt64: DappyContract.Dappy} <- {} + self.data = {} + for key in nft.keys { + let x <- nft[key] <- nil + self.data[key] = x?.data + let old <- temp[key] <- x + destroy old + } + self.nft <- temp + destroy nft + + PackNFT.totalSupply = PackNFT.totalSupply + (1 as UInt64) + self.id = PackNFT.totalSupply + + } + + pub fun withdrawDappies(): @{UInt64: DappyContract.Dappy} { + let ret: @{UInt64: DappyContract.Dappy} <- {} + for id in self.dappyIDs { + let x <- self.nft[id] <- nil + let old <- ret[id] <- x + destroy old + } + self.dappyIDs = [] + self.data = {} + return <- ret + + // let ret <- self.nft <-nil + // return <- ret + } + + pub fun getIDs(): [UInt64] { + return self.dappyIDs + } + + pub fun getData(): {UInt64: DappyContract.Template} { + return self.data + } + + destroy () { + destroy self.nft + } + } + + pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { + + pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + + let token <- self.ownedNFTs.remove(key: withdrawID) + ?? panic("Cannot withdraw: NFT does not exist in the collection") + emit Withdraw(id: token.id, from:self.owner?.address) + return <-token + + } + + pub fun deposit(token: @NonFungibleToken.NFT) { + + let token <- token + let id = token.id + let oldToken <- self.ownedNFTs[id] <- token + + if self.owner?.address != nil { + emit Deposit(id: id, to: self.owner?.address) + } + + destroy oldToken + + } + + pub fun getIDs(): [UInt64] { + + return self.ownedNFTs.keys + + } + + pub fun borrowNFT(id: UInt64): auth &NonFungibleToken.NFT { + + return &self.ownedNFTs[id] as auth &NonFungibleToken.NFT + + } + + init () { + + self.ownedNFTs <- {} + + } + + destroy() { + + destroy self.ownedNFTs + + } + + } + + pub fun createEmptyCollection(): @Collection { + + return <-create Collection() + + } + + pub fun createFromDappies(dappies: @{UInt64: + DappyContract.Dappy}): @NFT { + + return <- create NFT( + nft: <- dappies + ) + + } + + init() { + + self.CollectionStoragePath = /storage/PackNFTCollection + self.CollectionPublicPath = /public/PackNFTCollection + self.CollectionPrivatePath = /private/PackNFTCollection + + self.totalSupply = 0 + + } +} + \ No newline at end of file diff --git a/cadence/scripts/ListGalleryCollection.cdc b/cadence/scripts/ListGalleryCollection.cdc new file mode 100644 index 0000000..b2158a0 --- /dev/null +++ b/cadence/scripts/ListGalleryCollection.cdc @@ -0,0 +1,19 @@ +import GalleryContract from "../contracts/GalleryContract.cdc" + +pub fun main(galleryAddress: Address): {UInt64: GalleryContract.GalleryData} { + + let account = getAccount(galleryAddress) + + let galleryRef = account + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath + ) + .borrow() + ?? panic ("Could not borrow Gallery ref") + + let galleryCollection = galleryRef.getGalleryCollection() + + return galleryCollection + +} + \ No newline at end of file diff --git a/cadence/scripts/ListStorefrontListings.cdc b/cadence/scripts/ListStorefrontListings.cdc new file mode 100644 index 0000000..626ba3c --- /dev/null +++ b/cadence/scripts/ListStorefrontListings.cdc @@ -0,0 +1,30 @@ +import NFTStorefront from "../contracts/NFTStorefront.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import DappyContract from "../contracts/DappyContract.cdc" + +pub fun main(addr: Address): {UInt64: NFTStorefront.ListingDetails} { + + let account = getAccount(addr) + + let storefrontRef = account + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath + ) + .borrow() + ?? panic ("Could not borrow StorefrontPublic ref") + + let listings = storefrontRef.getListingIDs() + + let ret: {UInt64: NFTStorefront.ListingDetails} = {} + for id in listings { + + let listingRef = storefrontRef.borrowListing(listingResourceID: id) + + ret[id] = listingRef?.getDetails() + } + + return ret + +} + \ No newline at end of file diff --git a/cadence/scripts/Unsafe.cdc b/cadence/scripts/Unsafe.cdc new file mode 100644 index 0000000..653c6c0 --- /dev/null +++ b/cadence/scripts/Unsafe.cdc @@ -0,0 +1,3 @@ +pub fun main(): AnyStruct { + return unsafeRandom() +} diff --git a/cadence/tests/Storefront.test.js b/cadence/tests/Storefront.test.js new file mode 100644 index 0000000..4145523 --- /dev/null +++ b/cadence/tests/Storefront.test.js @@ -0,0 +1,271 @@ +import path from "path" +import { + emulator, + init, + executeScript, + getAccountAddress, + mintFlow +} from "flow-js-testing" +import * as storefront from "./src/Storefront"; +import * as dappyContract from "./src/DappyContract"; +import { fundAccountWithFUSD, createFUSDVault, mintFUSD, getFUSDBalance } from "./src/FUSD"; + +jest.setTimeout(50000); + +const TEST_DAPPY = { + templateID: 1, + dna: "FF5A9D.FFE922.60C5E5.0", + name: "Panda Dappy", + price: "7.00000000" +} + +const TEST_FAMILY = { + name: "Pride Dappies", + price: "30.00000000", + familyID: 1 +} + +describe("NFTStorefront", () => { + beforeEach(async () => { + const basePath = path.resolve(__dirname, "../"); + const port = 8080; + init(basePath, port); + return emulator.start(port, false); + }); + + afterEach(async () => { + return emulator.stop(); + }); + + it("deploys Storefront contract", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await storefront.deployNFTStorefront() + }); + + it("deploys DappyNFT contract", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + await mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + }); + + it("Should create 1 DappyNFT collection", async () => { + const recipient = await getAccountAddress("DappyRecipient") + await mintFlow(recipient, "10.0") + let DappyAdmin = await dappyContract.getDappyAdminAddress() + await mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + await storefront.createDappyNFTCollection(recipient) + }); + + it("creates Admin Gallery", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + await mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + await storefront.deployPackNFT() + await storefront.deployNFTStorefront() + await storefront.deployGalleryContract() + await storefront.createAdminGallery(DappyAdmin) + }); + + it("Should list 1 dappy for sale", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + await storefront.deployPackNFT() + await storefront.deployNFTStorefront() + await storefront.deployGalleryContract() + + await dappyContract.createDappyTemplate(TEST_DAPPY) + const recipient = await getAccountAddress("DappyRecipient") + await mintFlow(recipient, "10.0") + await fundAccountWithFUSD(recipient, "100.00") + await createFUSDVault(DappyAdmin) + await mintFUSD(DappyAdmin, "100.00") + await dappyContract.createDappyCollection(recipient) + await dappyContract.mintDappy(recipient, TEST_DAPPY) + await storefront.createDappyNFTCollection(recipient) + await storefront.createNFTStorefront(recipient) + + const saleCuts = { + [DappyAdmin]: "1.0", + [recipient]: "8.0" + } + + const salePrice = "11.0" + + await storefront.createAdminGallery(DappyAdmin) + await storefront.putDappyInStorefront(recipient, 1, salePrice) // list dappy with id=1 for sale + + const allListingDetails = await storefront.listStorefrontListings(recipient) + const dappyID = Object.values(allListingDetails)[0].nftID + expect(dappyID).toBe(1) + const listingResourceID = parseInt(Object.keys(allListingDetails)[0]) + + await storefront.addGalleryListing(listingResourceID, recipient) + + const gallery = await storefront.listGalleryCollection() + + const nftID = Object.values(gallery)[0].listingDetails.nftID + const dappyCollection = Object.values(gallery)[0].dappyCollection + const sellerAddress = Object.values(gallery)[0].sellerAddress + const galleryID = parseInt(Object.keys(gallery)[0]) + expect(nftID).toBe(1) + expect(sellerAddress).toBe(recipient) + expect(parseInt(Object.keys(dappyCollection)[0])).toBe(nftID) + expect(galleryID).toBe(listingResourceID) + + // emulator.setLogging(true) + // console.log(allListingDetails) + // console.log(gallery) + // console.log(dappyCollection) + }); + + + it("Should purchase 1 dappy", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + await storefront.deployPackNFT() + await storefront.deployNFTStorefront() + await storefront.deployGalleryContract() + + await dappyContract.createDappyTemplate(TEST_DAPPY) + const recipient = await getAccountAddress("DappyRecipient") + await mintFlow(recipient, "10.0") + await fundAccountWithFUSD(recipient, "100.00") + await createFUSDVault(DappyAdmin) + await mintFUSD(DappyAdmin, "100.00") + await dappyContract.createDappyCollection(recipient) + await dappyContract.mintDappy(recipient, TEST_DAPPY) + await storefront.createDappyNFTCollection(recipient) + await storefront.createNFTStorefront(recipient) + + const salePrice = "11.0" + await storefront.createAdminGallery(DappyAdmin) + await storefront.putDappyInStorefront(recipient, 1, salePrice) // list dappy with id=1 for sale + let allListingDetails = await storefront.listStorefrontListings(recipient) + const dappyID = Object.values(allListingDetails)[0].nftID + expect(dappyID).toBe(1) + const listingResourceID = parseInt(Object.keys(allListingDetails)[0]) + await storefront.addGalleryListing(listingResourceID, recipient) + let gallery = await storefront.listGalleryCollection() + + const galleryID = parseInt(Object.keys(gallery)[0]) + expect(galleryID).toBe(listingResourceID) + + const buyer = await getAccountAddress("DappyBuyer") + await mintFlow(buyer, "10.0") + await createFUSDVault(buyer) + await mintFUSD(buyer, "100.00") + await dappyContract.createDappyCollection(buyer) + + await storefront.purchaseDappyStorefront(buyer, galleryID, recipient) + + await storefront.removeGalleryListing(galleryID, recipient) + + gallery = await storefront.listGalleryCollection() + allListingDetails = await storefront.listStorefrontListings(recipient) + expect(Object.keys(gallery).length).toBe(0) + expect(Object.keys(allListingDetails).length).toBe(1) + expect(Object.values(allListingDetails)[0].purchased).toBe(true) + + await storefront.cleanupStorefront(listingResourceID, recipient) + allListingDetails = await storefront.listStorefrontListings(recipient) + + expect(Object.keys(allListingDetails).length).toBe(0) + + // emulator.setLogging(true) + }); + + it("Should purchase 1 pack of 3 dappies", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() + await storefront.deployPackNFT() + await storefront.deployNFTStorefront() + await storefront.deployGalleryContract() + + await dappyContract.createDappyTemplate(TEST_DAPPY) + await dappyContract.createDappyFamily(TEST_FAMILY) + await dappyContract.addTemplateToFamily(TEST_FAMILY, TEST_DAPPY) + + const recipient = await getAccountAddress("DappyRecipient") + await mintFlow(recipient, "10.0") + await fundAccountWithFUSD(recipient, "100.00") + await createFUSDVault(DappyAdmin) + await mintFUSD(DappyAdmin, "100.00") + + await dappyContract.createDappyCollection(recipient) + const templateIDs = Array(3).fill(TEST_DAPPY.templateID) + await dappyContract.batchMintDappyFromFamily(TEST_FAMILY.familyID, templateIDs, TEST_FAMILY.price, recipient) + const userDappies = await dappyContract.listUserDappies(recipient) + expect(Object.keys(userDappies)).toHaveLength(templateIDs.length) + // console.log(userDappies) + let dappyIDs = Object.keys(userDappies).map( (value) => parseInt(value)) + + await storefront.createPackNFTCollection(recipient) + await storefront.createNFTStorefront(recipient) + + const salePrice = "110.0" + await storefront.createAdminGallery(DappyAdmin) + await storefront.putPackInStorefront(recipient, dappyIDs, salePrice) + + let allListingDetails = await storefront.listStorefrontListings(recipient) + const packID = Object.values(allListingDetails)[0].nftID + expect(packID).toBe(1) + + const listingResourceID = parseInt(Object.keys(allListingDetails)[0]) + await storefront.addGalleryListing(listingResourceID, recipient) + let gallery = await storefront.listGalleryCollection() + + const galleryID = parseInt(Object.keys(gallery)[0]) + expect(galleryID).toBe(listingResourceID) + + const buyer = await getAccountAddress("DappyBuyer") + await mintFlow(buyer, "10.0") + await createFUSDVault(buyer) + let beforeBalance = 200.00 + await mintFUSD(buyer, beforeBalance.toFixed(8) ) + await dappyContract.createDappyCollection(buyer) + + await storefront.purchasePackStorefront(buyer, galleryID, recipient) + + const dappyReceived = await dappyContract.listUserDappies(buyer) + expect(Object.keys(dappyReceived).length).toBe(3) + expect(dappyReceived['3']).toEqual(TEST_DAPPY) + + await storefront.removeGalleryListing(galleryID, recipient) + + gallery = await storefront.listGalleryCollection() + allListingDetails = await storefront.listStorefrontListings(recipient) + expect(Object.keys(gallery).length).toBe(0) + expect(Object.keys(allListingDetails).length).toBe(1) + expect(Object.values(allListingDetails)[0].purchased).toBe(true) + + await storefront.cleanupStorefront(listingResourceID, recipient) + allListingDetails = await storefront.listStorefrontListings(recipient) + + expect(Object.keys(allListingDetails).length).toBe(0) + let afterBalance = parseFloat(await getFUSDBalance(buyer)) + + expect(afterBalance).toBe( beforeBalance - salePrice) + + // emulator.setLogging(true) + }); + +}) + \ No newline at end of file diff --git a/cadence/tests/jest.config.js b/cadence/tests/jest.config.js index dfb7ef3..0c58436 100644 --- a/cadence/tests/jest.config.js +++ b/cadence/tests/jest.config.js @@ -2,4 +2,10 @@ module.exports = { testEnvironment: "node", verbose: true, coveragePathIgnorePatterns: ["/node_modules/"], + projects: [{ + "displayName": "Dappy Cadence Tests", + // "testMatch": ["/**/*.test.js"], + "testMatch": ["/**/Storefront.test.js"], + // "testMatch": ["/**/DappyContract.test.js"] + }] }; \ No newline at end of file diff --git a/cadence/tests/package-lock.json b/cadence/tests/package-lock.json index 2c7390d..ff5c2c8 100644 --- a/cadence/tests/package-lock.json +++ b/cadence/tests/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "tests", "version": "1.0.0", "license": "ISC", "devDependencies": { diff --git a/cadence/tests/package.json b/cadence/tests/package.json index aa3d457..4bdc10e 100644 --- a/cadence/tests/package.json +++ b/cadence/tests/package.json @@ -4,7 +4,7 @@ "description": "", "main": "deploy.test.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest --runInBand --verbose" }, "keywords": [], "author": "", diff --git a/cadence/tests/src/DappyContract.js b/cadence/tests/src/DappyContract.js index a12943f..d8484db 100644 --- a/cadence/tests/src/DappyContract.js +++ b/cadence/tests/src/DappyContract.js @@ -11,7 +11,9 @@ export const getDappyAdminAddress = async () => getAccountAddress("DappyAdmin") export const deployDappyContract = async () => { const DappyAdmin = await getAccountAddress("DappyAdmin") await mintFlow(DappyAdmin, "10.0") - const addressMap = { FungibleToken: "0xee82856bf20e2aa6" } + const addressMap = { + FungibleToken: "0xee82856bf20e2aa6" + } await deployContractByName({ to: DappyAdmin, name: "DappyContract", addressMap }) } diff --git a/cadence/tests/src/Storefront.js b/cadence/tests/src/Storefront.js new file mode 100644 index 0000000..787237c --- /dev/null +++ b/cadence/tests/src/Storefront.js @@ -0,0 +1,153 @@ +import { query } from "@onflow/fcl"; +import { + getAccountAddress, + mintFlow, + deployContractByName, + sendTransaction, + executeScript +} from "flow-js-testing" +import * as dappyContract from "./DappyContract"; + + +export const deployNonFungibleToken = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { FungibleToken: "0xee82856bf20e2aa6" } + await deployContractByName({ to: DappyAdmin, name: "NonFungibleToken", addressMap }) +} + +export const deployNFTStorefront = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { + FungibleToken: "0xee82856bf20e2aa6", + NonFungibleToken: DappyAdmin + } + await deployContractByName({ to: DappyAdmin, name: "NFTStorefront", addressMap }) +} + +export const deployDappyNFT = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { + FungibleToken: "0xee82856bf20e2aa6", + NonFungibleToken: DappyAdmin, + DappyContract: DappyAdmin + } + await deployContractByName({ to: DappyAdmin, name: "DappyNFT", addressMap }) +} + +export const deployPackNFT = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { + FungibleToken: "0xee82856bf20e2aa6", + NonFungibleToken: DappyAdmin, + DappyContract: DappyAdmin + } + await deployContractByName({ to: DappyAdmin, name: "PackNFT", addressMap }) +} + +export const createDappyNFTCollection = async(recipient) => { + const name = "CreateDappyNFTCollection" + const signers = [recipient] + await sendTransaction({ name, signers }) +} + +export const createPackNFTCollection = async(recipient) => { + const name = "CreatePackNFTCollection" + const signers = [recipient] + await sendTransaction({ name, signers }) +} + +export const createNFTStorefront = async(recipient) => { + const name = "CreateNFTStorefront" + const signers = [recipient] + await sendTransaction({ name, signers }) +} + +export const deployGalleryContract = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { + DappyContract: DappyAdmin, + NFTStorefront: DappyAdmin, + DappyNFT: DappyAdmin, + PackNFT: DappyAdmin, + } + await deployContractByName({ to: DappyAdmin, name: "GalleryContract", addressMap }) +} + +export const createAdminGallery = async(admin) => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "CreateAdminGallery" + const addressMap = { + GalleryContract: DappyAdmin, + } + const signers = [admin] + await sendTransaction({ name, signers, addressMap }) +} + +export const putDappyInStorefront = async (recipient, dappyID, salePrice) => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "PutDappyInStorefront" + const signers = [recipient] + const args = [dappyID, salePrice, DappyAdmin] + await sendTransaction({ name, signers, args }) +} + +export const putPackInStorefront = async (recipient, dappyIDs, salePrice) => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "PutPackInStorefront" + const signers = [recipient] + const args = [dappyIDs, salePrice, DappyAdmin] + await sendTransaction({ name, signers, args }) +} + +export const listStorefrontListings = async (recipient) => { + const name = "ListStorefrontListings" + const args = [recipient] + const allListingDetails = await executeScript({ name, args }) + return allListingDetails +} + +export const listGalleryCollection = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "ListGalleryCollection" + const args = [DappyAdmin] + const gallery = await executeScript({ name, args }) + return gallery +} + +export const purchaseDappyStorefront = async (buyerAddress, listingResourceID, sellerAddress) => { + const name = "PurchaseDappyStorefront" + const signers = [buyerAddress] + const args = [listingResourceID, sellerAddress] + await sendTransaction({ name, signers, args }) +} + +export const purchasePackStorefront = async (buyerAddress, listingResourceID, sellerAddress) => { + const name = "PurchasePackStorefront" + const signers = [buyerAddress] + const args = [listingResourceID, sellerAddress] + await sendTransaction({ name, signers, args }) +} + +export const cleanupStorefront = async (listingResourceID, sellerAddress) => { + const buyer = await getAccountAddress("DappyBuyer") + const name = "CleanUpStorefront" + const signers = [buyer] + const args = [listingResourceID, sellerAddress] + await sendTransaction({ name, signers, args }) +} + +export const addGalleryListing = async (listingResourceID, recipient) => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "AddGalleryListing" + const signers = [recipient] + const args = [DappyAdmin, listingResourceID] + await sendTransaction({ name, signers, args }) +} + +export const removeGalleryListing = async (galleryID, recipient) => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "RemoveGalleryListing" + const signers = [recipient] + const args = [DappyAdmin, galleryID] + await sendTransaction({ name, signers, args }) +} diff --git a/cadence/transactions/AddGalleryListing.cdc b/cadence/transactions/AddGalleryListing.cdc new file mode 100644 index 0000000..c2bea63 --- /dev/null +++ b/cadence/transactions/AddGalleryListing.cdc @@ -0,0 +1,40 @@ +import GalleryContract from "../contracts/GalleryContract.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +transaction(adminAddress: Address, listingResourceID: UInt64) { + + let listingPublic: &{NFTStorefront.ListingPublic} + let galleryRef: &{GalleryContract.GalleryPublic} + let sellerAddress: Address + + prepare (acct: AuthAccount) + { + + let adminAccount = getAccount(adminAddress) + self.sellerAddress = acct.address + + self.galleryRef = adminAccount + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath) + .borrow() + ?? panic("Could not borrow Gallery ref") + + let storefrontRef = acct + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath + ) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + self.listingPublic = storefrontRef.borrowListing(listingResourceID: listingResourceID) + ?? panic ("Could not borrow Listing ref") + } + + execute { + self.galleryRef.addListing( + listingPublic: self.listingPublic, + sellerAddress: self.sellerAddress + ) + } + +} \ No newline at end of file diff --git a/cadence/transactions/BreedDappies.cdc b/cadence/transactions/BreedDappies.cdc new file mode 100644 index 0000000..f7462af --- /dev/null +++ b/cadence/transactions/BreedDappies.cdc @@ -0,0 +1,139 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import FUSD from "../contracts/FUSD.cdc" + +transaction (maleID: UInt64, femaleID: UInt64) { + + let collectionRef: &DappyContract.Collection + let dappies: {UInt64: DappyContract.Template} + let vaultRef: &FUSD.Vault + var random: UInt64 + let step: UInt64 + + // Dappy DNA MUST be parsed into array before passing to this transaction + let maleID: UInt64 + let femaleID: UInt64 + + prepare(acct: AuthAccount) { + self.maleID = maleID + self.femaleID = femaleID + self.random = 52090100 // to test on playground + self.step = 77 // to test on playground + fun reconstructDNA(_ prepDNA: [String]): String { + pre { + prepDNA.length > 3: "DNA must have at least 4 sequences" + prepDNA.length < 7: "DNA must have at most 6 sequences" + } + var construct:String = "" + let n = prepDNA.length + var i = 0 + while i < n - 1 { + construct = construct.concat(prepDNA[i]).concat(".") + i = i + 1 + } + construct = construct.concat(prepDNA[n-1]) + return construct + } + + self.collectionRef = acct.borrow<&DappyContract.Collection>(from: DappyContract.CollectionStoragePath) + ?? panic("Could not borrow collection ref") + self.dappies =self.collectionRef.listDappies() + self.dappies[self.maleID]??panic("Male ID does not exist in collection") + self.dappies[self.femaleID]??panic("Female ID does not exist in collection") + self.vaultRef = acct.borrow<&FUSD.Vault>(from: /storage/fusdVault) ?? panic("Could not borrow FUSD vault") + + } + + execute { + fun parseDNA(_ dna: String): [String] { + var i = 0 + var buffer = "" + var ret: [String] = [] + while i < dna.length { + let c = dna.slice(from: i, upTo: i+1) + if c != "." { + buffer = buffer.concat( c ) + } else { + ret.append(buffer) + buffer = "" + } + i = i + 1 + } + if buffer != "" { + ret.append(buffer) + } + return ret + } + fun luck(_ mod: UInt64): UInt64 { + let play = false // unsafeRandom will not work in playground + let x: UInt64 = play? self.random : unsafeRandom() + self.random = self.random + self.step + return x - x/mod * mod + } + fun max(_ a: UInt64, _ b: UInt64): UInt64 { + return a>b ? a:b + } + fun min(_ a: UInt64, _ b: UInt64): UInt64 { + return a( + NFTStorefront.StorefrontPublicPath) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + } + + execute { + + self.storefrontRef.cleanup(listingResourceID: listingResourceID) + + } +} + \ No newline at end of file diff --git a/cadence/transactions/CreateAdminGallery.cdc b/cadence/transactions/CreateAdminGallery.cdc new file mode 100644 index 0000000..8f7209b --- /dev/null +++ b/cadence/transactions/CreateAdminGallery.cdc @@ -0,0 +1,19 @@ +import GalleryContract from "../contracts/GalleryContract.cdc" + +transaction { + prepare(acct: AuthAccount) { + + let oldGallery <- acct.load<@AnyResource>(from: GalleryContract.GalleryStoragePath) + + destroy oldGallery + + acct.unlink(GalleryContract.GalleryPublicPath) + + let gallery <- GalleryContract.createEmptyGallery() + + acct.save(<-gallery, to: GalleryContract.GalleryStoragePath) + + acct.link<&{GalleryContract.GalleryPublic}>(GalleryContract.GalleryPublicPath, target: GalleryContract.GalleryStoragePath) + + } +} \ No newline at end of file diff --git a/cadence/transactions/CreateCollection.cdc b/cadence/transactions/CreateCollection.cdc index bcede04..fefa922 100644 --- a/cadence/transactions/CreateCollection.cdc +++ b/cadence/transactions/CreateCollection.cdc @@ -1,9 +1,69 @@ import DappyContract from "../contracts/DappyContract.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import PackNFT from "../contracts/PackNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" transaction { prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@AnyResource>(from: DappyContract.CollectionStoragePath) + destroy collectionRef + acct.unlink(DappyContract.CollectionPublicPath) + + // DappyNFT + + let dappyCollectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + destroy dappyCollectionRef + acct.unlink(DappyNFT.CollectionPublicPath) + acct.unlink(DappyNFT.CollectionPrivatePath) + + // PackNFT + + let packCollectionRef <- acct.load<@PackNFT.Collection>(from: PackNFT.CollectionStoragePath) + destroy packCollectionRef + acct.unlink(PackNFT.CollectionPublicPath) + acct.unlink(PackNFT.CollectionPrivatePath) + + // Storefront + + let storefrontRef <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + destroy storefrontRef + acct.unlink(NFTStorefront.StorefrontPublicPath) + + // Creation section + // + // + let collection <- DappyContract.createEmptyCollection() acct.save<@DappyContract.Collection>(<-collection, to: DappyContract.CollectionStoragePath) acct.link<&{DappyContract.CollectionPublic}>(DappyContract.CollectionPublicPath, target: DappyContract.CollectionStoragePath) + + // DappyNFT Collection + + let dappyCollection <- DappyNFT.createEmptyCollection() + acct.save<@DappyNFT.Collection>(<-dappyCollection, to: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPublicPath, target: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPrivatePath, target: DappyNFT.CollectionStoragePath) + + // PackNFT Collection + + let packCollection <- PackNFT.createEmptyCollection() + acct.save<@PackNFT.Collection>(<-packCollection, to: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPublicPath, target: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPrivatePath, target: PackNFT.CollectionStoragePath) + + // NFTStorefront + + let storefront <- NFTStorefront.createStorefront() + + acct.save<@NFTStorefront.Storefront>(<- storefront, to: NFTStorefront.StorefrontStoragePath) + + acct.link<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath, target: NFTStorefront.StorefrontStoragePath) + } } \ No newline at end of file diff --git a/cadence/transactions/CreateDappyNFTCollection.cdc b/cadence/transactions/CreateDappyNFTCollection.cdc new file mode 100644 index 0000000..61ae51d --- /dev/null +++ b/cadence/transactions/CreateDappyNFTCollection.cdc @@ -0,0 +1,15 @@ +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" + +transaction { + prepare(acct: AuthAccount) { + + let collection <- DappyNFT.createEmptyCollection() + acct.save<@DappyNFT.Collection>(<-collection, to: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPublicPath, target: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPrivatePath, target: DappyNFT.CollectionStoragePath) + + } +} \ No newline at end of file diff --git a/cadence/transactions/CreateNFTStorefront.cdc b/cadence/transactions/CreateNFTStorefront.cdc new file mode 100644 index 0000000..fca08db --- /dev/null +++ b/cadence/transactions/CreateNFTStorefront.cdc @@ -0,0 +1,16 @@ +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +transaction { + + prepare(acct: AuthAccount) { + + let storefront <- NFTStorefront.createStorefront() + + acct.save<@NFTStorefront.Storefront>(<- storefront, to: NFTStorefront.StorefrontStoragePath) + + acct.link<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath, target: NFTStorefront.StorefrontStoragePath) + + } + +} \ No newline at end of file diff --git a/cadence/transactions/CreatePackNFTCollection.cdc b/cadence/transactions/CreatePackNFTCollection.cdc new file mode 100644 index 0000000..c8de286 --- /dev/null +++ b/cadence/transactions/CreatePackNFTCollection.cdc @@ -0,0 +1,15 @@ +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import PackNFT from "../contracts/PackNFT.cdc" + +transaction { + prepare(acct: AuthAccount) { + + let collection <- PackNFT.createEmptyCollection() + acct.save<@PackNFT.Collection>(<-collection, to: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPublicPath, target: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPrivatePath, target: PackNFT.CollectionStoragePath) + + } +} \ No newline at end of file diff --git a/cadence/transactions/DeleteCollection.cdc b/cadence/transactions/DeleteCollection.cdc index 8710931..adac322 100644 --- a/cadence/transactions/DeleteCollection.cdc +++ b/cadence/transactions/DeleteCollection.cdc @@ -1,12 +1,35 @@ import DappyContract from "../contracts/DappyContract.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import PackNFT from "../contracts/PackNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" -transaction() { - prepare(acct: AuthAccount) { - let collectionRef <- acct.load<@DappyContract.Collection>(from: DappyContract.CollectionStoragePath) - ?? panic("Could not borrow collection reference") - destroy collectionRef - acct.unlink(DappyContract.CollectionPublicPath) - - } -} \ No newline at end of file + transaction() { + prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@AnyResource>(from: DappyContract.CollectionStoragePath) + destroy collectionRef + acct.unlink(DappyContract.CollectionPublicPath) + + // DappyNFT + + let dappyCollectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + destroy dappyCollectionRef + acct.unlink(DappyNFT.CollectionPublicPath) + acct.unlink(DappyNFT.CollectionPrivatePath) + + // PackNFT + + let packCollectionRef <- acct.load<@PackNFT.Collection>(from: PackNFT.CollectionStoragePath) + destroy packCollectionRef + acct.unlink(PackNFT.CollectionPublicPath) + acct.unlink(PackNFT.CollectionPrivatePath) + + // Storefront + + let storefront <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + destroy storefront + acct.unlink(NFTStorefront.StorefrontPublicPath) + + } + } \ No newline at end of file diff --git a/cadence/transactions/DeleteDappyNFTCollection.cdc b/cadence/transactions/DeleteDappyNFTCollection.cdc new file mode 100644 index 0000000..eda4e0e --- /dev/null +++ b/cadence/transactions/DeleteDappyNFTCollection.cdc @@ -0,0 +1,16 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +transaction() { + prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + ?? panic("Could not borrow collection reference") + + destroy collectionRef + + acct.unlink(DappyNFT.CollectionPublicPath) + + acct.unlink(DappyNFT.CollectionPrivatePath) + + } +} \ No newline at end of file diff --git a/cadence/transactions/DeleteNFTStorefront.cdc b/cadence/transactions/DeleteNFTStorefront.cdc new file mode 100644 index 0000000..672a746 --- /dev/null +++ b/cadence/transactions/DeleteNFTStorefront.cdc @@ -0,0 +1,12 @@ +import NFTStorefront from "../contracts/NFTStorefront.cdc" +transaction() { + prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + + destroy collectionRef + + acct.unlink(NFTStorefront.StorefrontPublicPath) + + } +} \ No newline at end of file diff --git a/cadence/transactions/PrepareDappyContract.cdc b/cadence/transactions/PrepareDappyContract.cdc new file mode 100644 index 0000000..bab7884 --- /dev/null +++ b/cadence/transactions/PrepareDappyContract.cdc @@ -0,0 +1,36 @@ +import FUSD from "../contracts/FUSD.cdc" +import DappyContract from "../contracts/DappyContract.cdc" + +// This transaction should be signed by DappyContract owner account +transaction() { + + prepare (acct: AuthAccount) { + + // create col + let admin = acct.borrow<&DappyContract.Admin>(from: DappyContract.AdminStoragePath) ?? panic("Admin") + + for id in DappyContract.listTemplates().keys { + admin.destroyTemplate(dappyID: id) + } + + admin.createTemplate( dna: "FF5A9D.FFE922.60C5E5.0", name: "Panda Dappy") + admin.createTemplate( dna: "94DFF6.F6ABBA.94DFF6.1", name: "Tranzi Dappy") + admin.createTemplate( dna: "74ee15.cae36f.6b6b49.7fc48f.0", name: "Queen Dappy") + admin.createTemplate( dna: "D61774.9D5098.1F429C.1", name: "Bibi Dappy") + admin.createTemplate( dna: "FF5A9D.FFAA47.FFE922.B6E927.60C5E5.7320D3", name: "Queery Dappy") + admin.createTemplate( dna: "F8EF38.8D5FA8.211F20.2", name: "Nobi Dappy") + admin.createTemplate( dna: "55fb59.b931ed.be7e39.519494.3", name: "Adonis Dappy") + admin.createTemplate( dna: "F571A4.972E90.18469E.211F20", name: "Fludi Dappy") + admin.createTemplate( dna: "BF1E6C.DA4A97.EA5CA3.FBE1E4.E84B56.4", name: "Lesli Dappy") + admin.createTemplate( dna: "A3A5A4.8D5FA8.211F20.2", name: "Asel Dappy") + admin.createTemplate( dna: "A3A5A4.BCDA84.211F20.2", name: "Agent Dappy") + admin.createTemplate( dna: "001DED.E84B56.211F20.2", name: "Polly Dappy") + admin.createTemplate( dna: "D50E8D.5BBD70.068DCF.0", name: "Poldi Dappy") + admin.createTemplate( dna: "df1f4f.ac069b.25443c.1922ff.1", name: "Lucienne Dappy") + admin.createTemplate( dna: "ad634b.798f9d.6c2af1.19a9f7.3", name: "Mohammad Dappy") + + log(DappyContract.listTemplates()) + + } + +} diff --git a/cadence/transactions/PurchaseDappyStorefront.cdc b/cadence/transactions/PurchaseDappyStorefront.cdc new file mode 100644 index 0000000..10d0e7e --- /dev/null +++ b/cadence/transactions/PurchaseDappyStorefront.cdc @@ -0,0 +1,58 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import FUSD from "../contracts/FUSD.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +transaction(listingResourceID:UInt64, sellerAddress: Address) { + // signed by buyer + + let storefrontRef: &{NFTStorefront.StorefrontPublic} + let receiverRef: &{DappyContract.CollectionPublic} + let vaultRef: &FungibleToken.Vault + let listingRef: &NFTStorefront.Listing{NFTStorefront.ListingPublic} + + prepare(acct: AuthAccount) { + + // Seller + let sellerAccount = getAccount(sellerAddress) + self.storefrontRef = sellerAccount + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + self.listingRef = self.storefrontRef.borrowListing(listingResourceID: listingResourceID) + ?? panic ("Could not borrow Listing ref") + + // Buyer + self.receiverRef = acct + .getCapability<&{DappyContract.CollectionPublic}>( + DappyContract.CollectionPublicPath) + .borrow() + ?? panic ("Could not borrow Dappy Collection ref") + + self.vaultRef = acct + .borrow<&FungibleToken.Vault>( + from: /storage/fusdVault) + ?? panic ("Could not borrow FUSD Vault ref") + + } + + execute { + + let amount = self.listingRef.getDetails().salePrice + let vault <- self.vaultRef.withdraw(amount: amount) + let nft <- self.listingRef.purchase(payment: <- vault) as! @DappyNFT.NFT + let dappy <- nft.withdrawDappy() + ?? panic("cannot withdraw Dappy from provider") + self.receiverRef.deposit(token: <- dappy) + destroy nft + + } +} + + // log("dappy.id") + // log(dappy.id) + // log("*************************") \ No newline at end of file diff --git a/cadence/transactions/PurchasePackStorefront.cdc b/cadence/transactions/PurchasePackStorefront.cdc new file mode 100644 index 0000000..f31f884 --- /dev/null +++ b/cadence/transactions/PurchasePackStorefront.cdc @@ -0,0 +1,63 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import FUSD from "../contracts/FUSD.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" +import PackNFT from "../contracts/PackNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +transaction(listingResourceID:UInt64, sellerAddress: Address) { + // signed by buyer + + let storefrontRef: &{NFTStorefront.StorefrontPublic} + let receiverRef: &{DappyContract.CollectionPublic} + let vaultRef: &FungibleToken.Vault + let listingRef: &NFTStorefront.Listing{NFTStorefront.ListingPublic} + + prepare(acct: AuthAccount) { + + // Seller + let sellerAccount = getAccount(sellerAddress) + self.storefrontRef = sellerAccount + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + self.listingRef = self.storefrontRef.borrowListing(listingResourceID: listingResourceID) + ?? panic ("Could not borrow Listing ref") + + assert ( + self.listingRef.getDetails().nftType == Type<@PackNFT.NFT>(), + message: "Purchase wrong NFT type" + ) + + + // Buyer + self.receiverRef = acct + .getCapability<&{DappyContract.CollectionPublic}>( + DappyContract.CollectionPublicPath) + .borrow() + ?? panic ("Could not borrow Dappy Collection ref") + + self.vaultRef = acct + .borrow<&FungibleToken.Vault>( + from: /storage/fusdVault) + ?? panic ("Could not borrow FUSD Vault ref") + + } + + execute { + + let amount = self.listingRef.getDetails().salePrice + let vault <- self.vaultRef.withdraw(amount: amount) + let nft <- self.listingRef.purchase(payment: <- vault) as! @PackNFT.NFT + let dappies <- nft.withdrawDappies() + for key in dappies.keys { + let x <- dappies[key] <- nil + self.receiverRef.deposit(token: <- x!) + } + destroy dappies + destroy nft + + } +} diff --git a/cadence/transactions/PutDappyInStorefront.cdc b/cadence/transactions/PutDappyInStorefront.cdc new file mode 100644 index 0000000..dd4fc0e --- /dev/null +++ b/cadence/transactions/PutDappyInStorefront.cdc @@ -0,0 +1,83 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import FUSD from "../contracts/FUSD.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" +import GalleryContract from "../contracts/GalleryContract.cdc" + +transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &DappyNFT.Collection + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + let sellerAddress: Address + let managerRef: &{NFTStorefront.StorefrontManager} + + prepare(acct: AuthAccount) { + + self.sellerAddress = acct.address + + let adminAccount = getAccount(adminAddress) + + self.dappyColRef = acct + .borrow<&DappyContract.Collection>( + from: DappyContract.CollectionStoragePath + ) + ?? panic ("Could not borrow Dappy Col ref") + + self.nftColRef = acct + .borrow<&DappyNFT.Collection>( + from: DappyNFT.CollectionStoragePath + ) + ?? panic ("Could not borrow NFT Col ref") + + self.managerRef = acct + .borrow<&{NFTStorefront.StorefrontManager}>( + from: NFTStorefront.StorefrontStoragePath + ) + ?? panic ("Could not borrow StorefrontManager ref") + + self.nftProviderCapability = acct + .getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( + DappyNFT.CollectionPrivatePath + ) + + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + self.saleCuts = [ + NFTStorefront.SaleCut( + receiver: receiver, + amount: salePrice + ) + ] + + } + + execute { + + let dappy <- self.dappyColRef.withdraw(withdrawID: dappyID) + + let nft <- DappyNFT.createFromDappy(dappy: <- dappy) + + let nftID = nft.id + + let nftType = Type<@DappyNFT.NFT>() + let salePaymentVaultType = Type<@FUSD.Vault>() + + self.nftColRef.deposit(token: <- nft) + + let listingResourceID = self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: self.saleCuts + ) + + } +} + \ No newline at end of file diff --git a/cadence/transactions/PutPackInStorefront.cdc b/cadence/transactions/PutPackInStorefront.cdc new file mode 100644 index 0000000..57de2bb --- /dev/null +++ b/cadence/transactions/PutPackInStorefront.cdc @@ -0,0 +1,92 @@ +import DappyContract from "../contracts/DappyContract.cdc" +import FUSD from "../contracts/FUSD.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" +import PackNFT from "../contracts/PackNFT.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" +import GalleryContract from "../contracts/GalleryContract.cdc" + +transaction(dappyIDs: [UInt64], salePrice: UFix64, adminAddress: Address) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &PackNFT.Collection + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + let sellerAddress: Address + let managerRef: &{NFTStorefront.StorefrontManager} + + prepare(acct: AuthAccount) { + + self.sellerAddress = acct.address + + let adminAccount = getAccount(adminAddress) + + self.dappyColRef = acct + .borrow<&DappyContract.Collection>( + from: DappyContract.CollectionStoragePath + ) + ?? panic ("Could not borrow Dappy Col ref") + + self.nftColRef = acct + .borrow<&PackNFT.Collection>( + from: PackNFT.CollectionStoragePath + ) + ?? panic ("Could not borrow NFT Col ref") + + self.managerRef = acct + .borrow<&{NFTStorefront.StorefrontManager}>( + from: NFTStorefront.StorefrontStoragePath + ) + ?? panic ("Could not borrow StorefrontManager ref") + + self.nftProviderCapability = acct + .getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( + PackNFT.CollectionPrivatePath + ) + + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + + self.saleCuts = [ + NFTStorefront.SaleCut( + receiver: receiver, + amount: salePrice + ) + ] + + } + + execute { + + let dappies: @{UInt64: DappyContract.Dappy} <- {} + + for dappyID in dappyIDs { + + let dappy <- self.dappyColRef.withdraw(withdrawID: + dappyID) + let old <- dappies[dappyID] <- dappy + destroy old + + } + + let nft <- PackNFT.createFromDappies(dappies: <- dappies) + + let nftID = nft.id + let nftType = Type<@PackNFT.NFT>() + let salePaymentVaultType = Type<@FUSD.Vault>() + self.nftColRef.deposit(token: <- nft) + + let listingResourceID = self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: self.saleCuts + ) + + } + +} + \ No newline at end of file diff --git a/cadence/transactions/RemoveGalleryListing.cdc b/cadence/transactions/RemoveGalleryListing.cdc new file mode 100644 index 0000000..2af49cb --- /dev/null +++ b/cadence/transactions/RemoveGalleryListing.cdc @@ -0,0 +1,30 @@ +import GalleryContract from "../contracts/GalleryContract.cdc" +import NFTStorefront from "../contracts/NFTStorefront.cdc" + +transaction (adminAddress: Address, listingResourceID: UInt64) { + + let galleryRef: &{GalleryContract.GalleryPublic} + let sellerAddress: Address + + prepare (acct: AuthAccount){ + + let account = getAccount(adminAddress) + self.sellerAddress = acct.address + + self.galleryRef = account + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath) + .borrow() + ?? panic("Could not borrow Gallery ref") + + } + + execute { + + self.galleryRef.removeListing( + listingResourceID: listingResourceID, + sellerAddress: self.sellerAddress) + + } + +} \ No newline at end of file diff --git a/crypto-dappy.code-workspace b/crypto-dappy.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/crypto-dappy.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/flow.json b/flow.json index e82d940..8f319cc 100644 --- a/flow.json +++ b/flow.json @@ -6,7 +6,6 @@ } }, "contracts": { - "DappyContract": "./cadence/contracts/DappyContract.cdc", "FUSD": { "source": "./cadence/contracts/FUSD.cdc", "aliases": { @@ -19,7 +18,44 @@ "testnet": "9a0766d93b6608b7", "emulator": "ee82856bf20e2aa6" } + }, + "DappyContract": { + "source": "./cadence/contracts/DappyContract.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, + "NonFungibleToken": { + "source": "./cadence/contracts/NonFungibleToken.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, + "NFTStorefront": { + "source": "./cadence/contracts/NFTStorefront.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, + "DappyNFT": { + "source": "./cadence/contracts/DappyNFT.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, + "PackNFT": { + "source": "./cadence/contracts/PackNFT.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, + "GalleryContract": { + "source": "./cadence/contracts/GalleryContract.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } } + }, "networks": { "emulator": "127.0.0.1:3569", @@ -30,7 +66,22 @@ "emulator-account": { "address": "f8d6e0586b0a20c7", "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" + }, + "MyAdmin": { + "address": "f8d6e0586b0a20c7", + "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" } }, - "deployments": {} + "deployments": { + "testnet": { + "MyAdmin": [ + "NonFungibleToken", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" + ] + }, + "emulator": { + "emulator-account": [ + "FungibleToken", "NonFungibleToken", "FUSD", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" + ] + } + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2fc7b03..f880c87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "crypto-dappy", "version": "0.1.0", "dependencies": { "@onflow/fcl": "^0.0.73", @@ -15,6 +16,8 @@ "faker": "^5.5.3", "pleasejs": "^0.4.2", "react": "^17.0.2", + "react-dnd": "^14.0.4", + "react-dnd-html5-backend": "^14.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", @@ -2849,6 +2852,21 @@ "node": ">= 8" } }, + "node_modules/@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -7078,6 +7096,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -16396,6 +16424,43 @@ "node": ">=6" } }, + "node_modules/react-dnd": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.4.tgz", + "integrity": "sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz", + "integrity": "sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw==", + "dependencies": { + "dnd-core": "14.0.1" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -16765,6 +16830,14 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -23797,6 +23870,21 @@ } } }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -27123,6 +27211,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -34246,6 +34344,26 @@ } } }, + "react-dnd": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.4.tgz", + "integrity": "sha512-AFJJXzUIWp5WAhgvI85ESkDCawM0lhoVvfo/lrseLXwFdH3kEO3v8I2C81QPqBW2UEyJBIPStOhPMGYGFtq/bg==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz", + "integrity": "sha512-QgN6rYrOm4UUj6tIvN8ovImu6uP48xBXF2rzVsp6tvj6d5XQ7OjHI4SJ/ZgGobOneRAU3WCX4f8DGCYx0tuhlw==", + "requires": { + "dnd-core": "14.0.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -34549,6 +34667,14 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/package.json b/package.json index 1f1faaf..bfa2699 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "faker": "^5.5.3", "pleasejs": "^0.4.2", "react": "^17.0.2", + "react-dnd": "^14.0.4", + "react-dnd-html5-backend": "^14.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", @@ -19,6 +21,7 @@ }, "scripts": { "start": "react-scripts start", + "dev": "BROWSER=none PORT=4000 react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/public/assets/Pack4.png b/public/assets/Pack4.png new file mode 100644 index 0000000..42d7734 Binary files /dev/null and b/public/assets/Pack4.png differ diff --git a/public/assets/dappy-animated.svg b/public/assets/dappy-animated.svg new file mode 100644 index 0000000..66bb3f2 --- /dev/null +++ b/public/assets/dappy-animated.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Atoms.css b/src/components/Atoms.css index dd2f981..46df242 100644 --- a/src/components/Atoms.css +++ b/src/components/Atoms.css @@ -35,6 +35,14 @@ justify-content: center; } +.btn-storefront { + width: 60%; + margin: 0 auto; + display: flex; + justify-content: center; + border: solid darkgreen 2px; +} + .btn-icon { font-size: 1.2rem; margin-right: .2rem diff --git a/src/components/BreedPanel.comp.js b/src/components/BreedPanel.comp.js new file mode 100644 index 0000000..a391b97 --- /dev/null +++ b/src/components/BreedPanel.comp.js @@ -0,0 +1,71 @@ +import React, { useEffect, useState } from 'react' + +import { useDrop } from 'react-dnd' +import { useMarket } from '../providers/MarketProvider' +import { useUser } from '../providers/UserProvider' +import Dappy from './Dappy' + +export default function BreedPanel() { + + const { mates, addMate, breedDappies } = useMarket() + const { fetchUserDappies, newDappies } = useUser() + const [breed, setBreed] = useState(false); //change to false + + const [, drop] = useDrop(() => ({ + accept: 'box', + drop: item => addMate(item), + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }) + })); + + useEffect(() => { + + }, [newDappies]) + + const onBreed = async () => { + await breedDappies(mates) + // breading done, fetch the new ones + await fetchUserDappies() + setBreed(true) + setTimeout(() => { + setBreed(false) + fetchUserDappies() + }, 5000) + } + + return ( +
+
Drop here
+
to breed
+
+
+ {mates.map((dappy, i) => ( + + + )) + } +
+ {newDappies && newDappies.map((dappy, i) => ( + + )) + } +
+ +
+
+
Make Dappies +
+
+ + ) +} diff --git a/src/components/Dappy.js b/src/components/Dappy.js index 4a1c397..4ed8369 100644 --- a/src/components/Dappy.js +++ b/src/components/Dappy.js @@ -7,14 +7,16 @@ import Stripes from './DappyStripes'; export default function Dappy({ dna = "FF5A9D.FFE922.60C5E5.0" }) { return ( - - - - - +
+ + + + + +
) } diff --git a/src/components/DappyCard.css b/src/components/DappyCard.css index bc69497..426b820 100644 --- a/src/components/DappyCard.css +++ b/src/components/DappyCard.css @@ -1,3 +1,7 @@ +.dappy-card__draggable { + opacity: 1; +} + .dappy-card__border { position: relative; margin-bottom: 2.5rem; @@ -38,6 +42,7 @@ opacity: .5; } + .collected { position: absolute; top: 5.5rem; @@ -51,6 +56,47 @@ box-shadow: .5rem .5rem 1rem rgba(0,0,0,.5); } +.collector span { + display:block; + position:absolute; + z-index:2; + font-family: "Monument Bold"; + font-size: 0.8rem; + transform: rotate(-15deg); + top: 1.5rem; +} + +.collector { + top: 13rem; + left: -1rem; + mix-blend-mode: screen; + background: white; + color: black; + width: 80px; + height: 80px; + position: absolute; + text-align: center; + opacity: .3; +} +.collector:before, + .collector:after { + content: ""; + position: absolute; + top: 0; + left: 0; + height: 80px; + width: 80px; + background: white; + color: black; + +} +.collector:before { + transform: rotate(30deg); +} +.collector:after { + transform: rotate(60deg); +} + .img-large { width: 100% !important; } diff --git a/src/components/DappyCard.js b/src/components/DappyCard.js index 1ba16ee..e77d815 100644 --- a/src/components/DappyCard.js +++ b/src/components/DappyCard.js @@ -1,28 +1,67 @@ import React from 'react' import { useHistory } from 'react-router-dom' +import { useDrag } from 'react-dnd' + import { useUser } from '../providers/UserProvider' +import { useMarket } from '../providers/MarketProvider' + import Dappy from './Dappy' import "./DappyCard.css" +import PriceButton from './PriceButton' + export default function DappyCard({ dappy, store, designer }) { - const { userDappies, mintDappy } = useUser() + + const { userDappies, mintDappy, fetchUserDappies } = useUser() + const { purchaseDappy } = useMarket() + const history = useHistory() const { id, dna, image, name, rarity, price, type, serialNumber } = dappy const owned = userDappies.some(d => d?.id === dappy?.id) + const [{ opacity }, dragRef] = useDrag( + () => ({ + type: 'box', + item: { dappy }, + collect: (monitor) => ({ + opacity: monitor.isDragging() ? 0.5 : 1 + }) + }), + [] + ) + + const onPurchase = async() => { + await purchaseDappy(dappy) + await fetchUserDappies() + } + + const onMint = async() => { + await mintDappy(id, price) + await fetchUserDappies() + } + const DappyButton = () => ( -
mintDappy(id, price)} +
{parseInt(price)} FUSD
) + + const StorefrontButton = () => ( +
+ {parseInt(price)} FUSD +
+ ) const PackButton = () => ( +
history.push(`/packs/${id}`)} - className="btn btn-bordered btn-light btn-dappy"> + onClick={() => history.push(`/packs/${id}`)} + className={`btn btn-bordered btn-light btn-dappy ${dappy.sellerAddress && "btn-storefront"}`}> More
) @@ -36,7 +75,7 @@ export default function DappyCard({ dappy, store, designer }) { ) return ( -
+
{type === "Dappy" ? : Pack @@ -52,11 +91,24 @@ export default function DappyCard({ dappy, store, designer }) { {designer ? : <> - {!owned && type === "Dappy" && } + {!owned && type === "Dappy" && !dappy.listingResourceID &&} + {!owned && type === "Dappy" && dappy.listingResourceID && } + + {!owned && type === "Dappy" && + dappy.listingResourceID && + dappy.sellerAddress && +
Collector
Sale
+ } + {!owned && type === "Pack" && } + {!owned && type === "Pack" && dappy.sellerAddress && +
Collector
Sale
+ } } + {!store && owned && !designer && } + {store && owned && !designer &&
Collected
}
) diff --git a/src/components/DappyEyes.js b/src/components/DappyEyes.js index faff448..d267b9a 100644 --- a/src/components/DappyEyes.js +++ b/src/components/DappyEyes.js @@ -10,26 +10,28 @@ export default function Eyes({ color, outline, reflection }) { d="M360.968 167C316.238 167 278.111 195.299 263.52 234.972C248.928 195.299 210.802 167 166.065 167C108.734 167 62.2588 213.475 62.2588 270.807C62.2588 328.138 108.734 374.613 166.065 374.613C210.802 374.613 248.928 346.314 263.52 306.642C278.111 346.314 316.238 374.613 360.968 374.613C418.299 374.613 464.774 328.138 464.774 270.807C464.774 213.475 418.299 167 360.968 167Z" fill={outline} /> - {/* Right eye */} - - {/* Right eye reflection */} - - {/* Left eye */} - - {/* Left eye reflection */} - + + {/* Right eye */} + + {/* Right eye reflection */} + + {/* Left eye */} + + {/* Left eye reflection */} + + ) } diff --git a/src/components/DappyList.css b/src/components/DappyList.css index c79b587..b4a896b 100644 --- a/src/components/DappyList.css +++ b/src/components/DappyList.css @@ -1,3 +1,49 @@ +@keyframes blink { + 0% {transform: scale(.8);} + 100% {transform: scale(1.5);} + } + + @-webkit-keyframes blink { + 0% {transform: scale(.8);} + 100% {transform: scale(1.5);} + +} + +.right_panel .dappy_eye { + animation: blink .5s alternate 0s infinite; + -webkit-animation: blink .5s alternate 0s infinite; + transform-origin: 50% 50%; +} + +@keyframes fadeOut { + 0% { opacity: 1; transform: scale(.8);} + 99% { opacity: 0.01;width: 100%; height: 100%; transform: scale(1.5);} + 100% { opacity: 0;width: 0; height: 0;} +} + +@-webkit-keyframes fadeOut { + 0% { opacity: 1; transform: scale(.8);} + 99% { opacity: 0.01;width: 100%; height: 100%; transform: scale(1.5);} + 100% { opacity: 0;width: 0; height: 0;} +} + +.baby_dappies.show { + animation: fadeOut 5s; + -webkit-animation: fadeOut 5s; + animation-fill-mode: forwards; +} + +.baby_dappies { + opacity: 0; +} + +.right_panel .dappy_eye { +animation: blink .5s alternate 0s infinite; +-webkit-animation: blink .5s alternate 0s infinite; +transform-origin: 50% 50%; +} + + .dappy-list__wrapper { margin: 5rem auto; width: 80%; @@ -6,4 +52,73 @@ justify-items: center; align-items: center; justify-content: center; -} \ No newline at end of file +} + +.left-panel__wrapper { + position: fixed; + width: 7rem; + left: 0; + display: flex; + flex-direction:column; + top: 50%; + transform: translate(0, -50%); +} + +.right-panel__wrapper { + position: fixed; + width: 7rem; + right: 0; + display: flex; + flex-direction:column; + top: 50%; + transform: translate(0, -50%); + +} + +.right_panel { + border-radius: 0.5rem; + border: 3px solid #600c14; + height: 20rem; + position: relative; + display: flex; + flex-direction: column; +} + + +.left_panel { + border-radius: 0.5rem; + border: 3px solid #600c14; + height: 20rem; + position:relative; + display: flex; + flex-direction: column; +} + +.left_panel .dappy_wrapper, .baby_dappies .dappy_wrapper { + width: 60px; + height: 60px; + margin: 0 auto; +} + +.left_panel svg, .baby_dappies svg { + width: 100%; + height: 100%; +} + +.input-bottom { + margin-top:auto; +} + +.input-bottom input { + display: block; + margin: .3rem; + width: 5rem; +} + +input[type=number] { + text-align:right; +} + +.btn-bottom { + margin-top:.5rem; +} diff --git a/src/components/DappyList.js b/src/components/DappyList.js index 0c9f9e4..70ec7c7 100644 --- a/src/components/DappyList.js +++ b/src/components/DappyList.js @@ -6,16 +6,18 @@ import './DappyList.css' export default function DappyList({ dappies, store, designer }) { return ( -
- {dappies.map((dappy, i) => ( - - )) - } -
+ <> +
+ {dappies.map((dappy, i) => ( + + )) + } +
+ ) } diff --git a/src/components/PackPanel.comp.js b/src/components/PackPanel.comp.js new file mode 100644 index 0000000..32ac02d --- /dev/null +++ b/src/components/PackPanel.comp.js @@ -0,0 +1,61 @@ +import React, { useEffect } from 'react' + +import { useDrop } from 'react-dnd' +import { useMarket } from '../providers/MarketProvider' +import { useInput } from '../hooks/use-input.hook' + +import Dappy from './Dappy' + +export default function PackPanel() { + + const { packPrice, userPack, addToPack, listPackForSale } = useMarket() + + const { value: wantPrice, setValue: setPrice, bind: bindPrice } = useInput(packPrice); + + useEffect(() => { + setPrice(packPrice) + }, [setPrice, packPrice]) + + const [, drop] = useDrop(() => ({ + accept: 'box', + drop: item => addToPack(item), + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }) + })); + + return ( + <> +
+
Drag here
+
to pack
+
+ +
+ {userPack.map((dappy, i) => ( + + + )) + } +
+ + +
+
+
listPackForSale(userPack,parseFloat(wantPrice))} + className="btn btn-bordered btn-light btn-bottom"> +
Sell Pack +
+
+ + + + + ) +} diff --git a/src/components/PriceButton.css b/src/components/PriceButton.css new file mode 100644 index 0000000..9d9b464 --- /dev/null +++ b/src/components/PriceButton.css @@ -0,0 +1,31 @@ +.price-button__wrapper { + display:none; + border: white solid 1px; + padding: 0.5rem ; + margin: 0.5rem; +} + +@keyframes showForm { + 0% { + opacity: 0; + transform: scale(0) + } + + 100% { + opacity: 1; + transform: scale(1) + } +} + +.price-button__wrapper.show { + display: block; + animation: showForm 1s ease-in-out both; +} + +.price-button__wrapper > .btn { + margin: 1rem; +} + +.dappy-form__item label { + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/src/components/PriceButton.js b/src/components/PriceButton.js new file mode 100644 index 0000000..b893dde --- /dev/null +++ b/src/components/PriceButton.js @@ -0,0 +1,54 @@ +import React, { useState } from 'react' + +import { useMarket } from "../providers/MarketProvider" +import { useInput } from '../hooks/use-input.hook' + +import './PriceButton.css' + +export default function PriceButton({ dappy }) { + + const { listDappyForSale } = useMarket(); + + const [sell, setSell] = useState(false); + + const defaultPrice = parseFloat(dappy.price).toFixed(8).slice(0, -6) + + const { value: wantPrice, bind: bindPrice } = useInput(defaultPrice); + + const clickShow = () => { + setSell(!sell); + } + + const clickSell = (wantPrice) => { + listDappyForSale(dappy, wantPrice) + } + + return ( + <> + +
+
+ + +
+
clickSell(wantPrice)} + className="btn btn-bordered btn-light"> + List for Sale +
+
+ +
clickShow()} + className="btn btn-bordered btn-light btn-dappy"> + {sell ? + : + + } + {parseInt(dappy.price)} FUSD +
+ + + ) +} + \ No newline at end of file diff --git a/src/components/Untitled-1.xml b/src/components/Untitled-1.xml new file mode 100644 index 0000000..55b91a9 --- /dev/null +++ b/src/components/Untitled-1.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js index c3bb34b..8326d6f 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -5,5 +5,10 @@ config({ "discovery.wallet": process.env.REACT_APP_WALLET_DISCOVERY, "0xFungibleToken": process.env.REACT_APP_FT_CONTRACT, "0xFUSD": process.env.REACT_APP_FUSD_CONTRACT, - "0xDappy": process.env.REACT_APP_DAPPY_CONTRACT + "0xDappy": process.env.REACT_APP_DAPPY_CONTRACT, + "0xNonFungibleToken": process.env.REACT_APP_NFT_CONTRACT, + "0xNFTStorefront": process.env.REACT_APP_NFTSTOREFRONT_CONTRACT, + "0xMyDappyNFT": process.env.REACT_APP_DAPPYNFT_CONTRACT, + "0xPackNFT": process.env.REACT_APP_PACKNFT_CONTRACT, + "0xGalleryContract": process.env.REACT_APP_GALLERY_CONTRACT }) \ No newline at end of file diff --git a/src/flow/breed-dappies.tx.js b/src/flow/breed-dappies.tx.js new file mode 100644 index 0000000..1acd90d --- /dev/null +++ b/src/flow/breed-dappies.tx.js @@ -0,0 +1,137 @@ +export const BREED_DAPPIES = ` +import DappyContract from 0xDappy +import FUSD from 0xFUSD + +transaction (maleID: UInt64, femaleID: UInt64) { + + let collectionRef: &DappyContract.Collection + let dappies: {UInt64: DappyContract.Template} + let vaultRef: &FUSD.Vault + var random: UInt64 + let step: UInt64 + + // Dappy DNA MUST be parsed into array before passing to this transaction + let maleID: UInt64 + let femaleID: UInt64 + + prepare(acct: AuthAccount) { + self.maleID = maleID + self.femaleID = femaleID + self.random = 52090100 // to test on playground + self.step = 77 // to test on playground + fun reconstructDNA(_ prepDNA: [String]): String { + pre { + prepDNA.length > 3: "DNA must have at least 4 sequences" + prepDNA.length < 7: "DNA must have at most 6 sequences" + } + var construct:String = "" + let n = prepDNA.length + var i = 0 + while i < n - 1 { + construct = construct.concat(prepDNA[i]).concat(".") + i = i + 1 + } + construct = construct.concat(prepDNA[n-1]) + return construct + } + + self.collectionRef = acct.borrow<&DappyContract.Collection>(from: DappyContract.CollectionStoragePath) + ?? panic("Could not borrow collection ref") + self.dappies =self.collectionRef.listDappies() + self.dappies[self.maleID]??panic("Male ID does not exist in collection") + self.dappies[self.femaleID]??panic("Female ID does not exist in collection") + self.vaultRef = acct.borrow<&FUSD.Vault>(from: /storage/fusdVault) ?? panic("Could not borrow FUSD vault") + + } + + execute { + fun parseDNA(_ dna: String): [String] { + var i = 0 + var buffer = "" + var ret: [String] = [] + while i < dna.length { + let c = dna.slice(from: i, upTo: i+1) + if c != "." { + buffer = buffer.concat( c ) + } else { + ret.append(buffer) + buffer = "" + } + i = i + 1 + } + if buffer != "" { + ret.append(buffer) + } + return ret + } + fun luck(_ mod: UInt64): UInt64 { + let play = false // unsafeRandom will not work in playground + let x: UInt64 = play? self.random : unsafeRandom() + self.random = self.random + self.step + return x - x/mod * mod + } + fun max(_ a: UInt64, _ b: UInt64): UInt64 { + return a>b ? a:b + } + fun min(_ a: UInt64, _ b: UInt64): UInt64 { + return a(<-collection, to: DappyContract.CollectionStoragePath) - acct.link<&{DappyContract.CollectionPublic}>(DappyContract.CollectionPublicPath, target: DappyContract.CollectionStoragePath) - } +transaction { + prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@AnyResource>(from: DappyContract.CollectionStoragePath) + destroy collectionRef + acct.unlink(DappyContract.CollectionPublicPath) + + // DappyNFT + + let dappyCollectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + destroy dappyCollectionRef + acct.unlink(DappyNFT.CollectionPublicPath) + acct.unlink(DappyNFT.CollectionPrivatePath) + + // PackNFT + + let packCollectionRef <- acct.load<@PackNFT.Collection>(from: PackNFT.CollectionStoragePath) + destroy packCollectionRef + acct.unlink(PackNFT.CollectionPublicPath) + acct.unlink(PackNFT.CollectionPrivatePath) + + // Storefront + + let storefrontRef <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + destroy storefrontRef + acct.unlink(NFTStorefront.StorefrontPublicPath) + + // Creation section + // + // + + let collection <- DappyContract.createEmptyCollection() + acct.save<@DappyContract.Collection>(<-collection, to: DappyContract.CollectionStoragePath) + acct.link<&{DappyContract.CollectionPublic}>(DappyContract.CollectionPublicPath, target: DappyContract.CollectionStoragePath) + + // DappyNFT Collection + + let dappyCollection <- DappyNFT.createEmptyCollection() + acct.save<@DappyNFT.Collection>(<-dappyCollection, to: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPublicPath, target: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPrivatePath, target: DappyNFT.CollectionStoragePath) + + // PackNFT Collection + + let packCollection <- PackNFT.createEmptyCollection() + acct.save<@PackNFT.Collection>(<-packCollection, to: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPublicPath, target: PackNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(PackNFT.CollectionPrivatePath, target: PackNFT.CollectionStoragePath) + + // NFTStorefront + + let storefront <- NFTStorefront.createStorefront() + + acct.save<@NFTStorefront.Storefront>(<- storefront, to: NFTStorefront.StorefrontStoragePath) + + acct.link<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath, target: NFTStorefront.StorefrontStoragePath) + } +} ` \ No newline at end of file diff --git a/src/flow/create-nft-collection.tx.old.js b/src/flow/create-nft-collection.tx.old.js new file mode 100644 index 0000000..9203452 --- /dev/null +++ b/src/flow/create-nft-collection.tx.old.js @@ -0,0 +1,24 @@ +export const CREATE_NFT_COLLECTION = ` + import NonFungibleToken from 0xNonFungibleToken + import DappyNFT from 0xMyDappyNFT + import NFTStorefront from 0xNFTStorefront + + transaction { + prepare(acct: AuthAccount) { + + let collection <- DappyNFT.createEmptyCollection() + acct.save<@DappyNFT.Collection>(<-collection, to: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPublicPath, target: DappyNFT.CollectionStoragePath) + + acct.link<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(DappyNFT.CollectionPrivatePath, target: DappyNFT.CollectionStoragePath) + + let storefront <- NFTStorefront.createStorefront() + + acct.save<@NFTStorefront.Storefront>(<- storefront, to: NFTStorefront.StorefrontStoragePath) + + acct.link<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath, target: NFTStorefront.StorefrontStoragePath) + + } + } +` \ No newline at end of file diff --git a/src/flow/delete-collection.tx.js b/src/flow/delete-collection.tx.js index 448f27b..98ed097 100644 --- a/src/flow/delete-collection.tx.js +++ b/src/flow/delete-collection.tx.js @@ -1,12 +1,48 @@ export const DELETE_COLLECTION = ` import DappyContract from 0xDappy + import DappyNFT from 0xMyDappyNFT + import PackNFT from 0xPackNFT + import NFTStorefront from 0xNFTStorefront + transaction() { prepare(acct: AuthAccount) { + let collectionRef <- acct.load<@DappyContract.Collection>(from: DappyContract.CollectionStoragePath) ?? panic("Could not borrow collection reference") destroy collectionRef acct.unlink(DappyContract.CollectionPublicPath) + + // DappyNFT + + let dappyCollectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + ?? panic("Could not borrow DappyNFT collection reference") + + destroy dappyCollectionRef + + acct.unlink(DappyNFT.CollectionPublicPath) + + acct.unlink(DappyNFT.CollectionPrivatePath) + + // PackNFT + + let packCollectionRef <- acct.load<@PackNFT.Collection>(from: PackNFT.CollectionStoragePath) + ?? panic("Could not borrow PackNFT collection reference") + + destroy packCollectionRef + + acct.unlink(PackNFT.CollectionPublicPath) + + acct.unlink(PackNFT.CollectionPrivatePath) + + // Storefront + + let storefront <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + + destroy storefront + + acct.unlink(NFTStorefront.StorefrontPublicPath) + } } ` \ No newline at end of file diff --git a/src/flow/delete-nft-collection.tx.old.js b/src/flow/delete-nft-collection.tx.old.js new file mode 100644 index 0000000..28c9ef7 --- /dev/null +++ b/src/flow/delete-nft-collection.tx.old.js @@ -0,0 +1,25 @@ +export const DELETE_NFT_COLLECTION = ` + import DappyNFT from 0xMyDappyNFT + import NFTStorefront from 0xNFTStorefront + + transaction() { + prepare(acct: AuthAccount) { + + let collectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + ?? panic("Could not borrow collection reference") + + destroy collectionRef + + acct.unlink(DappyNFT.CollectionPublicPath) + + acct.unlink(DappyNFT.CollectionPrivatePath) + + let storefront <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + + destroy storefront + + acct.unlink(NFTStorefront.StorefrontPublicPath) + + } + } +` \ No newline at end of file diff --git a/src/flow/list-gallery-collection.script.js b/src/flow/list-gallery-collection.script.js new file mode 100644 index 0000000..2c52c05 --- /dev/null +++ b/src/flow/list-gallery-collection.script.js @@ -0,0 +1,20 @@ +export const LIST_GALLERY_COLLECTION = ` +import GalleryContract from 0xGalleryContract + +pub fun main(galleryAddress: Address): {UInt64: GalleryContract.GalleryData} { + + let account = getAccount(galleryAddress) + + let galleryRef = account + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath + ) + .borrow() + ?? panic ("Could not borrow Gallery ref") + + let galleryCollection = galleryRef.getGalleryCollection() + + return galleryCollection +} + +` \ No newline at end of file diff --git a/src/flow/list-user-dappies-ids.script.js b/src/flow/list-user-dappies-ids.script.js new file mode 100644 index 0000000..8829645 --- /dev/null +++ b/src/flow/list-user-dappies-ids.script.js @@ -0,0 +1,15 @@ +export const LIST_USER_DAPPIES_IDS = ` + import DappyContract from 0xDappy + + pub fun main(addr: Address): [UInt64]? { + let account = getAccount(addr) + + if let ref = account.getCapability<&{DappyContract.CollectionPublic}>(DappyContract.CollectionPublicPath) + .borrow() { + let dappies = ref.getIDs() + return dappies + } + + return nil + } +` \ No newline at end of file diff --git a/src/flow/purchase-dappy-storefront.tx.js b/src/flow/purchase-dappy-storefront.tx.js new file mode 100644 index 0000000..c01e74c --- /dev/null +++ b/src/flow/purchase-dappy-storefront.tx.js @@ -0,0 +1,57 @@ +export const PURCHASE_DAPPY_STOREFRONT = ` +import DappyContract from 0xDappy +import FUSD from 0xFUSD +import NonFungibleToken from 0xNonFungibleToken +import FungibleToken from 0xFungibleToken +import DappyNFT from 0xMyDappyNFT +import NFTStorefront from 0xNFTStorefront + +transaction(listingResourceID:UInt64, sellerAddress: Address) { + // signed by buyer + + let storefrontRef: &{NFTStorefront.StorefrontPublic} + let receiverRef: &{DappyContract.CollectionPublic} + let vaultRef: &FungibleToken.Vault + let listingRef: &NFTStorefront.Listing{NFTStorefront.ListingPublic} + + prepare(acct: AuthAccount) { + + // Seller + let sellerAccount = getAccount(sellerAddress) + self.storefrontRef = sellerAccount + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + self.listingRef = self.storefrontRef.borrowListing(listingResourceID: listingResourceID) + ?? panic ("Could not borrow Listing ref") + + // Buyer + self.receiverRef = acct + .getCapability<&{DappyContract.CollectionPublic}>( + DappyContract.CollectionPublicPath) + .borrow() + ?? panic ("Could not borrow Dappy Collection ref") + + self.vaultRef = acct + .borrow<&FungibleToken.Vault>( + from: /storage/fusdVault) + ?? panic ("Could not borrow FUSD Vault ref") + + } + + execute { + + let amount = self.listingRef.getDetails().salePrice + let vault <- self.vaultRef.withdraw(amount: amount) + let nft <- self.listingRef.purchase(payment: <- vault) as! @DappyNFT.NFT + let dappy <- nft.withdrawDappy() + ?? panic("cannot withdraw Dappy from provider") + self.receiverRef.deposit(token: <- dappy) + destroy nft + + } +} + +` \ No newline at end of file diff --git a/src/flow/purchase-pack-storefront.tx.js b/src/flow/purchase-pack-storefront.tx.js new file mode 100644 index 0000000..669600c --- /dev/null +++ b/src/flow/purchase-pack-storefront.tx.js @@ -0,0 +1,84 @@ +export const PURCHASE_PACK_STOREFRONT =` +import DappyContract from 0xDappy +import FungibleToken from 0xFungibleToken +import NonFungibleToken from 0xNonFungibleToken +import FUSD from 0xFUSD +import PackNFT from 0xPackNFT +import NFTStorefront from 0xNFTStorefront +import GalleryContract from 0xGalleryContract + +transaction(adminAddress: Address, listingResourceID:UInt64, sellerAddress: Address) { + // signed by buyer + + let storefrontRef: &{NFTStorefront.StorefrontPublic} + let receiverRef: &{DappyContract.CollectionPublic} + let vaultRef: &FungibleToken.Vault + let listingRef: &NFTStorefront.Listing{NFTStorefront.ListingPublic} + + let galleryRef: &{GalleryContract.GalleryPublic} + + + prepare(acct: AuthAccount) { + + // Seller + let sellerAccount = getAccount(sellerAddress) + self.storefrontRef = sellerAccount + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath) + .borrow() + ?? panic ("Could not borrow Storefront ref") + + self.listingRef = self.storefrontRef.borrowListing(listingResourceID: listingResourceID) + ?? panic ("Could not borrow Listing ref") + + assert ( + self.listingRef.getDetails().nftType == Type<@PackNFT.NFT>(), + message: "Purchase wrong NFT type" + ) + + + // Buyer + self.receiverRef = acct + .getCapability<&{DappyContract.CollectionPublic}>( + DappyContract.CollectionPublicPath) + .borrow() + ?? panic ("Could not borrow Dappy Collection ref") + + self.vaultRef = acct + .borrow<&FungibleToken.Vault>( + from: /storage/fusdVault) + ?? panic ("Could not borrow FUSD Vault ref") + + // Admin ref to storefront + let adminAccount = getAccount(adminAddress) + self.galleryRef = adminAccount + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath) + .borrow() + ?? panic("Could not borrow Gallery ref") + + } + + execute { + + let amount = self.listingRef.getDetails().salePrice + let vault <- self.vaultRef.withdraw(amount: amount) + let nft <- self.listingRef.purchase(payment: <- vault) as! @PackNFT.NFT + + let dappies <- nft.withdrawDappies() + + for key in dappies.keys { + let x <- dappies[key] <- nil + self.receiverRef.deposit(token: <- x!) + } + destroy dappies + destroy nft + + // Storefront cleanup + self.galleryRef.removeListing( + listingResourceID: listingResourceID, + sellerAddress: sellerAddress) + + } +} +` \ No newline at end of file diff --git a/src/flow/put-dappy-storefront.tx.js b/src/flow/put-dappy-storefront.tx.js new file mode 100644 index 0000000..1911fd5 --- /dev/null +++ b/src/flow/put-dappy-storefront.tx.js @@ -0,0 +1,99 @@ +export const PUT_DAPPY_STOREFRONT = ` +import DappyContract from 0xDappy +import FungibleToken from 0xFungibleToken +import NonFungibleToken from 0xNonFungibleToken +import FUSD from 0xFUSD +import DappyNFT from 0xMyDappyNFT +import NFTStorefront from 0xNFTStorefront +import GalleryContract from 0xGalleryContract + +transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &DappyNFT.Collection + let managerRef: &{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic} + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + let galleryRef: &{GalleryContract.GalleryPublic} + let sellerAddress: Address + + prepare(acct: AuthAccount) { + + self.sellerAddress = acct.address + + let adminAccount = getAccount(adminAddress) + self.galleryRef =adminAccount + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath + ) + .borrow() + ?? panic ("Could not borrow GalleryPublic from Admin") + + self.dappyColRef = acct + .borrow<&DappyContract.Collection>( + from: DappyContract.CollectionStoragePath + ) + ?? panic ("Could not borrow Dappy Col ref") + + self.nftColRef = acct + .borrow<&DappyNFT.Collection>( + from: DappyNFT.CollectionStoragePath + ) + ?? panic ("Could not borrow NFT Col ref") + + self.managerRef = acct + .borrow<&{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic}>( + from: NFTStorefront.StorefrontStoragePath + ) + ?? panic ("Could not borrow StorefrontManager ref") + + self.nftProviderCapability = acct + .getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( + DappyNFT.CollectionPrivatePath + ) + + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + self.saleCuts = [ + NFTStorefront.SaleCut( + receiver: receiver, + amount: salePrice + ) + ] + + } + + execute { + + let dappy <- self.dappyColRef.withdraw(withdrawID: dappyID) + + let nft <- DappyNFT.createFromDappy(dappy: <- dappy) + + let nftID = nft.id + + let nftType = Type<@DappyNFT.NFT>() + let salePaymentVaultType = Type<@FUSD.Vault>() + + self.nftColRef.deposit(token: <- nft) + + let listingResourceID = self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: self.saleCuts + ) + + let listingPublic = self.managerRef + .borrowListing(listingResourceID: listingResourceID)! + + self.galleryRef.addListing( + listingPublic: listingPublic, + sellerAddress: self.sellerAddress + ) + + } +} +` \ No newline at end of file diff --git a/src/flow/put-pack-storefront.tx.js b/src/flow/put-pack-storefront.tx.js new file mode 100644 index 0000000..4b6a93e --- /dev/null +++ b/src/flow/put-pack-storefront.tx.js @@ -0,0 +1,109 @@ +export const PUT_PACK_STOREFRONT = ` +import DappyContract from 0xDappy +import FUSD from 0xFUSD +import FungibleToken from 0xFungibleToken +import NonFungibleToken from 0xNonFungibleToken +import PackNFT from 0xPackNFT +import NFTStorefront from 0xNFTStorefront +import GalleryContract from 0xGalleryContract + +transaction(dappyIDs: [UInt64], salePrice: UFix64, adminAddress: Address) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &PackNFT.Collection + let managerRef: &{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic} + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + let sellerAddress: Address + let galleryRef: &{GalleryContract.GalleryPublic} + + prepare(acct: AuthAccount) { + + self.sellerAddress = acct.address + + let adminAccount = getAccount(adminAddress) + + self.galleryRef =adminAccount + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath + ) + .borrow() + ?? panic ("Could not borrow GalleryPublic from Admin") + + self.dappyColRef = acct + .borrow<&DappyContract.Collection>( + from: DappyContract.CollectionStoragePath + ) + ?? panic ("Could not borrow Dappy Col ref") + + self.nftColRef = acct + .borrow<&PackNFT.Collection>( + from: PackNFT.CollectionStoragePath + ) + ?? panic ("Could not borrow NFT Col ref") + + self.managerRef = acct + .borrow<&{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic}>( + from: NFTStorefront.StorefrontStoragePath + ) + ?? panic ("Could not borrow StorefrontManager ref") + + self.nftProviderCapability = acct + .getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( + PackNFT.CollectionPrivatePath + ) + + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + + self.saleCuts = [ + NFTStorefront.SaleCut( + receiver: receiver, + amount: salePrice + ) + ] + + } + + execute { + + let dappies: @{UInt64: DappyContract.Dappy} <- {} + + for dappyID in dappyIDs { + + let dappy <- self.dappyColRef.withdraw(withdrawID: + dappyID) + let old <- dappies[dappyID] <- dappy + destroy old + + } + + let nft <- PackNFT.createFromDappies(dappies: <- dappies) + + let nftID = nft.id + let nftType = Type<@PackNFT.NFT>() + let salePaymentVaultType = Type<@FUSD.Vault>() + self.nftColRef.deposit(token: <- nft) + + let listingResourceID = self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: self.saleCuts + ) + + let listingPublic = self.managerRef + .borrowListing(listingResourceID: listingResourceID)! + + self.galleryRef.addListing( + listingPublic: listingPublic, + sellerAddress: self.sellerAddress + ) + + } + + } +` \ No newline at end of file diff --git a/src/flow/remove-gallery-listing.tx.js b/src/flow/remove-gallery-listing.tx.js new file mode 100644 index 0000000..536bc48 --- /dev/null +++ b/src/flow/remove-gallery-listing.tx.js @@ -0,0 +1,32 @@ +export const REMOVE_GALLERY_LISTING=` +import GalleryContract from 0xGalleryContract +import NFTStorefront from 0xNFTStorefront + +transaction (adminAddress: Address, listingResourceID: UInt64) { + + let galleryRef: &{GalleryContract.GalleryPublic} + let sellerAddress: Address + + prepare (acct: AuthAccount){ + + let account = getAccount(adminAddress) + self.sellerAddress = acct.address + + self.galleryRef = account + .getCapability<&{GalleryContract.GalleryPublic}>( + GalleryContract.GalleryPublicPath) + .borrow() + ?? panic("Could not borrow Gallery ref") + + } + + execute { + + self.galleryRef.removeListing( + listingResourceID: listingResourceID, + sellerAddress: self.sellerAddress) + + } + +} +` \ No newline at end of file diff --git a/src/hooks/use-breed-dappies.hook.js b/src/hooks/use-breed-dappies.hook.js new file mode 100644 index 0000000..8b3254f --- /dev/null +++ b/src/hooks/use-breed-dappies.hook.js @@ -0,0 +1,86 @@ +import { useReducer } from 'react' +import { mutate, tx } from '@onflow/fcl' + +import { useTxs } from '../providers/TxProvider' +import { BREED_DAPPIES } from '../flow/breed-dappies.tx' +const BREED_MAX_CAP = 2 + +export default function useBreedDappies() { + + const { addTx, runningTxs } = useTxs() + + const reducer = (state, action) => { + switch (action.type) { + case 'ADD': + //skip if dappy exists or total dappies is 2 + if (state.data.length >= BREED_MAX_CAP) return { ...state } + for (const d of state.data) { + if (d.serialNumber === action.payload.serialNumber) return { ...state } + } + return { + ...state, + data: [...state.data, action.payload] + } + case 'RESET': + return { + ...state, + data: [] + } + default: + throw new Error("Error in useUserPack reducer") + } + } + + const [state, dispatch] = useReducer(reducer, { + data: [] + }) + + const addMate = ({ dappy }) => { + dispatch({ type: 'ADD', payload: dappy }) + } + + const breedDappies = async ( dappies) => { + + if (dappies.length !== BREED_MAX_CAP ) + { + // TODO: replace with proper error reporting + alert("Need both male and female to breed") + return + } + + if (runningTxs) { + alert("Transactions are still running. Please wait for them to finish first.") + return + } + + const maleID = parseInt(dappies[0].serialNumber) + const femaleID = parseInt(dappies[1].serialNumber) + // console.log(dappies) + // console.log(maleID, femaleID) + try { + let res = await mutate({ + cadence: BREED_DAPPIES, + limit: 6000, + args: (arg, t) => [ + arg(maleID, t.UInt64), + arg(femaleID, t.UInt64) + ] + }) + addTx(res) + await tx(res).onceSealed() + + dispatch({ type: 'RESET' }) + + + } catch (error) { + console.error(error, error.stack) + } + + } + + return { + ...state, + addMate, + breedDappies + } +} diff --git a/src/hooks/use-collection.hook.js b/src/hooks/use-collection.hook.js index 8d5bac8..71478d3 100644 --- a/src/hooks/use-collection.hook.js +++ b/src/hooks/use-collection.hook.js @@ -4,6 +4,7 @@ import { mutate, query, tx } from '@onflow/fcl' import { CHECK_COLLECTION } from '../flow/check-collection.script' import { DELETE_COLLECTION } from '../flow/delete-collection.tx' import { CREATE_COLLECTION } from '../flow/create-collection.tx' + import { useTxs } from '../providers/TxProvider' export default function useCollection(user) { @@ -31,27 +32,47 @@ export default function useCollection(user) { }, []) const createCollection = async () => { + let res = await mutate({ cadence: CREATE_COLLECTION, - limit: 55 - + limit: 200 }) addTx(res) await tx(res).onceSealed() + setCollection(true) + } const deleteCollection = async () => { + + // import { LIST_USER_DAPPIES_IDS } from '../flow/list-user-dappies-ids.script' + // console.log("THIS IS A TEST") + // try { + // let res = await query({ + // cadence: LIST_USER_DAPPIES_IDS, + // args: (arg, t) => [arg(user?.addr, t.Address)] + // }) + // console.log(res) + // } catch (err) { + // console.err(err, err.stack) + // } + // console.log("THIS IS A TEST") + // return + try { + let res = await mutate({ cadence: DELETE_COLLECTION, limit: 75 }) addTx(res) await tx(res).onceSealed() + setCollection(false) + } catch (err) { - console.log(err) + console.error(err, err.stack) } } diff --git a/src/hooks/use-dappy-packs.hook.js b/src/hooks/use-dappy-packs.hook.js index 846e893..01632d1 100644 --- a/src/hooks/use-dappy-packs.hook.js +++ b/src/hooks/use-dappy-packs.hook.js @@ -1,9 +1,10 @@ -import { mutate, query, tx } from '@onflow/fcl' +import { mutate, query, tx, config } from '@onflow/fcl' import { useEffect, useReducer } from 'react' import { LIST_DAPPIES_IN_PACK } from '../flow/list-dappies-in-pack.script' import { MINT_DAPPIES_FROM_PACK } from '../flow/mint-dappies-from-pack.tx' import { LIST_PACKS } from '../flow/list-packs.scripts' +import { LIST_GALLERY_COLLECTION } from '../flow/list-gallery-collection.script' import { GET_PACK } from '../flow/get-pack.script' import { defaultReducer } from '../reducer/defaultReducer' import { useUser } from '../providers/UserProvider' @@ -20,18 +21,66 @@ export default function useDappyPacks() { const { runningTxs, addTx } = useTxs() useEffect(() => { + + const getGalleryPacks = async () => { + try { + + const galleryPacks = [] + + const adminAddress = await config().get("0xGalleryContract") + let galleryCollection = await query({ + cadence: LIST_GALLERY_COLLECTION, + args: (arg, t) => [arg(adminAddress, t.Address)] + }) + + for (const [listingResourceID, galleryData] of Object.entries(galleryCollection)) { + // Make sure that type is PackNFT + if ( + Object.keys(galleryData.dappyCollection).length > 1 + && galleryData.listingDetails.nftType.endsWith("PackNFT.NFT") + ) { + + const pack = { + name: galleryData.packName, + familyID: listingResourceID, + price: galleryData.listingDetails.salePrice, + templates: Object.values(galleryData.dappyCollection), + sellerAddress: galleryData.sellerAddress + } + + galleryPacks.push(pack) + + } + } + + return galleryPacks + + } catch (err) { + console.error(err, err.stack) + dispatch({ type: 'ERROR' }) + } + + } + const fetchPacks = async () => { dispatch({ type: 'PROCESSING' }) + + let galleryPacks = await getGalleryPacks() + try { const res = await query({ cadence: LIST_PACKS }) - dispatch({ type: 'SUCCESS', payload: res }) + dispatch({ type: 'SUCCESS', payload: galleryPacks.concat(res) }) } catch (err) { + console.error(err, err.stack) dispatch({ type: 'ERROR' }) } + } + fetchPacks() + }, []) const fetchPackDetails = async (packID) => { diff --git a/src/hooks/use-dappy-templates.hook.js b/src/hooks/use-dappy-templates.hook.js index a4c4d41..d85a36f 100644 --- a/src/hooks/use-dappy-templates.hook.js +++ b/src/hooks/use-dappy-templates.hook.js @@ -1,6 +1,7 @@ import { useEffect, useReducer } from 'react' -import { query } from '@onflow/fcl' +import { query, config } from '@onflow/fcl' +import { LIST_GALLERY_COLLECTION } from '../flow/list-gallery-collection.script' import { LIST_DAPPY_TEMPLATES } from '../flow/list-dappy-templates.script' import { defaultReducer } from '../reducer/defaultReducer' import DappyClass from '../utils/DappyClass' @@ -12,12 +13,48 @@ export default function useDappyTemplates() { const fetchDappyTemplates = async () => { dispatch({ type: 'PROCESSING' }) try { + + let galleryDappies = [] + + const adminAddress = await config().get("0xGalleryContract") + let galleryCollection = await query({ + cadence: LIST_GALLERY_COLLECTION, + args: (arg, t) => [arg(adminAddress, t.Address)] + }) + + for (const [listingResourceID, galleryData] of Object.entries(galleryCollection)) { + // Make sure that type is DappyNFT + if ( + Object.keys(galleryData.dappyCollection).length === 1 + && galleryData.listingDetails.nftType.endsWith("DappyNFT.NFT") + ) { + + const [key, dappyData] = Object.entries(galleryData.dappyCollection)[0] + dappyData.serialNumber = parseInt(key) + let dappy = new DappyClass( + dappyData.templateID, + dappyData.dna, + dappyData.name, + galleryData.listingDetails.salePrice, + dappyData.serialNumber + ) + dappy.sellerAddress = galleryData.sellerAddress + dappy.listingResourceID = parseInt(listingResourceID) + + galleryDappies.push(dappy) + } + + } + let res = await query({ cadence: LIST_DAPPY_TEMPLATES }) + let mappedDappies = Object.values(res).map(d => { return new DappyClass(d?.templateID, d?.dna, d?.name, d?.price) }) - dispatch({ type: 'SUCCESS', payload: mappedDappies }) + + dispatch({ type: 'SUCCESS', payload: galleryDappies.concat(mappedDappies) }) } catch (err) { + console.error(err, err.stack) dispatch({ type: 'ERROR' }) } } diff --git a/src/hooks/use-input.hook.js b/src/hooks/use-input.hook.js new file mode 100644 index 0000000..6fda92f --- /dev/null +++ b/src/hooks/use-input.hook.js @@ -0,0 +1,17 @@ +import { useState } from "react"; + +export const useInput = initialValue => { + const [value, setValue] = useState(initialValue); + + return { + value, + setValue, + reset: () => setValue(""), + bind: { + value, + onChange: event => { + setValue(event.target.value); + } + } + }; +}; \ No newline at end of file diff --git a/src/hooks/use-user-dappies.hook.js b/src/hooks/use-user-dappies.hook.js index ceeacfd..1e4338a 100644 --- a/src/hooks/use-user-dappies.hook.js +++ b/src/hooks/use-user-dappies.hook.js @@ -2,6 +2,7 @@ import { useEffect, useReducer } from 'react' import { mutate, query, tx } from '@onflow/fcl' import { LIST_USER_DAPPIES } from '../flow/list-user-dappies.script' + import { MINT_DAPPY } from '../flow/mint-dappy.tx' import { userDappyReducer } from '../reducer/userDappyReducer' import { useTxs } from '../providers/TxProvider' @@ -9,33 +10,36 @@ import DappyClass from '../utils/DappyClass' export default function useUserDappies(user, collection, getFUSDBalance) { const [state, dispatch] = useReducer(userDappyReducer, { - oading: false, + loading: false, error: false, data: [] }) const { addTx, runningTxs } = useTxs() - useEffect(() => { - const fetchUserDappies = async () => { - dispatch({ type: 'PROCESSING' }) - try { - let res = await query({ - cadence: LIST_USER_DAPPIES, - args: (arg, t) => [arg(user?.addr, t.Address)] - }) - let mappedDappies = [] + const fetchUserDappies = async () => { + dispatch({ type: 'PROCESSING' }) + try { + let res = await query({ + cadence: LIST_USER_DAPPIES, + args: (arg, t) => [arg(user?.addr, t.Address)] + }) - for (let key in res) { - const element = res[key] - let dappy = new DappyClass(element.templateID, element.dna, element.name, element.price, key) - mappedDappies.push(dappy) - } + let mappedDappies = [] - dispatch({ type: 'SUCCESS', payload: mappedDappies }) - } catch (err) { - dispatch({ type: 'ERROR' }) + for (let key in res) { + const element = res[key] + const serialNumber = parseInt(key) + let dappy = new DappyClass(element.templateID, element.dna, element.name, element.price, serialNumber) + mappedDappies.push(dappy) } + dispatch({ type: 'SUCCESS', payload: mappedDappies }) + } catch (err) { + dispatch({ type: 'ERROR' }) + console.log(err) } + } + + useEffect(() => { fetchUserDappies() //eslint-disable-next-line }, []) @@ -57,7 +61,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { }) addTx(res) await tx(res).onceSealed() - await addDappy(templateID) await getFUSDBalance() } catch (error) { console.log(error) @@ -70,9 +73,10 @@ export default function useUserDappies(user, collection, getFUSDBalance) { cadence: LIST_USER_DAPPIES, args: (arg, t) => [arg(user?.addr, t.Address)] }) + // TODO: if serialNumber is missing, need to prompt user to refresh const dappies = Object.values(res) const dappy = dappies.find(d => d?.templateID === templateID) - const newDappy = new DappyClass(dappy.templateID, dappy.dna, dappy.name) + const newDappy = new DappyClass(dappy.templateID, dappy.dna, dappy.name, dappy.price, dappy.serialNumber) dispatch({ type: 'ADD', payload: newDappy }) } catch (err) { console.log(err) @@ -101,6 +105,7 @@ export default function useUserDappies(user, collection, getFUSDBalance) { ...state, mintDappy, addDappy, - batchAddDappies + batchAddDappies, + fetchUserDappies } } diff --git a/src/hooks/use-user-pack.hook.js b/src/hooks/use-user-pack.hook.js new file mode 100644 index 0000000..8263687 --- /dev/null +++ b/src/hooks/use-user-pack.hook.js @@ -0,0 +1,183 @@ +import { useReducer } from 'react' +import { mutate, tx, config } from '@onflow/fcl' + +import { useTxs } from '../providers/TxProvider' + +import { PUT_DAPPY_STOREFRONT } from '../flow/put-dappy-storefront.tx' +import { PUT_PACK_STOREFRONT } from '../flow/put-pack-storefront.tx' +import { PURCHASE_DAPPY_STOREFRONT } from '../flow/purchase-dappy-storefront.tx' +import { PURCHASE_PACK_STOREFRONT } from '../flow/purchase-pack-storefront.tx' +import { REMOVE_GALLERY_LISTING } from '../flow/remove-gallery-listing.tx' + +const PACK_MAX_CAP = 4 + +export default function useUserPack() { + + const { addTx, runningTxs } = useTxs() + + const reducer = (state, action) => { + switch (action.type) { + case 'ADD': + //skip if dappy exists or total dappies is 4 + if (state.data.length >= PACK_MAX_CAP) return { ...state } + for (const d of state.data) { + if (d.serialNumber === action.payload.serialNumber) return { ...state } + } + const price = parseFloat(action.payload.price) + return { + ...state, + data: [...state.data, action.payload], + price: state.price + price + } + default: + throw new Error("Error in useUserPack reducer") + } + } + + const [state, dispatch] = useReducer(reducer, { + data: [], + price: 0.0 + }) + + const addToPack = ({ dappy }) => { + dispatch({ type: 'ADD', payload: dappy }) + } + + const removeFromPack = async ({ dappy }) => { + } + + const purchaseDappy = async (dappy) => { + + if (runningTxs) { + alert("Transactions are still running. Please wait for them to finish first.") + return + } + + const listingResourceID = dappy.listingResourceID + const sellerAddress = dappy.sellerAddress + + try { + let res = await mutate({ + cadence: PURCHASE_DAPPY_STOREFRONT, + limit: 200, + args: (arg, t) => [ + arg(listingResourceID, t.UInt64), + arg(sellerAddress, t.Address) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.error(error, error.stack) + } + + // TODO: merge this transaction with above + try { + const adminAddress = await config().get("0xGalleryContract") + let res = await mutate({ + cadence: REMOVE_GALLERY_LISTING, + limit: 200, + args: (arg, t) => [ + arg(adminAddress, t.Address), + arg(listingResourceID, t.UInt64) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.error(error, error.stack) + } + + } + + const purchasePackStorefront = async (listingResourceID, sellerAddress) => { + + console.log(listingResourceID, sellerAddress) + const adminAddress = await config().get("0xGalleryContract") + + try { + let res = await mutate({ + cadence: PURCHASE_PACK_STOREFRONT, + limit: 200, + args: (arg, t) => [ + arg(adminAddress, t.Address), + arg(listingResourceID, t.UInt64), + arg(sellerAddress, t.Address) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.error(error, error.stack) + } + + } + + const listPackForSale = async ( dappies, wantPrice) => { + + if (runningTxs) { + alert("Transactions are still running. Please wait for them to finish first.") + return + } + + const dappyIDs = dappies.map( (value) => value.serialNumber) + const salePrice = parseFloat(wantPrice).toFixed(8) + const adminAddress = await config().get("0xGalleryContract") + + try { + let res = await mutate({ + cadence: PUT_PACK_STOREFRONT, + limit: 300, + args: (arg, t) => [ + arg(dappyIDs, t.Array(t.UInt64)), + arg(salePrice, t.UFix64), + arg(adminAddress, t.Address) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.error(error, error.stack) + } + + } + + const listDappyForSale = async (dappy, wantPrice) => { + + if (runningTxs) { + alert("Transactions are still running. Please wait for them to finish first.") + return + } + + const dappyID = dappy.serialNumber + const salePrice = parseFloat(wantPrice).toFixed(8) + const adminAddress = await config().get("0xGalleryContract") + + try { + let res = await mutate({ + cadence: PUT_DAPPY_STOREFRONT, + limit: 200, + args: (arg, t) => [ + arg(dappyID, t.UInt64), + arg(salePrice, t.UFix64), + arg(adminAddress, t.Address) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.error(error, error.stack) + } + + } + + return { + ...state, + addToPack, + removeFromPack, + listDappyForSale, + listPackForSale, + purchaseDappy, + purchasePackStorefront + } +} diff --git a/src/pages/Collection.page.js b/src/pages/Collection.page.js index 0c4a25f..93bb496 100644 --- a/src/pages/Collection.page.js +++ b/src/pages/Collection.page.js @@ -1,12 +1,16 @@ -import React from 'react' +import React, { useEffect} from 'react' import DappyList from '../components/DappyList' import Header from '../components/Header' import { useUser } from '../providers/UserProvider' +import PackPanel from '../components/PackPanel.comp' +import BreedPanel from '../components/BreedPanel.comp' export default function Collection() { const { collection, createCollection, deleteCollection, userDappies } = useUser() + useEffect( () => {}, [userDappies]) + return ( <>
createCollection()}>Enable Collection
: <> + +
deleteCollection()}>Delete Collection
} + ) } diff --git a/src/pages/PackDetails.page.js b/src/pages/PackDetails.page.js index 2b483cc..f0d807d 100644 --- a/src/pages/PackDetails.page.js +++ b/src/pages/PackDetails.page.js @@ -1,18 +1,25 @@ import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' -import useDappyPacks from '../hooks/use-dappy-packs.hook' import Spinner from '../components/Spinner' import "./PackDetails.page.css" +import { useMarket } from '../providers/MarketProvider' +import { Pack } from '../utils/PackClass' export default function PackDetails() { const [pack, setPack] = useState(null) const [dappies, setDappies] = useState([]) const { packID } = useParams() - const { fetchDappiesOfPack, mintFromPack, fetchPackDetails } = useDappyPacks() + + const { fetchDappiesOfPack, mintFromPack, fetchPackDetails, dappyPacks, purchasePackStorefront } = useMarket() useEffect(() => { - fetchDappies() + + if (!packID.startsWith("UserPack")) + fetchDappies() + else + fetchUserPack() + //eslint-disable-next-line }, []) @@ -23,19 +30,48 @@ export default function PackDetails() { setPack(packDetails) } + const fetchUserPack = async () => { + console.log(dappyPacks) + // TODO: Bug: cannot refresh page, the state is gone + let id = (packID.replace("UserPack", "")) + let found = dappyPacks.find(ele => ele.familyID === id) + let packDetails = new Pack( + found?.familyID, + found?.name, + found?.price, + found?.sellerAddress + ) + setDappies(found?.templates.map(ele => ele.templateID)) + setPack(packDetails) + } + + const clickPurchase = async () => { + const listingResourceID = parseInt(packID.replace("UserPack", "") ) + purchasePackStorefront(listingResourceID, pack.sellerAddress) + } + if (!pack) return return (
- Pack + Pack

{pack?.name}

-
mintFromPack(packID, dappies, pack?.price)} - className="btn btn-bordered btn-light" - style={{ width: "60%", margin: "0 auto", display: "flex", justifyContent: "center" }}> - {parseInt(pack?.price)} FUSD -
+ {!pack?.sellerAddress ? +
mintFromPack(packID, dappies, pack?.price)} + className="btn btn-bordered btn-light" + style={{ width: "60%", margin: "0 auto", display: "flex", justifyContent: "center" }}> + {parseInt(pack?.price)} FUSD +
+ : +
clickPurchase()} + className="btn btn-bordered btn-light btn-storefront" + style={{ width: "60%", margin: "0 auto", display: "flex", justifyContent: "center" }}> + {parseInt(pack?.price)} FUSD +
+ }

Dappies included:

{dappies.map((d, i) => ` #${d} `)} diff --git a/src/pages/Packs.page.js b/src/pages/Packs.page.js index 7bb8a87..ba70ecf 100644 --- a/src/pages/Packs.page.js +++ b/src/pages/Packs.page.js @@ -16,7 +16,7 @@ export default function Packs() { subtitle={<>Join the pack drop to get more dappies} /> - new Pack(p?.familyID, p?.name))} store /> + new Pack(p?.familyID, p?.name, p?.price, p?.sellerAddress))} store /> ) diff --git a/src/providers/MarketProvider.js b/src/providers/MarketProvider.js new file mode 100644 index 0000000..0277d23 --- /dev/null +++ b/src/providers/MarketProvider.js @@ -0,0 +1,68 @@ +import React, { createContext, useContext } from 'react' +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' +import userUserPack from '../hooks/use-user-pack.hook' +import useDappyPacks from '../hooks/use-dappy-packs.hook' +import useBreedDappies from '../hooks/use-breed-dappies.hook' + +const UserContext = createContext() + +export default function MarketProvider({ children }) { + + const { + data: userPack, + price: packPrice, + addToPack, + removeFromPack, + listDappyForSale, + listPackForSale, + purchaseDappy, + purchasePackStorefront + } + = userUserPack() + + const { + data: dappyPacks, + fetchDappiesOfPack, + mintFromPack, + fetchPackDetails + } = useDappyPacks() + + const { + data: mates, + addMate, + breedDappies + } = useBreedDappies() + + + + + return ( + + + {children} + + + ) +} + +export const useMarket = () => { + return useContext(UserContext) +} diff --git a/src/providers/Providers.comp.js b/src/providers/Providers.comp.js index b5b4cbf..e1cdfae 100644 --- a/src/providers/Providers.comp.js +++ b/src/providers/Providers.comp.js @@ -3,6 +3,7 @@ import { BrowserRouter } from 'react-router-dom' import UserProvider from "./UserProvider" import TxProvider from './TxProvider' import AuthProvider from './AuthProvider' +import MarketProvider from './MarketProvider' export default function Providers({ children }) { return ( @@ -10,9 +11,11 @@ export default function Providers({ children }) { -

- {children} -
+ +
+ {children} +
+
diff --git a/src/providers/UserProvider.js b/src/providers/UserProvider.js index cc5e5e6..e46eeab 100644 --- a/src/providers/UserProvider.js +++ b/src/providers/UserProvider.js @@ -11,7 +11,7 @@ export default function UserProvider({ children }) { const { user } = useAuth() const { collection, createCollection, deleteCollection } = useCollection(user) const { data: balance, createFUSDVault, getFUSDBalance } = useFUSD(user) - const { data: userDappies, addDappy, batchAddDappies, mintDappy } = useUserDappies(user, collection, getFUSDBalance) + const { data: userDappies, addDappy, batchAddDappies, mintDappy, fetchUserDappies, newDappies } = useUserDappies(user, collection, getFUSDBalance) return ( { error: false } case 'SUCCESS': + const oldIDs = state.data?.map( v => v.serialNumber) + const newDappies = oldIDs? + action.payload.filter( + v => !oldIDs.includes( v.serialNumber) + ) : [] return { ...state, loading: false, error: false, - data: action.payload + data: action.payload, + newDappies: newDappies } - case 'ADD': + case 'ADD': return { ...state, - data: [...state.data, action.payload] + data: [...state.data, action.payload], + newDappies: [action.payload] } case 'ERROR': return { diff --git a/src/utils/DappyClass.js b/src/utils/DappyClass.js index 4fbdd35..aa7a21b 100644 --- a/src/utils/DappyClass.js +++ b/src/utils/DappyClass.js @@ -8,7 +8,7 @@ class DappyClass { this.name = name this.price = price || 0 this.serialNumber = serialNumber || 0 - } +} get rarity() { switch (parseDNA(this.dna).length - 1) { diff --git a/src/utils/PackClass.js b/src/utils/PackClass.js index 4aad8d6..e126075 100644 --- a/src/utils/PackClass.js +++ b/src/utils/PackClass.js @@ -1,11 +1,12 @@ import { ULTRARARE } from "../config/dappies.config" export class Pack { - constructor(id, name, price) { + constructor(id, name, price, sellerAddress) { this._id = id this.name = name this.dappies = [] this.price = price + this.sellerAddress = sellerAddress } get rarity() { @@ -17,7 +18,11 @@ export class Pack { } get id() { - return `Pack${this._id}`; + return this.sellerAddress ? + `UserPack${this._id}` + : + `Pack${this._id}` + } get size() { @@ -25,6 +30,9 @@ export class Pack { } get image() { - return `${process.env.PUBLIC_URL}/assets/${this.id}.png` + return this.sellerAddress ? + `${process.env.PUBLIC_URL}/assets/Pack4.png` + : + `${process.env.PUBLIC_URL}/assets/${this.id}.png` } } \ No newline at end of file diff --git a/src/utils/dappies.utils.js b/src/utils/dappies.utils.js index 64ff92a..0c46336 100644 --- a/src/utils/dappies.utils.js +++ b/src/utils/dappies.utils.js @@ -103,7 +103,7 @@ export const createThemedDNA = (rarity, base_color, full_random, theme) => { } dna += createRandomNumber(rarity) - + return dna } @@ -116,6 +116,7 @@ export const generateDappies = (dappies = DEFAULT_DAPPIES) => { price: calculatePrice(d?.dna?.length) } }) + return generatedDappies } diff --git a/up.sh b/up.sh new file mode 100755 index 0000000..45208fc --- /dev/null +++ b/up.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +flow accounts remove-contract GalleryContract --network testnet --signer MyAdmin + +flow accounts remove-contract NonFungibleToken --network testnet --signer MyAdmin + +flow accounts remove-contract DappyNFT --network testnet --signer MyAdmin + +flow accounts remove-contract PackNFT --network testnet --signer MyAdmin + +flow accounts remove-contract NFTStorefront --network testnet --signer MyAdmin + +flow accounts remove-contract DappyContract --network testnet --signer MyAdmin + +flow project deploy --network testnet + +flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyAdmin --network testnet + +flow transactions send cadence/transactions/PrepareDappyContract.cdc --signer MyAdmin --network testnet \ No newline at end of file