diff --git a/contracts/FlowtyDrops.cdc b/contracts/FlowtyDrops.cdc index 65efee6..21c166e 100644 --- a/contracts/FlowtyDrops.cdc +++ b/contracts/FlowtyDrops.cdc @@ -3,14 +3,14 @@ import "FungibleToken" import "MetadataViews" pub contract FlowtyDrops { - pub let ContainerStoragePath: StoragePath pub let ContainerPublicPath: PublicPath - // TODO: Event definitions - // - DropAdded - // - Phase Started - // - Phase Ended + pub let MinterStoragePath: StoragePath + pub let MinterPrivatePath: PrivatePath + + pub event DropAdded(address: Address, id: UInt64, name: String, description: String, imageUrl: String, start: UInt64?, end: UInt64?) + // - Minted // Interface to expose all the components necessary to participate in a drop @@ -19,6 +19,15 @@ pub contract FlowtyDrops { pub fun borrowPhase(index: Int): &{PhasePublic} pub fun borrowActivePhases(): [&{PhasePublic}] pub fun borrowAllPhases(): [&{PhasePublic}] + pub fun mint( + payment: @FungibleToken.Vault, + amount: Int, + phaseIndex: Int, + expectedType: Type, + receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>, + commissionReceiver: Capability<&{FungibleToken.Receiver}> + ): @FungibleToken.Vault + pub fun getDetails(): DropDetails } pub resource Drop: DropPublic { @@ -128,6 +137,10 @@ pub contract FlowtyDrops { return <- self.phases.remove(at: index) } + pub fun getDetails(): DropDetails { + return self.details + } + init(details: DropDetails, minterCap: Capability<&{Minter}>, phases: @[Phase]) { pre { minterCap.check(): "minter capability is not valid" @@ -283,14 +296,30 @@ pub contract FlowtyDrops { pub resource interface ContainerPublic { pub fun borrowDropPublic(id: UInt64): &{DropPublic}? + pub fun getIDs(): [UInt64] } // Contains drops. - pub resource Container { + pub resource Container: ContainerPublic { pub let drops: @{UInt64: Drop} pub fun addDrop(_ drop: @Drop) { - // TODO: emit DropAdded event + let details = drop.getDetails() + + let phases = drop.borrowAllPhases() + assert(phases.length > 0, message: "drops must have at least one phase to be added to a container") + + let firstPhaseDetails = phases[0].getDetails() + + emit DropAdded( + address: self.owner!.address, + id: drop.uuid, + name: details.display.name, + description: details.display.description, + imageUrl: details.display.thumbnail.uri(), + start: firstPhaseDetails.switch.getStart(), + end: firstPhaseDetails.switch.getEnd() + ) destroy self.drops.insert(key: drop.uuid, <-drop) } @@ -310,6 +339,10 @@ pub contract FlowtyDrops { return &self.drops[id] as &{DropPublic}? } + pub fun getIDs(): [UInt64] { + return self.drops.keys + } + init() { self.drops <- {} } @@ -334,8 +367,12 @@ pub contract FlowtyDrops { init() { let identifier = "FlowtyDrops_".concat(self.account.address.toString()) let containerIdentifier = identifier.concat("_Container") + let minterIdentifier = identifier.concat("_Minter") self.ContainerStoragePath = StoragePath(identifier: containerIdentifier)! self.ContainerPublicPath = PublicPath(identifier: containerIdentifier)! + + self.MinterPrivatePath = PrivatePath(identifier: minterIdentifier)! + self.MinterStoragePath = StoragePath(identifier: minterIdentifier)! } } \ No newline at end of file diff --git a/contracts/nft/OpenEditionNFT.cdc b/contracts/nft/OpenEditionNFT.cdc index 3fc8298..3b39811 100644 --- a/contracts/nft/OpenEditionNFT.cdc +++ b/contracts/nft/OpenEditionNFT.cdc @@ -38,8 +38,6 @@ pub contract OpenEditionNFT: NonFungibleToken, ViewResolver { /// Storage and Public Paths pub let CollectionStoragePath: StoragePath pub let CollectionPublicPath: PublicPath - pub let MinterStoragePath: StoragePath - pub let MinterPrivatePath: PrivatePath /// The core resource that represents a Non Fungible Token. /// New instances will be created using the NFTMinter resource @@ -259,7 +257,7 @@ pub contract OpenEditionNFT: NonFungibleToken, ViewResolver { } ) case Type(): - return FlowtyDrops.DropResolver(cap: OpenEditionNFT.account.getCapability<&{FlowtyDrops.ContainerPublic}>(OpenEditionNFT.MinterPrivatePath)) + return FlowtyDrops.DropResolver(cap: OpenEditionNFT.account.getCapability<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath)) } return nil } @@ -283,8 +281,6 @@ pub contract OpenEditionNFT: NonFungibleToken, ViewResolver { // Set the named paths self.CollectionStoragePath = /storage/openEditionNFT self.CollectionPublicPath = /public/openEditionNFT - self.MinterStoragePath = /storage/openEditionMinter - self.MinterPrivatePath = /private/openEditionMinter // Create a Collection resource and save it to storage let collection <- create Collection() @@ -298,20 +294,10 @@ pub contract OpenEditionNFT: NonFungibleToken, ViewResolver { // Create a Minter resource and save it to storage let minter <- create NFTMinter() - self.account.save(<-minter, to: self.MinterStoragePath) - let minterCap = self.account.link<&NFTMinter{FlowtyDrops.Minter}>(self.MinterPrivatePath, target: self.MinterStoragePath) + self.account.save(<-minter, to: FlowtyDrops.MinterStoragePath) + let minterCap = self.account.link<&NFTMinter{FlowtyDrops.Minter}>(FlowtyDrops.MinterPrivatePath, target: FlowtyDrops.MinterStoragePath) ?? panic("unable to link minter capability") emit ContractInitialized() - - let dropDisplay = MetadataViews.Display( - name: "Sample Open Edition", - description: "This is a sample Open Edition NFT Drop utilizing flowty drops for minting", - thumbnail: MetadataViews.IPFSFile(cid: "QmNtDmxuyBeA6YJht3ADMJCCqLoG3SPf3S7DYavZTeFUy7", path: nil) - ) - let drop <- DropFactory.createEndlessOpenEditionDrop(price: 1.0, paymentTokenType: Type<@FlowToken.Vault>(), dropDisplay: dropDisplay, minterCap: minterCap) - let container <- FlowtyDrops.createContainer() - container.addDrop(<- drop) - self.account.save(<-container, to: FlowtyDrops.ContainerStoragePath) } } \ No newline at end of file diff --git a/scripts/get_drop_details.cdc b/scripts/get_drop_details.cdc new file mode 100644 index 0000000..7125342 --- /dev/null +++ b/scripts/get_drop_details.cdc @@ -0,0 +1,17 @@ +import "FlowtyDrops" +import "ViewResolver" + +pub fun main(contractAddress: Address, contractName: String, dropID: UInt64): FlowtyDrops.DropDetails { + let resolver = getAccount(contractAddress).contracts.borrow<&ViewResolver>(name: contractName) + ?? panic("contract does not implement ViewResolver interface") + + let dropResolver = resolver.resolveView(Type())! as! FlowtyDrops.DropResolver + + let container = dropResolver.borrowContainer() + ?? panic("drop container not found") + + let drop = container.borrowDropPublic(id: dropID) + ?? panic("drop not found") + + return drop.getDetails() +} \ No newline at end of file diff --git a/scripts/get_drop_ids.cdc b/scripts/get_drop_ids.cdc new file mode 100644 index 0000000..ff887d4 --- /dev/null +++ b/scripts/get_drop_ids.cdc @@ -0,0 +1,22 @@ +import "FlowtyDrops" +import "ViewResolver" + +pub fun main(contractAddress: Address, contractName: String): [UInt64] { + let resolver = getAccount(contractAddress).contracts.borrow<&ViewResolver>(name: contractName) + if resolver == nil { + return [] + } + + let tmp = resolver!.resolveView(Type()) + if tmp == nil { + return [] + } + + let dropResolver = tmp! as! FlowtyDrops.DropResolver + + if let dropContainer = dropResolver.borrowContainer() { + return dropContainer.getIDs() + } + + return [] +} \ No newline at end of file diff --git a/scripts/get_price_at_phase.cdc b/scripts/get_price_at_phase.cdc new file mode 100644 index 0000000..da8b492 --- /dev/null +++ b/scripts/get_price_at_phase.cdc @@ -0,0 +1,20 @@ +import "FlowtyDrops" +import "ViewResolver" + +pub fun main(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int, minter: Address, numToMint: Int, paymentIdentifier: String): UFix64? { + let paymentTokenType = CompositeType(paymentIdentifier)! + + let resolver = getAccount(contractAddress).contracts.borrow<&ViewResolver>(name: contractName) + ?? panic("contract does not implement ViewResolver interface") + + let dropResolver = resolver.resolveView(Type())! as! FlowtyDrops.DropResolver + + let container = dropResolver.borrowContainer() + ?? panic("drop container not found") + + let drop = container.borrowDropPublic(id: dropID) + ?? panic("drop not found") + + let phase = drop.borrowPhase(index: phaseIndex) + return phase.getDetails().pricer.getPrice(num: numToMint, paymentTokenType: paymentTokenType, minter: minter) +} \ No newline at end of file diff --git a/tests/FlowtyDrops_tests.cdc b/tests/FlowtyDrops_tests.cdc index 6847713..29d6a40 100644 --- a/tests/FlowtyDrops_tests.cdc +++ b/tests/FlowtyDrops_tests.cdc @@ -1,11 +1,63 @@ import Test import "test_helpers.cdc" +import "FlowToken" +import "FlowtyDrops" + +pub let defaultEndlessOpenEditionName = "Default Endless Open Edition" pub fun setup() { deployAll() } +pub fun afterEach() { + txExecutor("drops/remove_all_drops.cdc", [openEditionAccount], [], nil, nil) +} + pub fun testImports() { Test.assert(scriptExecutor("import_all.cdc", [])! as! Bool, message: "failed to import all") } +pub fun test_OpenEditionNFT_getPrice() { + let minter = Test.createAccount() + + let dropID = createDefaultEndlessOpenEditionDrop() + let price = getPriceAtPhase( + contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: minter.address, numToMint: 1, paymentIdentifier: Type<@FlowToken.Vault>().identifier + ) + Test.assertEqual(1.0, price) + + let priceMultiple = getPriceAtPhase( + contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: minter.address, numToMint: 10, paymentIdentifier: Type<@FlowToken.Vault>().identifier + ) + Test.assertEqual(10.0, priceMultiple) +} + +pub fun test_OpenEditionNFT_getDetails() { + let dropID = createDefaultEndlessOpenEditionDrop() + let details = getDropDetails(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID) + Test.assertEqual(details.display.name, defaultEndlessOpenEditionName) +} + +// ------------------------------------------------------------------------ +// Helper functions section + +pub fun createDefaultEndlessOpenEditionDrop(): UInt64 { + return createEndlessOpenEditionDrop( + acct: openEditionAccount, + name: "Default Endless Open Edition", + description: "This is a placeholder description", + ipfsCid: "1234", + ipfsPath: nil, + price: 1.0, + paymentIdentifier: Type<@FlowToken.Vault>().identifier, + minterPrivatePath: FlowtyDrops.MinterPrivatePath + ) +} + +pub fun getPriceAtPhase(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int, minter: Address, numToMint: Int, paymentIdentifier: String): UFix64 { + return scriptExecutor("get_price_at_phase.cdc", [contractAddress, contractName, dropID, phaseIndex, minter, numToMint, paymentIdentifier])! as! UFix64 +} + +pub fun getDropDetails(contractAddress: Address, contractName: String, dropID: UInt64): FlowtyDrops.DropDetails { + return scriptExecutor("get_drop_details.cdc", [contractAddress, contractName, dropID])! as! FlowtyDrops.DropDetails +} \ No newline at end of file diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index 6d69116..2c70875 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -2,6 +2,7 @@ import Test import "NonFungibleToken" import "FlowToken" +import "FlowtyDrops" // Helper functions. All of the following were taken from // https://github.com/onflow/Offers/blob/fd380659f0836e5ce401aa99a2975166b2da5cb0/lib/cadence/test/Offers.cdc @@ -145,7 +146,8 @@ pub let Account0xd = Address(0x000000000000000d) pub let Account0xe = Address(0x000000000000000e) pub let serviceAccount = Test.getAccount(Account0x5) -pub let dropsAccount = Test.getAccount(Account0x5) +pub let dropsAccount = Test.getAccount(Account0x6) +pub let openEditionAccount = Test.getAccount(Account0x7) // Flow Token constants pub let flowTokenStoragePath = /storage/flowTokenVault @@ -173,4 +175,59 @@ pub fun heartbeat() { pub fun getCurrentTime(): UFix64 { return scriptExecutor("util/get_current_time.cdc", [])! as! UFix64 +} + +pub fun mintFromDrop( + minter: Test.Account, + contractAddress: Address, + contractName: String, + numToMint: Int, + totalCost: UFix64, + paymentIdentifier: String, + paymentStoragePath: StoragePath, + paymentReceiverPath: PublicPath, + dropID: UInt64, + dropPhaseIndex: Int, + nftIdentifier: String, + commissionReceiver: Address +) { + let args = [ + contractAddress, + contractName, + numToMint, + totalCost, + paymentIdentifier, + paymentStoragePath, + paymentReceiverPath, + dropID, + dropPhaseIndex, + nftIdentifier, + commissionReceiver + ] + txExecutor("drops/mint.cdc", [minter], args, nil, nil) +} + +pub fun getDropIDs( + contractAddress: Address, + contractName: String +): [UInt64] { + return scriptExecutor("get_drop_ids.cdc", [contractAddress, contractName])! as! [UInt64] +} + +pub fun createEndlessOpenEditionDrop( + acct: Test.Account, + name: String, + description: String, + ipfsCid: String, + ipfsPath: String?, + price: UFix64, + paymentIdentifier: String, + minterPrivatePath: PrivatePath +): UInt64 { + txExecutor("drops/add_endless_open_edition.cdc", [acct], [ + name, description, ipfsCid, ipfsPath, price, paymentIdentifier, minterPrivatePath + ], nil, nil) + + let e = Test.eventsOfType(Type()).removeLast() as! FlowtyDrops.DropAdded + return e.id } \ No newline at end of file diff --git a/transactions/drops/add_endless_open_edition.cdc b/transactions/drops/add_endless_open_edition.cdc new file mode 100644 index 0000000..9c66d28 --- /dev/null +++ b/transactions/drops/add_endless_open_edition.cdc @@ -0,0 +1,39 @@ +import "FlowtyDrops" +import "DropFactory" + +import "MetadataViews" + +transaction( + name: String, + description: String, + ipfsCid: String, + ipfsPath: String?, + price: UFix64, + paymentIdentifier: String, + minterPrivatePath: PrivatePath +) { + prepare(acct: AuthAccount) { + if acct.borrow<&AnyResource>(from: FlowtyDrops.ContainerStoragePath) == nil { + acct.save(<- FlowtyDrops.createContainer(), to: FlowtyDrops.ContainerStoragePath) + + acct.unlink(FlowtyDrops.ContainerPublicPath) + acct.link<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath, target: FlowtyDrops.ContainerStoragePath) + } + + let minter = acct.getCapability<&{FlowtyDrops.Minter}>(minterPrivatePath) + assert(minter.check(), message: "minter capability is not valid") + + let container = acct.borrow<&FlowtyDrops.Container>(from: FlowtyDrops.ContainerStoragePath) + ?? panic("drops container not found") + + let paymentType = CompositeType(paymentIdentifier) ?? panic("invalid payment identifier") + + let dropDisplay = MetadataViews.Display( + name: name, + description: description, + thumbnail: MetadataViews.IPFSFile(cid: ipfsCid, path: ipfsPath) + ) + let drop <- DropFactory.createEndlessOpenEditionDrop(price: 1.0, paymentTokenType: paymentType, dropDisplay: dropDisplay, minterCap: minter) + container.addDrop(<- drop) + } +} diff --git a/transactions/drops/mint.cdc b/transactions/drops/mint.cdc new file mode 100644 index 0000000..132af24 --- /dev/null +++ b/transactions/drops/mint.cdc @@ -0,0 +1,63 @@ +import "ViewResolver" +import "MetadataViews" +import "NonFungibleToken" +import "FungibleToken" + +import "FlowtyDrops" + +transaction( + contractAddress: Address, + contractName: String, + numToMint: Int, + totalCost: UFix64, + paymentIdentifier: String, + paymentStoragePath: StoragePath, + paymentReceiverPath: PublicPath, + dropID: UInt64, + dropPhaseIndex: Int, + nftIdentifier: String, + commissionAddress: Address +) { + prepare(acct: AuthAccount) { + let resolver = getAccount(contractAddress).contracts.borrow<&ViewResolver>(name: contractName) + ?? panic("ViewResolver contract interface not found on contract address + name") + + let collectionData = resolver.resolveView(Type())! as! MetadataViews.NFTCollectionData + if acct.borrow<&AnyResource>(from: collectionData.storagePath) == nil { + acct.save(<- collectionData.createEmptyCollection(), to: collectionData.storagePath) + + acct.link<&{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(collectionData.publicPath, target: collectionData.storagePath) + acct.link<&{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection, NonFungibleToken.Provider}>(collectionData.providerPath, target: collectionData.storagePath) + } + let receiverCap = acct.getCapability<&{NonFungibleToken.CollectionPublic}>(collectionData.publicPath) + + let expectedNftType = CompositeType(nftIdentifier) ?? panic("invalid nft identifier") + + let vault = acct.borrow<&{FungibleToken.Provider}>(from: paymentStoragePath) + ?? panic("could not borrow token provider") + + let paymentVault <- vault.withdraw(amount: totalCost) + + let dropResolver = resolver.resolveView(Type())! as! FlowtyDrops.DropResolver + let dropContainer = dropResolver.borrowContainer() + ?? panic("unable to borrow drop container") + + let drop = dropContainer.borrowDropPublic(id: dropID) ?? panic("drop not found") + + let commissionReceiver = getAccount(commissionAddress).getCapability<&{FungibleToken.Receiver}>(paymentReceiverPath) + let remainder <- drop.mint( + payment: <-paymentVault, + amount: numToMint, + phaseIndex: dropPhaseIndex, + expectedType: expectedNftType, + receiverCap: receiverCap, + commissionReceiver: commissionReceiver + ) + + if remainder.balance > 0.0 { + acct.borrow<&{FungibleToken.Receiver}>(from: paymentStoragePath)!.deposit(from: <-remainder) + } else { + destroy remainder + } + } +} \ No newline at end of file diff --git a/transactions/drops/remove_all_drops.cdc b/transactions/drops/remove_all_drops.cdc new file mode 100644 index 0000000..7d27b8b --- /dev/null +++ b/transactions/drops/remove_all_drops.cdc @@ -0,0 +1,11 @@ +import "FlowtyDrops" + +transaction { + prepare(acct: AuthAccount) { + if let container = acct.borrow<&FlowtyDrops.Container>(from: FlowtyDrops.ContainerStoragePath) { + for id in container.getIDs() { + destroy container.removeDrop(id: id) + } + } + } +} \ No newline at end of file