UNPKG

@silvana-one/nft

Version:
525 lines 24.6 kB
/** * The `NFTAdvancedAdminContract` is an implementation of an admin contract that uses a whitelist to control access to certain actions within the NFT ecosystem. * This contract ensures that only whitelisted addresses can perform specific actions such as minting, updating, transferring, buying, or selling NFTs. * It also introduces functionality for pausing and resuming the contract, upgrading the contract's verification key, and transferring ownership. */ import { __decorate, __metadata } from "tslib"; import { Bool, method, Permissions, PublicKey, SmartContract, State, state, VerificationKey, UInt64, Provable, Field, AccountUpdate, Mina, UInt32, Struct, } from "o1js"; import { Whitelist } from "@silvana-one/storage"; import { MintRequest, NFTState, MintParamsOption, PauseEvent, OwnershipChangeEvent, TransferEvent, } from "../interfaces/index.js"; import { VerificationKeyUpgradeData, } from "@silvana-one/upgradable"; export { NFTAdvancedAdminContract, AdminData }; /** * Represents pause-related data, containing flags for pause functionality. */ class AdminData extends Struct({ /** Indicates whether the contract can be paused. */ canPause: Bool, /** Indicates whether the contract is currently paused. */ isPaused: Bool, /** Indicates whether the contract can change the royalty fee. */ allowChangeRoyalty: Bool, /** Indicates whether the contract can change the transfer fee. */ allowChangeTransferFee: Bool, /** Indicates whether the contract can change the base URI. */ allowChangeBaseUri: Bool, /** Indicates whether the contract can change the creator. */ allowChangeCreator: Bool, /** Indicates whether the contract can change the admin. */ allowChangeAdmin: Bool, /** Indicates whether the contract can change the name. */ allowChangeName: Bool, }) { static new(params = {}) { const { canPause, isPaused, allowChangeRoyalty, allowChangeTransferFee, allowChangeBaseUri, allowChangeCreator, allowChangeAdmin, allowChangeName, } = params; return new AdminData({ canPause: Bool(canPause ?? true), isPaused: Bool(isPaused ?? false), allowChangeRoyalty: Bool(allowChangeRoyalty ?? false), allowChangeTransferFee: Bool(allowChangeTransferFee ?? false), allowChangeBaseUri: Bool(allowChangeBaseUri ?? false), allowChangeCreator: Bool(allowChangeCreator ?? false), allowChangeAdmin: Bool(allowChangeAdmin ?? false), allowChangeName: Bool(allowChangeName ?? false), }); } /** * Packs the pause data into a `Field`. * @returns A `Field` representing the packed pause data. */ pack() { return Field.fromBits([ this.isPaused, this.canPause, this.allowChangeRoyalty, this.allowChangeTransferFee, this.allowChangeBaseUri, this.allowChangeCreator, this.allowChangeAdmin, this.allowChangeName, ]); } /** * Unpacks a `Field` into `PauseData`. * @param field The `Field` to unpack. * @returns An instance of `PauseData`. */ static unpack(field) { const [isPaused, canPause, allowChangeRoyalty, allowChangeTransferFee, allowChangeBaseUri, allowChangeCreator, allowChangeAdmin, allowChangeName,] = field.toBits(8); return new AdminData({ canPause, isPaused, allowChangeRoyalty, allowChangeTransferFee, allowChangeBaseUri, allowChangeCreator, allowChangeAdmin, allowChangeName, }); } static isPaused(field) { return field.toBits(8)[0]; } } const NFTAdvancedAdminContractErrors = { contractIsPaused: "Contract is paused", notWhitelisted: "Address not whitelisted", senderNotWhitelisted: "Sender address not whitelisted", cannotMint: "Cannot mint", verificationKeyHashNotFound: "Verification key hash not found", cannotUpgradeVerificationKey: "Cannot upgrade verification key", }; /** * Constructs the `NFTAdvancedAdmin` class, an admin contract that uses a whitelist to control access. * @param params Object containing the upgrade contract constructor. * @returns The `NFTAdvancedAdmin` class. */ function NFTAdvancedAdminContract(params) { const { upgradeContract } = params; /** * The `NFTWhitelistedAdmin` class ensures that only whitelisted addresses can perform specific actions such as minting, updating, transferring, buying, or selling NFTs. * It also provides functionality for pausing and resuming the contract, upgrading the contract's verification key, and transferring ownership. */ class NFTAdvancedAdmin extends SmartContract { constructor() { super(...arguments); /** The public key of the admin or owner of the contract. */ this.admin = State(); /** The public key of the Upgrade Authority Contract. */ this.upgradeAuthority = State(); /** The root hash of the Merkle tree representing the whitelist. */ this.whitelist = State(); /** Packed field containing pause-related flags. */ this.data = State(); this.events = { /** Emitted when the contract's verification key is upgraded. */ upgradeVerificationKey: Field, /** Emitted when the contract is paused. */ pause: PauseEvent, /** Emitted when the contract is resumed. */ resume: PauseEvent, /** Emitted when ownership of the contract changes. */ ownershipChange: OwnershipChangeEvent, /** Emitted when the whitelist is updated. */ updateWhitelist: Whitelist, }; } /** * Deploys the `NFTWhitelistedAdmin` contract with the provided initial settings. * @param props Deployment properties. */ async deploy(props) { await super.deploy(props); this.admin.set(props.admin); this.upgradeAuthority.set(props.upgradeAuthority); this.whitelist.set(props.whitelist); this.data.set(props.adminData.pack()); this.account.zkappUri.set(props.uri); this.account.permissions.set({ ...Permissions.default(), // 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(), send: Permissions.proof(), setZkappUri: Permissions.impossible(), setTokenSymbol: Permissions.impossible(), }); } /** * Ensures that the transaction is authorized by the contract owner. * @returns An `AccountUpdate` representing the admin's signed transaction. */ async ensureOwnerSignature() { const admin = this.admin.getAndRequireEquals(); const adminUpdate = AccountUpdate.createSigned(admin); adminUpdate.body.useFullCommitment = Bool(true); // prevent memo and fee change return adminUpdate; } /** Gets the upgrade contract constructor. */ get getUpgradeContractConstructor() { return upgradeContract; } /** * Retrieves the `UpgradeAuthorityBase` contract instance. * @returns An instance of the upgrade authority contract. */ async getUpgradeContract() { return new this.getUpgradeContractConstructor(this.upgradeAuthority.getAndRequireEquals()); } /** * Upgrades the contract's verification key using the Upgrade Authority Contract. * @param vk The new verification key. */ async upgradeVerificationKey(vk) { await this.ensureOwnerSignature(); const upgradeContract = await this.getUpgradeContract(); // fetchAccount() should be called before calling this method // this code should be changed after verification key precondition // will be added to the Mina protocol const previousVerificationKeyHash = Provable.witness(Field, () => { const account = Mina.getAccount(this.address); const vkHash = account.zkapp?.verificationKey?.hash; if (!vkHash) { throw Error(NFTAdvancedAdminContractErrors.verificationKeyHashNotFound); } return vkHash; }); const data = new VerificationKeyUpgradeData({ address: this.address, tokenId: this.tokenId, previousVerificationKeyHash, newVerificationKeyHash: vk.hash, }); const upgradeAuthorityAnswer = await upgradeContract.verifyUpgradeData(data); upgradeAuthorityAnswer.isVerified.assertTrue(NFTAdvancedAdminContractErrors.cannotUpgradeVerificationKey); this.account.verificationKey.set(vk); this.upgradeAuthority.set(upgradeAuthorityAnswer.nextUpgradeAuthority.orElse(this.upgradeAuthority.getAndRequireEquals())); this.emitEvent("upgradeVerificationKey", vk.hash); } /** * Determines if the minting request can proceed by checking if the owner and sender are whitelisted. * @param mintRequest The minting request parameters. * @returns A `MintParamsOption` indicating if minting is allowed. */ async canMint(mintRequest) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); const whitelist = this.whitelist.getAndRequireEquals(); const ownerAmount = await whitelist.getWhitelistedAmount(mintRequest.owner); ownerAmount.isSome.assertTrue(NFTAdvancedAdminContractErrors.notWhitelisted); const sender = this.sender.getUnconstrained(); const senderUpdate = AccountUpdate.createSigned(sender); senderUpdate.body.useFullCommitment = Bool(true); // prevent memo and fee change const senderAmount = await whitelist.getWhitelistedAmount(sender); senderAmount.isSome.assertTrue(NFTAdvancedAdminContractErrors.senderNotWhitelisted); const mintParams = await Provable.witnessAsync(MintParamsOption, async () => { // only creator can mint // can be changed in the future to support CMS return MintParamsOption.none(); }); return mintParams; } /** * Checks whether the NFT's state can be updated, ensuring the new owner is whitelisted. * @param input The current state of the NFT. * @param output The desired new state of the NFT. * @returns A `Bool` indicating whether the update is permitted. */ async canUpdate(input, output) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); const whitelist = this.whitelist.getAndRequireEquals(); return (await whitelist.getWhitelistedAmount(output.owner)).isSome.and((await whitelist.getWhitelistedAmount(input.owner)).isSome); } /** * Verifies if the transfer between `from` and `to` addresses is allowed based on whitelist status. * @param address The address of the NFT. * @param from The sender's public key. * @param to The receiver's public key. * @returns A `Bool` indicating whether the transfer is permitted. */ async canTransfer(transferEvent) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); const { to, from, price } = transferEvent; const whitelist = this.whitelist.getAndRequireEquals(); const toAmount = await whitelist.getWhitelistedAmount(to); const fromAmount = await whitelist.getWhitelistedAmount(from); const toAmountAllowed = toAmount .orElse(UInt64.from(0)) .greaterThanOrEqual(price.orElse(UInt64.zero)); const fromAmountAllowed = fromAmount .orElse(UInt64.from(0)) .greaterThanOrEqual(price.orElse(UInt64.zero)); return toAmountAllowed .and(fromAmountAllowed) .and(toAmount.isSome) .and(fromAmount.isSome); } /** * Updates the whitelist's Merkle root and the associated off-chain storage reference. * @param whitelistRoot The new whitelist root. * @param storage The storage reference for the whitelist data. */ async updateWhitelist(whitelist) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); await this.ensureOwnerSignature(); this.whitelist.set(whitelist); this.emitEvent("updateWhitelist", whitelist); } /** * Pauses the contract, preventing certain administrative actions from being performed. */ async pause() { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.canPause.assertTrue(); adminData.isPaused = Bool(true); this.data.set(adminData.pack()); this.emitEvent("pause", new PauseEvent({ isPaused: Bool(true) })); } /** * Resumes the contract, allowing administrative actions to be performed again. */ async resume() { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.canPause.assertTrue(); adminData.isPaused = Bool(false); this.data.set(adminData.pack()); this.emitEvent("resume", new PauseEvent({ isPaused: Bool(false) })); } /** * Transfers ownership of the contract to a new admin. * @param newOwner The public key of the new admin. * @returns The public key of the old owner. */ async transferOwnership(to) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); await this.ensureOwnerSignature(); const from = this.admin.getAndRequireEquals(); this.admin.set(to); this.emitEvent("ownershipChange", new OwnershipChangeEvent({ from, to, })); return from; } async canChangeVerificationKey(vk, address, tokenId) { AdminData.isPaused(this.data.getAndRequireEquals()).assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); const upgradeContract = await this.getUpgradeContract(); // fetchAccount() should be called before calling this method // TODO: this code should be changed after verification key precondition // will be added to the Mina protocol const previousVerificationKeyHash = Provable.witness(Field, () => { const account = Mina.getAccount(address, tokenId); const vkHash = account.zkapp?.verificationKey?.hash; if (!vkHash) { throw Error("Verification key hash not found"); } return vkHash; }); const data = new VerificationKeyUpgradeData({ address: address, tokenId: tokenId, previousVerificationKeyHash, newVerificationKeyHash: vk.hash, }); const upgradeAuthorityAnswer = await upgradeContract.verifyUpgradeData(data); return upgradeAuthorityAnswer.isVerified; } /** * Determines if the name can be changed for a Collection. */ async canChangeName(name) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeName; } /** * Determines if the creator can be changed for a Collection. */ async canChangeCreator(creator) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeCreator; } /** * Determines if the base URI can be changed for a Collection. */ async canChangeBaseUri(baseUri) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeBaseUri; } /** * Determines if the royalty fee can be changed for a Collection. */ async canChangeRoyalty(royaltyFee) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeRoyalty; } /** * Determines if the transfer fee can be changed for a Collection. */ async canChangeTransferFee(transferFee) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeTransferFee; } /** * Determines if the admin contract can be changed for a Collection. */ async canSetAdmin(admin) { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.allowChangeAdmin; } /** * Determines if the collection can be paused. */ async canPause() { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.canPause; } /** * Determines if the collection can be resumed. */ async canResume() { await this.ensureOwnerSignature(); const adminData = AdminData.unpack(this.data.getAndRequireEquals()); adminData.isPaused.assertFalse(NFTAdvancedAdminContractErrors.contractIsPaused); return adminData.canPause; } } __decorate([ state(PublicKey), __metadata("design:type", Object) ], NFTAdvancedAdmin.prototype, "admin", void 0); __decorate([ state(PublicKey), __metadata("design:type", Object) ], NFTAdvancedAdmin.prototype, "upgradeAuthority", void 0); __decorate([ state(Whitelist), __metadata("design:type", Object) ], NFTAdvancedAdmin.prototype, "whitelist", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NFTAdvancedAdmin.prototype, "data", void 0); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "upgradeVerificationKey", null); __decorate([ method.returns(MintParamsOption), __metadata("design:type", Function), __metadata("design:paramtypes", [MintRequest]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canMint", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [NFTState, NFTState]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canUpdate", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [TransferEvent]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canTransfer", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [Whitelist]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "updateWhitelist", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "pause", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "resume", null); __decorate([ method.returns(PublicKey), __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "transferOwnership", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey, PublicKey, Field]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeVerificationKey", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [Field]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeName", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeCreator", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [Field]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeBaseUri", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [UInt32]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeRoyalty", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [UInt64]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canChangeTransferFee", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey]), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canSetAdmin", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canPause", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NFTAdvancedAdmin.prototype, "canResume", null); return NFTAdvancedAdmin; } //# sourceMappingURL=advanced.js.map