From fb1d57451782ebb93e4748ea14fee422b232ffc4 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Sat, 14 Dec 2024 01:14:56 +0400 Subject: [PATCH] Storefront v2 --- cadence/contracts/Recipe.cdc | 516 -------------------------- cadence/transactions/purchase_nft.cdc | 34 +- flow.json | 223 ++++++----- 3 files changed, 130 insertions(+), 643 deletions(-) delete mode 100644 cadence/contracts/Recipe.cdc diff --git a/cadence/contracts/Recipe.cdc b/cadence/contracts/Recipe.cdc deleted file mode 100644 index 814f9df..0000000 --- a/cadence/contracts/Recipe.cdc +++ /dev/null @@ -1,516 +0,0 @@ -import "FungibleToken" -import "NonFungibleToken" -import "Burner" - -/// NB: This contract is no longer supported. NFT Storefront V2 is recommended - -/// 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. -/// -access(all) contract NFTStorefront { - - access(all) entitlement CreateListing - access(all) entitlement RemoveListing - - /// 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. - /// - access(all) 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. - /// - access(all) 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. - /// - access(all) event ListingAvailable( - storefrontAddress: Address, - listingResourceID: UInt64, - nftType: Type, - nftID: UInt64, - ftVaultType: Type, - price: UFix64 - ) - - /// ListingCompleted - /// The listing has been resolved. It has either been purchased, or removed and destroyed. - /// - access(all) event ListingCompleted( - listingResourceID: UInt64, - storefrontResourceID: UInt64, - purchased: Bool, - nftType: Type, - nftID: UInt64 - ) - - /// StorefrontStoragePath - /// The location in storage that a Storefront resource should be located. - access(all) let StorefrontStoragePath: StoragePath - - /// StorefrontPublicPath - /// The public location for a Storefront link. - access(all) let StorefrontPublicPath: PublicPath - - - /// SaleCut - /// A struct representing a recipient that must be sent a certain amount - /// of the payment when a token is sold. - /// - access(all) 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. - access(all) let receiver: Capability<&{FungibleToken.Receiver}> - - /// The amount of the payment FungibleToken that will be paid to the receiver. - access(all) 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. - /// - access(all) 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. - access(all) var storefrontID: UInt64 - /// Whether this listing has been purchased or not. - access(all) var purchased: Bool - /// The Type of the NonFungibleToken.NFT that is being listed. - access(all) let nftType: Type - /// The ID of the NFT within that type. - access(all) let nftID: UInt64 - /// The Type of the FungibleToken that payments must be made in. - access(all) let salePaymentVaultType: Type - /// The amount that must be paid in the specified FungibleToken. - access(all) let salePrice: UFix64 - /// This specifies the division of payment between recipients. - access(all) let saleCuts: [SaleCut] - - /// setToPurchased - /// Irreversibly set this listing as purchased. - /// - access(contract) fun setToPurchased() { - self.purchased = true - } - - /// initializer - /// - init ( - nftType: Type, - nftID: UInt64, - salePaymentVaultType: Type, - saleCuts: [SaleCut], - storefrontID: UInt64 - ) { - self.storefrontID = storefrontID - self.purchased = false - self.nftType = nftType - self.nftID = nftID - self.salePaymentVaultType = salePaymentVaultType - // Store the cuts - assert(saleCuts.length > 0, message: "Listing must have at least one payment cut recipient") - self.saleCuts = saleCuts - - // Calculate the total price from the cuts - var salePrice = 0.0 - // Perform initial check on capabilities, and calculate sale price from cut amounts. - for cut in self.saleCuts { - // Make sure we can borrow the receiver. - // We will check this again when the token is sold. - cut.receiver.borrow() - ?? panic("Cannot borrow receiver") - // Add the cut amount to the total price - salePrice = salePrice + cut.amount - } - assert(salePrice > 0.0, message: "Listing must have non-zero price") - - // Store the calculated sale price - self.salePrice = salePrice - } - } - - - /// ListingPublic - /// An interface providing a useful public interface to a Listing. - /// - access(all) 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. - /// - access(all) fun borrowNFT(): &{NonFungibleToken.NFT}? - - /// purchase - /// Purchase the listing, buying the token. - /// This pays the beneficiaries and returns the token to the buyer. - /// - access(all) fun purchase(payment: @{FungibleToken.Vault}): @{NonFungibleToken.NFT} - - /// getDetails - /// - access(all) 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. - /// - access(all) resource Listing: ListingPublic, Burner.Burnable { - // Event to be emitted when this listing is destroyed. - // If the listing has not been purchased, we regard it as completed here. - // There is a separate event in purchase for purchased listings - access(all) event ResourceDestroyed( - listingResourceID: UInt64 = self.uuid, - storefrontResourceID: UInt64 = self.details.storefrontID, - purchased: Bool = self.details.purchased, - nftType: String = self.details.nftType.identifier, - nftID: UInt64 = self.details.nftID - ) - - access(contract) fun burnCallback() { - // 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, - nftType: self.details.nftType, - nftID: self.details.nftID - ) - } - } - - /// 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 - - /// 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. - /// - access(all) fun borrowNFT(): &{NonFungibleToken.NFT}? { - let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.getDetails().nftID) - assert(ref != nil, message: "Could not borrow a reference to the NFT") - 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}?) - } - - /// 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). - /// - access(all) fun getDetails(): ListingDetails { - return self.details - } - - /// purchase - /// Purchase the listing, buying the token. - /// This pays the beneficiaries and returns the token to the buyer. - /// - access(all) fun purchase(payment: @{FungibleToken.Vault}): @{NonFungibleToken.NFT} { - pre { - self.details.purchased == false: "listing has already been purchased" - payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token" - payment.balance == self.details.salePrice: "payment vault does not contain requested price" - } - - // Make sure the listing cannot be purchased again. - self.details.setToPurchased() - - - // Fetch the token to return to the purchaser. - let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID) - // Neither receivers nor providers are trustworthy, they must implement the correct - // interface but beyond complying with its pre/post conditions they are not gauranteed - // to implement the functionality behind the interface in any given way. - // Therefore we cannot trust the Collection resource behind the interface, - // and we must check the NFT resource it gives us to make sure that it is the correct one. - assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type") - assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID") - - // Rather than aborting the transaction if any receiver is absent when we try to pay it, - // we send the cut to the first valid receiver. - // The first receiver should therefore either be the seller, or an agreed recipient for - // any unpaid cuts. - var residualReceiver: &{FungibleToken.Receiver}? = nil - - // Pay each beneficiary their amount of the payment. - for cut in self.details.saleCuts { - if let receiver = cut.receiver.borrow() { - let paymentCut <- payment.withdraw(amount: cut.amount) - receiver.deposit(from: <-paymentCut) - if (residualReceiver == nil) { - residualReceiver = receiver - } - } - } - - assert(residualReceiver != nil, message: "No valid payment receivers") - - // At this point, if all recievers were active and availabile, then the payment Vault will have - // zero tokens left, and this will functionally be a no-op that consumes the empty vault - residualReceiver!.deposit(from: <-payment) - - // If the listing is purchased, we regard it as completed here. - // Otherwise we regard it as completed in the destructor. - - emit ListingCompleted( - listingResourceID: self.uuid, - storefrontResourceID: self.details.storefrontID, - purchased: self.details.purchased, - nftType: self.details.nftType, - nftID: self.details.nftID - ) - - return <-nft - } - - /// initializer - /// - init ( - nftProviderCapability: Capability, - nftType: Type, - nftID: UInt64, - salePaymentVaultType: Type, - saleCuts: [SaleCut], - storefrontID: UInt64 - ) { - // Store the sale information - self.details = ListingDetails( - nftType: nftType, - nftID: nftID, - salePaymentVaultType: salePaymentVaultType, - saleCuts: saleCuts, - storefrontID: storefrontID - ) - - // Store the NFT provider - self.nftProviderCapability = nftProviderCapability - - // Check that the provider contains the NFT. - // We will check it again when the token is sold. - // We cannot move this into a function because initializers cannot call member functions. - let provider = self.nftProviderCapability.borrow() - assert(provider != nil, message: "cannot borrow nftProviderCapability") - - let nft = provider!.borrowNFT(self.details.nftID) - // This will precondition assert if the token is not available. - assert(nft != nil, message: "Could not borrow a reference to the NFT") - assert(nft!.isInstance(self.details.nftType), message: "token is not of specified type") - assert(nft?.id == self.details.nftID, message: "token does not have specified ID") - } - } - - /// StorefrontManager - /// An interface for adding and removing Listings within a Storefront, - /// intended for use by the Storefront's own - /// - access(all) resource interface StorefrontManager { - /// createListing - /// Allows the Storefront owner to create and insert Listings. - /// - access(CreateListing) fun createListing( - nftProviderCapability: Capability, - nftType: Type, - nftID: UInt64, - salePaymentVaultType: Type, - saleCuts: [SaleCut] - ): UInt64 - /// removeListing - /// Allows the Storefront owner to remove any sale listing, acepted or not. - /// - access(RemoveListing) fun removeListing(listingResourceID: UInt64) - } - - /// StorefrontPublic - /// An interface to allow listing and borrowing Listings, and purchasing items via Listings - /// in a Storefront. - /// - access(all) resource interface StorefrontPublic { - access(all) view fun getListingIDs(): [UInt64] - access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? { - post { - result == nil || result!.getType() == Type<@Listing>(): - "Cannot borrow a non-NFTStorefront.Listing!" - } - } - access(all) 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. - /// - access(all) resource Storefront: StorefrontManager, StorefrontPublic { - // Event to be emitted when this storefront is destroyed. - access(all) event ResourceDestroyed( - storefrontResourceID: UInt64 = self.uuid - ) - - /// The dictionary of Listing uuids to Listing resources. - access(self) var listings: @{UInt64: Listing} - - /// insert - /// Create and publish a Listing for an NFT. - /// - access(CreateListing) fun createListing( - nftProviderCapability: Capability, - nftType: Type, - nftID: UInt64, - salePaymentVaultType: Type, - saleCuts: [SaleCut] - ): UInt64 { - let listing <- create Listing( - nftProviderCapability: nftProviderCapability, - nftType: nftType, - nftID: nftID, - salePaymentVaultType: salePaymentVaultType, - saleCuts: saleCuts, - storefrontID: self.uuid - ) - - let listingResourceID = listing.uuid - let listingPrice = listing.getDetails().salePrice - - // Add the new listing to the dictionary. - let oldListing <- self.listings[listingResourceID] <- listing - // Note that oldListing will always be nil, but we have to handle it. - - Burner.burn(<-oldListing) - - emit ListingAvailable( - storefrontAddress: self.owner?.address!, - listingResourceID: listingResourceID, - nftType: nftType, - nftID: nftID, - ftVaultType: salePaymentVaultType, - price: listingPrice - ) - - return listingResourceID - } - - - /// removeListing - /// Remove a Listing that has not yet been purchased from the collection and destroy it. - /// - access(RemoveListing) fun removeListing(listingResourceID: UInt64) { - let listing <- self.listings.remove(key: listingResourceID) - ?? panic("missing Listing") - - // This will emit a ListingCompleted event. - Burner.burn(<-listing) - } - - /// getListingIDs - /// Returns an array of the Listing resource IDs that are in the collection - /// - access(all) view 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. - /// - access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? { - if self.listings[listingResourceID] != nil { - return &self.listings[listingResourceID] as &{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. - /// - access(all) 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") - Burner.burn(<-listing) - } - - /// constructor - /// - init () { - self.listings <- {} - - // Let event consumers know that this storefront exists - emit StorefrontInitialized(storefrontResourceID: self.uuid) - } - } - - /// createStorefront - /// Make creating a Storefront publicly accessible. - /// - access(all) fun createStorefront(): @Storefront { - return <-create Storefront() - } - - init () { - self.StorefrontStoragePath = /storage/NFTStorefront - self.StorefrontPublicPath = /public/NFTStorefront - } -} \ No newline at end of file diff --git a/cadence/transactions/purchase_nft.cdc b/cadence/transactions/purchase_nft.cdc index 583f8f8..f004949 100644 --- a/cadence/transactions/purchase_nft.cdc +++ b/cadence/transactions/purchase_nft.cdc @@ -1,34 +1,34 @@ import "FungibleToken" import "NonFungibleToken" import "ExampleNFT" -import "NFTStorefront" +import "NFTStorefrontV2" transaction { let paymentVault: @{FungibleToken.Vault} let exampleNFTCollection: &ExampleNFT.Collection - let storefront: auth(NFTStorefront.CreateListing) &NFTStorefront.Storefront - let listing: &{NFTStorefront.ListingPublic} + let storefront: auth(NFTStorefrontV2.CreateListing) &NFTStorefrontV2.Storefront + let listing: &{NFTStorefrontV2.ListingPublic} var listingResourceId: UInt64 - prepare(acct: auth(Storage, Capabilities, NFTStorefront.CreateListing) &Account) { + prepare(acct: auth(Storage, Capabilities, NFTStorefrontV2.CreateListing) &Account) { // Create and save the storefront - let storefront <- NFTStorefront.createStorefront() - acct.storage.save(<-storefront, to: NFTStorefront.StorefrontStoragePath) + let storefront <- NFTStorefrontV2.createStorefront() + acct.storage.save(<-storefront, to: NFTStorefrontV2.StorefrontStoragePath) // Publish the storefront capability to the public path - let storefrontCap = acct.capabilities.storage.issue<&{NFTStorefront.StorefrontPublic}>( - NFTStorefront.StorefrontStoragePath + let storefrontCap = acct.capabilities.storage.issue<&{NFTStorefrontV2.StorefrontPublic}>( + NFTStorefrontV2.StorefrontStoragePath ) - acct.capabilities.publish(storefrontCap, at: NFTStorefront.StorefrontPublicPath) + acct.capabilities.publish(storefrontCap, at: NFTStorefrontV2.StorefrontPublicPath) // Borrow the storefront reference using the public capability path - let storefrontRef = acct.capabilities.borrow<&{NFTStorefront.StorefrontPublic}>( - NFTStorefront.StorefrontPublicPath + let storefrontRef = acct.capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>( + NFTStorefrontV2.StorefrontPublicPath ) ?? panic("Could not borrow Storefront from provided address") // Borrow the storefront reference directly from storage - self.storefront = acct.storage.borrow( - from: NFTStorefront.StorefrontStoragePath + self.storefront = acct.storage.borrow( + from: NFTStorefrontV2.StorefrontStoragePath ) ?? panic("Could not borrow Storefront with CreateListing authorization from storage") // Borrow the NFTMinter from the caller's storage @@ -72,13 +72,17 @@ transaction { nftID: nftID, salePaymentVaultType: Type<@{FungibleToken.Vault}>(), saleCuts: [ - NFTStorefront.SaleCut( + NFTStorefrontV2.SaleCut( receiver: acct.capabilities.get<&{FungibleToken.Receiver}>( /public/flowTokenReceiver )!, amount: 1.0 ) - ] + ], + marketplacesCapability: nil, + customID: nil, + commissionAmount: 0.0, + expiry: UInt64(getCurrentBlock().timestamp + UFix64(60 * 60 * 24)) ) log("Listing created successfully") diff --git a/flow.json b/flow.json index 8557e9e..bb89fd1 100644 --- a/flow.json +++ b/flow.json @@ -1,113 +1,112 @@ { - "contracts": { - "ExampleNFT": { - "source": "./cadence/contracts/ExampleNFT.cdc", - "aliases": { - "emulator": "f8d6e0586b0a20c7" - } - }, - "NFTStorefront": { - "source": "./cadence/contracts/Recipe.cdc", - "aliases": { - "emulator": "f8d6e0586b0a20c7" - } - } - }, - "dependencies": { - "Burner": { - "source": "mainnet://f233dcee88fe0abe.Burner", - "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "FlowToken": { - "source": "mainnet://1654653399040a61.FlowToken", - "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", - "aliases": { - "emulator": "0ae53cb6e3f42a79", - "mainnet": "1654653399040a61", - "testnet": "7e60df042a9c0868" - } - }, - "FungibleToken": { - "source": "mainnet://f233dcee88fe0abe.FungibleToken", - "hash": "050328d01c6cde307fbe14960632666848d9b7ea4fef03ca8c0bbfb0f2884068", - "aliases": { - "emulator": "ee82856bf20e2aa6", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "FungibleTokenMetadataViews": { - "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", - "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", - "aliases": { - "emulator": "ee82856bf20e2aa6", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "FungibleTokenSwitchboard": { - "source": "mainnet://f233dcee88fe0abe.FungibleTokenSwitchboard", - "hash": "10f94fe8803bd1c2878f2323bf26c311fb4fb2beadba9f431efdb1c7fa46c695", - "aliases": { - "emulator": "ee82856bf20e2aa6", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "MetadataViews": { - "source": "mainnet://1d7e57aa55817448.MetadataViews", - "hash": "10a239cc26e825077de6c8b424409ae173e78e8391df62750b6ba19ffd048f51", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - }, - "NonFungibleToken": { - "source": "mainnet://1d7e57aa55817448.NonFungibleToken", - "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - }, - "ViewResolver": { - "source": "mainnet://1d7e57aa55817448.ViewResolver", - "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - } - }, - "networks": { - "emulator": "127.0.0.1:3569", - "mainnet": "access.mainnet.nodes.onflow.org:9000", - "testing": "127.0.0.1:3569", - "testnet": "access.devnet.nodes.onflow.org:9000" - }, - "accounts": { - "emulator-account": { - "address": "f8d6e0586b0a20c7", - "key": { - "type": "file", - "location": "emulator-account.pkey" - } - } - }, - "deployments": { - "emulator": { - "emulator-account": [ - "NFTStorefront", - "ExampleNFT" - ] - } - } -} \ No newline at end of file + "contracts": { + "ExampleNFT": { + "source": "./cadence/contracts/ExampleNFT.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7" + } + } + }, + "dependencies": { + "NFTStorefrontV2": { + "source": "mainnet://4eb8a10cb9f87357.NFTStorefrontV2", + "hash": "f455b556f350f20b71de45ed71da42f0c60ab4d003e4e164e9f6e9d35e3c4c4b", + "aliases": { + "mainnet": "4eb8a10cb9f87357", + "testnet": "2d55b98eb200daef" + } + }, + "Burner": { + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FlowToken": { + "source": "mainnet://1654653399040a61.FlowToken", + "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", + "aliases": { + "emulator": "0ae53cb6e3f42a79", + "mainnet": "1654653399040a61", + "testnet": "7e60df042a9c0868" + } + }, + "FungibleToken": { + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "050328d01c6cde307fbe14960632666848d9b7ea4fef03ca8c0bbfb0f2884068", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenMetadataViews": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", + "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenSwitchboard": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenSwitchboard", + "hash": "10f94fe8803bd1c2878f2323bf26c311fb4fb2beadba9f431efdb1c7fa46c695", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "MetadataViews": { + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "10a239cc26e825077de6c8b424409ae173e78e8391df62750b6ba19ffd048f51", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "NonFungibleToken": { + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "ViewResolver": { + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": { + "type": "file", + "location": "emulator-account.pkey" + } + } + }, + "deployments": { + "emulator": { + "emulator-account": ["ExampleNFT"] + } + } +}