Skip to content

Commit

Permalink
contract factory first attempt, OpenEdition factory template
Browse files Browse the repository at this point in the history
  • Loading branch information
austinkline committed May 30, 2024
1 parent ced139d commit c9677a3
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 138 deletions.
1 change: 1 addition & 0 deletions args/create_open_edition_contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"type": "String","value": "MyCollection"},{"type": "Dictionary","value": [{"key": {"type": "String","value": "collectionDisplay"},"value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.NFTCollectionDisplay","fields": [{"name": "name","value": {"type": "String","value": "My Collection"}},{"name": "description","value": {"type": "String","value": "This is a test collection made by Flowty's contract factory!"}},{"name": "externalURL","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.ExternalURL","fields": [{"name": "url","value": {"type": "String","value": "https://flowty.io"}}]}}},{"name": "squareImage","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.Media","fields": [{"name": "file","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.IPFSFile","fields": [{"name": "cid","value": {"type": "String","value": "QmWWLhnkPR3ejavNtzeJcdG9fwcBHKwBVEP4pZ9rGbdHEM"}},{"name": "path","value": {"type": "Optional","value": null}}]}}},{"name": "mediaType","value": {"type": "String","value": "image/png"}}]}}},{"name": "bannerImage","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.Media","fields": [{"name": "file","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.IPFSFile","fields": [{"name": "cid","value": {"type": "String","value": "QmYD8e5s59qYFFQXref1YzyqW1WKYUMPxfqVDEis2s23BF"}},{"name": "path","value": {"type": "Optional","value": null}}]}}},{"name": "mediaType","value": {"type": "String","value": "image/png"}}]}}},{"name": "socials","value": {"type": "Dictionary","value": [{"key": {"type": "String","value": "twitter"},"value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.ExternalURL","fields": [{"name": "url","value": {"type": "String","value": "https://x.com/flowty_io"}}]}}}]}}]}}},{"key": {"type": "String","value": "data"},"value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.NFTMetadata.Metadata","fields": [{"name": "name","value": {"type": "String","value": "Fluid"}},{"name": "description","value": {"type": "String","value": "Fluid is an open edition collection generated by the Flowty contract factory!"}},{"name": "thumbnail","value": {"type": "Struct","value": {"id": "A.f8d6e0586b0a20c7.MetadataViews.IPFSFile","fields": [{"name": "cid","value": {"type": "String","value": "QmWWLhnkPR3ejavNtzeJcdG9fwcBHKwBVEP4pZ9rGbdHEM"}},{"name": "path","value": {"type": "Optional","value": null}}]}}},{"name": "traits","value": {"type": "Optional","value": null}},{"name": "editions","value": {"type": "Optional","value": null}},{"name": "externalURL","value": {"type": "Optional","value": null}},{"name": "data","value": {"type": "Dictionary","value": []}}]}}}]}]
23 changes: 19 additions & 4 deletions contracts/FlowtyDrops.cdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "NonFungibleToken"
import "FungibleToken"
import "MetadataViews"
import "AddressUtils"

access(all) contract FlowtyDrops {
access(all) let ContainerStoragePath: StoragePath
Expand Down Expand Up @@ -83,6 +84,8 @@ access(all) 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, data: data)
assert(phase.details.switcher.hasStarted() && !phase.details.switcher.hasEnded(), message: "phase is not active")
assert(mintedNFTs.length == amount, message: "incorrect number of items returned")

// distribute to receiver
let receiver = receiverCap.borrow() ?? panic("could not borrow receiver capability")
Expand Down Expand Up @@ -310,12 +313,24 @@ access(all) contract FlowtyDrops {
}

