@minatokens/nft
Version:
Mina NFT Standard draft library
345 lines (312 loc) • 11.1 kB
text/typescript
import {
Field,
PublicKey,
Bool,
SmartContract,
method,
state,
State,
VerificationKey,
} from "o1js";
import { Storage } from "@minatokens/storage";
import {
NFTData,
NFTDataPacked,
NFTState,
NFTImmutableState,
UpdateEvent,
TransferExtendedParams,
UpgradeVerificationKeyEvent,
PauseEvent,
OwnershipChangeEvent,
UpgradeVerificationKeyData,
NFTStateStruct,
NFTTransactionContext,
} from "../interfaces/index.js";
export { NFT };
const NftErrors = {
cannotChangeMetadataVerificationKeyHash:
"Cannot change metadata verification key hash",
cannotChangeOwner: "Cannot change owner",
cannotChangeStorage: "Cannot change storage",
cannotChangePauseState: "Cannot change pause state",
noPermissionToPause: "No permission to pause",
nftAlreadyPaused: "NFT is already paused",
nftIsNotPaused: "NFT is not paused",
nftIsPaused: "NFT is paused",
cannotChangeName: "Cannot change name",
cannotChangeMetadata: "Cannot change metadata",
noMetadataVerificationKey: "No metadata verification key",
};
/**
* The NFT Contract represents an individual NFT within a collection.
*
* It manages the state and behavior of a single NFT, including ownership, metadata,
* storage, pricing, and permissions. The contract provides functionality for updating
* NFT properties with proofs and permissions, transferring ownership, selling and buying NFTs,
* upgrading the verification key, and pausing or resuming the NFT.
*/
class NFT extends SmartContract {
/** The name of the NFT (`Field`). */
name = State<Field>();
/** The metadata associated with the NFT (`Field`). */
metadata = State<Field>();
/** Holds off-chain storage information, e.g., IPFS hash (`Storage`). */
storage = State<Storage>();
/** A packed field containing additional NFT data and flags (`Field`). */
packedData = State<NFTDataPacked>();
/** The hash of the verification key used for metadata proofs (`Field`). */
metadataVerificationKeyHash = State<Field>();
/** Events emitted by the NFT contract. */
events = {
update: UpdateEvent,
transfer: OwnershipChangeEvent,
approve: PublicKey,
upgradeVerificationKey: UpgradeVerificationKeyEvent,
pause: PauseEvent,
resume: PauseEvent,
};
.returns(NFTStateStruct)
async getState(): Promise<NFTStateStruct> {
const name = this.name.getAndRequireEquals();
const metadata = this.metadata.getAndRequireEquals();
const storage = this.storage.getAndRequireEquals();
const packedData = this.packedData.getAndRequireEquals();
const metadataVerificationKeyHash =
this.metadataVerificationKeyHash.getAndRequireEquals();
return new NFTStateStruct({
name,
metadata,
storage,
packedData,
metadataVerificationKeyHash,
});
}
/**
* Updates the NFT's state with provided proofs and permissions.
*
* @param input - The current state of the NFT (`NFTState`).
* @param output - The desired new state of the NFT (`NFTState`).
* @param creator - The public key of the creator (`PublicKey`).
* @returns The hash of the metadata verification key (`Field`).
*/
.returns(Field)
async update(
input: NFTState,
output: NFTState,
creator: PublicKey
): Promise<Field> {
const name = this.name.getAndRequireEquals();
const metadata = this.metadata.getAndRequireEquals();
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
const owner = data.owner;
const storage = this.storage.getAndRequireEquals();
const metadataVerificationKeyHash =
this.metadataVerificationKeyHash.getAndRequireEquals();
// Check that the metadata verification key exists
metadataVerificationKeyHash.assertNotEquals(
Field(0),
NftErrors.noMetadataVerificationKey
);
// We do not check if the NFT is paused here
// It is the responsibility of metadata zkProgram to check if the NFT is paused,
// similar to data.isPaused.assertFalse(NftErrors.nftIsPaused);
// Assert that the public input matches the NFT state
NFTState.assertEqual(
input,
new NFTState({
immutableState: new NFTImmutableState({
canChangeOwnerByProof: data.canChangeOwnerByProof,
canTransfer: data.canTransfer,
canApprove: data.canApprove,
canChangeMetadata: data.canChangeMetadata,
canChangeStorage: data.canChangeStorage,
canChangeName: data.canChangeName,
canChangeMetadataVerificationKeyHash:
data.canChangeMetadataVerificationKeyHash,
canPause: data.canPause,
address: this.address,
tokenId: this.tokenId,
id: data.id,
}),
name,
metadata,
storage,
owner,
approved: data.approved,
version: data.version,
isPaused: data.isPaused,
metadataVerificationKeyHash,
creator,
context: input.context,
oracleAddress: input.oracleAddress,
})
);
// assert that the read-only fields are not changed
input.creator.assertEquals(output.creator);
NFTTransactionContext.assertEqual(input.context, output.context);
input.oracleAddress.assertEquals(output.oracleAddress);
// Check permissions and set new state
name
.equals(output.name)
.not()
.and(data.canChangeName.not())
.assertFalse(NftErrors.cannotChangeName);
this.name.set(output.name);
metadata
.equals(output.metadata)
.not()
.and(data.canChangeMetadata.not())
.assertFalse(NftErrors.cannotChangeMetadata);
this.metadata.set(output.metadata);
metadataVerificationKeyHash
.equals(output.metadataVerificationKeyHash)
.not()
.and(data.canChangeMetadataVerificationKeyHash.not())
.assertFalse(NftErrors.cannotChangeMetadataVerificationKeyHash);
this.metadataVerificationKeyHash.set(output.metadataVerificationKeyHash);
owner
.equals(output.owner)
.not()
.and(data.canChangeOwnerByProof.not())
.assertFalse(NftErrors.cannotChangeOwner);
Storage.equals(storage, output.storage)
.not()
.and(data.canChangeStorage.not())
.assertFalse(NftErrors.cannotChangeStorage);
this.storage.set(output.storage);
data.approved
.equals(output.approved)
.not()
.and(data.canChangeOwnerByProof.not())
.assertFalse(NftErrors.cannotChangeOwner);
data.isPaused
.equals(output.isPaused)
.not()
.and(data.canPause.not())
.assertFalse(NftErrors.cannotChangePauseState);
// recursive proofs can increase the version by more than 1
output.version.assertGreaterThan(data.version);
data.owner = output.owner;
data.approved = output.approved;
data.version = output.version;
this.packedData.set(data.pack());
const event = new UpdateEvent({
name: output.name,
metadata: output.metadata,
storage: output.storage,
owner: output.owner,
approved: output.approved,
version: output.version,
isPaused: output.isPaused,
metadataVerificationKeyHash: output.metadataVerificationKeyHash,
});
this.emitEvent("update", event);
return metadataVerificationKeyHash;
}
/**
* Transfers ownership of the NFT from one user to another.
*
* @param params - The parameters for the transfer (`TransferExtendedParams`).
* @returns The public key of the old owner (`PublicKey`).
*/
.returns(TransferExtendedParams)
async transfer(
params: TransferExtendedParams
): Promise<TransferExtendedParams> {
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
data.canTransfer.assertTrue(NftErrors.cannotChangeOwner);
data.isPaused.assertFalse(NftErrors.nftIsPaused);
const owner = data.owner;
const approved = data.approved;
params.transferByOwner = owner.equals(params.from);
owner
.equals(params.from)
.or(
approved
.equals(params.from)
.and(approved.equals(PublicKey.empty()).not())
)
.assertTrue(NftErrors.cannotChangeOwner);
params.from = owner;
params.approved = approved;
const version = data.version.add(1);
data.version = version;
data.approved = PublicKey.empty();
data.owner = params.to;
this.packedData.set(data.pack());
this.emitEvent(
"transfer",
new OwnershipChangeEvent({
from: owner,
to: params.to,
})
);
return params;
}
/**
* Transfers ownership of the NFT from one user to another.
*
* @param approved - The public key of the approved address (`PublicKey`).
* @returns The public key of the owner (`PublicKey`).
*/
.returns(PublicKey)
async approveAddress(approved: PublicKey): Promise<PublicKey> {
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
data.isPaused.assertFalse(NftErrors.nftIsPaused);
data.approved = approved;
this.packedData.set(data.pack());
this.emitEvent("approve", approved);
return data.owner;
}
/**
* Upgrades the verification key used by the NFT contract.
*
* @param vk - The new verification key (`VerificationKey`).
* @returns An owner public key to be checked by the Collection contract and the Boolean flag indicating if the owner's authorization is required
*/
.returns(UpgradeVerificationKeyData)
async upgradeVerificationKey(
vk: VerificationKey
): Promise<UpgradeVerificationKeyData> {
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
const version = data.version.add(1);
data.version = version;
this.account.verificationKey.set(vk);
this.packedData.set(data.pack());
return new UpgradeVerificationKeyData({
owner: data.owner,
isOwnerApprovalRequired: data.requireOwnerAuthorizationToUpgrade,
});
}
/**
* Pauses the NFT, disabling certain actions.
*
* @returns An owner public key to be checked by the Collection contract
*/
.returns(PublicKey)
async pause(): Promise<PublicKey> {
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
data.canPause.assertTrue(NftErrors.noPermissionToPause);
data.isPaused.assertFalse(NftErrors.nftAlreadyPaused);
data.isPaused = Bool(true);
this.packedData.set(data.pack());
this.emitEvent("pause", new PauseEvent({ isPaused: Bool(true) }));
return data.owner;
}
/**
* Resumes the NFT, re-enabling actions.
*
* @returns An owner public key to be checked by the Collection contract
*/
.returns(PublicKey)
async resume(): Promise<PublicKey> {
const data = NFTData.unpack(this.packedData.getAndRequireEquals());
data.canPause.assertTrue(NftErrors.noPermissionToPause);
data.isPaused.assertTrue(NftErrors.nftIsNotPaused);
data.isPaused = Bool(false);
this.packedData.set(data.pack());
this.emitEvent("resume", new PauseEvent({ isPaused: Bool(false) }));
return data.owner;
}
}