Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add methods to borrow an auth version of each phase detail interface #4

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 31 additions & 1 deletion contracts/DropFactory.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 19 additions & 1 deletion contracts/FlowtyAddressVerifiers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 27 additions & 11 deletions contracts/FlowtyDrops.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// 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(
Expand All @@ -24,7 +24,8 @@
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
}
Expand All @@ -42,7 +43,8 @@
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"
Expand All @@ -69,7 +71,7 @@

// 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")
Expand All @@ -94,15 +96,20 @@
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}
}

pub fun borrowActivePhases(): [&{PhasePublic}] {
let arr: [&{PhasePublic}] = []
var count = 0
while count < self.phases.length {
let ref = self.borrowPhase(index: count)
let ref = self.borrowPhasePublic(index: count)

Check warning on line 112 in contracts/FlowtyDrops.cdc

View check run for this annotation

Codecov / codecov/patch

contracts/FlowtyDrops.cdc#L112

Added line #L112 was not covered by tests
let switch = ref.getDetails().switch
if switch.hasStarted() && !switch.hasEnded() {
arr.append(ref)
Expand All @@ -118,7 +125,7 @@
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
}
Expand Down Expand Up @@ -212,6 +219,18 @@
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
}
Expand Down Expand Up @@ -243,9 +262,6 @@
// 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
Expand All @@ -272,7 +288,7 @@
}

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"
Expand Down
6 changes: 5 additions & 1 deletion contracts/FlowtyPricers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions contracts/FlowtySwitches.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion contracts/nft/OpenEditionNFT.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions scripts/can_mint_at_phase.cdc
Original file line number Diff line number Diff line change
@@ -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<FlowtyDrops.DropResolver>())! 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: {})
}
2 changes: 1 addition & 1 deletion scripts/get_price_at_phase.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
19 changes: 19 additions & 0 deletions scripts/has_phase_ended.cdc
Original file line number Diff line number Diff line change
@@ -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<FlowtyDrops.DropResolver>())! 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()
}
19 changes: 19 additions & 0 deletions scripts/has_phase_started.cdc
Original file line number Diff line number Diff line change
@@ -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<FlowtyDrops.DropResolver>())! 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()
}
3 changes: 3 additions & 0 deletions scripts/util/get_current_time.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fun main(): UFix64 {
return getCurrentBlock().timestamp
}
2 changes: 1 addition & 1 deletion tests/FlowtyAddressVerifiers_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Loading
Loading