DeploySEALED
~!░?~○●&▒╱◆#╱?%▪◇#!%●%▒╱□^░^○$&■╱^$@○○╳●█■●□░&╳▒▒╳▫■╱!▪~@╳&?▫$~~
Transaction ID
Execution Fee
0.00003448 FLOWTransaction Summary
DeployContract deployment
Contract deployment
Script Arguments
0nameString
SemesterZeroV2
1codeString
import FungibleToken from 0xf233dcee88fe0abe
import NonFungibleToken from 0x1d7e57aa55817448
import MetadataViews from 0x1d7e57aa55817448
import ViewResolver from 0x1d7e57aa55817448
/// SemesterZeroV2 - Clean implementation for Flunks: Semester Zero NFT Collection
/// Features:
/// - Pin evolution: Base → Silver → Gold → Special
/// - Patch evolution: Base → Retro → Punk → Nerdy
/// - GUM cost tracking for each evolution tier
/// - Location-based NFTs (Paradise Motel, Crystal Springs, etc.)
/// - Custom metadata at mint with evolution/reveal capabilities
/// - Traits revealed and locked during first evolution
///
/// V2: Fresh start without legacy Chapter5/GumDrop baggage
access(all) contract SemesterZeroV2: NonFungibleToken, ViewResolver {
// ========================================
// PATHS
// ========================================
access(all) let CollectionStoragePath: StoragePath
access(all) let CollectionPublicPath: PublicPath
access(all) let AdminStoragePath: StoragePath
// ========================================
// EVENTS
// ========================================
access(all) event ContractInitialized()
access(all) event NFTMinted(nftID: UInt64, recipient: Address, nftType: String, location: String, timestamp: UFix64)
access(all) event NFTEvolved(nftID: UInt64, owner: Address, oldTier: String, newTier: String, timestamp: UFix64)
access(all) event NFTBurned(nftID: UInt64, owner: Address, timestamp: UFix64)
access(all) event Withdraw(id: UInt64, from: Address?)
access(all) event Deposit(id: UInt64, to: Address?)
// ========================================
// STATE VARIABLES
// ========================================
access(all) var totalSupply: UInt64
// ========================================
// NFT RESOURCE
// ========================================
access(all) resource NFT: NonFungibleToken.NFT {
access(all) let id: UInt64
access(all) let nftType: String // "Pin" or "Patch"
access(all) let location: String // "Paradise Motel", "Crystal Springs", etc.
access(all) let recipient: Address
access(all) let mintedAt: UFix64
access(all) let serialNumber: UInt64
access(all) var metadata: {String: String} // Mutable for evolution
access(all) var evolutionTier: String // Pins: "Base", "Silver", "Gold", "Special" | Patches: "Base", "Retro", "Punk", "Nerdy"
access(all) var traitsLocked: Bool // Traits get locked after first evolution
init(id: UInt64, recipient: Address, serialNumber: UInt64, nftType: String, location: String, initialMetadata: {String: String}) {
self.id = id
self.nftType = nftType
self.location = location
self.recipient = recipient
self.mintedAt = getCurrentBlock().timestamp
self.serialNumber = serialNumber
self.metadata = initialMetadata
self.evolutionTier = "Base"
self.traitsLocked = false
}
// Admin can evolve NFT - updates image, tier, and locks traits on first evolution
access(contract) fun evolve(newMetadata: {String: String}, newTier: String) {
self.metadata = newMetadata
self.evolutionTier = newTier
// Lock traits after first evolution (traits revealed = traits locked)
if !self.traitsLocked {
self.traitsLocked = true
}
}
access(all) view fun getViews(): [Type] {
return [
Type<MetadataViews.Display>(),
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>(),
Type<MetadataViews.Royalties>(),
Type<MetadataViews.ExternalURL>(),
Type<MetadataViews.Serial>()
]
}
access(all) fun resolveView(_ view: Type): AnyStruct? {
switch view {
case Type<MetadataViews.Display>():
return MetadataViews.Display(
name: self.metadata["name"]!,
description: self.metadata["description"]!,
thumbnail: MetadataViews.HTTPFile(url: self.metadata["image"]!)
)
case Type<MetadataViews.NFTCollectionData>():
return SemesterZeroV2.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
case Type<MetadataViews.NFTCollectionDisplay>():
return SemesterZeroV2.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
case Type<MetadataViews.Royalties>():
let royaltyCap = SemesterZeroV2.getRoyaltyReceiverCapability()
if !royaltyCap.check() {
return MetadataViews.Royalties([])
}
return MetadataViews.Royalties([
MetadataViews.Royalty(
receiver: royaltyCap,
cut: 0.10,
description: "Flunks: Semester Zero creator royalty"
)
])
case Type<MetadataViews.ExternalURL>():
return MetadataViews.ExternalURL("https://flunks.net")
case Type<MetadataViews.Serial>():
return MetadataViews.Serial(self.serialNumber)
}
return nil
}
access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
return <-SemesterZeroV2.createEmptyCollection(nftType: Type<@SemesterZeroV2.NFT>())
}
}
// ========================================
// NFT COLLECTION
// ========================================
access(all) resource Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection {
access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
init() {
self.ownedNFTs <- {}
}
access(all) view fun getLength(): Int {
return self.ownedNFTs.length
}
access(all) view fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
return &self.ownedNFTs[id]
}
access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
let token <- self.ownedNFTs.remove(key: withdrawID)
?? panic("NFT not found in collection")
let nft <- token as! @SemesterZeroV2.NFT
emit Withdraw(id: nft.id, from: self.owner?.address)
return <-nft
}
access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
let nft <- token as! @SemesterZeroV2.NFT
let id = nft.id
let oldToken <- self.ownedNFTs[id] <- nft
destroy oldToken
emit Deposit(id: id, to: self.owner?.address)
}
access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
return {Type<@SemesterZeroV2.NFT>(): true}
}
access(all) view fun isSupportedNFTType(type: Type): Bool {
return type == Type<@SemesterZeroV2.NFT>()
}
access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
return <-SemesterZeroV2.createEmptyCollection(nftType: Type<@SemesterZeroV2.NFT>())
}
// Borrow specific NFT with full access (needed for evolution function)
access(all) view fun borrowSemesterZeroNFT(id: UInt64): &NFT? {
if self.ownedNFTs[id] != nil {
let ref = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}?
return ref as! &NFT?
}
return nil
}
// MetadataViews.ResolverCollection - Required for Token List
access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
return nft as &{ViewResolver.Resolver}
}
return nil
}
}
// ========================================
// ADMIN RESOURCE
// ========================================
access(all) resource Admin {
// GUM costs for evolution (stored in Admin resource)
access(all) var evolutionCosts: {String: UFix64}
init() {
// Initialize with default costs
self.evolutionCosts = {
"Pin_Silver": 100.0,
"Pin_Gold": 250.0,
"Pin_Special": 500.0,
"Patch_Retro": 100.0,
"Patch_Punk": 250.0,
"Patch_Nerdy": 500.0
}
}
/// Mint NFT with custom type and metadata
/// nftType: "Pin" or "Patch"
/// location: "Paradise Motel", "Crystal Springs", etc.
/// metadata: Must include "name", "description", "image", and any traits
access(all) fun mintNFT(
recipientAddress: Address,
nftType: String,
location: String,
metadata: {String: String}
) {
pre {
nftType == "Pin" || nftType == "Patch": "NFT type must be Pin or Patch"
metadata["name"] != nil: "Metadata must include name"
metadata["description"] != nil: "Metadata must include description"
metadata["image"] != nil: "Metadata must include image"
}
// Get recipient's collection capability
let recipientCap = getAccount(recipientAddress)
.capabilities.get<&SemesterZeroV2.Collection>(SemesterZeroV2.CollectionPublicPath)
assert(recipientCap.check(), message: "Recipient does not have SemesterZeroV2 collection set up")
let recipient = recipientCap.borrow()!
// Mint NFT
let nftID = SemesterZeroV2.totalSupply
let serialNumber = SemesterZeroV2.totalSupply + 1
// Ensure metadata has required fields
var fullMetadata = metadata
fullMetadata["nftType"] = nftType
fullMetadata["location"] = location
fullMetadata["serialNumber"] = serialNumber.toString()
fullMetadata["evolutionTier"] = "Base"
fullMetadata["collection"] = "Flunks: Semester Zero"
let nft <- create NFT(
id: nftID,
recipient: recipientAddress,
serialNumber: serialNumber,
nftType: nftType,
location: location,
initialMetadata: fullMetadata
)
SemesterZeroV2.totalSupply = SemesterZeroV2.totalSupply + 1
// Deposit to recipient
recipient.deposit(token: <-nft)
emit NFTMinted(
nftID: nftID,
recipient: recipientAddress,
nftType: nftType,
location: location,
timestamp: getCurrentBlock().timestamp
)
}
/// Evolve an NFT
/// Pins: Base → Silver → Gold → Special
/// Patches: Base → Retro → Punk → Nerdy
/// newMetadata: Must include updated "image"
/// Traits get locked after first evolution
/// NOTE: GUM payment happens off-chain (Supabase) - this function just updates the NFT
access(all) fun evolveNFT(
userAddress: Address,
nftID: UInt64,
newTier: String,
newMetadata: {String: String}
) {
pre {
newTier == "Silver" || newTier == "Gold" || newTier == "Special" || newTier == "Retro" || newTier == "Punk" || newTier == "Nerdy": "Invalid evolution tier"
newMetadata["image"] != nil: "New metadata must include image"
}
// Get user's collection
let collectionRef = getAccount(userAddress)
.capabilities.get<&SemesterZeroV2.Collection>(SemesterZeroV2.CollectionPublicPath)
.borrow()
?? panic("User does not have SemesterZeroV2 collection")
// Borrow the NFT
let nftRef = collectionRef.borrowSemesterZeroNFT(id: nftID)
?? panic("Could not borrow NFT reference")
let oldTier = nftRef.evolutionTier
// Update metadata to include evolution tier
var fullMetadata = newMetadata
fullMetadata["evolutionTier"] = newTier
// Evolve the NFT
nftRef.evolve(newMetadata: fullMetadata, newTier: newTier)
emit NFTEvolved(
nftID: nftID,
owner: userAddress,
oldTier: oldTier,
newTier: newTier,
timestamp: getCurrentBlock().timestamp
)
}
/// Update GUM costs for evolution (admin only)
access(all) fun updatePinCosts(silverCost: UFix64, goldCost: UFix64, specialCost: UFix64) {
self.evolutionCosts["Pin_Silver"] = silverCost
self.evolutionCosts["Pin_Gold"] = goldCost
self.evolutionCosts["Pin_Special"] = specialCost
}
access(all) fun updatePatchCosts(retroCost: UFix64, punkCost: UFix64, nerdyCost: UFix64) {
self.evolutionCosts["Patch_Retro"] = retroCost
self.evolutionCosts["Patch_Punk"] = punkCost
self.evolutionCosts["Patch_Nerdy"] = nerdyCost
}
/// Get current evolution costs
access(all) fun getEvolutionCosts(): {String: UFix64} {
return self.evolutionCosts
}
/// Burn (permanently destroy) an NFT from a collection
access(all) fun burnNFTFromCollection(collection: auth(NonFungibleToken.Withdraw) &Collection, nftID: UInt64) {
assert(collection.ownedNFTs[nftID] != nil, message: "NFT does not exist in collection")
let nft <- collection.withdraw(withdrawID: nftID)
let ownerAddress = collection.owner!.address
emit NFTBurned(nftID: nftID, owner: ownerAddress, timestamp: getCurrentBlock().timestamp)
destroy nft
}
}
// ========================================
// CONTRACT FUNCTIONS
// ========================================
access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
return <- create Collection()
}
access(all) fun getRoyaltyReceiverCapability(): Capability<&{FungibleToken.Receiver}> {
return getAccount(0xbfffec679fff3a94)
.capabilities
.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
}
// Contract-level view resolver for marketplace compatibility
access(all) view fun getContractViews(resourceType: Type?): [Type] {
return [
Type<MetadataViews.NFTCollectionData>(),
Type<MetadataViews.NFTCollectionDisplay>()
]
}
access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
switch viewType {
case Type<MetadataViews.NFTCollectionData>():
return MetadataViews.NFTCollectionData(
storagePath: SemesterZeroV2.CollectionStoragePath,
publicPath: SemesterZeroV2.CollectionPublicPath,
publicCollection: Type<&SemesterZeroV2.Collection>(),
publicLinkedType: Type<&SemesterZeroV2.Collection>(),
createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
return <-SemesterZeroV2.createEmptyCollection(nftType: Type<@SemesterZeroV2.NFT>())
})
)
case Type<MetadataViews.NFTCollectionDisplay>():
let squareMedia = MetadataViews.Media(
file: MetadataViews.HTTPFile(url: "https://storage.googleapis.com/flunks_public/branding/flunks-logo-square.png"),
mediaType: "image/png"
)
let bannerMedia = MetadataViews.Media(
file: MetadataViews.HTTPFile(url: "https://storage.googleapis.com/flunks_public/branding/flunks-banner.png"),
mediaType: "image/png"
)
return MetadataViews.NFTCollectionDisplay(
name: "Flunks: Semester Zero",
description: "Collectible Pins and Patches from your journey through Flunks: Semester Zero. Evolve your NFTs as you progress!",
externalURL: MetadataViews.ExternalURL("https://flunks.net"),
squareImage: squareMedia,
bannerImage: bannerMedia,
socials: {
"twitter": MetadataViews.ExternalURL("https://twitter.com/FlunksNFT"),
"discord": MetadataViews.ExternalURL("https://discord.gg/flunks")
}
)
}
return nil
}
// ========================================
// INIT
// ========================================
init() {
// Set paths
self.CollectionStoragePath = /storage/SemesterZeroV2Collection
self.CollectionPublicPath = /public/SemesterZeroV2Collection
self.AdminStoragePath = /storage/SemesterZeroV2Admin
// Initialize state
self.totalSupply = 0
// Create and store Admin resource
self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
emit ContractInitialized()
}
}
Cadence Script
1transaction(name: String, code: String ) {
2 prepare(signer: auth(AddContract) &Account) {
3 signer.contracts.add(name: name, code: code.utf8 )
4 }
5 }