access(all) resource interface Minter {
access(all) fun mint(payment: @{FungibleToken.Vault}, amount: Int, phase: &Phase, data: {String: AnyStruct}): @[{NonFungibleToken.NFT}] {
post {
phase.details.switcher.hasStarted() && !phase.details.switcher.hasEnded(): "phase is not active"
result.length == amount: "incorrect number of items returned"
access(contract) fun mint(payment: @{FungibleToken.Vault}, amount: Int, phase: &FlowtyDrops.Phase, data: {String: AnyStruct}): @[{NonFungibleToken.NFT}] {
let resourceAddress = AddressUtils.parseAddress(self.getType())!
let receiver = getAccount(resourceAddress).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver).borrow()
?? panic("invalid flow token receiver")
receiver.deposit(from: <-payment)

let nfts: @[{NonFungibleToken.NFT}] <- []

var count = 0
while count < amount {
count = count + 1
nfts.append(<- self.createNextNFT())
}

return <- nfts
}

access(contract) fun createNextNFT(): @{NonFungibleToken.NFT}
}

access(all) struct DropResolver {
Expand Down
10 changes: 10 additions & 0 deletions contracts/FlowtyMinters.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "FungibleToken"
import "NonFungibleToken"
import "FlowtyDrops"
import "NFTMetadata"
import "AddressUtils"

access(all) contract interface FlowtyMinters {
access(all) resource interface Minter: FlowtyDrops.Minter {
}
}
93 changes: 93 additions & 0 deletions contracts/nft/BaseCollection.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import "NonFungibleToken"
import "ViewResolver"
import "MetadataViews"
import "NFTMetadata"
import "FlowtyDrops"
import "AddressUtils"
import "StringUtils"
import "BaseNFTVars"

access(all) contract interface BaseCollection: ViewResolver {
// The base collection is an interface that attmepts to take more boilerplate
// off of NFT-standard compliant definitions.
access(all) resource interface Collection: NonFungibleToken.Collection {
access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
access(all) var nftType: Type

access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
pre {
token.getType() == self.nftType: "unexpected nft type being deposited"
}

destroy self.ownedNFTs.insert(key: token.uuid, <-token)
}

access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
return &self.ownedNFTs[id]
}

access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
return {
self.nftType: true
}
}

access(all) view fun isSupportedNFTType(type: Type): Bool {
return type == self.nftType
}

access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
return <- self.ownedNFTs.remove(key: withdrawID)!
}
}

access(all) view fun getContractViews(resourceType: Type?): [Type] {
return [
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>()
]
}

access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
if resourceType == nil {
return nil
}

let rt = resourceType!
let segments = rt.identifier.split(separator: ".")
let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_")

let addr = AddressUtils.parseAddress(rt)!
let acct = getAccount(addr)

switch viewType {
case Type<MetadataViews.NFTCollectionData>():
let segments = rt.identifier.split(separator: ".")
let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_")

return MetadataViews.NFTCollectionData(
storagePath: StoragePath(identifier: pathIdentifier)!,
publicPath: PublicPath(identifier: pathIdentifier)!,
publicCollection: Type<&{NonFungibleToken.Collection}>(),
publicLinkedType: Type<&{NonFungibleToken.Collection}>(),
createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} {
let addr = AddressUtils.parseAddress(rt)!
let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])!
return <- c.createEmptyCollection(nftType: rt)
}
)
case Type<MetadataViews.NFTCollectionDisplay>():
let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])!
let md = c.MetadataCap.borrow()
if md == nil {
return nil
}

return md!.collectionInfo.collectionDisplay
case Type<FlowtyDrops.DropResolver>():
return FlowtyDrops.DropResolver(cap: acct.capabilities.get<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath))
}

return nil
}
}
86 changes: 3 additions & 83 deletions contracts/nft/BaseNFT.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "MetadataViews"
import "BaseNFTVars"
import "FlowtyDrops"
import "NFTMetadata"
import "UniversalCollection"

// A few primary challenges that have come up in thinking about how to define base-level interfaces
// for collections and NFTs:
Expand All @@ -22,8 +23,6 @@ import "NFTMetadata"
// off the ground, but doesn't close the door to customization in the future. This could come at the cost of duplicated resource definitions,
// or could have the risk of circular imports depending on how we resolve certain pieces of information about a collection.
access(all) contract interface BaseNFT: ViewResolver {


access(all) resource interface NFT: NonFungibleToken.NFT {
// This is the id entry that corresponds to an NFTs NFTMetadata.Container entry.
// Some NFTs might share the same data, so we want to permit reusing storage where possible
Expand Down Expand Up @@ -100,90 +99,11 @@ access(all) contract interface BaseNFT: ViewResolver {

return nil
}
}

// The base collection is an interface that attmepts to take more boilerplate
// off of NFT-standard compliant definitions.
access(all) resource interface Collection: NonFungibleToken.Collection {
access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
access(all) var nftType: Type

access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
pre {
token.getType() == self.nftType: "unexpected nft type being deposited"
}

destroy self.ownedNFTs.insert(key: token.uuid, <-token)
}

access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
return &self.ownedNFTs[id]
}

access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
return {
self.nftType: true
}
}

access(all) view fun isSupportedNFTType(type: Type): Bool {
return type == self.nftType
}

access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
return <- self.ownedNFTs.remove(key: withdrawID)!
access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
return <- UniversalCollection.createCollection(nftType: self.getType())
}
}

access(all) view fun getContractViews(resourceType: Type?): [Type] {
return [
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>()
]
}

