From 5e2bb4d5161e5453f87d26f982bad3c4068ba586 Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Wed, 20 Oct 2021 21:43:36 +0700 Subject: [PATCH 01/12] Flip Fest - First Milestone - Team Ancient Machine --- package.json | 2 + src/components/BreedPanel.comp.js | 29 ++++++++++++ src/components/Dappy.js | 20 +++++---- src/components/DappyCard.css | 4 ++ src/components/DappyCard.js | 20 ++++++++- src/components/DappyList.css | 71 ++++++++++++++++++++++++++++++ src/components/DappyList.js | 24 +++++----- src/components/PackPanel.comp.js | 58 ++++++++++++++++++++++++ src/components/PriceButton.css | 31 +++++++++++++ src/components/PriceButton.js | 38 ++++++++++++++++ src/hooks/use-input.hook.js | 17 +++++++ src/hooks/use-user-dappies.hook.js | 2 +- src/hooks/use-user-pack.hook.js | 52 ++++++++++++++++++++++ src/pages/Collection.page.js | 5 +++ src/providers/MarketProvider.js | 29 ++++++++++++ src/providers/Providers.comp.js | 9 ++-- 16 files changed, 386 insertions(+), 25 deletions(-) create mode 100644 src/components/BreedPanel.comp.js create mode 100644 src/components/PackPanel.comp.js create mode 100644 src/components/PriceButton.css create mode 100644 src/components/PriceButton.js create mode 100644 src/hooks/use-input.hook.js create mode 100644 src/hooks/use-user-pack.hook.js create mode 100644 src/providers/MarketProvider.js diff --git a/package.json b/package.json index 1f1faaf..de58790 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", diff --git a/src/components/BreedPanel.comp.js b/src/components/BreedPanel.comp.js new file mode 100644 index 0000000..e28cab2 --- /dev/null +++ b/src/components/BreedPanel.comp.js @@ -0,0 +1,29 @@ +import React from 'react' + +import { useDrop } from 'react-dnd' + +export default function BreedPanel() { + + const [, drop] = useDrop(() => ({ + accept: 'box', + drop: item => { }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }) + })); + + return ( +
+
Drop here
+
to breed
+
+
+
+
+
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..33a45d2 100644 --- a/src/components/DappyCard.css +++ b/src/components/DappyCard.css @@ -1,3 +1,7 @@ +.dappy-card__draggable { + draggable: true; +} + .dappy-card__border { position: relative; margin-bottom: 2.5rem; diff --git a/src/components/DappyCard.js b/src/components/DappyCard.js index 1ba16ee..17b58a5 100644 --- a/src/components/DappyCard.js +++ b/src/components/DappyCard.js @@ -1,16 +1,32 @@ import React from 'react' import { useHistory } from 'react-router-dom' +import { useDrag } from 'react-dnd' + import { useUser } from '../providers/UserProvider' import Dappy from './Dappy' import "./DappyCard.css" +import PriceButton from './PriceButton' + export default function DappyCard({ dappy, store, designer }) { + const { userDappies, mintDappy } = useUser() 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 DappyButton = () => (
mintDappy(id, price)} @@ -36,7 +52,7 @@ export default function DappyCard({ dappy, store, designer }) { ) return ( -
+
{type === "Dappy" ? : Pack @@ -57,6 +73,8 @@ export default function DappyCard({ dappy, store, designer }) { } + {!store && owned && !designer && } + {store && owned && !designer &&
Collected
}
) diff --git a/src/components/DappyList.css b/src/components/DappyList.css index c79b587..13aa56f 100644 --- a/src/components/DappyList.css +++ b/src/components/DappyList.css @@ -6,4 +6,75 @@ justify-items: center; align-items: center; justify-content: center; +} + + + +.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 { + width: 60px; + height: 60px; + margin: 0 auto; +} + +.left_panel 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; } \ No newline at end of file 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..f319de0 --- /dev/null +++ b/src/components/PackPanel.comp.js @@ -0,0 +1,58 @@ +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 } = useMarket() + + const { value: wantPrice, setValue: setPrice, bind: bindPrice, reset: resetPrice } = useInput(packPrice); + + useEffect (() => { + setPrice(packPrice) + },[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) => ( + + + )) + } +
+ + +
+
+
+
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..ba67bb5 --- /dev/null +++ b/src/components/PriceButton.js @@ -0,0 +1,38 @@ +import React, { useState } from 'react' + +import './PriceButton.css' + +export default function PriceButton({ dappy }) { + + const [sell, setSell] = useState(false); + + const clickShow = () => { + setSell(!sell); + } + + return ( + <> + +
+
+ + +
+
+ List for Sale +
+
+ +
clickShow()} + className="btn btn-bordered btn-light btn-dappy"> + {sell ? + : + + } + {parseInt(dappy.price)} FUSD +
+ + + ) +} 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..9ea2552 100644 --- a/src/hooks/use-user-dappies.hook.js +++ b/src/hooks/use-user-dappies.hook.js @@ -9,7 +9,7 @@ import DappyClass from '../utils/DappyClass' export default function useUserDappies(user, collection, getFUSDBalance) { const [state, dispatch] = useReducer(userDappyReducer, { - oading: false, + loading: false, error: false, data: [] }) diff --git a/src/hooks/use-user-pack.hook.js b/src/hooks/use-user-pack.hook.js new file mode 100644 index 0000000..86074e4 --- /dev/null +++ b/src/hooks/use-user-pack.hook.js @@ -0,0 +1,52 @@ +import { useReducer } from 'react' + +export default function useUserPack() { + const reducer = (state, action) => { + switch (action.type) { + case 'ADD': + //skip if dappy exists or total dappies is 4 + console.log(state.data) + if (state.data.length >= 4) return { ...state } + for (const d of state.data ) { + if (d.id === action.payload.id) return { ...state} + } + const price = parseFloat(action.payload.price) + return { + ...state, + data: [...state.data, action.payload], + price: state.price + price + } + case 'NEW': + return { + ...state, + data: [...state.data, action.payload], + price: parseFloat(action.payload.price) + } + case 'REMOVE': + return { + ...state, + data: [...state.data, action.payload] + } + 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}) => { + } + + return { + ...state, + addToPack, + removeFromPack + } +} diff --git a/src/pages/Collection.page.js b/src/pages/Collection.page.js index 0c4a25f..7f00816 100644 --- a/src/pages/Collection.page.js +++ b/src/pages/Collection.page.js @@ -3,6 +3,8 @@ 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() @@ -17,10 +19,13 @@ export default function Collection() { {!collection ?
createCollection()}>Enable Collection
: <> + +
deleteCollection()}>Delete Collection
} + ) } diff --git a/src/providers/MarketProvider.js b/src/providers/MarketProvider.js new file mode 100644 index 0000000..61fd146 --- /dev/null +++ b/src/providers/MarketProvider.js @@ -0,0 +1,29 @@ +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' + +const UserContext = createContext() + +export default function MarketProvider({ children }) { + + const { data: userPack, price: packPrice, addToPack, removeFromPack } = userUserPack() + + 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} +
+
From 270bce4b19823b62ac149de237538b5218d7a5f3 Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Thu, 21 Oct 2021 03:22:30 +0700 Subject: [PATCH 02/12] Add DappyNFT wrapper for Dappy --- cadence/contracts/DappyNFT.cdc | 112 +++++ cadence/contracts/GalleryContract.cdc | 86 ++++ cadence/contracts/NFTStorefront.cdc | 470 ++++++++++++++++++ cadence/contracts/NonFungibleToken.cdc | 98 ++++ cadence/tests/Storefront.test.js | 97 ++++ cadence/tests/src/Storefront.js | 55 ++ .../transactions/CreateDappyNFTCollection.cdc | 15 + cadence/transactions/CreateNFTStorefront.cdc | 16 + .../transactions/ListDappyInStorefront.cdc | 80 +++ 9 files changed, 1029 insertions(+) create mode 100644 cadence/contracts/DappyNFT.cdc create mode 100644 cadence/contracts/GalleryContract.cdc create mode 100644 cadence/contracts/NFTStorefront.cdc create mode 100644 cadence/contracts/NonFungibleToken.cdc create mode 100644 cadence/tests/Storefront.test.js create mode 100644 cadence/tests/src/Storefront.js create mode 100644 cadence/transactions/CreateDappyNFTCollection.cdc create mode 100644 cadence/transactions/CreateNFTStorefront.cdc create mode 100644 cadence/transactions/ListDappyInStorefront.cdc diff --git a/cadence/contracts/DappyNFT.cdc b/cadence/contracts/DappyNFT.cdc new file mode 100644 index 0000000..beef25c --- /dev/null +++ b/cadence/contracts/DappyNFT.cdc @@ -0,0 +1,112 @@ +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 + pub let nft: @DappyContract.Dappy + + pub init(nft: @DappyContract.Dappy) { + + self.id = nft.id + self.nft <- nft + DappyNFT.totalSupply = DappyNFT.totalSupply + UInt64(1) + + } + + 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): &NonFungibleToken.NFT { + return &self.ownedNFTs[id] as &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..4d7be93 --- /dev/null +++ b/cadence/contracts/GalleryContract.cdc @@ -0,0 +1,86 @@ +import DappyContract from "./DappyContract.cdc" +import NFTStorefront from "./NFTStorefront.cdc" + +pub contract GalleryContract { + + pub event GalleryListingAdded( + storefrontResourceID: UInt64, + sellerAddress: Address + ) + + pub event GalleryListingRemoved( + storefrontResourceID: UInt64, + sellerAddress: Address + ) + + pub struct GalleryData { + + pub let listingDetails: NFTStorefront.ListingDetails + pub let sellerAddress: Address + + init ( + listingDetails: NFTStorefront.ListingDetails, + sellerAddress: Address + ) { + + self.listingDetails = listingDetails + self.sellerAddress = sellerAddress + + } + + } + + pub resource Gallery { + + access(contract) let galleryCollection: {UInt64: GalleryData} + + init() { + 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 galleryData = GalleryData( + listingDetails: details, + sellerAddress: sellerAddress) + + self.galleryCollection[details.storefrontID] = galleryData + + emit GalleryListingAdded( + storefrontResourceID: details.storefrontID, + sellerAddress: sellerAddress + ) + + } + + // Add a listing to gallery + pub fun removeListing ( + storefrontResourceID: UInt64, + sellerAddress: Address + ) { + + pre { + // 1. naively check if the address hold this listing no more + } + + self.galleryCollection.remove(key: storefrontResourceID) + + emit GalleryListingRemoved( + storefrontResourceID: storefrontResourceID, + sellerAddress: sellerAddress + ) + + } + + } + +} \ No newline at end of file diff --git a/cadence/contracts/NFTStorefront.cdc b/cadence/contracts/NFTStorefront.cdc new file mode 100644 index 0000000..06bacbe --- /dev/null +++ b/cadence/contracts/NFTStorefront.cdc @@ -0,0 +1,470 @@ +import FungibleToken from "./FungibleToken.cdc" +import NonFungibleToken from "./NonFungibleToken.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, + nftID: UInt64, + 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. + // The ID of the NFT within that type. + pub let nftID: UInt64 + // The Type of the FungibleToken that payments must be made in. + // 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 ( + nftID: UInt64, + saleCuts: [SaleCut], + storefrontID: UInt64 + ) { + self.storefrontID = storefrontID + self.purchased = false + self.nftID = nftID + + // 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(): &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 + } + + + // 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(): &NonFungibleToken.NFT { + let ref = self.nftProviderCapability.borrow()!.borrowNFT(id: self.getDetails().nftID) + //- CANNOT DO THIS IN PRECONDITION: "member of restricted type is not accessible: isInstance" + assert(ref.id == self.getDetails().nftID, message: "token has wrong ID") + return ref as &NonFungibleToken.NFT + } + + // 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 + } + + // 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.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.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}>, + nftID: UInt64, + saleCuts: [SaleCut], + storefrontID: UInt64 + ) { + // Store the sale information + self.details = ListingDetails( + nftID: nftID, + 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.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}>, + nftID: UInt64, + 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) + } + + // 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}>, + nftID: UInt64, + saleCuts: [SaleCut] + ): UInt64 { + let listing <- create Listing( + nftProviderCapability: nftProviderCapability, + nftID: nftID, + 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, + nftID: nftID, + 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 + } + + // 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..fa13c6e --- /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): &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): &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/tests/Storefront.test.js b/cadence/tests/Storefront.test.js new file mode 100644 index 0000000..2c6f8ab --- /dev/null +++ b/cadence/tests/Storefront.test.js @@ -0,0 +1,97 @@ +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 } 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.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.only("Should list 1 dappy for sale", async () => { + let DappyAdmin = await dappyContract.getDappyAdminAddress() + mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() + await storefront.deployNFTStorefront() + await dappyContract.deployDappyContract() + 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.deployDappyNFT() + await storefront.createDappyNFTCollection(recipient) + await storefront.createNFTStorefront(recipient) + + const saleCuts = { + [DappyAdmin]: "1.0", + [recipient]: "8.0" + } + + // emulator.setLogging(true) + await storefront.listDappyInStorefront(recipient, 1, saleCuts) // list dappy with id=1 for sale + + // const userListings = await dappyContract.listStorefront(recipient) + + }); + + + +}) \ No newline at end of file diff --git a/cadence/tests/src/Storefront.js b/cadence/tests/src/Storefront.js new file mode 100644 index 0000000..f2ff0e8 --- /dev/null +++ b/cadence/tests/src/Storefront.js @@ -0,0 +1,55 @@ +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 createDappyNFTCollection = async(recipient) => { + const name = "CreateDappyNFTCollection" + const signers = [recipient] + await sendTransaction({ name, signers }) +} + +export const createNFTStorefront = async(recipient) => { + const name = "CreateNFTStorefront" + const signers = [recipient] + await sendTransaction({ name, signers }) +} + +export const listDappyInStorefront = async (recipient, dappyID, saleCuts) => { + const name = "ListDappyInStorefront" + const signers = [recipient] + const args = [dappyID, saleCuts] + await sendTransaction({ name, signers, args }) + + // let intType: Type = Type() + // let type: Type = something.getType() +} \ 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/ListDappyInStorefront.cdc b/cadence/transactions/ListDappyInStorefront.cdc new file mode 100644 index 0000000..c319b43 --- /dev/null +++ b/cadence/transactions/ListDappyInStorefront.cdc @@ -0,0 +1,80 @@ +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(dappyID: UInt64, saleCuts: {Address: UFix64}) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &DappyNFT.Collection + let managerRef: &{NFTStorefront.StorefrontManager} + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + + prepare(acct: AuthAccount) { + + 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 + ) + + self.saleCuts = [] + + for key in saleCuts.keys { + let account = getAccount(key) + let receiver = account + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + self.saleCuts.append( + NFTStorefront.SaleCut( + receiver: receiver, + amount: saleCuts[key]!) + ) + } + + } + + execute { + + let dappy <- self.dappyColRef.withdraw(withdrawID: dappyID) + + let nft <- DappyNFT.createFromDappy(dappy: <- dappy) + + let nftID = nft.id + + let nftType = Type<&AnyResource>() + + self.nftColRef.deposit(token: <- nft) + + + self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftID: nftID, + saleCuts: self.saleCuts + ) + + + } +} \ No newline at end of file From a6bbf941d4437cfa31c69d39753c0d5d46199cdc Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Fri, 22 Oct 2021 12:05:53 +0700 Subject: [PATCH 03/12] Unit Test for NFTStorefront --- cadence/contracts/DappyNFT.cdc | 1 + cadence/contracts/NFTStorefront.cdc | 12 ++++++++++ cadence/scripts/ListStorefrontListings.cdc | 24 +++++++++++++++++++ cadence/tests/Storefront.test.js | 11 +++++---- cadence/tests/jest.config.js | 5 ++++ cadence/tests/package.json | 2 +- cadence/tests/src/DappyContract.js | 4 +++- cadence/tests/src/Storefront.js | 14 +++++++---- ...torefront.cdc => PutDappyInStorefront.cdc} | 0 flow.json | 1 + 10 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 cadence/scripts/ListStorefrontListings.cdc rename cadence/transactions/{ListDappyInStorefront.cdc => PutDappyInStorefront.cdc} (100%) diff --git a/cadence/contracts/DappyNFT.cdc b/cadence/contracts/DappyNFT.cdc index beef25c..78bfee0 100644 --- a/cadence/contracts/DappyNFT.cdc +++ b/cadence/contracts/DappyNFT.cdc @@ -67,6 +67,7 @@ pub contract DappyNFT: NonFungibleToken { } pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { + return &self.ownedNFTs[id] as &NonFungibleToken.NFT } diff --git a/cadence/contracts/NFTStorefront.cdc b/cadence/contracts/NFTStorefront.cdc index 06bacbe..5e808b8 100644 --- a/cadence/contracts/NFTStorefront.cdc +++ b/cadence/contracts/NFTStorefront.cdc @@ -346,6 +346,7 @@ pub contract NFTStorefront { // pub resource interface StorefrontPublic { pub fun getListingIDs(): [UInt64] + pub fun getAllListingDetails(): {UInt64: NFTStorefront.ListingDetails} pub fun borrowListing(listingResourceID: UInt64): &Listing{ListingPublic}? pub fun cleanup(listingResourceID: UInt64) } @@ -409,6 +410,17 @@ pub contract NFTStorefront { 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. // diff --git a/cadence/scripts/ListStorefrontListings.cdc b/cadence/scripts/ListStorefrontListings.cdc new file mode 100644 index 0000000..ceaa3dc --- /dev/null +++ b/cadence/scripts/ListStorefrontListings.cdc @@ -0,0 +1,24 @@ +import NFTStorefront from "../contracts/NFTStorefront.cdc" +import DappyNFT from "../contracts/DappyNFT.cdc" + +pub fun main(addr: Address): [UInt64] { + + let account = getAccount(addr) + + let storefrontRef = account + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath + ) + .borrow() + ?? panic ("Could not borrow StorefrontPublic ref") + + let listings = storefrontRef.getListingIDs() + + let ref = storefrontRef.borrowListing(listingResourceID: listings[0]) + + let details = ref!.getDetails() + + return listings + +} + \ No newline at end of file diff --git a/cadence/tests/Storefront.test.js b/cadence/tests/Storefront.test.js index 2c6f8ab..4bebf40 100644 --- a/cadence/tests/Storefront.test.js +++ b/cadence/tests/Storefront.test.js @@ -85,13 +85,14 @@ describe("NFTStorefront", () => { [recipient]: "8.0" } + await storefront.putDappyInStorefront(recipient, 1, saleCuts) // list dappy with id=1 for sale + // emulator.setLogging(true) - await storefront.listDappyInStorefront(recipient, 1, saleCuts) // list dappy with id=1 for sale + const userListings = await storefront.listStorefrontListings(recipient) - // const userListings = await dappyContract.listStorefront(recipient) + console.log(userListings) }); - - -}) \ No newline at end of file + +}) diff --git a/cadence/tests/jest.config.js b/cadence/tests/jest.config.js index dfb7ef3..b205106 100644 --- a/cadence/tests/jest.config.js +++ b/cadence/tests/jest.config.js @@ -2,4 +2,9 @@ module.exports = { testEnvironment: "node", verbose: true, coveragePathIgnorePatterns: ["/node_modules/"], + projects: [{ + "displayName": "Dappy Cadence Tests", + // "testMatch": ["/**/*.test.js"], + "testMatch": ["/**/Storefront.test.js"] + }] }; \ No newline at end of file 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 index f2ff0e8..e033169 100644 --- a/cadence/tests/src/Storefront.js +++ b/cadence/tests/src/Storefront.js @@ -44,12 +44,16 @@ export const createNFTStorefront = async(recipient) => { await sendTransaction({ name, signers }) } -export const listDappyInStorefront = async (recipient, dappyID, saleCuts) => { - const name = "ListDappyInStorefront" +export const putDappyInStorefront = async (recipient, dappyID, saleCuts) => { + const name = "PutDappyInStorefront" const signers = [recipient] const args = [dappyID, saleCuts] await sendTransaction({ name, signers, args }) - - // let intType: Type = Type() - // let type: Type = something.getType() +} + +export const listStorefrontListings = async (recipient) => { + const name = "ListStorefrontListings" + const args = [recipient] + const listings = await executeScript({ name, args }) + return listings } \ No newline at end of file diff --git a/cadence/transactions/ListDappyInStorefront.cdc b/cadence/transactions/PutDappyInStorefront.cdc similarity index 100% rename from cadence/transactions/ListDappyInStorefront.cdc rename to cadence/transactions/PutDappyInStorefront.cdc diff --git a/flow.json b/flow.json index e82d940..3fea05d 100644 --- a/flow.json +++ b/flow.json @@ -7,6 +7,7 @@ }, "contracts": { "DappyContract": "./cadence/contracts/DappyContract.cdc", + "NFTStorefront": "./cadence/contracts/NFTStorefront.cdc", "FUSD": { "source": "./cadence/contracts/FUSD.cdc", "aliases": { From 5be564106aa989edcaf46d93dac7bb4624a96ce7 Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Sat, 23 Oct 2021 03:05:38 +0700 Subject: [PATCH 04/12] Unit Test for Gallery --- cadence/contracts/DappyNFT.cdc | 4 +- cadence/contracts/GalleryContract.cdc | 36 ++++- cadence/contracts/NFTStorefront.cdc | 45 +++++-- cadence/contracts/NonFungibleToken.cdc | 4 +- cadence/scripts/ListGalleryCollection.cdc | 19 +++ cadence/scripts/ListStorefrontListings.cdc | 28 ++-- cadence/tests/Storefront.test.js | 38 ++++-- cadence/tests/jest.config.js | 5 +- cadence/tests/package-lock.json | 1 + cadence/tests/src/Storefront.js | 37 ++++- cadence/transactions/CreateAdminGallery.cdc | 13 ++ .../transactions/DeleteDappyNFTCollection.cdc | 16 +++ cadence/transactions/DeleteNFTStorefront.cdc | 12 ++ cadence/transactions/PutDappyInStorefront.cdc | 69 ++++++---- crypto-dappy.code-workspace | 8 ++ flow.back.json | 64 +++++++++ flow.json | 45 ++++++- package-lock.json | 126 ++++++++++++++++++ package.json | 1 + src/components/PriceButton.js | 20 ++- src/config/config.js | 6 +- src/flow/create-nft-collection.tx.js | 24 ++++ src/flow/delete-nft-collection.tx.js | 25 ++++ src/flow/put-dappy-storefront.tx.js | 82 ++++++++++++ src/hooks/use-collection.hook.js | 21 ++- src/hooks/use-user-dappies.hook.js | 7 +- src/hooks/use-user-pack.hook.js | 39 +++++- src/providers/MarketProvider.js | 11 +- 28 files changed, 727 insertions(+), 79 deletions(-) create mode 100644 cadence/scripts/ListGalleryCollection.cdc create mode 100644 cadence/transactions/CreateAdminGallery.cdc create mode 100644 cadence/transactions/DeleteDappyNFTCollection.cdc create mode 100644 cadence/transactions/DeleteNFTStorefront.cdc create mode 100644 crypto-dappy.code-workspace create mode 100644 flow.back.json create mode 100644 src/flow/create-nft-collection.tx.js create mode 100644 src/flow/delete-nft-collection.tx.js create mode 100644 src/flow/put-dappy-storefront.tx.js diff --git a/cadence/contracts/DappyNFT.cdc b/cadence/contracts/DappyNFT.cdc index 78bfee0..7d7fa15 100644 --- a/cadence/contracts/DappyNFT.cdc +++ b/cadence/contracts/DappyNFT.cdc @@ -66,9 +66,9 @@ pub contract DappyNFT: NonFungibleToken { } - pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { + pub fun borrowNFT(id: UInt64): auth &NonFungibleToken.NFT { - return &self.ownedNFTs[id] as &NonFungibleToken.NFT + return &self.ownedNFTs[id] as auth &NonFungibleToken.NFT } diff --git a/cadence/contracts/GalleryContract.cdc b/cadence/contracts/GalleryContract.cdc index 4d7be93..dadca20 100644 --- a/cadence/contracts/GalleryContract.cdc +++ b/cadence/contracts/GalleryContract.cdc @@ -3,6 +3,9 @@ import NFTStorefront from "./NFTStorefront.cdc" pub contract GalleryContract { + pub let GalleryStoragePath: StoragePath + pub let GalleryPublicPath: PublicPath + pub event GalleryListingAdded( storefrontResourceID: UInt64, sellerAddress: Address @@ -29,8 +32,24 @@ pub contract GalleryContract { } } + + pub resource interface GalleryPublic { + + pub fun addListing ( + listingPublic: &NFTStorefront.Listing{NFTStorefront.ListingPublic}, + sellerAddress: Address + ) + + pub fun removeListing ( + storefrontResourceID: UInt64, + sellerAddress: Address + ) + + pub fun getGalleryCollection (): {UInt64: GalleryData} + + } - pub resource Gallery { + pub resource Gallery: GalleryPublic { access(contract) let galleryCollection: {UInt64: GalleryData} @@ -38,9 +57,13 @@ pub contract GalleryContract { self.galleryCollection = {} } + pub fun getGalleryCollection (): {UInt64: GalleryData} { + return self.galleryCollection + } + // Add a listing to gallery pub fun addListing ( - listingPublic: &{NFTStorefront.ListingPublic}, + listingPublic: &NFTStorefront.Listing{NFTStorefront.ListingPublic}, sellerAddress: Address ) { @@ -83,4 +106,13 @@ pub contract GalleryContract { } + pub fun createEmptyGallery(): @Gallery { + return <- create Gallery() + } + + init() { + self.GalleryStoragePath = /storage/DappyGallery + self.GalleryPublicPath = /public/DappyGallery + } + } \ No newline at end of file diff --git a/cadence/contracts/NFTStorefront.cdc b/cadence/contracts/NFTStorefront.cdc index 5e808b8..0873140 100644 --- a/cadence/contracts/NFTStorefront.cdc +++ b/cadence/contracts/NFTStorefront.cdc @@ -1,5 +1,5 @@ -import FungibleToken from "./FungibleToken.cdc" -import NonFungibleToken from "./NonFungibleToken.cdc" +import NonFungibleToken from "../contracts/NonFungibleToken.cdc" +import FungibleToken from "../contracts/FungibleToken.cdc" // NFTStorefront // @@ -58,7 +58,9 @@ pub contract NFTStorefront { pub event ListingAvailable( storefrontAddress: Address, listingResourceID: UInt64, + nftType: Type, nftID: UInt64, + ftVaultType: Type, price: UFix64 ) @@ -113,9 +115,11 @@ pub contract NFTStorefront { // 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. @@ -131,13 +135,17 @@ pub contract NFTStorefront { // 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") @@ -170,8 +178,9 @@ pub contract NFTStorefront { // 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(): &NonFungibleToken.NFT + pub fun borrowNFT(): auth &NonFungibleToken.NFT + // purchase // Purchase the listing, buying the token. // This pays the beneficiaries and returns the token to the buyer. @@ -202,11 +211,15 @@ pub contract NFTStorefront { // 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(): &NonFungibleToken.NFT { + 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 as &NonFungibleToken.NFT + + return ref } // getDetails @@ -225,6 +238,7 @@ pub contract NFTStorefront { 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" } @@ -238,6 +252,7 @@ pub contract NFTStorefront { // 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, @@ -295,13 +310,17 @@ pub contract NFTStorefront { // 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 ) @@ -317,6 +336,7 @@ pub contract NFTStorefront { // 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") } } @@ -331,7 +351,9 @@ pub contract NFTStorefront { // pub fun createListing( nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, + nftType: Type, nftID: UInt64, + salePaymentVaultType: Type, saleCuts: [SaleCut] ): UInt64 // removeListing @@ -346,10 +368,10 @@ pub contract NFTStorefront { // pub resource interface StorefrontPublic { pub fun getListingIDs(): [UInt64] - pub fun getAllListingDetails(): {UInt64: NFTStorefront.ListingDetails} 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 @@ -364,12 +386,16 @@ pub contract NFTStorefront { // 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 ) @@ -385,7 +411,9 @@ pub contract NFTStorefront { emit ListingAvailable( storefrontAddress: self.owner?.address!, listingResourceID: listingResourceID, + nftType: nftType, nftID: nftID, + ftVaultType: salePaymentVaultType, price: listingPrice ) @@ -412,6 +440,7 @@ pub contract NFTStorefront { 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 { @@ -479,4 +508,4 @@ pub contract NFTStorefront { emit NFTStorefrontInitialized() } -} \ No newline at end of file +} diff --git a/cadence/contracts/NonFungibleToken.cdc b/cadence/contracts/NonFungibleToken.cdc index fa13c6e..656529b 100644 --- a/cadence/contracts/NonFungibleToken.cdc +++ b/cadence/contracts/NonFungibleToken.cdc @@ -58,7 +58,7 @@ pub contract interface NonFungibleToken { pub resource interface CollectionPublic { pub fun deposit(token: @NFT) pub fun getIDs(): [UInt64] - pub fun borrowNFT(id: UInt64): &NFT + pub fun borrowNFT(id: UInt64): auth &NFT } // Requirement for the the concrete resource type @@ -81,7 +81,7 @@ pub contract interface NonFungibleToken { // 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): &NFT { + pub fun borrowNFT(id: UInt64): auth &NFT { pre { self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" } 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 index ceaa3dc..cbd7160 100644 --- a/cadence/scripts/ListStorefrontListings.cdc +++ b/cadence/scripts/ListStorefrontListings.cdc @@ -1,24 +1,28 @@ 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] { +pub fun main(addr: Address): UInt64 { - let account = getAccount(addr) + let account = getAccount(addr) - let storefrontRef = account - .getCapability<&{NFTStorefront.StorefrontPublic}>( - NFTStorefront.StorefrontPublicPath - ) - .borrow() - ?? panic ("Could not borrow StorefrontPublic ref") + let storefrontRef = account + .getCapability<&{NFTStorefront.StorefrontPublic}>( + NFTStorefront.StorefrontPublicPath + ) + .borrow() + ?? panic ("Could not borrow StorefrontPublic ref") let listings = storefrontRef.getListingIDs() - let ref = storefrontRef.borrowListing(listingResourceID: listings[0]) + let listingRef = storefrontRef.borrowListing(listingResourceID: listings[0]) - let details = ref!.getDetails() - - return listings + let nftRef =listingRef!.borrowNFT() as! &DappyNFT.NFT + + var ret = nftRef.nft.id + + return ret } \ No newline at end of file diff --git a/cadence/tests/Storefront.test.js b/cadence/tests/Storefront.test.js index 4bebf40..5f9abea 100644 --- a/cadence/tests/Storefront.test.js +++ b/cadence/tests/Storefront.test.js @@ -40,6 +40,7 @@ describe("NFTStorefront", () => { it("deploys Storefront contract", async () => { let DappyAdmin = await dappyContract.getDappyAdminAddress() mintFlow(DappyAdmin, "10.0") + await storefront.deployNonFungibleToken() await storefront.deployNFTStorefront() }); @@ -62,12 +63,26 @@ describe("NFTStorefront", () => { await storefront.createDappyNFTCollection(recipient) }); - it.only("Should list 1 dappy for sale", async () => { + it("creates Admin Gallery", async () => { let DappyAdmin = await dappyContract.getDappyAdminAddress() - mintFlow(DappyAdmin, "10.0") + await mintFlow(DappyAdmin, "10.0") await storefront.deployNonFungibleToken() + await dappyContract.deployDappyContract() + await storefront.deployDappyNFT() 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.deployNFTStorefront() + await storefront.deployGalleryContract() + await dappyContract.createDappyTemplate(TEST_DAPPY) const recipient = await getAccountAddress("DappyRecipient") await mintFlow(recipient, "10.0") @@ -76,7 +91,6 @@ describe("NFTStorefront", () => { await mintFUSD(DappyAdmin, "100.00") await dappyContract.createDappyCollection(recipient) await dappyContract.mintDappy(recipient, TEST_DAPPY) - await storefront.deployDappyNFT() await storefront.createDappyNFTCollection(recipient) await storefront.createNFTStorefront(recipient) @@ -85,13 +99,21 @@ describe("NFTStorefront", () => { [recipient]: "8.0" } - await storefront.putDappyInStorefront(recipient, 1, saleCuts) // list dappy with id=1 for sale - - // emulator.setLogging(true) - const userListings = await storefront.listStorefrontListings(recipient) + const salePrice = "11.0" - console.log(userListings) + // emulator.setLogging(true) + await storefront.createAdminGallery(DappyAdmin) + await storefront.putDappyInStorefront(recipient, 1, salePrice) // list dappy with id=1 for sale + const dappyID = await storefront.listStorefrontListings(recipient) + expect(dappyID).toBe(1) + + const gallery = await storefront.listGalleryCollection() + const nftID = Object.values(gallery)[0].listingDetails.nftID + const sellerAddress = Object.values(gallery)[0].sellerAddress + expect(nftID).toBe(1) + expect(sellerAddress).toBe(recipient) + }); diff --git a/cadence/tests/jest.config.js b/cadence/tests/jest.config.js index b205106..67f78db 100644 --- a/cadence/tests/jest.config.js +++ b/cadence/tests/jest.config.js @@ -4,7 +4,8 @@ module.exports = { coveragePathIgnorePatterns: ["/node_modules/"], projects: [{ "displayName": "Dappy Cadence Tests", - // "testMatch": ["/**/*.test.js"], - "testMatch": ["/**/Storefront.test.js"] + "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/src/Storefront.js b/cadence/tests/src/Storefront.js index e033169..6db176e 100644 --- a/cadence/tests/src/Storefront.js +++ b/cadence/tests/src/Storefront.js @@ -1,3 +1,4 @@ +import { query } from "@onflow/fcl"; import { getAccountAddress, mintFlow, @@ -44,16 +45,44 @@ export const createNFTStorefront = async(recipient) => { await sendTransaction({ name, signers }) } -export const putDappyInStorefront = async (recipient, dappyID, saleCuts) => { +export const deployGalleryContract = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const addressMap = { + DappyContract: DappyAdmin, + NFTStorefront: 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, saleCuts] + const args = [dappyID, salePrice, DappyAdmin] await sendTransaction({ name, signers, args }) } export const listStorefrontListings = async (recipient) => { const name = "ListStorefrontListings" const args = [recipient] - const listings = await executeScript({ name, args }) - return listings + const dappyID = await executeScript({ name, args }) + return dappyID +} + +export const listGalleryCollection = async () => { + const DappyAdmin = await dappyContract.getDappyAdminAddress() + const name = "ListGalleryCollection" + const args = [DappyAdmin] + const dappyID = await executeScript({ name, args }) + return dappyID } \ No newline at end of file diff --git a/cadence/transactions/CreateAdminGallery.cdc b/cadence/transactions/CreateAdminGallery.cdc new file mode 100644 index 0000000..4328018 --- /dev/null +++ b/cadence/transactions/CreateAdminGallery.cdc @@ -0,0 +1,13 @@ +import GalleryContract from "../contracts/GalleryContract.cdc" + +transaction { + prepare(acct: AuthAccount) { + + 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/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/PutDappyInStorefront.cdc b/cadence/transactions/PutDappyInStorefront.cdc index c319b43..f888b77 100644 --- a/cadence/transactions/PutDappyInStorefront.cdc +++ b/cadence/transactions/PutDappyInStorefront.cdc @@ -4,17 +4,28 @@ 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, saleCuts: {Address: UFix64}) { +transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { let dappyColRef: &DappyContract.Collection let nftColRef: &DappyNFT.Collection - let managerRef: &{NFTStorefront.StorefrontManager} + 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 - prepare(acct: AuthAccount) { + 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>( @@ -29,7 +40,7 @@ transaction(dappyID: UInt64, saleCuts: {Address: UFix64}) { ?? panic ("Could not borrow NFT Col ref") self.managerRef = acct - .borrow<&{NFTStorefront.StorefrontManager}>( + .borrow<&{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic}>( from: NFTStorefront.StorefrontStoragePath ) ?? panic ("Could not borrow StorefrontManager ref") @@ -38,21 +49,17 @@ transaction(dappyID: UInt64, saleCuts: {Address: UFix64}) { .getCapability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>( DappyNFT.CollectionPrivatePath ) - - self.saleCuts = [] - - for key in saleCuts.keys { - let account = getAccount(key) - let receiver = account - .getCapability<&{FungibleToken.Receiver}>( - /public/fusdReceiver - ) - self.saleCuts.append( - NFTStorefront.SaleCut( - receiver: receiver, - amount: saleCuts[key]!) - ) - } + + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( + /public/fusdReceiver + ) + self.saleCuts = [ + NFTStorefront.SaleCut( + receiver: receiver, + amount: salePrice + ) + ] } @@ -64,17 +71,27 @@ transaction(dappyID: UInt64, saleCuts: {Address: UFix64}) { let nftID = nft.id - let nftType = Type<&AnyResource>() + let nftType = Type<@DappyNFT.NFT>() + let salePaymentVaultType = Type<@FUSD.Vault>() self.nftColRef.deposit(token: <- nft) - - self.managerRef.createListing( + let listingResourceID = self.managerRef.createListing( nftProviderCapability: self.nftProviderCapability, - nftID: nftID, + 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 +} + \ 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.back.json b/flow.back.json new file mode 100644 index 0000000..b70dfad --- /dev/null +++ b/flow.back.json @@ -0,0 +1,64 @@ +{ + "emulators": { + "default": { + "port": 3569, + "serviceAccount": "emulator-account" + } + }, + "contracts": { + "DappyContract": "./cadence/contracts/DappyContract.cdc", + "NFTStorefront": { + "source": "./cadence/contracts/NFTStorefront.cdc", + "aliases": { + "testnet": "0x94b06cfca1d8a476" + } + }, + "DappyNFT": { + "source": "./cadence/contracts/DappyNFT.cdc", + "aliases": { + "testnet": "0x510627df4617530f" + } + }, + "FUSD": { + "source": "./cadence/contracts/FUSD.cdc", + "aliases": { + "testnet": "0xe223d8a629e49c68" + } + }, + "FungibleToken": { + "source": "./cadence/contracts/FungibleToken.cdc", + "aliases": { + "testnet": "9a0766d93b6608b7", + "emulator": "ee82856bf20e2aa6" + } + }, + "NonFungibleToken": { + "source": "./cadence/contracts/NonFungibleToken.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" + }, + "MyWorldAdmin": { + "address": "96be1b89c734d1f4", + "key": "97410e191ba55eacf5b470699749a27f3653ff7afe9a7d390ab785033b6b2b76" + } + }, + "deployments": { + "testnet": { + "MyWorldAdmin": [ + "NonFungibleToken" + ] + } + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index 3fea05d..1f4827c 100644 --- a/flow.json +++ b/flow.json @@ -6,8 +6,6 @@ } }, "contracts": { - "DappyContract": "./cadence/contracts/DappyContract.cdc", - "NFTStorefront": "./cadence/contracts/NFTStorefront.cdc", "FUSD": { "source": "./cadence/contracts/FUSD.cdc", "aliases": { @@ -20,7 +18,38 @@ "testnet": "9a0766d93b6608b7", "emulator": "ee82856bf20e2aa6" } + }, + "DappyContract": { + "source": "./cadence/contracts/DappyContract.cdc", + "aliases": { + "testnet": "db3d539e48a805b7" + } + }, + "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" + } + }, + "GalleryContract": { + "source": "./cadence/contracts/GalleryContract.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } } + }, "networks": { "emulator": "127.0.0.1:3569", @@ -31,7 +60,17 @@ "emulator-account": { "address": "f8d6e0586b0a20c7", "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" + }, + "MyWorldAdmin": { + "address": "96be1b89c734d1f4", + "key": "97410e191ba55eacf5b470699749a27f3653ff7afe9a7d390ab785033b6b2b76" } }, - "deployments": {} + "deployments": { + "testnet": { + "MyWorldAdmin": [ + "NonFungibleToken", "DappyNFT", "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 de58790..bfa2699 100644 --- a/package.json +++ b/package.json @@ -21,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/src/components/PriceButton.js b/src/components/PriceButton.js index ba67bb5..27e5444 100644 --- a/src/components/PriceButton.js +++ b/src/components/PriceButton.js @@ -1,24 +1,39 @@ 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, setValue: setPrice, bind: bindPrice, reset: resetPrice } = useInput(defaultPrice); + const clickShow = () => { setSell(!sell); } + const clickSell = (wantPrice) => { + listDappyForSale(dappy, wantPrice) + } + return ( <>
- +
-
+
clickSell(wantPrice)} + className="btn btn-bordered btn-light"> List for Sale
@@ -36,3 +51,4 @@ export default function PriceButton({ dappy }) { ) } + \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js index c3bb34b..7d4e5d0 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -5,5 +5,9 @@ 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, + "0xGalleryContract": process.env.REACT_APP_GALLERY_CONTRACT }) \ No newline at end of file diff --git a/src/flow/create-nft-collection.tx.js b/src/flow/create-nft-collection.tx.js new file mode 100644 index 0000000..9203452 --- /dev/null +++ b/src/flow/create-nft-collection.tx.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-nft-collection.tx.js b/src/flow/delete-nft-collection.tx.js new file mode 100644 index 0000000..28c9ef7 --- /dev/null +++ b/src/flow/delete-nft-collection.tx.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/put-dappy-storefront.tx.js b/src/flow/put-dappy-storefront.tx.js new file mode 100644 index 0000000..5a1a324 --- /dev/null +++ b/src/flow/put-dappy-storefront.tx.js @@ -0,0 +1,82 @@ +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 + +transaction(dappyID: UInt64, salePrice: UFix64) { + + let dappyColRef: &DappyContract.Collection + let nftColRef: &DappyNFT.Collection + let managerRef: &{NFTStorefront.StorefrontManager} + let nftProviderCapability: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let saleCuts: [NFTStorefront.SaleCut] + + prepare(acct: AuthAccount) { + + 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 account = getAccount(acct.address) + let receiver = account + .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) + + self.managerRef.createListing( + nftProviderCapability: self.nftProviderCapability, + nftType: nftType, + nftID: nftID, + salePaymentVaultType: salePaymentVaultType, + saleCuts: self.saleCuts + ) + + } + +} + +` \ No newline at end of file diff --git a/src/hooks/use-collection.hook.js b/src/hooks/use-collection.hook.js index 8d5bac8..f3aaa50 100644 --- a/src/hooks/use-collection.hook.js +++ b/src/hooks/use-collection.hook.js @@ -4,6 +4,8 @@ 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 { DELETE_NFT_COLLECTION } from '../flow/delete-nft-collection.tx' +import { CREATE_NFT_COLLECTION } from '../flow/create-nft-collection.tx' import { useTxs } from '../providers/TxProvider' export default function useCollection(user) { @@ -31,13 +33,21 @@ export default function useCollection(user) { }, []) const createCollection = async () => { + let res = await mutate({ cadence: CREATE_COLLECTION, limit: 55 - }) addTx(res) await tx(res).onceSealed() + + let resNFT = await mutate({ + cadence: CREATE_NFT_COLLECTION, + limit: 55 + }) + addTx(resNFT) + await tx(resNFT).onceSealed() + setCollection(true) } @@ -50,6 +60,15 @@ export default function useCollection(user) { addTx(res) await tx(res).onceSealed() setCollection(false) + + let resNFT = await mutate({ + cadence: DELETE_NFT_COLLECTION, + limit: 75 + }) + addTx(resNFT) + await tx(resNFT).onceSealed() + + setCollection(false) } catch (err) { console.log(err) } diff --git a/src/hooks/use-user-dappies.hook.js b/src/hooks/use-user-dappies.hook.js index 9ea2552..770c892 100644 --- a/src/hooks/use-user-dappies.hook.js +++ b/src/hooks/use-user-dappies.hook.js @@ -27,7 +27,8 @@ export default function useUserDappies(user, collection, getFUSDBalance) { for (let key in res) { const element = res[key] - let dappy = new DappyClass(element.templateID, element.dna, element.name, element.price, key) + const serialNumber = parseInt(key) + let dappy = new DappyClass(element.templateID, element.dna, element.name, element.price, serialNumber) mappedDappies.push(dappy) } @@ -72,7 +73,7 @@ export default function useUserDappies(user, collection, getFUSDBalance) { }) 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 +102,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { ...state, mintDappy, addDappy, - batchAddDappies + batchAddDappies, } } diff --git a/src/hooks/use-user-pack.hook.js b/src/hooks/use-user-pack.hook.js index 86074e4..0d658b1 100644 --- a/src/hooks/use-user-pack.hook.js +++ b/src/hooks/use-user-pack.hook.js @@ -1,6 +1,14 @@ import { useReducer } from 'react' +import { mutate, tx } from '@onflow/fcl' + +import { useTxs } from '../providers/TxProvider' + +import { PUT_DAPPY_STOREFRONT } from '../flow/put-dappy-storefront.tx' export default function useUserPack() { + + const { addTx, runningTxs } = useTxs() + const reducer = (state, action) => { switch (action.type) { case 'ADD': @@ -44,9 +52,38 @@ export default function useUserPack() { const removeFromPack = async ({dappy}) => { } + const listDappyForSale = async (dappy, wantPrice) => { + + const dappyID = dappy.serialNumber + + const salePrice = parseFloat(wantPrice).toFixed(8) + + if (runningTxs) { + alert("Transactions are still running. Please wait for them to finish first.") + return + } + + try { + let res = await mutate({ + cadence: PUT_DAPPY_STOREFRONT, + limit: 100, + args: (arg, t) => [ + arg(dappyID, t.UInt64), + arg(salePrice, t.UFix64) + ] + }) + addTx(res) + await tx(res).onceSealed() + } catch (error) { + console.log(error) + } + + } + return { ...state, addToPack, - removeFromPack + removeFromPack, + listDappyForSale } } diff --git a/src/providers/MarketProvider.js b/src/providers/MarketProvider.js index 61fd146..3f82055 100644 --- a/src/providers/MarketProvider.js +++ b/src/providers/MarketProvider.js @@ -7,7 +7,13 @@ const UserContext = createContext() export default function MarketProvider({ children }) { - const { data: userPack, price: packPrice, addToPack, removeFromPack } = userUserPack() + const { + data: userPack, + price: packPrice, + addToPack, + removeFromPack, + listDappyForSale } + = userUserPack() return ( {children} From eef1a20662ef3107a460f06e0e1b0a63c8cb20e8 Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Mon, 25 Oct 2021 19:14:52 +0700 Subject: [PATCH 05/12] Done pack and dappy sale in storefront --- cadence/transactions/PutDappyInStorefront.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/transactions/PutDappyInStorefront.cdc b/cadence/transactions/PutDappyInStorefront.cdc index f888b77..a7b902e 100644 --- a/cadence/transactions/PutDappyInStorefront.cdc +++ b/cadence/transactions/PutDappyInStorefront.cdc @@ -78,7 +78,7 @@ transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { let listingResourceID = self.managerRef.createListing( nftProviderCapability: self.nftProviderCapability, - nftType: nftType, + let listingResourceID = self.managerRef.createListing( nftID: nftID, salePaymentVaultType: salePaymentVaultType, saleCuts: self.saleCuts From 3af324e3436877b421803d2505aff96484dac7bf Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Mon, 25 Oct 2021 19:15:13 +0700 Subject: [PATCH 06/12] Done with Milestone 2 --- .vscode/settings.json | 3 + cadence/contracts/DappyNFT.cdc | 39 ++++- cadence/contracts/GalleryContract.cdc | 102 +++++++---- cadence/contracts/NFTStorefront.cdc | 10 +- cadence/contracts/PackNFT.cdc | 151 ++++++++++++++++ cadence/scripts/ListStorefrontListings.cdc | 16 +- cadence/tests/Storefront.test.js | 163 +++++++++++++++++- cadence/tests/jest.config.js | 4 +- cadence/tests/src/Storefront.js | 77 ++++++++- cadence/transactions/AddGalleryListing.cdc | 40 +++++ cadence/transactions/CleanUpStorefront.cdc | 31 ++++ cadence/transactions/CreateAdminGallery.cdc | 6 + .../transactions/CreatePackNFTCollection.cdc | 15 ++ .../transactions/PurchaseDappyStorefront.cdc | 58 +++++++ .../transactions/PurchasePackStorefront.cdc | 63 +++++++ cadence/transactions/PutDappyInStorefront.cdc | 30 +--- cadence/transactions/PutPackInStorefront.cdc | 92 ++++++++++ cadence/transactions/RemoveGalleryListing.cdc | 30 ++++ flow.json | 8 +- public/assets/Pack4.png | Bin 0 -> 231650 bytes src/components/Atoms.css | 8 + src/components/DappyCard.js | 18 +- src/components/PackPanel.comp.js | 17 +- src/components/PriceButton.js | 2 +- src/config/config.js | 1 + src/flow/create-collection.tx.js | 34 +++- ....tx.js => create-nft-collection.tx.old.js} | 0 src/flow/delete-collection.tx.js | 36 ++++ ....tx.js => delete-nft-collection.tx.old.js} | 0 src/flow/list-gallery-collection.script.js | 20 +++ src/flow/list-user-dappies-ids.script.js | 15 ++ src/flow/purchase-dappy-storefront.tx.js | 57 ++++++ src/flow/purchase-pack-storefront.tx.js | 88 ++++++++++ src/flow/put-dappy-storefront.tx.js | 41 +++-- src/flow/put-pack-storefront.tx.js | 109 ++++++++++++ src/flow/remove-gallery-listing.tx.js | 32 ++++ src/hooks/use-collection.hook.js | 36 ++-- src/hooks/use-dappy-packs.hook.js | 53 +++++- src/hooks/use-dappy-templates.hook.js | 41 ++++- src/hooks/use-user-dappies.hook.js | 4 + src/hooks/use-user-pack.hook.js | 144 +++++++++++++--- src/pages/PackDetails.page.js | 56 ++++-- src/pages/Packs.page.js | 2 +- src/providers/MarketProvider.js | 32 +++- src/utils/DappyClass.js | 2 +- src/utils/PackClass.js | 14 +- up.sh | 15 ++ 47 files changed, 1651 insertions(+), 164 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 cadence/contracts/PackNFT.cdc create mode 100644 cadence/transactions/AddGalleryListing.cdc create mode 100644 cadence/transactions/CleanUpStorefront.cdc create mode 100644 cadence/transactions/CreatePackNFTCollection.cdc create mode 100644 cadence/transactions/PurchaseDappyStorefront.cdc create mode 100644 cadence/transactions/PurchasePackStorefront.cdc create mode 100644 cadence/transactions/PutPackInStorefront.cdc create mode 100644 cadence/transactions/RemoveGalleryListing.cdc create mode 100644 public/assets/Pack4.png rename src/flow/{create-nft-collection.tx.js => create-nft-collection.tx.old.js} (100%) rename src/flow/{delete-nft-collection.tx.js => delete-nft-collection.tx.old.js} (100%) create mode 100644 src/flow/list-gallery-collection.script.js create mode 100644 src/flow/list-user-dappies-ids.script.js create mode 100644 src/flow/purchase-dappy-storefront.tx.js create mode 100644 src/flow/purchase-pack-storefront.tx.js create mode 100644 src/flow/put-pack-storefront.tx.js create mode 100644 src/flow/remove-gallery-listing.tx.js create mode 100755 up.sh 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/DappyNFT.cdc b/cadence/contracts/DappyNFT.cdc index 7d7fa15..359ac63 100644 --- a/cadence/contracts/DappyNFT.cdc +++ b/cadence/contracts/DappyNFT.cdc @@ -18,14 +18,43 @@ pub contract DappyNFT: NonFungibleToken { pub resource NFT: NonFungibleToken.INFT { pub let id: UInt64 - pub let nft: @DappyContract.Dappy - + 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) + + } - self.id = nft.id - self.nft <- nft - DappyNFT.totalSupply = DappyNFT.totalSupply + UInt64(1) + 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 () { diff --git a/cadence/contracts/GalleryContract.cdc b/cadence/contracts/GalleryContract.cdc index dadca20..36d59db 100644 --- a/cadence/contracts/GalleryContract.cdc +++ b/cadence/contracts/GalleryContract.cdc @@ -1,5 +1,7 @@ -import DappyContract from "./DappyContract.cdc" -import NFTStorefront from "./NFTStorefront.cdc" +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 { @@ -7,43 +9,65 @@ pub contract GalleryContract { pub let GalleryPublicPath: PublicPath pub event GalleryListingAdded( - storefrontResourceID: UInt64, + listingResourceID: UInt64, sellerAddress: Address ) pub event GalleryListingRemoved( - storefrontResourceID: UInt64, + 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 + 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.Listing{NFTStorefront.ListingPublic}, + listingPublic: &{NFTStorefront.ListingPublic}, sellerAddress: Address ) pub fun removeListing ( - storefrontResourceID: UInt64, + listingResourceID: UInt64, sellerAddress: Address - ) + ) : GalleryData? pub fun getGalleryCollection (): {UInt64: GalleryData} @@ -51,19 +75,19 @@ pub contract GalleryContract { pub resource Gallery: GalleryPublic { - access(contract) let galleryCollection: {UInt64: GalleryData} + priv let galleryCollection: {UInt64: GalleryData} init() { self.galleryCollection = {} } - pub fun getGalleryCollection (): {UInt64: GalleryData} { + pub fun getGalleryCollection (): {UInt64: GalleryData} { return self.galleryCollection } // Add a listing to gallery pub fun addListing ( - listingPublic: &NFTStorefront.Listing{NFTStorefront.ListingPublic}, + listingPublic: &{NFTStorefront.ListingPublic}, sellerAddress: Address ) { @@ -72,35 +96,55 @@ pub contract GalleryContract { } 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 = "3-Pack" + + default: + panic("nftType is not supported: ".concat(nftType.identifier) ) + + } + let galleryData = GalleryData( listingDetails: details, - sellerAddress: sellerAddress) - - self.galleryCollection[details.storefrontID] = galleryData - - emit GalleryListingAdded( - storefrontResourceID: details.storefrontID, - sellerAddress: sellerAddress + sellerAddress: sellerAddress, + dappyCollection: dappyCollection, + packName: packName ) + self.galleryCollection[listingResourceID] = galleryData + } // Add a listing to gallery pub fun removeListing ( - storefrontResourceID: UInt64, + listingResourceID: UInt64, sellerAddress: Address - ) { + ): GalleryData? { pre { // 1. naively check if the address hold this listing no more } - self.galleryCollection.remove(key: storefrontResourceID) + let ret = self.galleryCollection.remove(key: listingResourceID) - emit GalleryListingRemoved( - storefrontResourceID: storefrontResourceID, - sellerAddress: sellerAddress - ) + return ret } @@ -115,4 +159,4 @@ pub contract GalleryContract { self.GalleryPublicPath = /public/DappyGallery } -} \ No newline at end of file +} diff --git a/cadence/contracts/NFTStorefront.cdc b/cadence/contracts/NFTStorefront.cdc index 0873140..bfb94b8 100644 --- a/cadence/contracts/NFTStorefront.cdc +++ b/cadence/contracts/NFTStorefront.cdc @@ -185,11 +185,13 @@ pub contract NFTStorefront { // 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 + pub fun purchase(payment: @FungibleToken.Vault): @NonFungibleToken.NFT // getDetails // pub fun getDetails(): ListingDetails + + pub fun getListingResourceID(): UInt64 } @@ -231,6 +233,11 @@ pub contract NFTStorefront { 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. @@ -509,3 +516,4 @@ pub contract NFTStorefront { emit NFTStorefrontInitialized() } } + \ 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/ListStorefrontListings.cdc b/cadence/scripts/ListStorefrontListings.cdc index cbd7160..626ba3c 100644 --- a/cadence/scripts/ListStorefrontListings.cdc +++ b/cadence/scripts/ListStorefrontListings.cdc @@ -3,7 +3,7 @@ import DappyNFT from "../contracts/DappyNFT.cdc" import NonFungibleToken from "../contracts/NonFungibleToken.cdc" import DappyContract from "../contracts/DappyContract.cdc" -pub fun main(addr: Address): UInt64 { +pub fun main(addr: Address): {UInt64: NFTStorefront.ListingDetails} { let account = getAccount(addr) @@ -15,12 +15,14 @@ pub fun main(addr: Address): UInt64 { ?? panic ("Could not borrow StorefrontPublic ref") let listings = storefrontRef.getListingIDs() - - let listingRef = storefrontRef.borrowListing(listingResourceID: listings[0]) - - let nftRef =listingRef!.borrowNFT() as! &DappyNFT.NFT - - var ret = nftRef.nft.id + + let ret: {UInt64: NFTStorefront.ListingDetails} = {} + for id in listings { + + let listingRef = storefrontRef.borrowListing(listingResourceID: id) + + ret[id] = listingRef?.getDetails() + } return ret diff --git a/cadence/tests/Storefront.test.js b/cadence/tests/Storefront.test.js index 5f9abea..4145523 100644 --- a/cadence/tests/Storefront.test.js +++ b/cadence/tests/Storefront.test.js @@ -8,7 +8,7 @@ import { } from "flow-js-testing" import * as storefront from "./src/Storefront"; import * as dappyContract from "./src/DappyContract"; -import { fundAccountWithFUSD, createFUSDVault, mintFUSD } from "./src/FUSD"; +import { fundAccountWithFUSD, createFUSDVault, mintFUSD, getFUSDBalance } from "./src/FUSD"; jest.setTimeout(50000); @@ -69,6 +69,7 @@ describe("NFTStorefront", () => { await storefront.deployNonFungibleToken() await dappyContract.deployDappyContract() await storefront.deployDappyNFT() + await storefront.deployPackNFT() await storefront.deployNFTStorefront() await storefront.deployGalleryContract() await storefront.createAdminGallery(DappyAdmin) @@ -80,6 +81,7 @@ describe("NFTStorefront", () => { await storefront.deployNonFungibleToken() await dappyContract.deployDappyContract() await storefront.deployDappyNFT() + await storefront.deployPackNFT() await storefront.deployNFTStorefront() await storefront.deployGalleryContract() @@ -101,20 +103,169 @@ describe("NFTStorefront", () => { const salePrice = "11.0" - // emulator.setLogging(true) await storefront.createAdminGallery(DappyAdmin) await storefront.putDappyInStorefront(recipient, 1, salePrice) // list dappy with id=1 for sale - - const dappyID = await storefront.listStorefrontListings(recipient) + + 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 67f78db..0c58436 100644 --- a/cadence/tests/jest.config.js +++ b/cadence/tests/jest.config.js @@ -4,8 +4,8 @@ module.exports = { coveragePathIgnorePatterns: ["/node_modules/"], projects: [{ "displayName": "Dappy Cadence Tests", - "testMatch": ["/**/*.test.js"], - // "testMatch": ["/**/Storefront.test.js"], + // "testMatch": ["/**/*.test.js"], + "testMatch": ["/**/Storefront.test.js"], // "testMatch": ["/**/DappyContract.test.js"] }] }; \ No newline at end of file diff --git a/cadence/tests/src/Storefront.js b/cadence/tests/src/Storefront.js index 6db176e..787237c 100644 --- a/cadence/tests/src/Storefront.js +++ b/cadence/tests/src/Storefront.js @@ -33,12 +33,29 @@ export const deployDappyNFT = async () => { } 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] @@ -49,7 +66,9 @@ export const deployGalleryContract = async () => { const DappyAdmin = await dappyContract.getDappyAdminAddress() const addressMap = { DappyContract: DappyAdmin, - NFTStorefront: DappyAdmin + NFTStorefront: DappyAdmin, + DappyNFT: DappyAdmin, + PackNFT: DappyAdmin, } await deployContractByName({ to: DappyAdmin, name: "GalleryContract", addressMap }) } @@ -72,17 +91,63 @@ export const putDappyInStorefront = async (recipient, dappyID, salePrice) => { 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 dappyID = await executeScript({ name, args }) - return dappyID + const allListingDetails = await executeScript({ name, args }) + return allListingDetails } export const listGalleryCollection = async () => { const DappyAdmin = await dappyContract.getDappyAdminAddress() const name = "ListGalleryCollection" const args = [DappyAdmin] - const dappyID = await executeScript({ name, args }) - return dappyID -} \ No newline at end of file + 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/CleanUpStorefront.cdc b/cadence/transactions/CleanUpStorefront.cdc new file mode 100644 index 0000000..5fc650e --- /dev/null +++ b/cadence/transactions/CleanUpStorefront.cdc @@ -0,0 +1,31 @@ +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} + + prepare(acct: AuthAccount) { + + // Seller + let sellerAccount = getAccount(sellerAddress) + self.storefrontRef = sellerAccount + .getCapability<&{NFTStorefront.StorefrontPublic}>( + 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 index 4328018..8f7209b 100644 --- a/cadence/transactions/CreateAdminGallery.cdc +++ b/cadence/transactions/CreateAdminGallery.cdc @@ -3,6 +3,12 @@ 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) 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/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 index a7b902e..dd4fc0e 100644 --- a/cadence/transactions/PutDappyInStorefront.cdc +++ b/cadence/transactions/PutDappyInStorefront.cdc @@ -10,23 +10,17 @@ 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 + let managerRef: &{NFTStorefront.StorefrontManager} + 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 @@ -40,7 +34,7 @@ transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { ?? panic ("Could not borrow NFT Col ref") self.managerRef = acct - .borrow<&{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic}>( + .borrow<&{NFTStorefront.StorefrontManager}>( from: NFTStorefront.StorefrontStoragePath ) ?? panic ("Could not borrow StorefrontManager ref") @@ -66,9 +60,9 @@ transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { execute { let dappy <- self.dappyColRef.withdraw(withdrawID: dappyID) - + let nft <- DappyNFT.createFromDappy(dappy: <- dappy) - + let nftID = nft.id let nftType = Type<@DappyNFT.NFT>() @@ -78,20 +72,12 @@ transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { let listingResourceID = self.managerRef.createListing( nftProviderCapability: self.nftProviderCapability, - let listingResourceID = self.managerRef.createListing( + 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/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/flow.json b/flow.json index 1f4827c..e72c4ea 100644 --- a/flow.json +++ b/flow.json @@ -43,6 +43,12 @@ "testnet": "96be1b89c734d1f4" } }, + "PackNFT": { + "source": "./cadence/contracts/PackNFT.cdc", + "aliases": { + "testnet": "96be1b89c734d1f4" + } + }, "GalleryContract": { "source": "./cadence/contracts/GalleryContract.cdc", "aliases": { @@ -69,7 +75,7 @@ "deployments": { "testnet": { "MyWorldAdmin": [ - "NonFungibleToken", "DappyNFT", "NFTStorefront", "GalleryContract" + "NonFungibleToken", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" ] } } diff --git a/public/assets/Pack4.png b/public/assets/Pack4.png new file mode 100644 index 0000000000000000000000000000000000000000..42d7734350616604a6a9a568a752a450d718bc02 GIT binary patch literal 231650 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 JPDHLkV1ge=&!YeU literal 0 HcmV?d00001 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/DappyCard.js b/src/components/DappyCard.js index 17b58a5..900a0c7 100644 --- a/src/components/DappyCard.js +++ b/src/components/DappyCard.js @@ -4,6 +4,8 @@ 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" @@ -12,6 +14,8 @@ import PriceButton from './PriceButton' export default function DappyCard({ dappy, store, designer }) { const { userDappies, mintDappy } = 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) @@ -28,16 +32,25 @@ export default function DappyCard({ dappy, store, designer }) { ) const DappyButton = () => ( -
mintDappy(id, price)} className="btn btn-bordered btn-light btn-dappy"> {parseInt(price)} FUSD
) + + const StorefrontButton = () => ( +
purchaseDappy(dappy)} + className="btn btn-bordered btn-light btn-storefront"> + {parseInt(price)} FUSD +
+ ) const PackButton = () => (
history.push(`/packs/${id}`)} + className="btn btn-bordered btn-light btn-dappy"> More
@@ -68,7 +81,8 @@ export default function DappyCard({ dappy, store, designer }) { {designer ? : <> - {!owned && type === "Dappy" && } + {!owned && type === "Dappy" && !dappy.listingResourceID &&} + {!owned && type === "Dappy" && dappy.listingResourceID && } {!owned && type === "Pack" && } } diff --git a/src/components/PackPanel.comp.js b/src/components/PackPanel.comp.js index f319de0..32ac02d 100644 --- a/src/components/PackPanel.comp.js +++ b/src/components/PackPanel.comp.js @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react' +import React, { useEffect } from 'react' import { useDrop } from 'react-dnd' import { useMarket } from '../providers/MarketProvider' @@ -8,13 +8,14 @@ import Dappy from './Dappy' export default function PackPanel() { - const { packPrice, userPack, addToPack } = useMarket() + const { packPrice, userPack, addToPack, listPackForSale } = useMarket() - const { value: wantPrice, setValue: setPrice, bind: bindPrice, reset: resetPrice } = useInput(packPrice); + const { value: wantPrice, setValue: setPrice, bind: bindPrice } = useInput(packPrice); - useEffect (() => { + useEffect(() => { setPrice(packPrice) - },[packPrice]) + }, [setPrice, packPrice]) + const [, drop] = useDrop(() => ({ accept: 'box', drop: item => addToPack(item), @@ -43,10 +44,12 @@ export default function PackPanel() { }
- +
-
+
listPackForSale(userPack,parseFloat(wantPrice))} + className="btn btn-bordered btn-light btn-bottom">
Sell Pack
diff --git a/src/components/PriceButton.js b/src/components/PriceButton.js index 27e5444..b893dde 100644 --- a/src/components/PriceButton.js +++ b/src/components/PriceButton.js @@ -13,7 +13,7 @@ export default function PriceButton({ dappy }) { const defaultPrice = parseFloat(dappy.price).toFixed(8).slice(0, -6) - const { value: wantPrice, setValue: setPrice, bind: bindPrice, reset: resetPrice } = useInput(defaultPrice); + const { value: wantPrice, bind: bindPrice } = useInput(defaultPrice); const clickShow = () => { setSell(!sell); diff --git a/src/config/config.js b/src/config/config.js index 7d4e5d0..8326d6f 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -9,5 +9,6 @@ config({ "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/create-collection.tx.js b/src/flow/create-collection.tx.js index d2059c2..7023f1b 100644 --- a/src/flow/create-collection.tx.js +++ b/src/flow/create-collection.tx.js @@ -1,11 +1,43 @@ export const CREATE_COLLECTION = ` import DappyContract from 0xDappy - + import NonFungibleToken from 0xNonFungibleToken + import DappyNFT from 0xMyDappyNFT + import PackNFT from 0xPackNFT + import NFTStorefront from 0xNFTStorefront + transaction { prepare(acct: AuthAccount) { + 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.js b/src/flow/create-nft-collection.tx.old.js similarity index 100% rename from src/flow/create-nft-collection.tx.js rename to src/flow/create-nft-collection.tx.old.js 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.js b/src/flow/delete-nft-collection.tx.old.js similarity index 100% rename from src/flow/delete-nft-collection.tx.js rename to src/flow/delete-nft-collection.tx.old.js 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..7c3d47a --- /dev/null +++ b/src/flow/purchase-pack-storefront.tx.js @@ -0,0 +1,88 @@ +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 c = nft.getIDs().length.toString() + // let m = c.concat(" << LENGTH") + // assert(false, message: m) + + 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 index 5a1a324..1911fd5 100644 --- a/src/flow/put-dappy-storefront.tx.js +++ b/src/flow/put-dappy-storefront.tx.js @@ -1,22 +1,34 @@ 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) { +transaction(dappyID: UInt64, salePrice: UFix64, adminAddress: Address) { let dappyColRef: &DappyContract.Collection let nftColRef: &DappyNFT.Collection - let managerRef: &{NFTStorefront.StorefrontManager} + 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 @@ -30,8 +42,8 @@ transaction(dappyID: UInt64, salePrice: UFix64) { ?? panic ("Could not borrow NFT Col ref") self.managerRef = acct - .borrow<&{NFTStorefront.StorefrontManager}>( - from: NFTStorefront.StorefrontStoragePath + .borrow<&{NFTStorefront.StorefrontManager, NFTStorefront.StorefrontPublic}>( + from: NFTStorefront.StorefrontStoragePath ) ?? panic ("Could not borrow StorefrontManager ref") @@ -40,9 +52,8 @@ transaction(dappyID: UInt64, salePrice: UFix64) { DappyNFT.CollectionPrivatePath ) - let account = getAccount(acct.address) - let receiver = account - .getCapability<&{FungibleToken.Receiver}>( + let receiver = acct + .getCapability<&{FungibleToken.Receiver}>( /public/fusdReceiver ) self.saleCuts = [ @@ -67,7 +78,7 @@ transaction(dappyID: UInt64, salePrice: UFix64) { self.nftColRef.deposit(token: <- nft) - self.managerRef.createListing( + let listingResourceID = self.managerRef.createListing( nftProviderCapability: self.nftProviderCapability, nftType: nftType, nftID: nftID, @@ -75,8 +86,14 @@ transaction(dappyID: UInt64, salePrice: UFix64) { 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-collection.hook.js b/src/hooks/use-collection.hook.js index f3aaa50..89d5521 100644 --- a/src/hooks/use-collection.hook.js +++ b/src/hooks/use-collection.hook.js @@ -4,8 +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 { DELETE_NFT_COLLECTION } from '../flow/delete-nft-collection.tx' -import { CREATE_NFT_COLLECTION } from '../flow/create-nft-collection.tx' + import { useTxs } from '../providers/TxProvider' export default function useCollection(user) { @@ -41,34 +40,37 @@ export default function useCollection(user) { addTx(res) await tx(res).onceSealed() - let resNFT = await mutate({ - cadence: CREATE_NFT_COLLECTION, - limit: 55 - }) - addTx(resNFT) - await tx(resNFT).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) - let resNFT = await mutate({ - cadence: DELETE_NFT_COLLECTION, - limit: 75 - }) - addTx(resNFT) - await tx(resNFT).onceSealed() - setCollection(false) + } catch (err) { console.log(err) } 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-user-dappies.hook.js b/src/hooks/use-user-dappies.hook.js index 770c892..e791e11 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' @@ -23,6 +24,7 @@ export default function useUserDappies(user, collection, getFUSDBalance) { cadence: LIST_USER_DAPPIES, args: (arg, t) => [arg(user?.addr, t.Address)] }) + let mappedDappies = [] for (let key in res) { @@ -35,6 +37,7 @@ export default function useUserDappies(user, collection, getFUSDBalance) { dispatch({ type: 'SUCCESS', payload: mappedDappies }) } catch (err) { dispatch({ type: 'ERROR' }) + console.log(err) } } fetchUserDappies() @@ -71,6 +74,7 @@ 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, dappy.price, dappy.serialNumber) diff --git a/src/hooks/use-user-pack.hook.js b/src/hooks/use-user-pack.hook.js index 0d658b1..ec99b43 100644 --- a/src/hooks/use-user-pack.hook.js +++ b/src/hooks/use-user-pack.hook.js @@ -1,22 +1,26 @@ -import { useReducer } from 'react' -import { mutate, tx } from '@onflow/fcl' +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' export default function useUserPack() { + const { addTx, runningTxs } = useTxs() - const reducer = (state, action) => { + const reducer = (state, action) => { switch (action.type) { case 'ADD': //skip if dappy exists or total dappies is 4 - console.log(state.data) if (state.data.length >= 4) return { ...state } - for (const d of state.data ) { - if (d.id === action.payload.id) return { ...state} + for (const d of state.data) { + if (d.id === action.payload.id) return { ...state } } const price = parseFloat(action.payload.price) return { @@ -29,53 +33,150 @@ export default function useUserPack() { ...state, data: [...state.data, action.payload], price: parseFloat(action.payload.price) - } + } case 'REMOVE': return { ...state, data: [...state.data, action.payload] - } + } default: throw new Error("Error in useUserPack reducer") } } - const [state, dispatch] = useReducer( reducer, { - data:[], + const [state, dispatch] = useReducer(reducer, { + data: [], price: 0.0 }) - const addToPack = ({dappy}) => { - dispatch({ type: 'ADD', payload: dappy}) + const addToPack = ({ dappy }) => { + dispatch({ type: 'ADD', payload: dappy }) } - const removeFromPack = async ({dappy}) => { + const removeFromPack = async ({ dappy }) => { } - const listDappyForSale = async (dappy, wantPrice) => { + const purchaseDappy = async (dappy) => { - const dappyID = dappy.serialNumber + 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: 100, + limit: 200, args: (arg, t) => [ - arg(dappyID, t.UInt64), - arg(salePrice, t.UFix64) + arg(dappyID, t.UInt64), + arg(salePrice, t.UFix64), + arg(adminAddress, t.Address) ] }) addTx(res) await tx(res).onceSealed() } catch (error) { - console.log(error) + console.error(error, error.stack) } } @@ -84,6 +185,9 @@ export default function useUserPack() { ...state, addToPack, removeFromPack, - listDappyForSale + listDappyForSale, + listPackForSale, + purchaseDappy, + purchasePackStorefront } } 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 index 3f82055..c807748 100644 --- a/src/providers/MarketProvider.js +++ b/src/providers/MarketProvider.js @@ -2,19 +2,32 @@ 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' const UserContext = createContext() export default function MarketProvider({ children }) { - const { - data: userPack, - price: packPrice, - addToPack, + const { + data: userPack, + price: packPrice, + addToPack, removeFromPack, - listDappyForSale } + listDappyForSale, + listPackForSale, + purchaseDappy, + purchasePackStorefront + } = userUserPack() + const { + data: dappyPacks, + fetchDappiesOfPack, + mintFromPack, + fetchPackDetails + } = useDappyPacks() + + return ( {children} 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/up.sh b/up.sh new file mode 100755 index 0000000..d3eb118 --- /dev/null +++ b/up.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +flow accounts remove-contract GalleryContract --network testnet --signer MyWorldAdmin + +flow accounts remove-contract NonFungibleToken --network testnet --signer MyWorldAdmin + +flow accounts remove-contract DappyNFT --network testnet --signer MyWorldAdmin + +flow accounts remove-contract PackNFT --network testnet --signer MyWorldAdmin + +flow accounts remove-contract NFTStorefront --network testnet --signer MyWorldAdmin + +flow project deploy --network testnet + +flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyWorldAdmin --network testnet \ No newline at end of file From 1b34b126c00c4bd69899089a5a69b878046632cb Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Wed, 27 Oct 2021 20:00:26 +0700 Subject: [PATCH 07/12] Milestone # Submission --- .vscode/launch.json | 15 + cadence/contracts/DappyContract.cdc | 19 +- cadence/contracts/DappyContract.original.cdc | 322 ++++++++++++++++++ cadence/contracts/GalleryContract.cdc | 2 +- cadence/scripts/Unsafe.cdc | 3 + cadence/transactions/BreedDappies.cdc | 134 ++++++++ cadence/transactions/CreateCollection.cdc | 60 ++++ cadence/transactions/DeleteCollection.cdc | 41 ++- cadence/transactions/PrepareDappyContract.cdc | 36 ++ flow.json | 9 +- public/assets/dappy-animated.svg | 172 ++++++++++ src/components/BreedPanel.comp.js | 48 ++- src/components/DappyCard.css | 44 ++- src/components/DappyCard.js | 32 +- src/components/DappyEyes.js | 42 +-- src/components/DappyList.css | 54 ++- src/components/Untitled-1.xml | 1 + src/flow/breed-dappies.tx.js | 137 ++++++++ src/flow/create-collection.tx.js | 91 +++-- src/flow/purchase-pack-storefront.tx.js | 4 - src/hooks/use-breed-dappies.hook.js | 86 +++++ src/hooks/use-collection.hook.js | 2 +- src/hooks/use-user-dappies.hook.js | 44 +-- src/hooks/use-user-pack.hook.js | 18 +- src/pages/Collection.page.js | 4 +- src/providers/MarketProvider.js | 14 +- src/providers/UserProvider.js | 4 +- src/reducer/userDappyReducer.js | 13 +- src/utils/dappies.utils.js | 3 +- up.sh | 6 +- 30 files changed, 1332 insertions(+), 128 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 cadence/contracts/DappyContract.original.cdc create mode 100644 cadence/scripts/Unsafe.cdc create mode 100644 cadence/transactions/BreedDappies.cdc create mode 100644 cadence/transactions/PrepareDappyContract.cdc create mode 100644 public/assets/dappy-animated.svg create mode 100644 src/components/Untitled-1.xml create mode 100644 src/flow/breed-dappies.tx.js create mode 100644 src/hooks/use-breed-dappies.hook.js 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/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/GalleryContract.cdc b/cadence/contracts/GalleryContract.cdc index 36d59db..fc65af3 100644 --- a/cadence/contracts/GalleryContract.cdc +++ b/cadence/contracts/GalleryContract.cdc @@ -114,7 +114,7 @@ pub contract GalleryContract { case Type<@PackNFT.NFT>(): let nftRef = listingPublic.borrowNFT() as! &PackNFT.NFT dappyCollection = nftRef.getData() - packName = "3-Pack" + packName = dappyCollection.keys.length.toString().concat("-Pack") default: panic("nftType is not supported: ".concat(nftType.identifier) ) 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/transactions/BreedDappies.cdc b/cadence/transactions/BreedDappies.cdc new file mode 100644 index 0000000..ff12054 --- /dev/null +++ b/cadence/transactions/BreedDappies.cdc @@ -0,0 +1,134 @@ +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(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/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/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/flow.json b/flow.json index e72c4ea..dd0391f 100644 --- a/flow.json +++ b/flow.json @@ -22,7 +22,7 @@ "DappyContract": { "source": "./cadence/contracts/DappyContract.cdc", "aliases": { - "testnet": "db3d539e48a805b7" + "testnet": "96be1b89c734d1f4" } }, "NonFungibleToken": { @@ -75,7 +75,12 @@ "deployments": { "testnet": { "MyWorldAdmin": [ - "NonFungibleToken", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" + "NonFungibleToken", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" + ] + }, + "emulator": { + "emulator-account": [ + "FungibleToken", "NonFungibleToken", "FUSD", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" ] } } 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/BreedPanel.comp.js b/src/components/BreedPanel.comp.js index e28cab2..a391b97 100644 --- a/src/components/BreedPanel.comp.js +++ b/src/components/BreedPanel.comp.js @@ -1,26 +1,68 @@ -import React from 'react' +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 => { }, + 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/DappyCard.css b/src/components/DappyCard.css index 33a45d2..426b820 100644 --- a/src/components/DappyCard.css +++ b/src/components/DappyCard.css @@ -1,5 +1,5 @@ .dappy-card__draggable { - draggable: true; + opacity: 1; } .dappy-card__border { @@ -42,6 +42,7 @@ opacity: .5; } + .collected { position: absolute; top: 5.5rem; @@ -55,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 900a0c7..e77d815 100644 --- a/src/components/DappyCard.js +++ b/src/components/DappyCard.js @@ -13,7 +13,7 @@ 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() @@ -31,9 +31,19 @@ export default function DappyCard({ dappy, store, designer }) { [] ) + const onPurchase = async() => { + await purchaseDappy(dappy) + await fetchUserDappies() + } + + const onMint = async() => { + await mintDappy(id, price) + await fetchUserDappies() + } + const DappyButton = () => (
mintDappy(id, price)} + onClick={onMint} className="btn btn-bordered btn-light btn-dappy"> {parseInt(price)} FUSD
@@ -41,17 +51,17 @@ export default function DappyCard({ dappy, store, designer }) { const StorefrontButton = () => (
purchaseDappy(dappy)} + onClick={onPurchase} className="btn btn-bordered btn-light btn-storefront"> {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
) @@ -83,7 +93,17 @@ export default function DappyCard({ dappy, store, designer }) { <> {!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
+ } } 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 13aa56f..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%; @@ -8,8 +54,6 @@ justify-content: center; } - - .left-panel__wrapper { position: fixed; width: 7rem; @@ -50,13 +94,13 @@ flex-direction: column; } -.left_panel .dappy_wrapper { +.left_panel .dappy_wrapper, .baby_dappies .dappy_wrapper { width: 60px; height: 60px; margin: 0 auto; } -.left_panel svg { +.left_panel svg, .baby_dappies svg { width: 100%; height: 100%; } @@ -77,4 +121,4 @@ input[type=number] { .btn-bottom { margin-top:.5rem; -} \ 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/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) + let collectionRef <- acct.load<@AnyResource>(from: DappyContract.CollectionStoragePath) + destroy collectionRef + acct.unlink(DappyContract.CollectionPublicPath) + + // DappyNFT - // DappyNFT Collection + let dappyCollectionRef <- acct.load<@DappyNFT.Collection>(from: DappyNFT.CollectionStoragePath) + destroy dappyCollectionRef + acct.unlink(DappyNFT.CollectionPublicPath) + acct.unlink(DappyNFT.CollectionPrivatePath) - 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 - // PackNFT Collection + let packCollectionRef <- acct.load<@PackNFT.Collection>(from: PackNFT.CollectionStoragePath) + destroy packCollectionRef + acct.unlink(PackNFT.CollectionPublicPath) + acct.unlink(PackNFT.CollectionPrivatePath) - 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 + // Storefront - let storefront <- NFTStorefront.createStorefront() + let storefrontRef <- acct.load<@NFTStorefront.Storefront>(from: NFTStorefront.StorefrontStoragePath) + destroy storefrontRef + acct.unlink(NFTStorefront.StorefrontPublicPath) - acct.save<@NFTStorefront.Storefront>(<- storefront, to: NFTStorefront.StorefrontStoragePath) - - acct.link<&{NFTStorefront.StorefrontPublic}>(NFTStorefront.StorefrontPublicPath, target: NFTStorefront.StorefrontStoragePath) + // 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/purchase-pack-storefront.tx.js b/src/flow/purchase-pack-storefront.tx.js index 7c3d47a..669600c 100644 --- a/src/flow/purchase-pack-storefront.tx.js +++ b/src/flow/purchase-pack-storefront.tx.js @@ -65,10 +65,6 @@ transaction(adminAddress: Address, listingResourceID:UInt64, sellerAddress: Addr let vault <- self.vaultRef.withdraw(amount: amount) let nft <- self.listingRef.purchase(payment: <- vault) as! @PackNFT.NFT - // let c = nft.getIDs().length.toString() - // let m = c.concat(" << LENGTH") - // assert(false, message: m) - let dappies <- nft.withdrawDappies() for key in dappies.keys { 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 89d5521..e74b9db 100644 --- a/src/hooks/use-collection.hook.js +++ b/src/hooks/use-collection.hook.js @@ -35,7 +35,7 @@ export default function useCollection(user) { let res = await mutate({ cadence: CREATE_COLLECTION, - limit: 55 + limit: 200 }) addTx(res) await tx(res).onceSealed() diff --git a/src/hooks/use-user-dappies.hook.js b/src/hooks/use-user-dappies.hook.js index e791e11..1fec475 100644 --- a/src/hooks/use-user-dappies.hook.js +++ b/src/hooks/use-user-dappies.hook.js @@ -16,31 +16,33 @@ export default function useUserDappies(user, collection, getFUSDBalance) { }) 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] - const serialNumber = parseInt(key) - let dappy = new DappyClass(element.templateID, element.dna, element.name, element.price, serialNumber) - mappedDappies.push(dappy) - } + let mappedDappies = [] - dispatch({ type: 'SUCCESS', payload: mappedDappies }) - } catch (err) { - dispatch({ type: 'ERROR' }) - console.log(err) + 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() + console.log("FETCH") //eslint-disable-next-line }, []) @@ -61,7 +63,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { }) addTx(res) await tx(res).onceSealed() - await addDappy(templateID) await getFUSDBalance() } catch (error) { console.log(error) @@ -107,5 +108,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { mintDappy, addDappy, batchAddDappies, + fetchUserDappies } } diff --git a/src/hooks/use-user-pack.hook.js b/src/hooks/use-user-pack.hook.js index ec99b43..8263687 100644 --- a/src/hooks/use-user-pack.hook.js +++ b/src/hooks/use-user-pack.hook.js @@ -9,8 +9,9 @@ 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' -export default function useUserPack() { +const PACK_MAX_CAP = 4 +export default function useUserPack() { const { addTx, runningTxs } = useTxs() @@ -18,9 +19,9 @@ export default function useUserPack() { switch (action.type) { case 'ADD': //skip if dappy exists or total dappies is 4 - if (state.data.length >= 4) return { ...state } + if (state.data.length >= PACK_MAX_CAP) return { ...state } for (const d of state.data) { - if (d.id === action.payload.id) return { ...state } + if (d.serialNumber === action.payload.serialNumber) return { ...state } } const price = parseFloat(action.payload.price) return { @@ -28,17 +29,6 @@ export default function useUserPack() { data: [...state.data, action.payload], price: state.price + price } - case 'NEW': - return { - ...state, - data: [...state.data, action.payload], - price: parseFloat(action.payload.price) - } - case 'REMOVE': - return { - ...state, - data: [...state.data, action.payload] - } default: throw new Error("Error in useUserPack reducer") } diff --git a/src/pages/Collection.page.js b/src/pages/Collection.page.js index 7f00816..93bb496 100644 --- a/src/pages/Collection.page.js +++ b/src/pages/Collection.page.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect} from 'react' import DappyList from '../components/DappyList' import Header from '../components/Header' import { useUser } from '../providers/UserProvider' @@ -9,6 +9,8 @@ import BreedPanel from '../components/BreedPanel.comp' export default function Collection() { const { collection, createCollection, deleteCollection, userDappies } = useUser() + useEffect( () => {}, [userDappies]) + return ( <>
{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/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 index d3eb118..43398c7 100755 --- a/up.sh +++ b/up.sh @@ -10,6 +10,10 @@ flow accounts remove-contract PackNFT --network testnet --signer MyWorldAdmin flow accounts remove-contract NFTStorefront --network testnet --signer MyWorldAdmin +flow accounts remove-contract DappyContract --network testnet --signer MyWorldAdmin + flow project deploy --network testnet -flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyWorldAdmin --network testnet \ No newline at end of file +flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyWorldAdmin --network testnet + +flow transactions send cadence/transactions/PrepareDappyContract.cdc --signer MyWorldAdmin --network testnet \ No newline at end of file From d9a3bf7dbf549bf89bffb29b347c2a0252604549 Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Thu, 28 Oct 2021 14:58:24 +0700 Subject: [PATCH 08/12] Fix White Eye Problem --- cadence/transactions/BreedDappies.cdc | 7 ++++++- src/hooks/use-collection.hook.js | 2 +- src/hooks/use-user-dappies.hook.js | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cadence/transactions/BreedDappies.cdc b/cadence/transactions/BreedDappies.cdc index ff12054..f7462af 100644 --- a/cadence/transactions/BreedDappies.cdc +++ b/cadence/transactions/BreedDappies.cdc @@ -84,7 +84,7 @@ transaction (maleID: UInt64, femaleID: UInt64) { fun gen(_ a: [String], _ b: [String]): String { // let eye = [ maleDNA[maleDNA.length - 1 ], femaleDNA[femaleDNA.length - 1 ] ] [ luck(2) ] - let eye = [ a.removeLast(), b.removeLast()] [ luck(2) ] + var eye = [ a.removeLast(), b.removeLast()] [ luck(2) ] var dna: String = "" var baseDNA = combo(a, b) @@ -97,6 +97,11 @@ transaction (maleID: UInt64, femaleID: UInt64) { dna = dna.concat(".") i = i + 1 } + + if eye.length < 2 { + eye = min( stripes-1, UInt64(eye.decodeHex()[0]) ).toString() + } // Fix white eye + dna = dna.concat(eye) return dna } diff --git a/src/hooks/use-collection.hook.js b/src/hooks/use-collection.hook.js index e74b9db..71478d3 100644 --- a/src/hooks/use-collection.hook.js +++ b/src/hooks/use-collection.hook.js @@ -72,7 +72,7 @@ export default function useCollection(user) { setCollection(false) } catch (err) { - console.log(err) + console.error(err, err.stack) } } diff --git a/src/hooks/use-user-dappies.hook.js b/src/hooks/use-user-dappies.hook.js index 1fec475..1e4338a 100644 --- a/src/hooks/use-user-dappies.hook.js +++ b/src/hooks/use-user-dappies.hook.js @@ -32,7 +32,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { 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' }) @@ -42,7 +41,6 @@ export default function useUserDappies(user, collection, getFUSDBalance) { useEffect(() => { fetchUserDappies() - console.log("FETCH") //eslint-disable-next-line }, []) From 07e6002131ff29e8b85d994043b0756107b3e72d Mon Sep 17 00:00:00 2001 From: Nwin Date: Thu, 28 Oct 2021 15:55:29 +0700 Subject: [PATCH 09/12] Nothing --- flow.json | 87 ------------------------------------------------------- 1 file changed, 87 deletions(-) delete mode 100644 flow.json diff --git a/flow.json b/flow.json deleted file mode 100644 index dd0391f..0000000 --- a/flow.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "emulators": { - "default": { - "port": 3569, - "serviceAccount": "emulator-account" - } - }, - "contracts": { - "FUSD": { - "source": "./cadence/contracts/FUSD.cdc", - "aliases": { - "testnet": "0xe223d8a629e49c68" - } - }, - "FungibleToken": { - "source": "./cadence/contracts/FungibleToken.cdc", - "aliases": { - "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", - "mainnet": "access.mainnet.nodes.onflow.org:9000", - "testnet": "access.devnet.nodes.onflow.org:9000" - }, - "accounts": { - "emulator-account": { - "address": "f8d6e0586b0a20c7", - "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" - }, - "MyWorldAdmin": { - "address": "96be1b89c734d1f4", - "key": "97410e191ba55eacf5b470699749a27f3653ff7afe9a7d390ab785033b6b2b76" - } - }, - "deployments": { - "testnet": { - "MyWorldAdmin": [ - "NonFungibleToken", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" - ] - }, - "emulator": { - "emulator-account": [ - "FungibleToken", "NonFungibleToken", "FUSD", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" - ] - } - } -} \ No newline at end of file From c279465744df732817fbb7161c5934c9ba558a99 Mon Sep 17 00:00:00 2001 From: Nwin Date: Thu, 28 Oct 2021 15:55:56 +0700 Subject: [PATCH 10/12] Nothing --- flow.back.json | 64 -------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 flow.back.json diff --git a/flow.back.json b/flow.back.json deleted file mode 100644 index b70dfad..0000000 --- a/flow.back.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "emulators": { - "default": { - "port": 3569, - "serviceAccount": "emulator-account" - } - }, - "contracts": { - "DappyContract": "./cadence/contracts/DappyContract.cdc", - "NFTStorefront": { - "source": "./cadence/contracts/NFTStorefront.cdc", - "aliases": { - "testnet": "0x94b06cfca1d8a476" - } - }, - "DappyNFT": { - "source": "./cadence/contracts/DappyNFT.cdc", - "aliases": { - "testnet": "0x510627df4617530f" - } - }, - "FUSD": { - "source": "./cadence/contracts/FUSD.cdc", - "aliases": { - "testnet": "0xe223d8a629e49c68" - } - }, - "FungibleToken": { - "source": "./cadence/contracts/FungibleToken.cdc", - "aliases": { - "testnet": "9a0766d93b6608b7", - "emulator": "ee82856bf20e2aa6" - } - }, - "NonFungibleToken": { - "source": "./cadence/contracts/NonFungibleToken.cdc", - "aliases": { - "testnet": "96be1b89c734d1f4" - } - } - }, - "networks": { - "emulator": "127.0.0.1:3569", - "mainnet": "access.mainnet.nodes.onflow.org:9000", - "testnet": "access.devnet.nodes.onflow.org:9000" - }, - "accounts": { - "emulator-account": { - "address": "f8d6e0586b0a20c7", - "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" - }, - "MyWorldAdmin": { - "address": "96be1b89c734d1f4", - "key": "97410e191ba55eacf5b470699749a27f3653ff7afe9a7d390ab785033b6b2b76" - } - }, - "deployments": { - "testnet": { - "MyWorldAdmin": [ - "NonFungibleToken" - ] - } - } -} \ No newline at end of file From 0e13bdd50d6f41be6ed4f566ccb62ee7fa3bec6b Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Thu, 28 Oct 2021 15:59:30 +0700 Subject: [PATCH 11/12] Nothing --- flow.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flow.json b/flow.json index dd0391f..067cfa5 100644 --- a/flow.json +++ b/flow.json @@ -68,8 +68,8 @@ "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" }, "MyWorldAdmin": { - "address": "96be1b89c734d1f4", - "key": "97410e191ba55eacf5b470699749a27f3653ff7afe9a7d390ab785033b6b2b76" + "address": "f8d6e0586b0a20c7", + "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" } }, "deployments": { From be3d8e33dbf20fb3c16c3c264cd952ed52b0b8af Mon Sep 17 00:00:00 2001 From: Nguyen Dang Le Date: Thu, 28 Oct 2021 16:02:58 +0700 Subject: [PATCH 12/12] Nothing --- flow.json | 4 ++-- up.sh | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flow.json b/flow.json index 067cfa5..8f319cc 100644 --- a/flow.json +++ b/flow.json @@ -67,14 +67,14 @@ "address": "f8d6e0586b0a20c7", "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" }, - "MyWorldAdmin": { + "MyAdmin": { "address": "f8d6e0586b0a20c7", "key": "ae1b44c0f5e8f6992ef2348898a35e50a8b0b9684000da8b1dade1b3bcd6ebee" } }, "deployments": { "testnet": { - "MyWorldAdmin": [ + "MyAdmin": [ "NonFungibleToken", "DappyContract", "DappyNFT", "PackNFT", "NFTStorefront", "GalleryContract" ] }, diff --git a/up.sh b/up.sh index 43398c7..45208fc 100755 --- a/up.sh +++ b/up.sh @@ -1,19 +1,19 @@ #!/bin/bash -flow accounts remove-contract GalleryContract --network testnet --signer MyWorldAdmin +flow accounts remove-contract GalleryContract --network testnet --signer MyAdmin -flow accounts remove-contract NonFungibleToken --network testnet --signer MyWorldAdmin +flow accounts remove-contract NonFungibleToken --network testnet --signer MyAdmin -flow accounts remove-contract DappyNFT --network testnet --signer MyWorldAdmin +flow accounts remove-contract DappyNFT --network testnet --signer MyAdmin -flow accounts remove-contract PackNFT --network testnet --signer MyWorldAdmin +flow accounts remove-contract PackNFT --network testnet --signer MyAdmin -flow accounts remove-contract NFTStorefront --network testnet --signer MyWorldAdmin +flow accounts remove-contract NFTStorefront --network testnet --signer MyAdmin -flow accounts remove-contract DappyContract --network testnet --signer MyWorldAdmin +flow accounts remove-contract DappyContract --network testnet --signer MyAdmin flow project deploy --network testnet -flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyWorldAdmin --network testnet +flow transactions send cadence/transactions/CreateAdminGallery.cdc --signer MyAdmin --network testnet -flow transactions send cadence/transactions/PrepareDappyContract.cdc --signer MyWorldAdmin --network testnet \ No newline at end of file +flow transactions send cadence/transactions/PrepareDappyContract.cdc --signer MyAdmin --network testnet \ No newline at end of file