UNPKG

@silvana-one/nft

Version:
1,027 lines 54.5 kB
/** * The NFT Collection Contract is responsible for managing a collection of NFTs. * It handles minting new NFTs, transferring ownership, buying, selling, * and interfacing with Admin Contracts for additional functionalities. * * @module CollectionContract */ import { __decorate, __metadata } from "tslib"; import { Field, PublicKey, AccountUpdate, Bool, method, state, State, Permissions, TokenContract, VerificationKey, UInt32, UInt64, Mina, Provable, } from "o1js"; import { NFT } from "./nft.js"; import { MintParams, MintRequest, TransferBySignatureParams, TransferByProofParams, CollectionData, NFTUpdateProof, NFTStateStruct, MintEvent, NFTUpdateEvent, TransferEvent, ApproveEvent, UpgradeVerificationKeyEvent, LimitMintingEvent, PauseNFTEvent, PauseEvent, SetNameEvent, SetBaseURLEvent, SetRoyaltyFeeEvent, SetTransferFeeEvent, SetAdminEvent, OwnershipChangeEvent, UInt64Option, MAX_ROYALTY_FEE, TransferExtendedParams, } from "../interfaces/index.js"; import { mulDiv } from "../util/index.js"; import { nftVerificationKeys } from "../vk.js"; export { CollectionFactory, CollectionErrors }; const CollectionErrors = { wrongMasterNFTaddress: "Master NFT address should be the same as the collection address", transferNotAllowed: "Transfers of tokens are not allowed, change the owner instead", collectionPaused: "Collection is currently paused", cannotMintMasterNFT: "Only the creator can mint the Master NFT", cannotMint: "Admin contract did not provide permission to mint", noPermissionToPause: "Not allowed to pause collection", noPermissionToResume: "Not allowed to resume collection", collectionNotPaused: "Collection is not paused", transferApprovalRequired: "Transfer approval is required", noPermissionToChangeName: "Not allowed to change collection name", noPermissionToChangeBaseUri: "Not allowed to change collection base URI", noPermissionToChangeCreator: "Not allowed to change collection creator", noPermissionToChangeRoyalty: "Not allowed to change royalty fee", noPermissionToChangeTransferFee: "Not allowed to change transfer fee", noPermissionToSetAdmin: "Not allowed to set admin contract", cannotUpgradeVerificationKey: "Cannot upgrade verification key", upgradeContractAddressNotSet: "Upgrade contract address is not set", adminContractAddressNotSet: "Admin contract address is not set", onlyOwnerCanUpgradeVerificationKey: "Only owner can upgrade verification key", invalidRoyaltyFee: "Royalty fee is too high, cannot be more than 100%", invalidOracleAddress: "Oracle address is invalid", pendingCreatorIsEmpty: "Pending creator address is empty", }; /** * Creates a new NFT Collection Contract class. * * @param params - Constructor parameters including admin and upgrade contracts, and network ID. * @returns The Collection class extending TokenContract and implementing required interfaces. */ function CollectionFactory(params) { const { adminContract, ownerContract, approvalContract, updateContract } = params; /** * The NFT Collection Contract manages a collection of NFTs. * It handles minting, transferring, buying, selling, and integrates with Admin Contracts. */ class Collection extends TokenContract { constructor() { super(...arguments); /** The name of the NFT collection. */ this.collectionName = State(); /** The public key of the creator of the collection. */ this.creator = State(); /** The public key of the Admin Contract. */ this.admin = State(); /** The base URL for the metadata of the NFTs in the collection. */ this.baseURL = State(); /** * A packed data field containing additional collection parameters, * such as flags and fee configurations. */ this.packedData = State(); /** The public key part (x) of the pending creator. The isOdd field is written to the packedData */ this.pendingCreatorX = State(); /** * Defines the events emitted by the contract. */ this.events = { mint: MintEvent, update: NFTUpdateEvent, transfer: TransferEvent, approve: ApproveEvent, upgradeNFTVerificationKey: UpgradeVerificationKeyEvent, upgradeVerificationKey: UpgradeVerificationKeyEvent, limitMinting: LimitMintingEvent, pause: PauseEvent, resume: PauseEvent, pauseNFT: PauseNFTEvent, resumeNFT: PauseNFTEvent, ownershipTransfer: OwnershipChangeEvent, ownershipAccepted: OwnershipChangeEvent, setName: SetNameEvent, setBaseURL: SetBaseURLEvent, setRoyaltyFee: SetRoyaltyFeeEvent, setTransferFee: SetTransferFeeEvent, setAdmin: SetAdminEvent, }; } /** * Deploys the NFT Collection Contract with the initial settings. * * @param props - Deployment properties including collection name, creator, admin, baseURL, symbol, and URL. */ async deploy(props) { await super.deploy(props); this.collectionName.set(props.collectionName); this.creator.set(props.creator); this.admin.set(props.admin); this.baseURL.set(props.baseURL); // Set the collection to be paused by default this.packedData.set(CollectionData.new({ isPaused: true, }).pack()); this.pendingCreatorX.set(PublicKey.empty().x); // Changes must be made if the number of state fields available on the Mina blockchain changes // This function should initialize ALL state fields due to the logic in the initialize() method this.account.zkappUri.set(props.url); this.account.tokenSymbol.set(props.symbol); this.account.permissions.set({ ...Permissions.default(), setVerificationKey: Permissions.VerificationKey.proofDuringCurrentVersion(), setPermissions: Permissions.impossible(), access: Permissions.proof(), send: Permissions.proof(), setZkappUri: Permissions.proof(), setTokenSymbol: Permissions.proof(), }); } /** * Initializes the collection with a master NFT and initial data. * * @param masterNFT - The master NFT parameters. * @param collectionData - Initial collection data including flags and configurations. */ async initialize(masterNFT, collectionData) { // Changes must be made if the number of state fields available on the Mina blockchain changes // as the next line relies on the fact that the state size is 8 Fields // and all 8 Field are initialized in deploy() this.account.provedState.requireEquals(Bool(false)); collectionData.royaltyFee.assertLessThanOrEqual(UInt32.from(MAX_ROYALTY_FEE), CollectionErrors.invalidRoyaltyFee); this.packedData.set(collectionData.pack()); masterNFT.address .equals(this.address) .assertTrue(CollectionErrors.wrongMasterNFTaddress); await this._mint(masterNFT); } /** * Overrides the approveBase method to prevent transfers of tokens. * * @param forest - The account update forest. */ async approveBase(forest) { throw Error(CollectionErrors.transferNotAllowed); } /** * Retrieves the Admin Contract instance. * * @returns The Admin Contract instance implementing NFTAdminBase. */ getAdminContract() { const admin = this.admin.getAndRequireEquals(); const AdminContract = adminContract(); return new AdminContract(admin); } /** * Retrieves the NFT Owner Contract instance. * * @returns The Owner Contract instance implementing NFTOwnerBase. */ getOwnerContract(address) { const OwnerContract = ownerContract(); return new OwnerContract(address); } /** * Retrieves the NFT Approval Contract instance. * * @returns The Approval Contract instance implementing NFTApprovalBase. */ getApprovalContract(address) { const ApprovalContract = approvalContract(); return new ApprovalContract(address); } /** * Retrieves the NFT Update Contract instance. * * @returns The Update Contract instance implementing NFTUpdateBase. */ getUpdateContract(address) { const UpdateContract = updateContract(); return new UpdateContract(address); } /** * Ensures that the transaction is authorized by the creator. * * @returns The AccountUpdate of the creator. */ async ensureCreatorSignature() { const creator = this.creator.getAndRequireEquals(); const creatorUpdate = AccountUpdate.createSigned(creator); creatorUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change return creatorUpdate; } /** * Ensures that the transaction is authorized by the NFT owner * * @returns The AccountUpdate of the NFT owner. */ async ensureOwnerSignature(owner) { const ownerUpdate = AccountUpdate.createSigned(owner); ownerUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change return ownerUpdate; } /** * Ensures that the collection is not paused. * * @returns The packed data of the collection. */ async ensureNotPaused() { const collectionData = CollectionData.unpack(this.packedData.getAndRequireEquals()); collectionData.isPaused.assertFalse(CollectionErrors.collectionPaused); return collectionData; } /** * Mints a new NFT directly by the creator. * * This method allows the creator of the collection to mint an NFT without requiring approval * from the admin contract. It ensures that the collection is not paused and that the caller * is the creator of the collection. A fee of 1 MINA is deducted from the creator's balance * to cover the cost of creating a new account. * * We do not constrain here the address of the NFT to allow for the Master NFT to be minted. * The Master NFT is the NFT with the same address as the Collection contract and it holds * the metadata for the collection. It can be minted only by the creator of the collection. * * @param params - The mint parameters containing details of the NFT to be minted. */ async mintByCreator(params) { const collectionData = await this.ensureNotPaused(); collectionData.mintingIsLimited.assertFalse(CollectionErrors.cannotMint); const creatorUpdate = await this.ensureCreatorSignature(); // Pay 1 MINA fee for a new account creatorUpdate.balance.subInPlace(1_000_000_000); await this._mint(params); } /** * Mints a new NFT with approval. * * @param mintRequest - The minting request containing parameters and proofs. */ async mint(mintRequest) { const collectionData = await this.ensureNotPaused(); collectionData.mintingIsLimited.assertFalse(CollectionErrors.cannotMint); const adminContract = this.getAdminContract(); // The admin contract checks that the sender is allowed to mint const mintParams = (await adminContract.canMint(mintRequest)).assertSome(CollectionErrors.cannotMint); mintParams.address.assertEquals(mintRequest.address); mintParams.data.owner.assertEquals(mintRequest.owner); // Prevent minting the Master NFT using this method mintParams.address .equals(this.address) .assertFalse(CollectionErrors.cannotMintMasterNFT); await this._mint(mintParams); } /** * Internal method to mint an NFT. * * @param params - The mint parameters. * @param collectionData - The current collection data. * @returns The MintEvent emitted. */ async _mint(params) { const { name, address, data, metadata, storage, metadataVerificationKeyHash, expiry, fee, tokenId, } = params; this.network.globalSlotSinceGenesis.requireBetween(UInt32.zero, expiry); data.version.assertEquals(UInt64.zero); data.isPaused .equals(Bool(false)) .or(data.canPause.equals(Bool(true))) .assertTrue(CollectionErrors.cannotMint); const packedData = data.pack(); const collectionTokenId = this.deriveTokenId(); collectionTokenId.assertEquals(tokenId); const update = AccountUpdate.createSigned(address, tokenId); update.body.useFullCommitment = Bool(true); // Prevent memo and fee change update.account.isNew.getAndRequireEquals().assertTrue(); // Mint 1 NFT this.internal.mint({ address: update, amount: 1_000_000_000 }); const verificationKey = Provable.witness(VerificationKey, () => { // This code does NOT create a constraint on the verification key // as this witness can be replaced during runtime // We use devnet to get future compatibility https://github.com/o1-labs/o1js/pull/1938 // As of writing this, 'testnet' is used in the o1js codebase const networkId = Mina.getNetworkId() === "mainnet" ? "mainnet" : "devnet"; const verificationKey = new VerificationKey({ data: nftVerificationKeys[networkId].vk.NFT.data, hash: Field(nftVerificationKeys[networkId].vk.NFT.hash), }); const vkHash = NFT._verificationKey?.hash; if (!verificationKey || !verificationKey.hash || !verificationKey.data) throw Error("NFT verification key is incorrect"); if (vkHash && vkHash.equals(verificationKey.hash).toBoolean() === false) throw Error("NFT verification key does not match the compiled verification key"); return verificationKey; }); const mainnetVerificationKeyHash = Field(nftVerificationKeys.mainnet.vk.NFT.hash); const devnetVerificationKeyHash = Field(nftVerificationKeys.devnet.vk.NFT.hash); // We check that the verification key hash is the same as the one // that was compiled at the time of the collection deployment if (Mina.getNetworkId() === "mainnet") { verificationKey.hash.assertEquals(mainnetVerificationKeyHash); } else { verificationKey.hash.assertEquals(devnetVerificationKeyHash); } update.body.update.verificationKey = { isSome: Bool(true), value: verificationKey, }; update.body.update.permissions = { isSome: Bool(true), value: { ...Permissions.default(), // NFT cannot be sent to other accounts, only owner can be changed send: Permissions.impossible(), // Allow the upgrade authority to set the verification key // even when there is no protocol upgrade setVerificationKey: Permissions.VerificationKey.proofDuringCurrentVersion(), setPermissions: Permissions.impossible(), access: Permissions.proof(), setZkappUri: Permissions.impossible(), setTokenSymbol: Permissions.impossible(), }, }; const initialState = new NFTStateStruct({ name, metadata, storage, packedData, metadataVerificationKeyHash, }); update.body.update.appState = NFTStateStruct.toFields(initialState).map((field) => ({ isSome: Bool(true), value: field, })); const event = new MintEvent({ initialState, address, tokenId, fee, }); this.emitEvent("mint", event); return event; } /** * Updates the NFT with admin approval. * * @param proof - The proof of the NFT update. * @param vk - The verification key. */ async update(proof, vk) { // The oracle address is optional and can be empty, NFT ZkProgram can verify the address // as it can be different for different NFTs. It should be empty for the update() call const oracleAddress = proof.publicInput.oracleAddress; oracleAddress .equals(PublicKey.empty()) .assertTrue(CollectionErrors.invalidOracleAddress); await this._update(proof, vk); } /** * Updates the NFT with admin approval and oracle approval. * * @param proof - The proof of the NFT update. * @param vk - The verification key. */ async updateWithOracle(proof, vk) { // The oracle address is optional and can be empty, NFT ZkProgram can verify the address // as it can be different for different NFTs. It should be non-empty for the updateWithOracle() call const oracleAddress = proof.publicInput.oracleAddress; oracleAddress .equals(PublicKey.empty()) .assertFalse(CollectionErrors.invalidOracleAddress); const oracle = this.getUpdateContract(oracleAddress); const canUpdate = await oracle.canUpdate(this.address, proof.publicInput.immutableState.address, proof.publicInput, proof.publicOutput); canUpdate.assertTrue(); await this._update(proof, vk); } /** * Updates the NFT with admin approval - internal method. * * @param proof - The proof of the NFT update. * @param vk - The verification key. */ async _update(proof, vk) { await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canUpdate = await adminContract.canUpdate(proof.publicInput, proof.publicOutput); canUpdate.assertTrue(); const creator = this.creator.getAndRequireEquals(); creator.assertEquals(proof.publicInput.creator); const tokenId = this.deriveTokenId(); tokenId.assertEquals(proof.publicInput.immutableState.tokenId); const nft = new NFT(proof.publicInput.immutableState.address, tokenId); const metadataVerificationKeyHash = await nft.update(proof.publicInput, proof.publicOutput, creator); // Verify the metadata update proof metadataVerificationKeyHash.assertEquals(vk.hash); proof.verify(vk); this.emitEvent("update", new NFTUpdateEvent({ address: proof.publicInput.immutableState.address, })); } /** * Approves an address to transfer an NFT. * * @param nftAddress - The address of the NFT. * @param approved - The approved public key. */ async approveAddress(nftAddress, approved) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(nftAddress, tokenId); const owner = await nft.approveAddress(approved); await this.ensureOwnerSignature(owner); this.emitEvent("approve", new ApproveEvent({ nftAddress, approved })); } /** * Transfers ownership of an NFT without admin approval. * * @param nftAddress - The address of the NFT. * @param approved - The approved public key. */ async approveAddressByProof(nftAddress, approved) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(nftAddress, tokenId); const owner = await nft.approveAddress(approved); const ownerContract = this.getOwnerContract(owner); const canApprove = await ownerContract.canApproveAddress(this.address, nftAddress, approved); canApprove.assertTrue(); this.emitEvent("approve", new ApproveEvent({ nftAddress, approved })); } /** * Transfers ownership of an NFT without admin approval. * This method should be used by wallets for collections that do not require transfer approval * and the owners of the NFTs which approve the transfer by signature * * @param address - The address of the NFT. * @param to - The recipient's public key. * @param price - The price of the NFT (optional). */ async transferBySignature(params) { const { address, to, price, context } = params; const collectionData = await this.ensureNotPaused(); collectionData.requireTransferApproval.assertFalse(CollectionErrors.transferApprovalRequired); const transferEventDraft = new TransferExtendedParams({ from: PublicKey.empty(), // will be added later to, collection: this.address, nft: address, fee: UInt64Option.none(), // will be added later price, transferByOwner: Bool(false), // will be added later approved: PublicKey.empty(), // will be added later context, }); await this._transfer({ transferEventDraft, transferFee: collectionData.transferFee, royaltyFee: collectionData.royaltyFee, }); } /** * Transfers ownership of an NFT using a proof in case the owner is a contract * Can be called by the owner or approved that should be a contracts * supporting NFTApprovalBase interface * * @param params - The transfer parameters. */ async transferByProof(params) { const { address, from, to, price, context } = params; const collectionData = await this.ensureNotPaused(); collectionData.requireTransferApproval.assertFalse(CollectionErrors.transferApprovalRequired); const transferEventDraft = new TransferExtendedParams({ from, to, collection: this.address, nft: address, fee: UInt64Option.none(), // will be added later price, transferByOwner: Bool(false), // will be added later approved: PublicKey.empty(), // will be added later context, }); const transferEvent = await this._transfer({ transferEventDraft, transferFee: collectionData.transferFee, royaltyFee: collectionData.royaltyFee, }); const approvalContract = this.getApprovalContract(from); // This operation is not atomic and the owner or approval contract cannot rely on the fact // that it is being called by the Collection contract // It is the responsibility of the owner contract to maintain the state // that allow for escrow-like agreement between the buyer and the seller // in case of the selling and buying of the NFT and return 'true' only if the // payment is made or guaranteed by the deposit of the funds in the owner contract // or the owner contract is able to verify that it is being called by the Collection contract // by setting the flag in its state as in the Offer contract example const canTransfer = await approvalContract.canTransfer(transferEvent); canTransfer.assertTrue(); } /** * Transfers ownership of an NFT using a proof in case the owner is a contract * Can be called by the owner or approved that should be a contracts * supporting NFTApprovalBase interface * * @param params - The transfer parameters. */ async adminApprovedTransferByProof(params) { const { address, from, to, price, context } = params; const collectionData = await this.ensureNotPaused(); const transferEventDraft = new TransferExtendedParams({ from, to, collection: this.address, nft: address, fee: UInt64Option.none(), // will be added later price, transferByOwner: Bool(false), // will be added later approved: PublicKey.empty(), // will be added later context, }); const transferEvent = await this._transfer({ transferEventDraft, transferFee: collectionData.transferFee, royaltyFee: collectionData.royaltyFee, }); const adminContract = this.getAdminContract(); const adminApprovedTransfer = await adminContract.canTransfer(transferEvent); adminApprovedTransfer.assertTrue(); const approvalContract = this.getApprovalContract(from); // This operation is not atomic and the owner or approval contract cannot rely on the fact // that it is being called by the Collection contract // It is the responsibility of the owner contract to maintain the state // that allow for escrow-like agreement between the buyer and the seller // in case of the selling and buying of the NFT and return 'true' only if the // payment is made or guaranteed by the deposit of the funds in the owner contract // or the owner contract is able to verify that it is being called by the Collection contract // by setting the flag in its state as in the Offer contract example const canTransfer = await approvalContract.canTransfer(transferEvent); canTransfer.assertTrue(); } /** * Transfers ownership of an NFT with admin approval. * * @param address - The address of the NFT. * @param to - The recipient's public key. * @param price - The price of the NFT (optional). */ async adminApprovedTransferBySignature(params) { const { address, to, price, context } = params; const collectionData = await this.ensureNotPaused(); const transferEventDraft = new TransferExtendedParams({ from: PublicKey.empty(), // will be added later to, collection: this.address, nft: address, fee: UInt64Option.none(), // will be added later price, transferByOwner: Bool(false), // will be added later approved: PublicKey.empty(), // will be added later context, }); const transferEvent = await this._transfer({ transferEventDraft, transferFee: collectionData.transferFee, royaltyFee: collectionData.royaltyFee, }); const adminContract = this.getAdminContract(); const canTransfer = await adminContract.canTransfer(transferEvent); canTransfer.assertTrue(); } /** * Internal method to transfer an NFT. * * This method handles the transfer logic and fee calculation. The fee is determined as follows: * - If a price is provided, the fee is calculated as (price * royaltyFee / MAX_ROYALTY_FEE) * - If no price is provided, the fixed transferFee is used to handle two cases: * when NFT is being sold and the price is not provided to the contract * when NFT is being transferred by the owner (without price) * - If the sender is the creator, no fee is charged * - The minimum fee is always the transferFee (unless sender is creator) * * @param transferEventDraft - The transfer event draft, containing the information about the transfer * @param transferFee - The transfer fee amount. * @param royaltyFee - The royalty fee amount. * @returns The TransferEvent emitted. */ async _transfer(params) { const { transferEventDraft, transferFee, royaltyFee } = params; const sender = this.sender.getUnconstrained(); // If the from is empty, we set the sender as the from and require signature from the sender const isFromEmpty = transferEventDraft.from.equals(PublicKey.empty()); transferEventDraft.from = Provable.if(isFromEmpty, sender, transferEventDraft.from); const tokenId = this.deriveTokenId(); const nft = new NFT(transferEventDraft.nft, tokenId); const transferEvent = await nft.transfer(transferEventDraft); const creator = this.creator.getAndRequireEquals(); let fee = Provable.if(transferEventDraft.price.isSome, // We cannot check the price here, so we just rely on owner contract // Malicious owner contracts can be blocked by the admin contract // or by setting the transfer fee to a higher value reflecting the market price mulDiv({ value: transferEventDraft.price.value, multiplier: UInt64.from(royaltyFee), denominator: UInt64.from(MAX_ROYALTY_FEE), }).result, transferFee); const isOwnedByCreator = transferEvent.from.equals(creator); fee = Provable.if(isOwnedByCreator, UInt64.zero, // The minimum fee is the transfer fee Provable.if(fee.lessThanOrEqual(transferFee), transferFee, fee)); const senderUpdate = AccountUpdate.createIf(fee.equals(UInt64.zero).not().or(isFromEmpty), sender); senderUpdate.requireSignature(); senderUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change senderUpdate.account.balance.requireBetween(fee, UInt64.MAXINT()); senderUpdate.send({ to: this.creator.getAndRequireEquals(), amount: fee, }); transferEvent.fee = UInt64Option.fromValue({ value: fee, isSome: fee.equals(UInt64.zero).not(), }); this.emitEvent("transfer", new TransferEvent({ ...transferEvent, })); return transferEvent; } /** * Upgrades the verification key of a specific NFT. * * @param address - The address of the NFT. * @param vk - The new verification key. */ async upgradeNFTVerificationKeyBySignature(address, vk) { await this.ensureNotPaused(); const sender = this.sender.getAndRequireSignature(); const data = await this._upgrade(address, vk); data.owner .equals(sender) .or(data.isOwnerApprovalRequired.not()) .assertTrue(CollectionErrors.onlyOwnerCanUpgradeVerificationKey); } /** * Upgrades the verification key of a specific NFT by Proof. * * @param address - The address of the NFT. * @param vk - The new verification key. */ async upgradeNFTVerificationKeyByProof(address, vk) { await this.ensureNotPaused(); const data = await this._upgrade(address, vk); const ownerContract = this.getOwnerContract(data.owner); const canUpgrade = await ownerContract.canChangeVerificationKey(this.address, address, vk); canUpgrade.assertTrue(); } async _upgrade(address, vk) { const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const adminContract = this.getAdminContract(); const canUpgrade = await adminContract.canChangeVerificationKey(vk, address, tokenId); canUpgrade.assertTrue(CollectionErrors.cannotUpgradeVerificationKey); const data = await nft.upgradeVerificationKey(vk); const event = new UpgradeVerificationKeyEvent({ address, tokenId, verificationKeyHash: vk.hash, }); this.emitEvent("upgradeNFTVerificationKey", event); return data; } /** * Upgrades the verification key of the collection contract. * * @param vk - The new verification key. */ async upgradeVerificationKey(vk) { await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canUpgrade = await adminContract.canChangeVerificationKey(vk, this.address, this.tokenId); canUpgrade.assertTrue(CollectionErrors.cannotUpgradeVerificationKey); this.account.verificationKey.set(vk); this.emitEvent("upgradeVerificationKey", new UpgradeVerificationKeyEvent({ address: this.address, tokenId: this.tokenId, verificationKeyHash: vk.hash, })); } /** * Limits further minting of NFTs in the collection. */ async limitMinting() { await this.ensureCreatorSignature(); const collectionData = await this.ensureNotPaused(); collectionData.mintingIsLimited = Bool(true); this.packedData.set(collectionData.pack()); this.emitEvent("limitMinting", new LimitMintingEvent({ mintingLimited: Bool(true) })); } /** * Pauses the collection, disabling certain actions. */ async pause() { const collectionData = await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canPause = await adminContract.canPause(); canPause.assertTrue(CollectionErrors.noPermissionToPause); collectionData.isPaused = Bool(true); this.packedData.set(collectionData.pack()); this.emitEvent("pause", new PauseEvent({ isPaused: Bool(true) })); } /** * Resumes the collection, re-enabling actions. */ async resume() { const collectionData = CollectionData.unpack(this.packedData.getAndRequireEquals()); collectionData.isPaused.assertTrue(CollectionErrors.collectionNotPaused); const adminContract = this.getAdminContract(); const canResume = await adminContract.canResume(); canResume.assertTrue(CollectionErrors.noPermissionToResume); collectionData.isPaused = Bool(false); this.packedData.set(collectionData.pack()); this.emitEvent("resume", new PauseEvent({ isPaused: Bool(false) })); } /** * Pauses a specific NFT, disabling its actions. * * @param address - The address of the NFT to pause. */ async pauseNFTBySignature(address) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const owner = await nft.pause(); await this.ensureOwnerSignature(owner); this.emitEvent("pauseNFT", new PauseNFTEvent({ isPaused: Bool(true), address })); } /** * Pauses a specific NFT, disabling its actions. * * @param address - The address of the NFT to pause. */ async pauseNFTByProof(address) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const owner = await nft.pause(); const ownerContract = this.getOwnerContract(owner); const canPause = await ownerContract.canPause(this.address, address); canPause.assertTrue(); this.emitEvent("pauseNFT", new PauseNFTEvent({ isPaused: Bool(true), address })); } /** * Resumes a specific NFT, re-enabling its actions. * * @param address - The address of the NFT to resume. */ async resumeNFT(address) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const owner = await nft.resume(); await this.ensureOwnerSignature(owner); this.emitEvent("resumeNFT", new PauseNFTEvent({ isPaused: Bool(false), address })); } /** * Resumes a specific NFT, re-enabling its actions. * * @param address - The address of the NFT to resume. */ async resumeNFTByProof(address) { await this.ensureNotPaused(); const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const owner = await nft.resume(); const ownerContract = this.getOwnerContract(owner); const canResume = await ownerContract.canResume(this.address, address); canResume.assertTrue(); this.emitEvent("resumeNFT", new PauseNFTEvent({ isPaused: Bool(false), address })); } /** * Sets a new name for the collection. * Requires owner signature and collection to not be paused. * Emits a 'setName' event with the new name. * * @param name - The new name for the collection as a Field value * @throws {Error} If caller lacks permission to change name */ async setName(name) { await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canChangeName = await adminContract.canChangeName(name); canChangeName.assertTrue(CollectionErrors.noPermissionToChangeName); this.collectionName.set(name); this.emitEvent("setName", new SetNameEvent({ name })); } /** * Updates the base URL for the collection's metadata. * Requires owner signature and collection to not be paused. * Emits a 'setBaseURL' event with the new URL. * * @param baseURL - The new base URL as a Field value * @throws {Error} If caller lacks permission to change base URI */ async setBaseURL(baseURL) { await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canChangeBaseUri = await adminContract.canChangeBaseUri(baseURL); canChangeBaseUri.assertTrue(CollectionErrors.noPermissionToChangeBaseUri); this.baseURL.set(baseURL); this.emitEvent("setBaseURL", new SetBaseURLEvent({ baseURL })); } /** * Sets a new admin address for the collection. * Requires owner signature and collection to not be paused. * Emits a 'setAdmin' event with the new admin address. * * @param admin - The public key of the new admin * @throws {Error} If caller lacks permission to set admin */ async setAdmin(admin) { await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canSetAdmin = await adminContract.canSetAdmin(admin); canSetAdmin.assertTrue(CollectionErrors.noPermissionToSetAdmin); this.admin.set(admin); this.emitEvent("setAdmin", new SetAdminEvent({ admin })); } /** * Updates the royalty fee for the collection. * Requires owner signature and collection to not be paused. * Emits a 'setRoyaltyFee' event with the new fee. * * @param royaltyFee - The new royalty fee as a UInt32 value * @throws {Error} If caller lacks permission to change royalty fee */ async setRoyaltyFee(royaltyFee) { const collectionData = await this.ensureNotPaused(); royaltyFee.assertLessThanOrEqual(UInt32.from(MAX_ROYALTY_FEE), CollectionErrors.invalidRoyaltyFee); const adminContract = this.getAdminContract(); const canChangeRoyalty = await adminContract.canChangeRoyalty(royaltyFee); canChangeRoyalty.assertTrue(CollectionErrors.noPermissionToChangeRoyalty); collectionData.royaltyFee = royaltyFee; this.packedData.set(collectionData.pack()); this.emitEvent("setRoyaltyFee", new SetRoyaltyFeeEvent({ royaltyFee })); } /** * Updates the transfer fee for the collection. * Requires owner signature and collection to not be paused. * Emits a 'setTransferFee' event with the new fee. * * @param transferFee - The new transfer fee as a UInt64 value * @throws {Error} If caller lacks permission to change transfer fee */ async setTransferFee(transferFee) { const collectionData = await this.ensureNotPaused(); const adminContract = this.getAdminContract(); const canChangeTransferFee = await adminContract.canChangeTransferFee(transferFee); canChangeTransferFee.assertTrue(CollectionErrors.noPermissionToChangeTransferFee); collectionData.transferFee = transferFee; this.packedData.set(collectionData.pack()); this.emitEvent("setTransferFee", new SetTransferFeeEvent({ transferFee })); } /** * Transfers ownership of the collection to a new creator. * This method is called transferOwnership as the Collection is implementing OwnableContract interface * For the Collection, the creator is the owner of the collection * * @param to - The public key of the new creator. * @returns The public key of the old creator. */ async transferOwnership(to) { await this.ensureCreatorSignature(); const collectionData = CollectionData.unpack(this.packedData.getAndRequireEquals()); collectionData.isPaused.assertFalse(CollectionErrors.collectionNotPaused); const adminContract = this.getAdminContract(); const canChangeCreator = await adminContract.canChangeCreator(to); canChangeCreator.assertTrue(CollectionErrors.noPermissionToChangeCreator); const from = this.creator.getAndRequireEquals(); // Pending creator public key can be empty, it cancels the transfer this.pendingCreatorX.set(to.x); collectionData.pendingCreatorIsOdd = to.isOdd; this.packedData.set(collectionData.pack()); this.emitEvent("ownershipTransfer", new OwnershipChangeEvent({ from, to, })); return from; } /** * Transfers ownership of the collection to a new owner. * * @param to - The public key of the new owner. * @returns The public key of the old owner. */ async acceptOwnership() { const collectionData = CollectionData.unpack(this.packedData.getAndRequireEquals()); collectionData.isPaused.assertFalse(CollectionErrors.collectionNotPaused); const pendingCreatorX = this.pendingCreatorX.getAndRequireEquals(); const pendingCreator = PublicKey.from({ x: pendingCreatorX, isOdd: collectionData.pendingCreatorIsOdd, }); const emptyPublicKey = PublicKey.empty(); pendingCreator .equals(emptyPublicKey) .assertFalse(CollectionErrors.pendingCreatorIsEmpty); // pendingCreator can be different from the sender, but it should sign the tx const pendingCreatorUpdate = AccountUpdate.createSigned(pendingCreator); pendingCreatorUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change // Check second time that the transfer is allowed const adminContract = this.getAdminContract(); const canChangeCreator = await adminContract.canChangeCreator(pendingCreator); canChangeCreator.assertTrue(CollectionErrors.noPermissionToChangeCreator); const from = this.creator.getAndRequireEquals(); this.pendingCreatorX.set(emptyPublicKey.x); collectionData.pendingCreatorIsOdd = Bool(emptyPublicKey.isOdd); this.creator.set(pendingCreator); this.packedData.set(collectionData.pack()); this.emitEvent("ownershipAccepted", new OwnershipChangeEvent({ from, to: pendingCreator, })); return from; } async getNFTState(address) { const tokenId = this.deriveTokenId(); const nft = new NFT(address, tokenId); const state = await nft.getState(); return state; } } __decorate([ state(Field), __metadata("design:type", Object) ], Collection.prototype, "collectionName", void 0); __decorate([ state(PublicKey), __metadata("design:type", Object) ], Collection.prototype, "creator", void 0); __decorate([ state(PublicKey), __metadata("design:type", Object) ], Collection.prototype, "admin", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], Collection.prototype, "baseURL", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], Collection.prototype, "packedData", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], Collection.prototype, "pendingCreatorX", void 0); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [MintParams, CollectionData]), __metadata("design:returntype", Promise) ], Collection.prototype, "initialize", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [MintParams]), __metadata("design:returntype", Promise) ], Collection.prototype, "mintByCreator", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [MintRequest]), __metadata("design:returntype", Promise) ], Collection.prototype, "mint", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [NFTUpdateProof, VerificationKey]), __metadata("design:returntype", Promise) ], Collection.prototype, "update", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [NFTUpdateProof, VerificationKey]), __metadata("design:returntype", Promise) ], Collection.prototype, "updateWithOracle", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey, PublicKey]), __metadata("design:returntype", Promise) ], Collection.prototype, "approveAddress", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey, PublicKey]), __metadata("design:returntype", Promise) ], Collection.prototype, "approveAddressByProof", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [TransferBySignatureParams]), __metadata("design:returntype", Promise) ], Collection.prototype, "transferBySignature", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [TransferByProofParams]), __metadata("design:returntype", Promise) ], Collection.prototype, "transferByProof", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [TransferByProofParams]), __metadata("design:returntype", Promise) ], Collection.prototype, "adminApprovedTransferByProof", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [TransferBySignatu