access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
if resourceType == nil {
return nil
}

let rt = resourceType!
let segments = rt.identifier.split(separator: ".")
let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_")

let addr = AddressUtils.parseAddress(rt)!
let acct = getAccount(addr)

switch viewType {
case Type<MetadataViews.NFTCollectionData>():
let segments = rt.identifier.split(separator: ".")
let pathIdentifier = StringUtils.join([segments[2], segments[1]], "_")

return MetadataViews.NFTCollectionData(
storagePath: StoragePath(identifier: pathIdentifier)!,
publicPath: PublicPath(identifier: pathIdentifier)!,
publicCollection: Type<&{NonFungibleToken.Collection}>(),
publicLinkedType: Type<&{NonFungibleToken.Collection}>(),
createEmptyCollectionFunction: fun(): @{NonFungibleToken.Collection} {
let addr = AddressUtils.parseAddress(rt)!
let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])!
return <- c.createEmptyCollection(nftType: rt)
}
)
case Type<MetadataViews.NFTCollectionDisplay>():
let c = getAccount(addr).contracts.borrow<&{BaseNFTVars}>(name: segments[2])!
let md = c.MetadataCap.borrow()
if md == nil {
return nil
}

return md!.collectionInfo.collectionDisplay
case Type<FlowtyDrops.DropResolver>():
return FlowtyDrops.DropResolver(cap: acct.capabilities.get<&{FlowtyDrops.ContainerPublic}>(FlowtyDrops.ContainerPublicPath))
}

return nil
}

access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}
}
1 change: 1 addition & 0 deletions contracts/nft/BaseNFTVars.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import "NFTMetadata"
access(all) contract interface BaseNFTVars {
access(all) var MetadataCap: Capability<&NFTMetadata.Container>
access(all) var totalSupply: UInt64

access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}
}
13 changes: 13 additions & 0 deletions contracts/nft/ContractFactory.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "ContractFactoryTemplate"
import "AddressUtils"

access(all) contract ContractFactory {
access(all) fun createContract(templateType: Type, acct: auth(AddContract) &Account, name: String, params: {String: AnyStruct}) {
let templateAddr = AddressUtils.parseAddress(templateType)!
let contractName = templateType.identifier.split(separator: ".")[2]
let templateContract = getAccount(templateAddr).contracts.borrow<&{ContractFactoryTemplate}>(name: contractName)
?? panic("provided type is not a ContractTemplateFactory")

templateContract.createContract(acct: acct, name: name, params: params)
}
}
47 changes: 47 additions & 0 deletions contracts/nft/ContractFactoryTemplate.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import "NonFungibleToken"
import "MetadataViews"
import "ViewResolver"

import "FlowtyDrops"
import "BaseNFT"
import "BaseNFTVars"
import "NFTMetadata"
import "UniversalCollection"
import "BaseCollection"

import "AddressUtils"

access(all) contract interface ContractFactoryTemplate {
access(all) fun createContract(acct: auth(AddContract) &Account, name: String, params: {String: AnyStruct})

access(all) fun getContractAddresses(): {String: Address} {
let d: {String: Address} = {
"NonFungibleToken": AddressUtils.parseAddress(Type<&{NonFungibleToken}>())!,
"MetadataViews": AddressUtils.parseAddress(Type<&MetadataViews>())!,
"ViewResolver": AddressUtils.parseAddress(Type<&{ViewResolver}>())!,
"FlowtyDrops": AddressUtils.parseAddress(Type<&FlowtyDrops>())!,
"BaseNFT": AddressUtils.parseAddress(Type<&{BaseNFT}>())!,
"BaseNFTVars": AddressUtils.parseAddress(Type<&{BaseNFTVars}>())!,
"NFTMetadata": AddressUtils.parseAddress(Type<&NFTMetadata>())!,
"UniversalCollection": AddressUtils.parseAddress(Type<&UniversalCollection>())!,
"BaseCollection": AddressUtils.parseAddress(Type<&{BaseCollection}>())!,
"AddressUtils": AddressUtils.parseAddress(Type<&AddressUtils>())!
}

return d
}

access(all) fun importLine(name: String, addr: Address): String {
return "import ".concat(name).concat(" from ").concat(addr.toString()).concat("\n")
}

access(all) fun generateImports(names: [String]): String {
let addresses = self.getContractAddresses()
var imports = ""
for n in names {
imports = imports.concat(self.importLine(name: n, addr: addresses[n] ?? panic("missing contract import address: ".concat(n))))
}

return imports
}
}
Loading

0 comments on commit c9677a3

Please sign in to comment.