UNPKG

@silvana-one/nft

Version:
556 lines 24.2 kB
import { Field, PublicKey, Bool, Struct, UInt32, UInt64, Provable, DynamicProof, FeatureFlags, Option, Gadgets, } from "o1js"; import { Storage } from "@silvana-one/storage"; export { MintParams, MintParamsOption, MintRequest, NFTDataPacked, NFTData, CollectionData, NFTState, NFTImmutableState, NFTUpdateProof, NFTStateStruct, UInt64Option, TransferBySignatureParams, TransferByProofParams, MAX_ROYALTY_FEE, NFTTransactionContext, TransferExtendedParams, }; class UInt64Option extends Option(UInt64) { } class NFTDataPacked extends Struct({ ownerX: Field, approvedX: Field, data: Field, }) { static assertEqual(a, b) { a.ownerX.assertEquals(b.ownerX); a.approvedX.assertEquals(b.approvedX); a.data.assertEquals(b.data); } } /** * Represents the on-chain state structure of an NFT. * The order of the fields is important and should match the NFT SmartContract. */ class NFTStateStruct extends Struct({ name: Field, metadata: Field, storage: Storage, packedData: NFTDataPacked, metadataVerificationKeyHash: Field, }) { /** * Creates an NFTStateStruct from an account's app state. * @param account The account containing the zkApp state. * @returns A new NFTStateStruct instance. */ static fromAccount(account) { if (!account.zkapp?.appState) { throw new Error("Invalid zkApp account state"); } if (NFTStateStruct.sizeInFields() !== account.zkapp?.appState.length) { throw new Error("Invalid NFTStateStruct size"); } return NFTStateStruct.fromFields(account.zkapp?.appState); } /** * Asserts that two NFTStateStruct instances are equal. * @param a The first NFTStateStruct instance. * @param b The second NFTStateStruct instance. */ static assertEqual(a, b) { a.name.assertEquals(b.name); a.metadata.assertEquals(b.metadata); Storage.assertEquals(a.storage, b.storage); NFTDataPacked.assertEqual(a.packedData, b.packedData); a.metadataVerificationKeyHash.assertEquals(b.metadataVerificationKeyHash); } } /** * Represents the immutable state of an NFT, containing read-only properties * and flags that determine the NFT's behavior and permissions. */ class NFTImmutableState extends Struct({ /** Determines if the NFT's ownership can be changed via a zero-knowledge proof (readonly). */ canChangeOwnerByProof: Bool, // readonly /** Specifies if the NFT's ownership can be transferred (readonly). */ canTransfer: Bool, // readonly /** Specifies if the NFT's approved address can be changed (readonly). */ canApprove: Bool, // readonly /** Indicates whether the NFT's metadata can be updated (readonly). */ canChangeMetadata: Bool, // readonly /** Determines whether the storage associated with the NFT can be altered (readonly). */ canChangeStorage: Bool, // readonly /** Specifies if the name of the NFT can be changed (readonly). */ canChangeName: Bool, // readonly /** Indicates whether the verification key hash for the metadata can be changed (readonly). */ canChangeMetadataVerificationKeyHash: Bool, // readonly /** Specifies if the NFT contract can be paused, preventing certain operations (readonly). */ canPause: Bool, // readonly /** The address of the NFT contract (readonly). */ address: PublicKey, // readonly /** The token ID associated with the NFT (readonly). */ tokenId: Field, // readonly /** The identifier of the NFT within the collection to be used off-chain(readonly). * It can be set to any value chosen by the creator for the new NFTs * and by default is set to 0. To uniquely identify the NFT, use the pair (NFT address, tokenId) or (collection address, NFT address) */ id: UInt64, // readonly }) { /** * Asserts that two NFTImmutableState instances are equal. * @param a The first NFTImmutableState instance. * @param b The second NFTImmutableState instance. */ static assertEqual(a, b) { a.canChangeOwnerByProof.assertEquals(b.canChangeOwnerByProof); a.canTransfer.assertEquals(b.canTransfer); a.canApprove.assertEquals(b.canApprove); a.canChangeMetadata.assertEquals(b.canChangeMetadata); a.canChangeStorage.assertEquals(b.canChangeStorage); a.canChangeName.assertEquals(b.canChangeName); a.canChangeMetadataVerificationKeyHash.assertEquals(b.canChangeMetadataVerificationKeyHash); a.canPause.assertEquals(b.canPause); a.address.assertEquals(b.address); a.tokenId.assertEquals(b.tokenId); a.id.assertEquals(b.id); } /** * Creates a new NFTImmutableState from NFTData and other parameters. * @param params The parameters including nftData, creator, address, and tokenId. * @returns A new NFTImmutableState instance. */ static fromNFTData(params) { const { nftData, address, tokenId } = params; return new NFTImmutableState({ address, tokenId, id: nftData.id, canChangeOwnerByProof: nftData.canChangeOwnerByProof, canTransfer: nftData.canTransfer, canApprove: nftData.canApprove, canChangeMetadata: nftData.canChangeMetadata, canChangeStorage: nftData.canChangeStorage, canChangeName: nftData.canChangeName, canChangeMetadataVerificationKeyHash: nftData.canChangeMetadataVerificationKeyHash, canPause: nftData.canPause, }); } } class NFTTransactionContext extends Struct({ /** Custom context that can be interpreted by the owner or approved contract. * Can hold Storage and root or two PublicKeys and UInt64 * In case of holding Storage and root, the contracts can fetch using witnessAsync any off-chain data with unlimited size * and verify it using the root. */ custom: Provable.Array(Field, 3), }) { static assertEqual(a, b) { for (let i = 0; i < 3; i++) { a.custom[i].assertEquals(b.custom[i]); } } } /** * Represents the full state of an NFT, including both immutable and mutable properties. */ class NFTState extends Struct({ /** The immutable state of the NFT. */ immutableState: NFTImmutableState, /** The name of the NFT. */ name: Field, /** The owner of the NFT. */ owner: PublicKey, /** The approved address of the NFT. */ approved: PublicKey, /** The metadata associated with the NFT. */ metadata: Field, /** The off-chain storage information (e.g., IPFS hash). */ storage: Storage, /** The version number of the NFT state. */ version: UInt64, /** Indicates whether the NFT contract is currently paused. */ isPaused: Bool, /** The hash of the verification key used for metadata proofs. */ metadataVerificationKeyHash: Field, /** The public key of the creator of the NFT (readonly). */ creator: PublicKey, // readonly /** The transaction context of the NFT. */ context: NFTTransactionContext, // readonly /** The oracle address to link the NFT update with the network and accounts state */ oracleAddress: PublicKey, // readonly }) { /** * Asserts that two NFTState instances are equal. * @param a The first NFTState instance. * @param b The second NFTState instance. */ static assertEqual(a, b) { NFTImmutableState.assertEqual(a.immutableState, b.immutableState); a.name.assertEquals(b.name); a.metadata.assertEquals(b.metadata); Storage.assertEquals(a.storage, b.storage); a.owner.assertEquals(b.owner); a.approved.assertEquals(b.approved); a.version.assertEquals(b.version); a.isPaused.assertEquals(b.isPaused); a.metadataVerificationKeyHash.assertEquals(b.metadataVerificationKeyHash); a.creator.assertEquals(b.creator); NFTTransactionContext.assertEqual(a.context, b.context); a.oracleAddress.assertEquals(b.oracleAddress); } /** * Creates a new NFTState from an NFTStateStruct and other parameters. * @param params The parameters including nftState, creator, address, and tokenId. * @returns A new NFTState instance. */ static fromNFTState(params) { const { nftState, creator, address, tokenId, context, oracleAddress } = params; const nftData = NFTData.unpack(nftState.packedData); const immutableState = NFTImmutableState.fromNFTData({ nftData, address, tokenId, }); return new NFTState({ immutableState, name: nftState.name, metadata: nftState.metadata, storage: nftState.storage, owner: nftData.owner, approved: nftData.approved, version: nftData.version, isPaused: nftData.isPaused, metadataVerificationKeyHash: nftState.metadataVerificationKeyHash, creator, context: context ?? NFTTransactionContext.empty(), oracleAddress: oracleAddress ?? PublicKey.empty(), }); } } /** * Represents a dynamic proof used for updating the state of an NFT. */ class NFTUpdateProof extends DynamicProof { static { this.publicInputType = NFTState; } static { this.publicOutputType = NFTState; } static { this.maxProofsVerified = 2; } static { this.featureFlags = FeatureFlags.allMaybe; } } /** * Represents the data associated with an NFT, including state and permission flags. */ class NFTData extends Struct({ /** The owner of the NFT. */ owner: PublicKey, /** The approved address of the NFT. */ approved: PublicKey, /** The version number of the NFT state. */ version: UInt64, /** The unique identifier of the NFT within the collection. */ id: UInt64, /** Determines whether the NFT's ownership can be changed via a zero-knowledge proof (readonly). * * It can be used only with update() and updateWithOracle() methods and * in this case overrides both canTransfer and canApprove flags used in the transfer methods */ canChangeOwnerByProof: Bool, // readonly /** Specifies if the NFT's ownership can be transferred (readonly). Applies * to transfer methods and can be bypassed by the update() and updateWithOracle() methods */ canTransfer: Bool, // readonly /** Specifies if the NFT's approved address can be changed (readonly). Transfer methods reset approved address to PublicKey.empty() * on transfer independently from the canApprove flag value */ canApprove: Bool, // readonly /** Indicates whether the NFT's metadata can be updated (readonly). */ canChangeMetadata: Bool, // readonly /** Determines whether the storage associated with the NFT can be altered (readonly). */ canChangeStorage: Bool, // readonly /** Specifies if the name of the NFT can be changed (readonly). */ canChangeName: Bool, // readonly /** Indicates whether the verification key hash for the metadata can be changed (readonly). */ canChangeMetadataVerificationKeyHash: Bool, // readonly /** Specifies if the NFT contract can be paused, preventing certain operations (readonly). */ canPause: Bool, // readonly /** Indicates whether the NFT contract is currently paused. */ isPaused: Bool, /** Determines whether the owner's authorization is required to upgrade the NFT's verification key (readonly). */ requireOwnerAuthorizationToUpgrade: Bool, // readonly }) { /** * Creates a new NFTData instance with optional parameters. * @param params The parameters to create the NFTData. * @returns A new NFTData instance. */ static new(params) { const { owner, approved, version, id, canChangeOwnerByProof, canTransfer, canApprove, canChangeMetadata, canChangeStorage, canChangeName, canChangeMetadataVerificationKeyHash, canPause, isPaused, requireOwnerAuthorizationToUpgrade, } = params; return new NFTData({ owner: typeof owner === "string" ? PublicKey.fromBase58(owner) : owner, approved: approved ? typeof approved === "string" ? PublicKey.fromBase58(approved) : approved : PublicKey.empty(), version: UInt64.from(BigInt(version ?? 0)), id: UInt64.from(BigInt(id ?? 0)), canChangeOwnerByProof: Bool(canChangeOwnerByProof ?? false), canTransfer: Bool(canTransfer ?? true), canApprove: Bool(canApprove ?? true), canChangeMetadata: Bool(canChangeMetadata ?? false), canChangeStorage: Bool(canChangeStorage ?? false), canChangeName: Bool(canChangeName ?? false), canChangeMetadataVerificationKeyHash: Bool(canChangeMetadataVerificationKeyHash ?? false), canPause: Bool(canPause ?? false), isPaused: Bool(isPaused ?? false), requireOwnerAuthorizationToUpgrade: Bool(requireOwnerAuthorizationToUpgrade ?? false), }); } /** * Packs the NFTData into a single Field for efficient storage. * @returns The packed Field representation of the NFTData. */ pack() { return new NFTDataPacked({ ownerX: this.owner.x, approvedX: this.approved.x, data: Field.fromBits([ this.canChangeOwnerByProof, this.canTransfer, this.canApprove, this.canChangeMetadata, this.canChangeStorage, this.canChangeName, this.canChangeMetadataVerificationKeyHash, this.canPause, this.isPaused, this.requireOwnerAuthorizationToUpgrade, this.owner.isOdd, this.approved.isOdd, ]) .add(Field(this.id.value).mul(Field(2 ** 12))) .add(Field(this.version.value).mul(Field(2 ** (12 + 64)))), }); } /** * Unpacks a Field into an NFTData instance. * @param packed The packed Field representation of the NFTData. * @returns A new NFTData instance. */ static unpack(packed) { const unpacked = Provable.witness(NFTData, () => { const bits = Gadgets.and(packed.data, Field(0xfffn), 12 + 64 + 64).toBits(12); const idField = Gadgets.and(packed.data, Field(0xffffffffffffffff000n), 12 + 64 + 64); const idBits = idField.toBits(64 + 12); // the next line relies on the constants 0xffffffffffffffff000n and 12 + 64 + 64 above const id = UInt64.Unsafe.fromField(Field.fromBits(idBits.slice(12, 64 + 12))); id.value.mul(Field(2 ** 12)).assertEquals(idField); const versionField = Gadgets.and(packed.data, Field(0xffffffffffffffff0000000000000000000n), 64 + 64 + 12); const versionBits = versionField.toBits(12 + 64 + 64); // the next line relies on the constants 0xffffffffffffffff0000000000000000000n and 12 + 64 + 64 above const version = UInt64.Unsafe.fromField(Field.fromBits(versionBits.slice(12 + 64, 12 + 64 + 64))); version.value.mul(Field(2 ** (12 + 64))).assertEquals(versionField); const canChangeOwnerByProof = bits[0]; const canTransfer = bits[1]; const canApprove = bits[2]; const canChangeMetadata = bits[3]; const canChangeStorage = bits[4]; const canChangeName = bits[5]; const canChangeMetadataVerificationKeyHash = bits[6]; const canPause = bits[7]; const isPaused = bits[8]; const requireOwnerAuthorizationToUpgrade = bits[9]; const ownerIsOdd = bits[10]; const approvedIsOdd = bits[11]; const owner = PublicKey.from({ x: packed.ownerX, isOdd: ownerIsOdd }); const approved = PublicKey.from({ x: packed.approvedX, isOdd: approvedIsOdd, }); return new NFTData({ owner, approved, id, version, canChangeOwnerByProof, canTransfer, canApprove, canChangeMetadata, canChangeStorage, canChangeName, canChangeMetadataVerificationKeyHash, canPause, isPaused, requireOwnerAuthorizationToUpgrade, }); }); NFTDataPacked.assertEqual(unpacked.pack(), packed); return unpacked; } } const MAX_ROYALTY_FEE = 100000; /** * Represents the data associated with an NFT collection, including configuration parameters and permission flags. */ class CollectionData extends Struct({ /** The royalty fee percentage (e.g., 1000 = 1%, 100 = 0.1%, 10000 = 10%, 100000 = 100%). */ royaltyFee: UInt32, // 1000 = 1%, 100 = 0.1%, 10000 = 10%, 100000 = 100% /** The transfer fee amount. */ transferFee: UInt64, /** If true, transferring NFTs requires approval from the admin contract. */ requireTransferApproval: Bool, /** If true, the minting is stopped and cannot be resumed. */ mintingIsLimited: Bool, /** Indicates whether the collection is currently paused. */ isPaused: Bool, /** The public key part (isOdd) of the pending creator. The x field is written to the contract state as pendingCreatorX */ pendingCreatorIsOdd: Bool, }) { /** * Creates a new CollectionData instance with specified parameters. * @param params The parameters to create the CollectionData. * @returns A new CollectionData instance. */ static new(params) { const { royaltyFee, transferFee, requireTransferApproval, mintingIsLimited, isPaused, } = params; return new CollectionData({ royaltyFee: UInt32.from(royaltyFee ?? 0), transferFee: UInt64.from(BigInt(transferFee ?? 0)), requireTransferApproval: Bool(requireTransferApproval ?? false), mintingIsLimited: Bool(mintingIsLimited ?? false), isPaused: Bool(isPaused ?? false), pendingCreatorIsOdd: Bool(PublicKey.empty().isOdd), }); } /** * Packs the CollectionData into a CollectionDataPacked representation for efficient storage. * @returns The packed CollectionDataPacked instance. */ pack() { return Field.fromBits([ this.isPaused, this.requireTransferApproval, this.mintingIsLimited, this.pendingCreatorIsOdd, ]) .add(Field(this.royaltyFee.value).mul(Field(2 ** 4))) .add(Field(this.transferFee.value).mul(Field(2 ** (4 + 32)))); } /** * Unpacks a CollectionDataPacked instance into a CollectionData instance. * @param packed The packed CollectionDataPacked instance. * @returns A new CollectionData instance. */ static unpack(packed) { const unpacked = Provable.witness(CollectionData, () => { const bits = Gadgets.and(packed, Field(0xfn), 4 + 32 + 64).toBits(4); const royaltyFeeField = Gadgets.and(packed, Field(0xffffffff0n), 4 + 32 + 64); const royaltyFeeBits = royaltyFeeField.toBits(4 + 32); // The next line relies on the constants 0xffffffff0n and 4 + 32 + 64 above const royaltyFee = UInt32.Unsafe.fromField(Field.fromBits(royaltyFeeBits.slice(4, 4 + 32))); royaltyFee.value.mul(Field(2 ** 4)).assertEquals(royaltyFeeField); const transferFeeField = Gadgets.and(packed, Field(0xffffffffffffffff000000000n), 4 + 32 + 64); const transferFeeBits = transferFeeField.toBits(4 + 32 + 64); // The next line relies on the constants 0xffffffffffffffff000000000n and 4 + 32 + 64 above const transferFee = UInt64.Unsafe.fromField(Field.fromBits(transferFeeBits.slice(4 + 32, 4 + 32 + 64))); transferFee.value .mul(Field(2 ** (4 + 32))) .assertEquals(transferFeeField); return new CollectionData({ isPaused: bits[0], requireTransferApproval: bits[1], mintingIsLimited: bits[2], pendingCreatorIsOdd: bits[3], royaltyFee, transferFee, }); }); unpacked.pack().assertEquals(packed); return unpacked; } static isPaused(packed) { return packed.toBits(4 + 32 + 64)[0]; } static requireTransferApproval(packed) { return packed.toBits(4 + 32 + 64)[1]; } } /** * Represents the parameters required for minting a new NFT. */ class MintParams extends Struct({ /** The name of the NFT. */ name: Field, /** The address of the NFT contract. */ address: PublicKey, /** The token ID of the NFT. */ tokenId: Field, /** The data associated with the NFT, including owner, approved, version, id, permissions and flags. */ data: NFTData, /** The fee associated with minting the NFT. */ fee: UInt64, /** The metadata associated with the NFT. */ metadata: Field, /** The off-chain storage information (e.g., IPFS hash). */ storage: Storage, /** The hash of the verification key used for metadata proofs. */ metadataVerificationKeyHash: Field, /** The expiry time slot for minting the NFT. */ expiry: UInt32, }) { } /** * Represents an optional MintParams, used in scenarios where minting may or may not be allowed. */ class MintParamsOption extends Option(MintParams) { } /** * Represents a request to mint a new NFT, used by the admin contract to determine if minting is allowed. */ class MintRequest extends Struct({ /** The address of the NFT contract where the NFT will be minted. */ address: PublicKey, /** The owner of the new NFT (can be different from the sender). */ owner: PublicKey, // can be different from the sender /** A custom value that can be interpreted by the admin contract. */ context: NFTTransactionContext, // should be interpreted by the admin contract }) { } /** * Represents the parameters required for transferring an NFT using a signature. */ class TransferBySignatureParams extends Struct({ /** The address of the NFT contract. */ address: PublicKey, /** The receiver's public key. */ to: PublicKey, /** Optional price for the transfer. */ price: UInt64Option, /** Custom value that can be interpreted by the owner or approved contract. */ context: NFTTransactionContext, }) { } /** * Represents the parameters required for transferring an NFT using a proof. */ class TransferByProofParams extends Struct({ /** The address of the NFT contract. */ address: PublicKey, /** The sender's public key. */ from: PublicKey, /** The receiver's public key. */ to: PublicKey, /** Optional price for the transfer. */ price: UInt64Option, /** Custom value that can be interpreted by the owner or approved contract. */ context: NFTTransactionContext, }) { } class TransferExtendedParams extends Struct({ /** The public key of the sender (current owner) before the transfer. */ from: PublicKey, /** The public key of the recipient (new owner) after the transfer. */ to: PublicKey, /** The public key of the collection. */ collection: PublicKey, /** The public key address of the NFT being transferred. */ nft: PublicKey, /** The fee paid for the transfer. */ fee: UInt64Option, /** The price of the NFT being transferred. */ price: UInt64Option, /** Indicates whether the transfer is by owner or by approved address. */ transferByOwner: Bool, /** The public key of the approved address. */ approved: PublicKey, /** Custom value that can be interpreted by the owner or approved contract. */ context: NFTTransactionContext, }) { } //# sourceMappingURL=types.js.map