diff --git a/README.md b/README.md new file mode 100644 index 0000000..56537f9 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Flowty Drops + +A system of contracts designed to make it easy to expose drops to platforms on the +Flow Blockchain. Using FlowtyDrops and its supporting contracts, you can easily integrate +an open framework for anyone to showcase your drop on their own site/platform + +## Overview + +FlowtyDrops is made up of a few core resources and structs: +1. @Drop - Coordinates drops. Drop resources contain an array of Phase resources, details, and a capability to a Minter + interface for it to us +2. @Phase - A stage of a drop. Many drops are segments into multiple stages (such as an allow-list followed by a public mint), phases + are a way to represent this. Phases dictate what accounts can mint, how many, for what price. Each phase is independent of others, + and be asked if it is active or not. +3. @Container - Holds Drop resources in it. +4. @{Minter} - A resource interface that creators can implement to be compatible with FlowtyDrops. When constructing a drop, you must + supply a `Capability<&{Minter}>` to it. +5. {Switch} - A struct interface that is responsible for whether a phase is active or not. For example, one implementation could be configured + to start a phase at a time in the future, while another could turn start based on block height. +6. {AddressVerifier} - A struct interface that is responsible for determining if an account is permitted to mint or not. For example, + one implementation might permit any account to mint as many as it wants, while another might check an allow-list. +7. {Pricer} - A struct interface that is responsible for the price of a mint. For example, one implementation could be for a set flat + fee while another could be dynamic based on an account's ownership of a certain collection + + +## Contracts + +1. FlowtyDrops - The primary contract. All core resources and structs can + be found here. All other contracts represent sample implementations + of the definitions found here +2. DropFactory - Helper method to create pre-configured popular drop options +3. FlowtyAddressVerifiers - Implementations of the AddressVerifiers struct + interface. AddressVerifiers handle whether a minter is permitted to + mint with the given parameters they are using. Some verifiers might + permit any kind of activity whereas others might require server-side + signatures or prescence on an allow-list. +4. FlowtyPricers - Implementations of the Pricer struct interface. Pricers + are responsible for handling how much an attempted mint should cost. + For example, you might make a drop free, or might configure a drop to + be a flat fee regardless of how many are being minted at once. +5. FlowtySwitches - Implementations of the Switch struct interface. + Switch are responsible for flagging if a drop is live or not. For + example, a drop might go live a certain unix timestamp and end at a + future date, or it might be on perpetually until manually turned off. \ No newline at end of file diff --git a/contracts/DropFactory.cdc b/contracts/DropFactory.cdc index a344f91..afdff2a 100644 --- a/contracts/DropFactory.cdc +++ b/contracts/DropFactory.cdc @@ -24,7 +24,37 @@ pub contract DropFactory { let switch = FlowtySwitches.AlwaysOn() // All addresses are allowed to participate - let addressVerifier = FlowtyAddressVerifiers.AllowAll() + let addressVerifier = FlowtyAddressVerifiers.AllowAll(maxPerMint: 10) + + // The cost of each mint is the same, and only permits one token type as payment + let pricer = FlowtyPricers.FlatPrice(price: price, paymentTokenType: paymentTokenType) + + let phaseDetails = FlowtyDrops.PhaseDetails(switch: switch, display: nil, pricer: pricer, addressVerifier: addressVerifier) + let phase <- FlowtyDrops.createPhase(details: phaseDetails) + + let dropDetails = FlowtyDrops.DropDetails(display: dropDisplay, medias: nil, commissionRate: 0.05) + let drop <- FlowtyDrops.createDrop(details: dropDetails, minterCap: minterCap, phases: <- [<-phase]) + + return <- drop + } + + pub fun createTimeBasedOpenEditionDrop( + price: UFix64, + paymentTokenType: Type, + dropDisplay: MetadataViews.Display, + minterCap: Capability<&{FlowtyDrops.Minter}>, + startUnix: UInt64?, + endUnix: UInt64? + ): @FlowtyDrops.Drop { + pre { + paymentTokenType.isSubtype(of: Type<@FungibleToken.Vault>()): "paymentTokenType must be a FungibleToken" + } + + // This switch turns on at a set unix timestamp (or is on by default if nil), and ends at the specified end date if provided + let switch = FlowtySwitches.TimestampSwitch(start: startUnix, end: endUnix) + + // All addresses are allowed to participate + let addressVerifier = FlowtyAddressVerifiers.AllowAll(maxPerMint: 10) // The cost of each mint is the same, and only permits one token type as payment let pricer = FlowtyPricers.FlatPrice(price: price, paymentTokenType: paymentTokenType) diff --git a/contracts/FlowtyAddressVerifiers.cdc b/contracts/FlowtyAddressVerifiers.cdc index f2e531d..2a33f02 100644 --- a/contracts/FlowtyAddressVerifiers.cdc +++ b/contracts/FlowtyAddressVerifiers.cdc @@ -7,7 +7,25 @@ pub contract FlowtyAddressVerifiers { /* The AllowAll AddressVerifier allows any address to mint without any verification */ - pub struct AllowAll: FlowtyDrops.AddressVerifier {} + pub struct AllowAll: FlowtyDrops.AddressVerifier { + pub var maxPerMint: Int + + pub fun canMint(addr: Address, num: Int, totalMinted: Int, data: {String: AnyStruct}): Bool { + return num <= self.maxPerMint + } + + pub fun setMaxPerMint(_ value: Int) { + self.maxPerMint = value + } + + init(maxPerMint: Int) { + pre { + maxPerMint > 0: "maxPerMint must be greater than 0" + } + + self.maxPerMint = maxPerMint + } + } /* The AllowList Verifier only lets a configured set of addresses participate in a drop phase. The number diff --git a/contracts/FlowtyDrops.cdc b/contracts/FlowtyDrops.cdc index 1b58bf9..dde93db 100644 --- a/contracts/FlowtyDrops.cdc +++ b/contracts/FlowtyDrops.cdc @@ -15,7 +15,7 @@ pub contract FlowtyDrops { // Interface to expose all the components necessary to participate in a drop // and to ask questions about a drop. pub resource interface DropPublic { - pub fun borrowPhase(index: Int): &{PhasePublic} + pub fun borrowPhasePublic(index: Int): &{PhasePublic} pub fun borrowActivePhases(): [&{PhasePublic}] pub fun borrowAllPhases(): [&{PhasePublic}] pub fun mint( @@ -24,7 +24,8 @@ pub contract FlowtyDrops { phaseIndex: Int, expectedType: Type, receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>, - commissionReceiver: Capability<&{FungibleToken.Receiver}> + commissionReceiver: Capability<&{FungibleToken.Receiver}>, + data: {String: AnyStruct} ): @FungibleToken.Vault pub fun getDetails(): DropDetails } @@ -42,7 +43,8 @@ pub contract FlowtyDrops { phaseIndex: Int, expectedType: Type, receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>, - commissionReceiver: Capability<&{FungibleToken.Receiver}> + commissionReceiver: Capability<&{FungibleToken.Receiver}>, + data: {String: AnyStruct} ): @FungibleToken.Vault { pre { expectedType.isSubtype(of: Type<@NonFungibleToken.NFT>()): "expected type must be an NFT" @@ -69,7 +71,7 @@ pub contract FlowtyDrops { // mint the nfts let minter = self.minterCap.borrow() ?? panic("minter capability could not be borrowed") - let mintedNFTs <- minter.mint(payment: <-withdrawn, amount: amount, phase: phase) + let mintedNFTs <- minter.mint(payment: <-withdrawn, amount: amount, phase: phase, data: data) // distribute to receiver let receiver = receiverCap.borrow() ?? panic("could not borrow receiver capability") @@ -94,7 +96,12 @@ pub contract FlowtyDrops { return <- payment } - pub fun borrowPhase(index: Int): &{PhasePublic} { + pub fun borrowPhase(index: Int): &Phase { + return &self.phases[index] as! &Phase + } + + + pub fun borrowPhasePublic(index: Int): &{PhasePublic} { return &self.phases[index] as! &{PhasePublic} } @@ -102,7 +109,7 @@ pub contract FlowtyDrops { let arr: [&{PhasePublic}] = [] var count = 0 while count < self.phases.length { - let ref = self.borrowPhase(index: count) + let ref = self.borrowPhasePublic(index: count) let switch = ref.getDetails().switch if switch.hasStarted() && !switch.hasEnded() { arr.append(ref) @@ -118,7 +125,7 @@ pub contract FlowtyDrops { let arr: [&{PhasePublic}] = [] var count = 0 while count < self.phases.length { - let ref = self.borrowPhase(index: count) + let ref = self.borrowPhasePublic(index: count) arr.append(ref) count = count + 1 } @@ -212,6 +219,18 @@ pub contract FlowtyDrops { return self.details } + pub fun borrowSwitchAuth(): auth &{Switch} { + return &self.details.switch as! auth &{Switch} + } + + pub fun borrowPricerAuth(): auth &{Pricer} { + return &self.details.pricer as! auth &{Pricer} + } + + pub fun borrowAddressVerifierAuth(): auth &{AddressVerifier} { + return &self.details.addressVerifier as! auth &{AddressVerifier} + } + init(details: PhaseDetails) { self.details = details } @@ -243,9 +262,6 @@ pub contract FlowtyDrops { // placecholder data dictionary to allow new fields to be accessed pub let data: {String: AnyStruct} - // TODO: how many can I mint at once? - // TODO: how many can I mint in total? - init(switch: {Switch}, display: MetadataViews.Display?, pricer: {Pricer}, addressVerifier: {AddressVerifier}) { self.switch = switch self.display = display @@ -272,7 +288,7 @@ pub contract FlowtyDrops { } pub resource interface Minter { - pub fun mint(payment: @FungibleToken.Vault, amount: Int, phase: &Phase): @[NonFungibleToken.NFT] { + pub fun mint(payment: @FungibleToken.Vault, amount: Int, phase: &Phase, data: {String: AnyStruct}): @[NonFungibleToken.NFT] { post { phase.details.switch.hasStarted() && !phase.details.switch.hasEnded(): "phase is not active" result.length == amount: "incorrect number of items returned" diff --git a/contracts/FlowtyPricers.cdc b/contracts/FlowtyPricers.cdc index aab56fd..7229275 100644 --- a/contracts/FlowtyPricers.cdc +++ b/contracts/FlowtyPricers.cdc @@ -12,7 +12,7 @@ pub contract FlowtyPricers { the number minter, or what address is minting */ pub struct FlatPrice: FlowtyDrops.Pricer { - pub let price: UFix64 + pub var price: UFix64 pub let paymentTokenType: Type pub fun getPrice(num: Int, paymentTokenType: Type, minter: Address): UFix64 { @@ -23,6 +23,10 @@ pub contract FlowtyPricers { return [self.paymentTokenType] } + pub fun setPrice(price: UFix64) { + self.price = price + } + init(price: UFix64, paymentTokenType: Type) { self.price = price self.paymentTokenType = paymentTokenType diff --git a/contracts/FlowtySwitches.cdc b/contracts/FlowtySwitches.cdc index 91eeeb9..2749ab5 100644 --- a/contracts/FlowtySwitches.cdc +++ b/contracts/FlowtySwitches.cdc @@ -69,8 +69,8 @@ pub contract FlowtySwitches { A timestamp switch has a start and an end time. */ pub struct TimestampSwitch: FlowtyDrops.Switch { - pub let start: UInt64? - pub let end: UInt64? + pub var start: UInt64? + pub var end: UInt64? pub fun hasStarted(): Bool { @@ -93,6 +93,14 @@ pub contract FlowtySwitches { return self.end } + pub fun setStart(start: UInt64?) { + self.start = start + } + + pub fun setEnd(end: UInt64?) { + self.end = end + } + init(start: UInt64?, end: UInt64?) { pre { start == nil || end == nil || start! < end!: "start must be less than end" diff --git a/contracts/nft/OpenEditionNFT.cdc b/contracts/nft/OpenEditionNFT.cdc index 4b78dd7..0fa4953 100644 --- a/contracts/nft/OpenEditionNFT.cdc +++ b/contracts/nft/OpenEditionNFT.cdc @@ -205,7 +205,7 @@ pub contract OpenEditionNFT: NonFungibleToken, ViewResolver { /// able to mint new NFTs /// pub resource NFTMinter: FlowtyDrops.Minter { - pub fun mint(payment: @FungibleToken.Vault, amount: Int, phase: &FlowtyDrops.Phase): @[NonFungibleToken.NFT] { + pub fun mint(payment: @FungibleToken.Vault, amount: Int, phase: &FlowtyDrops.Phase, data: {String: AnyStruct}): @[NonFungibleToken.NFT] { switch(payment.getType()) { case Type<@FlowToken.Vault>(): OpenEditionNFT.account.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault)!.deposit(from: <-payment) diff --git a/scripts/can_mint_at_phase.cdc b/scripts/can_mint_at_phase.cdc new file mode 100644 index 0000000..560f5b4 --- /dev/null +++ b/scripts/can_mint_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, totalMinted: Int, paymentIdentifier: String): Bool { + 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.borrowPhasePublic(index: phaseIndex) + return phase.getDetails().addressVerifier.canMint(addr: minter, num: numToMint, totalMinted: totalMinted, data: {}) +} \ No newline at end of file diff --git a/scripts/get_price_at_phase.cdc b/scripts/get_price_at_phase.cdc index da8b492..fa22dad 100644 --- a/scripts/get_price_at_phase.cdc +++ b/scripts/get_price_at_phase.cdc @@ -15,6 +15,6 @@ pub fun main(contractAddress: Address, contractName: String, dropID: UInt64, pha let drop = container.borrowDropPublic(id: dropID) ?? panic("drop not found") - let phase = drop.borrowPhase(index: phaseIndex) + let phase = drop.borrowPhasePublic(index: phaseIndex) return phase.getDetails().pricer.getPrice(num: numToMint, paymentTokenType: paymentTokenType, minter: minter) } \ No newline at end of file diff --git a/scripts/has_phase_ended.cdc b/scripts/has_phase_ended.cdc new file mode 100644 index 0000000..24ea213 --- /dev/null +++ b/scripts/has_phase_ended.cdc @@ -0,0 +1,19 @@ +import "FlowtyDrops" +import "ViewResolver" + +pub fun main(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int): Bool { + 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.borrowPhasePublic(index: phaseIndex) + + return phase.getDetails().switch.hasEnded() +} \ No newline at end of file diff --git a/scripts/has_phase_started.cdc b/scripts/has_phase_started.cdc new file mode 100644 index 0000000..f30f637 --- /dev/null +++ b/scripts/has_phase_started.cdc @@ -0,0 +1,19 @@ +import "FlowtyDrops" +import "ViewResolver" + +pub fun main(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int): Bool { + 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.borrowPhasePublic(index: phaseIndex) + + return phase.getDetails().switch.hasStarted() +} \ No newline at end of file diff --git a/scripts/util/get_current_time.cdc b/scripts/util/get_current_time.cdc new file mode 100644 index 0000000..bded294 --- /dev/null +++ b/scripts/util/get_current_time.cdc @@ -0,0 +1,3 @@ +pub fun main(): UFix64 { + return getCurrentBlock().timestamp +} \ No newline at end of file diff --git a/tests/FlowtyAddressVerifiers_tests.cdc b/tests/FlowtyAddressVerifiers_tests.cdc index 0bf0692..5728539 100644 --- a/tests/FlowtyAddressVerifiers_tests.cdc +++ b/tests/FlowtyAddressVerifiers_tests.cdc @@ -9,7 +9,7 @@ pub fun setup() { } pub fun test_FlowtyAddressVerifiers_AllowAll() { - let v = FlowtyAddressVerifiers.AllowAll() + let v = FlowtyAddressVerifiers.AllowAll(maxPerMint: 10) Test.assertEqual(true, v.canMint(addr: alice.address, num: 10, totalMinted: 10, data: {})) Test.assertEqual(nil, v.remainingForAddress(addr: alice.address, totalMinted: 10)) } diff --git a/tests/FlowtyDrops_tests.cdc b/tests/FlowtyDrops_tests.cdc index 1c86aad..421b633 100644 --- a/tests/FlowtyDrops_tests.cdc +++ b/tests/FlowtyDrops_tests.cdc @@ -84,11 +84,51 @@ pub fun test_OpenEditionNFT_mint() { Test.assertEqual(openEditionAccount.address, minterFee.to!) } +pub fun test_OpenEditionNFT_EditPhaseDetails() { + let dropID = createDefaultTimeBasedOpenEditionDrop() + Test.assertEqual(true, hasDropPhaseStarted(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + Test.assertEqual(false, hasDropPhaseEnded(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + + // now let's set a new start and end time + let currentTime = getCurrentTime() + let newStart = UInt64(currentTime + 5.0) + + txExecutor("drops/edit_timebased_phase_start_and_end.cdc", [openEditionAccount], [dropID, 0, newStart, newStart + UInt64(5)], nil, nil) + Test.assertEqual(false, hasDropPhaseStarted(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + Test.assertEqual(false, hasDropPhaseEnded(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + + // now move time such that the drop should have started + Test.moveTime(by: 6.0) + Test.assertEqual(true, hasDropPhaseStarted(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + Test.assertEqual(false, hasDropPhaseEnded(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) + + // now move time to expire the phase + Test.moveTime(by: 10.0) + Test.assertEqual(true, hasDropPhaseEnded(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0)) +} + +pub fun test_OpenEditionNFT_EditPrice() { + let dropID = createDefaultTimeBasedOpenEditionDrop() + Test.assertEqual(1.0, getPriceAtPhase(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: openEditionAccount.address, numToMint: 1, paymentIdentifier: exampleTokenIdentifier())) + + txExecutor("drops/edit_flat_price.cdc", [openEditionAccount], [dropID, 0, 2.0], nil, nil) + Test.assertEqual(2.0, getPriceAtPhase(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: openEditionAccount.address, numToMint: 1, paymentIdentifier: exampleTokenIdentifier())) +} + +pub fun test_OpenEditionNFT_EditMaxPerMint() { + let dropID = createDefaultTimeBasedOpenEditionDrop() + Test.assertEqual(true, canMintAtPhase(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: openEditionAccount.address, numToMint: 10, totalMinted: 0, paymentIdentifier: exampleTokenIdentifier())) + txExecutor("drops/edit_address_verifier_max_per_mint.cdc", [openEditionAccount], [dropID, 0, 5], nil, nil) + + Test.assertEqual(false, canMintAtPhase(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: openEditionAccount.address, numToMint: 10, totalMinted: 0, paymentIdentifier: exampleTokenIdentifier())) + Test.assertEqual(true, canMintAtPhase(contractAddress: openEditionAccount.address, contractName: "OpenEditionNFT", dropID: dropID, phaseIndex: 0, minter: openEditionAccount.address, numToMint: 5, totalMinted: 0, paymentIdentifier: exampleTokenIdentifier())) +} + // ------------------------------------------------------------------------ // Helper functions section pub fun createDefaultEndlessOpenEditionDrop(): UInt64 { - return createEndlessOpenEditionDrop( + return createEndlessOpenEditionDrop( acct: openEditionAccount, name: "Default Endless Open Edition", description: "This is a placeholder description", @@ -100,6 +140,23 @@ pub fun createDefaultEndlessOpenEditionDrop(): UInt64 { ) } +pub fun createDefaultTimeBasedOpenEditionDrop(): UInt64 { + let currentTime = getCurrentTime() + + return createTimebasedOpenEditionDrop( + acct: openEditionAccount, + name: "Default Time-based Open Edition", + description: "This is a placeholder description", + ipfsCid: "1234", + ipfsPath: nil, + price: 1.0, + paymentIdentifier: exampleTokenIdentifier(), + startUnix: UInt64(getCurrentTime()), + endUnix: UInt64(currentTime + 5.0), + 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 } diff --git a/tests/test_helpers.cdc b/tests/test_helpers.cdc index 82a0f17..ed0c02d 100644 --- a/tests/test_helpers.cdc +++ b/tests/test_helpers.cdc @@ -245,6 +245,26 @@ pub fun createEndlessOpenEditionDrop( return e.id } +pub fun createTimebasedOpenEditionDrop( + acct: Test.Account, + name: String, + description: String, + ipfsCid: String, + ipfsPath: String?, + price: UFix64, + paymentIdentifier: String, + startUnix: UInt64?, + endUnix: UInt64?, + minterPrivatePath: PrivatePath +): UInt64 { + txExecutor("drops/add_time_based_open_edition.cdc", [acct], [ + name, description, ipfsCid, ipfsPath, price, paymentIdentifier, startUnix, endUnix, minterPrivatePath + ], nil, nil) + + let e = Test.eventsOfType(Type()).removeLast() as! FlowtyDrops.DropAdded + return e.id +} + pub fun sendFlowTokens(fromAccount: Test.Account, toAccount: Test.Account, amount: UFix64) { txExecutor("util/send_flow_tokens.cdc", [fromAccount], [toAccount.address, amount], nil, nil) } @@ -259,4 +279,18 @@ pub fun mintExampleTokens(_ acct: Test.Account, _ amount: UFix64) { pub fun exampleTokenIdentifier(): String { return Type<@ExampleToken.Vault>().identifier +} + +pub fun hasDropPhaseStarted(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int): Bool { + return scriptExecutor("has_phase_started.cdc", [contractAddress, contractName, dropID, phaseIndex])! as! Bool +} + +pub fun hasDropPhaseEnded(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int): Bool { + return scriptExecutor("has_phase_ended.cdc", [contractAddress, contractName, dropID, phaseIndex])! as! Bool +} + +pub fun canMintAtPhase(contractAddress: Address, contractName: String, dropID: UInt64, phaseIndex: Int, minter: Address, numToMint: Int, totalMinted: Int, paymentIdentifier: String): Bool { + return scriptExecutor("can_mint_at_phase.cdc", [ + contractAddress, contractName, dropID, phaseIndex, minter, numToMint, totalMinted, paymentIdentifier + ])! as! Bool } \ No newline at end of file diff --git a/transactions/drops/add_time_based_open_edition.cdc b/transactions/drops/add_time_based_open_edition.cdc new file mode 100644 index 0000000..06e9c7a --- /dev/null +++ b/transactions/drops/add_time_based_open_edition.cdc @@ -0,0 +1,41 @@ +import "FlowtyDrops" +import "DropFactory" + +import "MetadataViews" + +transaction( + name: String, + description: String, + ipfsCid: String, + ipfsPath: String?, + price: UFix64, + paymentIdentifier: String, + startUnix: UInt64?, + endUnix: UInt64?, + 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.createTimeBasedOpenEditionDrop(price: price, paymentTokenType: paymentType, dropDisplay: dropDisplay, minterCap: minter, startUnix: startUnix, endUnix: endUnix) + container.addDrop(<- drop) + } +} diff --git a/transactions/drops/edit_address_verifier_max_per_mint.cdc b/transactions/drops/edit_address_verifier_max_per_mint.cdc new file mode 100644 index 0000000..826b789 --- /dev/null +++ b/transactions/drops/edit_address_verifier_max_per_mint.cdc @@ -0,0 +1,13 @@ +import "FlowtyDrops" +import "FlowtyAddressVerifiers" + +transaction(dropID: UInt64, phaseIndex: Int, maxPerMint: Int) { + prepare(acct: AuthAccount) { + let container = acct.borrow<&FlowtyDrops.Container>(from: FlowtyDrops.ContainerStoragePath) + ?? panic("container not found") + let drop = container.borrowDrop(id: dropID) ?? panic("drop not found") + let phase = drop.borrowPhase(index: phaseIndex) + let v = phase.borrowAddressVerifierAuth() as! auth &FlowtyAddressVerifiers.AllowAll + v.setMaxPerMint(maxPerMint) + } +} \ No newline at end of file diff --git a/transactions/drops/edit_flat_price.cdc b/transactions/drops/edit_flat_price.cdc new file mode 100644 index 0000000..a62ef2b --- /dev/null +++ b/transactions/drops/edit_flat_price.cdc @@ -0,0 +1,13 @@ +import "FlowtyDrops" +import "FlowtyPricers" + +transaction(dropID: UInt64, phaseIndex: Int, price: UFix64) { + prepare(acct: AuthAccount) { + let container = acct.borrow<&FlowtyDrops.Container>(from: FlowtyDrops.ContainerStoragePath) + ?? panic("container not found") + let drop = container.borrowDrop(id: dropID) ?? panic("drop not found") + let phase = drop.borrowPhase(index: phaseIndex) + let pricer = phase.borrowPricerAuth() as! auth &FlowtyPricers.FlatPrice + pricer.setPrice(price: price) + } +} \ No newline at end of file diff --git a/transactions/drops/edit_timebased_phase_start_and_end.cdc b/transactions/drops/edit_timebased_phase_start_and_end.cdc new file mode 100644 index 0000000..90e9157 --- /dev/null +++ b/transactions/drops/edit_timebased_phase_start_and_end.cdc @@ -0,0 +1,15 @@ +import "FlowtyDrops" +import "FlowtySwitches" + +transaction(dropID: UInt64, phaseIndex: Int, start: UInt64?, end: UInt64?) { + prepare(acct: AuthAccount) { + let container = acct.borrow<&FlowtyDrops.Container>(from: FlowtyDrops.ContainerStoragePath) + ?? panic("container not found") + let drop = container.borrowDrop(id: dropID) ?? panic("drop not found") + let phase = drop.borrowPhase(index: phaseIndex) + let s = phase.borrowSwitchAuth() as! auth &FlowtySwitches.TimestampSwitch + + s.setStart(start: start) + s.setEnd(end: end) + } +} \ No newline at end of file diff --git a/transactions/drops/mint.cdc b/transactions/drops/mint.cdc index 132af24..6e8bb4b 100644 --- a/transactions/drops/mint.cdc +++ b/transactions/drops/mint.cdc @@ -51,7 +51,8 @@ transaction( phaseIndex: dropPhaseIndex, expectedType: expectedNftType, receiverCap: receiverCap, - commissionReceiver: commissionReceiver + commissionReceiver: commissionReceiver, + data: {} ) if remainder.balance > 0.0 {