Smart Contract
FlowtyDrops
A.befbaccb5032a457.FlowtyDrops
1import NonFungibleToken from 0x1d7e57aa55817448
2import FungibleToken from 0xf233dcee88fe0abe
3import MetadataViews from 0x1d7e57aa55817448
4import AddressUtils from 0xa340dc0a4ec828ab
5import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
6import FungibleTokenRouter from 0x707c0b39a8d689cb
7
8// FlowtyDrops is a contract to help collections manage their primary sale needs on flow.
9// Multiple drops can be made for a single contract (like how TopShot has had lots of pack drops),
10// and can be split into phases to represent different behaviors over the course of a drop
11access(all) contract FlowtyDrops {
12 // The total number of nfts minted by this contract
13 access(all) var TotalMinted: UInt64
14
15 access(all) let ContainerStoragePath: StoragePath
16 access(all) let ContainerPublicPath: PublicPath
17
18 access(all) event DropAdded(address: Address, id: UInt64, name: String, description: String, imageUrl: String, start: UInt64?, end: UInt64?, nftType: String)
19 access(all) event DropRemoved(address: Address, id: UInt64)
20 access(all) event Minted(address: Address, dropID: UInt64, phaseID: UInt64, nftID: UInt64, nftType: String, totalMinted: UInt64)
21 access(all) event PhaseAdded(dropID: UInt64, dropAddress: Address, id: UInt64, index: Int, activeCheckerType: String, pricerType: String, addressVerifierType: String)
22 access(all) event PhaseRemoved(dropID: UInt64, dropAddress: Address, id: UInt64)
23
24 access(all) entitlement Owner
25 access(all) entitlement EditPhase
26
27 // Interface to expose all the components necessary to participate in a drop
28 // and to ask questions about a drop.
29 access(all) resource interface DropPublic {
30 access(all) view fun borrowPhasePublic(index: Int): &{PhasePublic}
31 access(all) view fun borrowActivePhases(): [&{PhasePublic}]
32 access(all) view fun borrowAllPhases(): [&{PhasePublic}]
33 access(all) fun mint(
34 payment: @{FungibleToken.Vault},
35 amount: Int,
36 phaseIndex: Int,
37 expectedType: Type,
38 receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>,
39 commissionReceiver: Capability<&{FungibleToken.Receiver}>?,
40 data: {String: AnyStruct}
41 ): @{FungibleToken.Vault} {
42 pre {
43 self.getDetails().paymentTokenTypes[payment.getType().identifier] == true: "unsupported payment token type"
44 receiverCap.check(): "unvalid nft receiver capability"
45 commissionReceiver == nil || commissionReceiver!.check(): "commission receiver must be nil or a valid capability"
46 self.getType() == Type<@Drop>(): "unsupported type implementing DropPublic"
47 expectedType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): "expected type must be an NFT"
48 expectedType.identifier == self.getDetails().nftType: "expected type does not match drop details type"
49 receiverCap.check(): "receiver capability is not valid"
50 }
51 }
52 access(all) view fun getDetails(): DropDetails
53 }
54
55 // A phase represents a stage of a drop. Some drops will only have one
56 // phase, while others could have many. For example, a drop with an allow list
57 // and a public mint would likely have two phases.
58 access(all) resource Phase: PhasePublic {
59 access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid)
60
61 access(all) let details: PhaseDetails
62
63 access(all) let data: {String: AnyStruct}
64 access(all) let resources: @{String: AnyResource}
65
66 // returns whether this phase of a drop has started.
67 access(all) view fun isActive(): Bool {
68 return self.details.activeChecker.hasStarted() && !self.details.activeChecker.hasEnded()
69 }
70
71 access(all) view fun getDetails(): PhaseDetails {
72 return self.details
73 }
74
75 access(EditPhase) view fun borrowActiveCheckerAuth(): auth(Mutate) &{ActiveChecker} {
76 return &self.details.activeChecker
77 }
78
79 access(EditPhase) view fun borrowPricerAuth(): auth(Mutate) &{Pricer} {
80 return &self.details.pricer
81 }
82
83 access(EditPhase) view fun borrowAddressVerifierAuth(): auth(Mutate) &{AddressVerifier} {
84 return &self.details.addressVerifier
85 }
86
87 init(details: PhaseDetails) {
88 self.details = details
89
90 self.data = {}
91 self.resources <- {}
92 }
93 }
94
95 // The primary resource of this contract. A drop has some top-level details, and some phase-specific details which are encapsulated
96 // by each phase.
97 access(all) resource Drop: DropPublic {
98 access(all) event ResourceDestroyed(
99 uuid: UInt64 = self.uuid,
100 minterAddress: Address = self.minterCap.address,
101 nftType: String = self.details.nftType,
102 totalMinted: Int = self.details.totalMinted
103 )
104
105 // phases represent the stages of a drop. For example, a drop might have an allowlist and a public mint phase.
106 access(self) let phases: @[Phase]
107 // the details of a drop. This includes things like display information and total number of mints
108 access(self) let details: DropDetails
109 // capability to mint nfts with. Regardless of where a drop is hosted, the minter itself is what is responsible for creating nfts
110 // and is used by the drop's mint method.
111 access(self) let minterCap: Capability<&{Minter}>
112
113 // general-purpose property bags which are needed to ensure extensibility of this resource
114 access(all) let data: {String: AnyStruct}
115 access(all) let resources: @{String: AnyResource}
116
117 access(all) fun mint(
118 payment: @{FungibleToken.Vault},
119 amount: Int,
120 phaseIndex: Int,
121 expectedType: Type,
122 receiverCap: Capability<&{NonFungibleToken.CollectionPublic}>,
123 commissionReceiver: Capability<&{FungibleToken.Receiver}>?,
124 data: {String: AnyStruct}
125 ): @{FungibleToken.Vault} {
126 pre {
127 self.phases.length > phaseIndex: "phase index is too high"
128 }
129
130 // validate the payment vault amount and type
131 let phase: &Phase = &self.phases[phaseIndex]
132 assert(
133 phase.details.addressVerifier.canMint(addr: receiverCap.address, num: amount, totalMinted: self.details.minters[receiverCap.address] ?? 0, data: {}),
134 message: "receiver address has exceeded their mint capacity"
135 )
136
137 let paymentAmount = phase.details.pricer.getPrice(num: amount, paymentTokenType: payment.getType(), minter: receiverCap.address)
138 assert(payment.balance >= paymentAmount, message: "payment balance is lower than payment amount")
139 let withdrawn <- payment.withdraw(amount: paymentAmount) // make sure that we have a fresh vault resource
140
141 // take commission
142 if commissionReceiver != nil && commissionReceiver!.check() {
143 let commission <- withdrawn.withdraw(amount: self.details.commissionRate * withdrawn.balance)
144 commissionReceiver!.borrow()!.deposit(from: <-commission)
145 }
146
147 // The balance of the payment sent to the creator is equal to the paymentAmount - fees
148 assert(paymentAmount * (1.0 - self.details.commissionRate) == withdrawn.balance, message: "incorrect payment amount")
149 assert(phase.details.pricer.getPaymentTypes().contains(withdrawn.getType()), message: "unsupported payment type")
150 assert(phase.details.activeChecker.hasStarted() && !phase.details.activeChecker.hasEnded(), message: "phase is not active")
151
152 // mint the nfts
153 let minter = self.minterCap.borrow() ?? panic("minter capability could not be borrowed")
154 let mintedNFTs: @[{NonFungibleToken.NFT}] <- minter.mint(payment: <-withdrawn, amount: amount, phase: phase, data: data)
155 assert(mintedNFTs.length == amount, message: "incorrect number of items returned")
156
157 // distribute to receiver
158 let receiver = receiverCap.borrow() ?? panic("could not borrow receiver capability")
159 self.details.addMinted(num: mintedNFTs.length, addr: receiverCap.address)
160
161 while mintedNFTs.length > 0 {
162 let nft <- mintedNFTs.removeFirst()
163
164 let nftType = nft.getType()
165 FlowtyDrops.TotalMinted = FlowtyDrops.TotalMinted + 1
166 emit Minted(address: receiverCap.address, dropID: self.uuid, phaseID: phase.uuid, nftID: nft.id, nftType: nftType.identifier, totalMinted: FlowtyDrops.TotalMinted)
167
168 // validate that every nft is the right type
169 assert(nftType == expectedType, message: "unexpected nft type was minted")
170
171 receiver.deposit(token: <-nft)
172 }
173
174 // cleanup
175 destroy mintedNFTs
176
177 // return excess payment
178 return <- payment
179 }
180
181 access(Owner) view fun borrowPhase(index: Int): auth(EditPhase) &Phase {
182 return &self.phases[index]
183 }
184
185
186 access(all) view fun borrowPhasePublic(index: Int): &{PhasePublic} {
187 return &self.phases[index]
188 }
189
190 access(all) view fun borrowActivePhases(): [&{PhasePublic}] {
191 var arr: [&{PhasePublic}] = []
192 var count = 0
193 while count < self.phases.length {
194 let ref = self.borrowPhasePublic(index: count)
195 let activeChecker = ref.getDetails().activeChecker
196 if activeChecker.hasStarted() && !activeChecker.hasEnded() {
197 arr = arr.concat([ref])
198 }
199
200 count = count + 1
201 }
202
203 return arr
204 }
205
206 access(all) view fun borrowAllPhases(): [&{PhasePublic}] {
207 var arr: [&{PhasePublic}] = []
208 var index = 0
209 while index < self.phases.length {
210 let ref = self.borrowPhasePublic(index: index)
211 arr = arr.concat([ref])
212 index = index + 1
213 }
214
215 return arr
216 }
217
218 access(Owner) fun addPhase(_ phase: @Phase) {
219 emit PhaseAdded(
220 dropID: self.uuid,
221 dropAddress: self.owner!.address,
222 id: phase.uuid,
223 index: self.phases.length,
224 activeCheckerType: phase.details.activeChecker.getType().identifier,
225 pricerType: phase.details.pricer.getType().identifier,
226 addressVerifierType: phase.details.addressVerifier.getType().identifier
227 )
228 self.phases.append(<-phase)
229 }
230
231 access(Owner) fun removePhase(index: Int): @Phase {
232 pre {
233 self.phases.length > index: "index is greater than length of phases"
234 }
235
236 let phase <- self.phases.remove(at: index)
237 emit PhaseRemoved(dropID: self.uuid, dropAddress: self.owner!.address, id: phase.uuid)
238
239 return <- phase
240 }
241
242 access(all) view fun getDetails(): DropDetails {
243 return self.details
244 }
245
246 init(details: DropDetails, minterCap: Capability<&{Minter}>, phases: @[Phase]) {
247 pre {
248 minterCap.check(): "minter capability is not valid"
249 }
250
251 self.phases <- phases
252 self.details = details
253 self.minterCap = minterCap
254
255 self.data = {}
256 self.resources <- {}
257 }
258 }
259
260 access(all) struct DropDetails {
261 access(all) let display: MetadataViews.Display
262 access(all) let medias: MetadataViews.Medias?
263 access(all) var totalMinted: Int
264 access(all) var minters: {Address: Int}
265 access(all) let commissionRate: UFix64
266 access(all) let nftType: String
267 access(all) let paymentTokenTypes: {String: Bool}
268
269 access(all) let data: {String: AnyStruct}
270
271 access(contract) fun addMinted(num: Int, addr: Address) {
272 self.totalMinted = self.totalMinted + num
273 if self.minters[addr] == nil {
274 self.minters[addr] = 0
275 }
276
277 self.minters[addr] = self.minters[addr]! + num
278 }
279
280 init(display: MetadataViews.Display, medias: MetadataViews.Medias?, commissionRate: UFix64, nftType: String, paymentTokenTypes: {String: Bool}) {
281 pre {
282 nftType != "": "nftType should be a composite type identifier"
283 }
284
285 self.display = display
286 self.medias = medias
287 self.totalMinted = 0
288 self.commissionRate = commissionRate
289 self.minters = {}
290 self.nftType = nftType
291 self.paymentTokenTypes = paymentTokenTypes
292
293 self.data = {}
294 }
295 }
296
297 // An ActiveChecker represents a phase being on or off, and holds information
298 // about whether a phase has started or not.
299 access(all) struct interface ActiveChecker {
300 // Signal that a phase has started. If the phase has not ended, it means that this activeChecker's phase
301 // is active
302 access(all) view fun hasStarted(): Bool
303 // Signal that a phase has ended. If an ActiveChecker has ended, minting will not work. That could mean
304 // the drop is over, or it could mean another phase has begun.
305 access(all) view fun hasEnded(): Bool
306
307 access(all) view fun getStart(): UInt64?
308 access(all) view fun getEnd(): UInt64?
309 }
310
311 access(all) resource interface PhasePublic {
312 // What does a phase need to be able to answer/manage?
313 // - What are the details of the phase being interactive with?
314 // - How many items are left in the current phase?
315 // - Can Address x mint on a phase?
316 // - What is the cost to mint for the phase I am interested in (for address x)?
317 access(all) view fun getDetails(): PhaseDetails
318 access(all) view fun isActive(): Bool
319 }
320
321 access(all) struct PhaseDetails {
322 // handles whether a phase is on or not
323 access(all) let activeChecker: {ActiveChecker}
324
325 // display information about a phase
326 access(all) let display: MetadataViews.Display?
327
328 // handles the pricing of a phase
329 access(all) let pricer: {Pricer}
330
331 // verifies whether an address is able to mint
332 access(all) let addressVerifier: {AddressVerifier}
333
334 // placecholder data dictionary to allow new fields to be accessed
335 access(all) let data: {String: AnyStruct}
336
337 init(activeChecker: {ActiveChecker}, display: MetadataViews.Display?, pricer: {Pricer}, addressVerifier: {AddressVerifier}) {
338 self.activeChecker = activeChecker
339 self.display = display
340 self.pricer = pricer
341 self.addressVerifier = addressVerifier
342
343 self.data = {}
344 }
345 }
346
347 // The AddressVerifier interface is responsible for determining whether an address is permitted to mint or not
348 access(all) struct interface AddressVerifier {
349 access(all) fun canMint(addr: Address, num: Int, totalMinted: Int, data: {String: AnyStruct}): Bool {
350 return true
351 }
352
353 access(all) fun remainingForAddress(addr: Address, totalMinted: Int): Int? {
354 return nil
355 }
356
357 access(all) view fun getMaxPerMint(addr: Address?, totalMinted: Int, data: {String: AnyStruct}): Int? {
358 return nil
359 }
360 }
361
362 // The pricer interface is responsible for the cost of a mint. It can vary by phase
363 access(all) struct interface Pricer {
364 access(all) fun getPrice(num: Int, paymentTokenType: Type, minter: Address?): UFix64
365 access(all) fun getPaymentTypes(): [Type]
366 }
367
368 access(all) resource interface Minter {
369 // mint is only able to be called either by this contract (FlowtyDrops) or the implementing contract.
370 // In its default implementation, it is assumed that the receiver capability for payment is the FungibleTokenRouter
371 access(contract) fun mint(payment: @{FungibleToken.Vault}, amount: Int, phase: &FlowtyDrops.Phase, data: {String: AnyStruct}): @[{NonFungibleToken.NFT}] {
372 let resourceAddress = AddressUtils.parseAddress(self.getType())!
373 let receiver = getAccount(resourceAddress).capabilities.get<&{FungibleToken.Receiver}>(FungibleTokenRouter.PublicPath).borrow()
374 ?? panic("missing receiver at fungible token router path")
375 receiver.deposit(from: <-payment)
376
377 let nfts: @[{NonFungibleToken.NFT}] <- []
378
379 var count = 0
380 while count < amount {
381 count = count + 1
382 nfts.append(<- self.createNextNFT())
383 }
384
385 return <- nfts
386 }
387
388 // required so that the minter interface has a way to create NFTs on its implementing resource
389 access(contract) fun createNextNFT(): @{NonFungibleToken.NFT}
390 }
391
392 // Struct to wrap obtaining a Drop container. Intended for use with the ViewResolver contract interface
393 access(all) struct DropResolver {
394 access(self) let cap: Capability<&{ContainerPublic}>
395
396 access(all) fun borrowContainer(): &{ContainerPublic}? {
397 return self.cap.borrow()
398 }
399
400 init(cap: Capability<&{ContainerPublic}>) {
401 pre {
402 cap.check(): "container capability is not valid"
403 }
404
405 self.cap = cap
406 }
407 }
408
409 access(all) resource interface ContainerPublic {
410 access(all) fun borrowDropPublic(id: UInt64): &{DropPublic}?
411 access(all) fun getIDs(): [UInt64]
412 }
413
414 // Container holds drops so that one address can host more than one drop at once
415 access(all) resource Container: ContainerPublic {
416 access(self) let drops: @{UInt64: Drop}
417
418 access(all) let data: {String: AnyStruct}
419 access(all) let resources: @{String: AnyResource}
420
421 access(Owner) fun addDrop(_ drop: @Drop) {
422 let details = drop.getDetails()
423
424 let phases = drop.borrowAllPhases()
425 assert(phases.length > 0, message: "drops must have at least one phase to be added to a container")
426
427 let firstPhaseDetails = phases[0].getDetails()
428
429 emit DropAdded(
430 address: self.owner!.address,
431 id: drop.uuid,
432 name: details.display.name,
433 description: details.display.description,
434 imageUrl: details.display.thumbnail.uri(),
435 start: firstPhaseDetails.activeChecker.getStart(),
436 end: firstPhaseDetails.activeChecker.getEnd(),
437 nftType: details.nftType
438 )
439 destroy self.drops.insert(key: drop.uuid, <-drop)
440 }
441
442 access(Owner) fun removeDrop(id: UInt64): @Drop {
443 pre {
444 self.drops.containsKey(id): "drop was not found"
445 }
446
447 emit DropRemoved(address: self.owner!.address, id: id)
448 return <- self.drops.remove(key: id)!
449 }
450
451 access(Owner) fun borrowDrop(id: UInt64): auth(Owner) &Drop? {
452 return &self.drops[id]
453 }
454
455 access(all) fun borrowDropPublic(id: UInt64): &{DropPublic}? {
456 return &self.drops[id]
457 }
458
459 access(all) fun getIDs(): [UInt64] {
460 return self.drops.keys
461 }
462
463 init() {
464 self.drops <- {}
465
466 self.data = {}
467 self.resources <- {}
468 }
469 }
470
471 access(all) fun createPhase(details: PhaseDetails): @Phase {
472 return <- create Phase(details: details)
473 }
474
475 access(all) fun createDrop(details: DropDetails, minterCap: Capability<&{Minter}>, phases: @[Phase]): @Drop {
476 return <- create Drop(details: details, minterCap: minterCap, phases: <- phases)
477 }
478
479 access(all) fun createContainer(): @Container {
480 return <- create Container()
481 }
482
483 access(all) fun getMinterStoragePath(type: Type): StoragePath {
484 let segments = type.identifier.split(separator: ".")
485 let identifier = "FlowtyDrops_Minter_".concat(segments[1]).concat("_").concat(segments[2])
486 return StoragePath(identifier: identifier)!
487 }
488
489 init() {
490 let identifier = "FlowtyDrops_".concat(self.account.address.toString())
491 let containerIdentifier = identifier.concat("_Container")
492 let minterIdentifier = identifier.concat("_Minter")
493
494 self.ContainerStoragePath = StoragePath(identifier: containerIdentifier)!
495 self.ContainerPublicPath = PublicPath(identifier: containerIdentifier)!
496
497 self.TotalMinted = 0
498 }
499}