diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 292a22bb..47dbba63 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,4 +13,4 @@ # Global: -* @OpenZeppelin/contracts-midnight-maintainers +* @OpenZeppelin/contracts-midnight-maintainers diff --git a/compact/tsconfig.json b/compact/tsconfig.json index 9c283308..18a3366a 100644 --- a/compact/tsconfig.json +++ b/compact/tsconfig.json @@ -1,25 +1,27 @@ { - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "lib": ["es2023"], - "module": "nodenext", - "target": "es2022", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "node16", - "sourceMap": true, - "rewriteRelativeImportExtensions": true, - "erasableSyntaxOnly": true, - "verbatimModuleSyntax": true - }, - "include": [ - "src/**/*" + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "lib": [ + "es2022" ], - "exclude": [ - "node_modules", - "dist" - ] + "module": "nodenext", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "nodenext", + "sourceMap": true, + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] } diff --git a/contracts/fungibleToken/src/test/FungibleToken.test.ts b/contracts/fungibleToken/src/test/FungibleToken.test.ts index 3939491c..3336bbac 100644 --- a/contracts/fungibleToken/src/test/FungibleToken.test.ts +++ b/contracts/fungibleToken/src/test/FungibleToken.test.ts @@ -1,7 +1,7 @@ import type { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { FungibleTokenSimulator } from './simulators/FungibleTokenSimulator'; -import * as utils from './utils/address'; +import { FungibleTokenSimulator } from './simulators/FungibleTokenSimulator.js'; +import * as utils from './utils/address.js'; // Metadata const EMPTY_STRING = ''; diff --git a/contracts/fungibleToken/src/test/simulators/FungibleTokenSimulator.ts b/contracts/fungibleToken/src/test/simulators/FungibleTokenSimulator.ts index fc898583..87ecba0b 100644 --- a/contracts/fungibleToken/src/test/simulators/FungibleTokenSimulator.ts +++ b/contracts/fungibleToken/src/test/simulators/FungibleTokenSimulator.ts @@ -18,8 +18,8 @@ import { import { type FungibleTokenPrivateState, FungibleTokenWitnesses, -} from '../../witnesses/FungibleTokenWitnesses'; -import type { IContractSimulator } from '../types/test'; +} from '../../witnesses/FungibleTokenWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of a FungibleToken contract for testing purposes. diff --git a/contracts/fungibleToken/src/test/utils/test.ts b/contracts/fungibleToken/src/test/utils/test.ts index d467e572..9fd2d4f6 100644 --- a/contracts/fungibleToken/src/test/utils/test.ts +++ b/contracts/fungibleToken/src/test/utils/test.ts @@ -6,7 +6,7 @@ import { QueryContext, emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; -import type { IContractSimulator } from '../types/test'; +import type { IContractSimulator } from '../types/test.js'; /** * Constructs a `CircuitContext` from the given state and sender information. diff --git a/contracts/fungibleToken/tsconfig.json b/contracts/fungibleToken/tsconfig.json index 3e90b0a9..4ae082c4 100644 --- a/contracts/fungibleToken/tsconfig.json +++ b/contracts/fungibleToken/tsconfig.json @@ -1,13 +1,17 @@ { - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts" + ], "compilerOptions": { "rootDir": "src", "outDir": "dist", "declaration": true, - "lib": ["ESNext"], + "lib": [ + "ES2022" + ], "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "allowJs": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": true, diff --git a/contracts/nonFungibleToken/package.json b/contracts/nonFungibleToken/package.json new file mode 100644 index 00000000..3afbc7d1 --- /dev/null +++ b/contracts/nonFungibleToken/package.json @@ -0,0 +1,32 @@ +{ + "name": "@openzeppelin-midnight/non-fungible-token", + "private": true, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "compact": "compact-compiler", + "build": "compact-builder && tsc", + "test": "vitest run", + "types": "tsc -p tsconfig.json --noEmit", + "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-midnight/compact": "workspace:^" + }, + "devDependencies": { + "@types/node": "22.14.0", + "ts-node": "^10.9.2", + "typescript": "^5.2.2", + "vitest": "^3.1.3" + } +} diff --git a/contracts/nonFungibleToken/src/NonFungibleToken.compact b/contracts/nonFungibleToken/src/NonFungibleToken.compact new file mode 100644 index 00000000..871bda25 --- /dev/null +++ b/contracts/nonFungibleToken/src/NonFungibleToken.compact @@ -0,0 +1,766 @@ +// SPDX-License-Identifier: MIT + +pragma language_version >= 0.15.0; + +/** + * @module NonFungibleToken + * @description An unshielded Non-Fungible Token library. + * + * @notice One notable difference regarding this implementation and the EIP20 spec + * consists of the token size. Uint<128> is used as the token size because Uint<256> + * cannot be supported. + * This is due to encoding limits on the midnight circuit backend: + * https://github.com/midnightntwrk/compactc/issues/929 + * + * @notice At the moment Midnight does not support contract-to-contract communication, but + * there are ongoing efforts to enable this in the future. Thus, the main circuits of this module + * restrict developers from sending tokens to contracts; however, we provide developers + * the ability to experiment with sending tokens to contracts using the `_unsafe` + * transfer methods. Once contract-to-contract communication is available we will follow the + * deprecation plan outlined below: + * + * Initial Minor Version Change: + * + * - Mark _unsafeTransfer as deprecated and emit a warning if possible. + * - Keep its implementation intact so existing callers continue to work. + * + * Later Major Version Change: + * + * - Drop _unsafeTransfer and remove `isContract` guard from `transfer`. + * - By this point, anyone using _unsafeTransfer should have migrated to the now C2C-capable `transfer`. + * + * @notice Missing Features and Improvements: + * + * - Uint256 token IDs + * - Transfer/Approval events + * - safeTransfer functions + * - _baseURI() support + * - An ERC165-like interface + */ + +module NonFungibleToken { + import CompactStandardLibrary; + import "../../node_modules/@openzeppelin-midnight/utils/src/Utils" prefix Utils_; + import "../../node_modules/@openzeppelin-midnight/utils/src/Initializable" prefix Initializable_; + + /// Public state + export sealed ledger _name: Opaque<"string">; + export sealed ledger _symbol: Opaque<"string">; + + /** + * @description Mapping from token IDs to their owner addresses. + * @type {Uint<128>} tokenId - The unique identifier for a token. + * @type {Either} owner - The owner address (public key or contract). + * @type {Map} + * @type {Map, Either>} _owners + */ + export ledger _owners: Map, Either>; + + /** + * @description Mapping from account addresses to their token balances. + * @type {Either} owner - The owner address. + * @type {Uint<128>} balance - The balance of the owner. + * @type {Map} + * @type {Map, Uint<128>>} _balances + */ + export ledger _balances: Map, Uint<128>>; + + /** + * @description Mapping from token IDs to approved addresses. + * @type {Uint<128>} tokenId - The unique identifier for a token. + * @type {Either} approved - The approved address (public key or contract). + * @type {Map} + * @type {Map, Either>} _tokenApprovals + */ + export ledger _tokenApprovals: Map, Either>; + + /** + * @description Mapping from owner addresses to operator approvals. + * @type {Either} owner - The owner address. + * @type {Either} operator - The operator address. + * @type {Boolean} approved - Whether the operator is approved. + * @type {Map>} + * @type {Map, Map, Boolean>>} _operatorApprovals + */ + export ledger _operatorApprovals: Map, Map, Boolean>>; + + /** + * @description Mapping from token IDs to their metadata URIs. + * @type {Uint<128>} tokenId - The unique identifier for a token. + * @type {Opaque<"string">} uri - The metadata URI for the token. + * @type {Map} + * @type {Map, Opaque<"string">>} _tokenURIs + */ + export ledger _tokenURIs: Map, Opaque<"string">>; + + /** + * @description Initializes the contract by setting the name and symbol. + * + * Requirements: + * - The contract must not have been initialized. + * + * @param {Opaque<"string">} name_ - The name of the token. + * @param {Opaque<"string">} symbol_ - The symbol of the token. + * @return {[]} - None. + */ + export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">): [] { + Initializable_initialize(); + _name = name_; + _symbol = symbol_; + } + + /** + * @description Returns the number of tokens in `owner`'s account. + * + * @circuitInfo k=10, rows=309 + * + * Requirements: + * - The contract must have been initialized. + * + * @param {Either)} owner - The account to query. + * @return {Uint<128>} - The number of tokens in `owner`'s account. + */ + export circuit balanceOf(owner: Either): Uint<128> { + Initializable_assertInitialized(); + if (!_balances.member(owner)) { + return 0; + } + + return _balances.lookup(owner); + } + + /** + * @description Returns the owner of the `tokenId` token. + * + * @circuitInfo k=10, rows=320 + * + * Requirements: + * - The contract must have been initialized. + * - The `tokenId` must exist. + * + * @param {Uint<128>} tokenId - The identifier for a token. + * @return {Either} - The account that owns the token. + */ + export circuit ownerOf(tokenId: Uint<128>): Either { + Initializable_assertInitialized(); + return _requireOwned(tokenId); + } + + /** + * @description Returns the token name. + * + * @circuitInfo k=10, rows=36 + * + * Requirements: + * - The contract must have been initialized. + * + * @return {Opaque<"string">} - The token name. + */ + export circuit name(): Opaque<"string"> { + Initializable_assertInitialized(); + return _name; + } + + /** + * @description Returns the symbol of the token. + * + * @circuitInfo k=10, rows=36 + * + * Requirements: + * - The contract must have been initialized. + * + * @return {Opaque<"string">} - The token symbol. + */ + export circuit symbol(): Opaque<"string"> { + Initializable_assertInitialized(); + return _symbol; + } + + /** + * @description Returns the token URI for the given `tokenId`. Returns the empty + * string if a tokenURI does not exist. + * + * @circuitInfo k=10, rows=326 + * + * Requirements: + * - The contract must have been initialized. + * - The `tokenId` must exist. + * + * @notice Midnight does not support native strings and string operations + * within the Compact language, e.g. concatenating a base URI + token ID are not possible + * like in other NFT implementations. Therefore, we propose the URI storage + * approach; whereby, NFTs may or may not have unique "base" URIs. + * It's up to the implementation to decide on how to handle this. + * + * @param {Uint<128>} tokenId - The identifier for a token. + * @returns {Opaque<"string">} - the token id's URI. + */ + export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { + Initializable_assertInitialized(); + _requireOwned(tokenId); + + if (!_tokenURIs.member(tokenId)) { + return Utils_emptyString(); + } + + return _tokenURIs.lookup(tokenId); + } + + /** + * @description Sets the the URI as `tokenURI` for the given `tokenId`. + * + * @circuitInfo k=10, rows=283 + * + * Requirements: + * - The contract must have been initialized. + * - The `tokenId` must exist. + * + * @notice The URI for a given NFT is usually set when the NFT is minted. + * + * @param {Uint<128>} tokenId - The identifier of the token. + * @param {Opaque<"string">} tokenURI - The URI of `tokenId`. + * @return {[]} - None. + */ + export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] { + Initializable_assertInitialized(); + _requireOwned(tokenId); + + return _tokenURIs.insert(tokenId, tokenURI); + } + + /** + * @description Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * @circuitInfo k=10, rows=993 + * + * Requirements: + * - The contract must have been initialized. + * - The caller must either own the token or be an approved operator. + * - `tokenId` must exist. + * + * @param {Either} to - The account receiving the approval + * @param {Uint<128>} tokenId - The token `to` may be permitted to transfer + * @return {[]} - None. + */ + export circuit approve( + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + const auth = left(own_public_key()); + _approve( + to, + tokenId, + auth + ); + } + + /** + * @description Returns the account approved for `tokenId` token. + * + * @circuitInfo k=10, rows=439 + * + * Requirements: + * - The contract must have been initialized. + * - `tokenId` must exist. + * + * @param {Uint<128>} tokenId - The token an account may be approved to manage + * @return {Either} Operator- The account approved to manage the token + */ + export circuit getApproved(tokenId: Uint<128>): Either { + Initializable_assertInitialized(); + _requireOwned(tokenId); + + return _getApproved(tokenId); + } + + /** + * @description Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} for any token owned by the caller. + * + * @circuitInfo k=10, rows=439 + * + * Requirements: + * - The contract must have been initialized. + * - The `operator` cannot be the address zero. + * + * @param {Either} operator - An operator to manage the caller's tokens + * @param {Boolean} approved - A boolean determining if `operator` may manage all tokens of the caller + * @return {[]} - None. + */ + export circuit setApprovalForAll( + operator: Either, + approved: Boolean + ): [] { + Initializable_assertInitialized(); + const owner = left(own_public_key()); + _setApprovalForAll( + owner, + operator, + approved + ); + } + + /** + * @description Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * @circuitInfo k=10, rows=621 + * + * Requirements: + * - The contract must have been initialized. + * + * @param {Either} owner - The owner of a token + * @param {Either} operator - An account that may operate on `owner`'s tokens + * @return {Boolean} - A boolean determining if `operator` is allowed to manage all of the tokens of `owner` + */ + export circuit isApprovedForAll( + owner: Either, + operator: Either + ): Boolean { + Initializable_assertInitialized(); + if (_operatorApprovals.member(owner) && _operatorApprovals.lookup(owner).member(operator)) { + return _operatorApprovals.lookup(owner).lookup(operator); + } else { + return false; + } + } + + /** + * @description Transfers `tokenId` token from `from` to `to`. + * + * @circuitInfo k=11, rows=2023 + * + * Requirements: + * - The contract must have been initialized. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `to` cannot be ContractAddress. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * @param {Either} from - The source account from which the token is being transfered + * @param {Either} to - The target account to transfer token to + * @param {Uint<128>} tokenId - The token being transfered + * @return {[]} - None. + */ + export circuit transferFrom( + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isContractAddress(to) "NonFungibleToken: Unsafe Transfer"; + + _unsafeTransferFrom(from, to, tokenId); + } + + /** + * @description Transfers `tokenId` token from `from` to `to`. It does NOT check if the recipient is a ContractAddress. + * + * @circuitInfo k=11, rows=2020 + * + * Requirements: + * - The contract must have been initialized. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. All transfers to external contracts will be permanently "stuck" at the + * ContractAddress + * + * @param {Either} from - The source account from which the token is being transfered + * @param {Either} to - The target account to transfer token to + * @param {Uint<128>} tokenId - The token being transfered + * @return {[]} - None. + */ + export circuit _unsafeTransferFrom( + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isKeyOrAddressZero(to) "NonFungibleToken: Invalid Receiver"; + // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists + // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here. + const auth = left(own_public_key()); + const previousOwner = _update( + to, + tokenId, + auth + ); + assert previousOwner == from "NonFungibleToken: Incorrect Owner"; + } + + /** + * @description Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist + * + * @circuitInfo k=10, rows=253 + * + * Requirements: + * - The contract must have been initialized. + * + * @param {Uint<128>} tokenId - The target token of the owner query + * @return {Either} - The owner of the token + */ + export circuit _ownerOf(tokenId: Uint<128>): Either { + Initializable_assertInitialized(); + if (!_owners.member(tokenId)) { + return burn_address(); + } + + return _owners.lookup(tokenId); + } + + /** + * @description Returns the approved address for `tokenId`. Returns the zero address if `tokenId` is not minted. + * + * @circuitInfo k=10, rows=253 + * + * Requirements: + * - The contract must have been initialized. + * + * @param {Uint<128>} tokenId - The token to query + * @return {Either} - An account approved to spend `tokenId` + */ + export circuit _getApproved(tokenId: Uint<128>): Either { + Initializable_assertInitialized(); + if (!_tokenApprovals.member(tokenId)) { + return burn_address(); + } + return _tokenApprovals.lookup(tokenId); + } + + /** + * @description Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in + * particular (ignoring whether it is owned by `owner`). + * + * @circuitInfo k=11, rows=1128 + * + * Requirements: + * - The contract must have been initialized. + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + * + * @param {Either} owner - Owner of the token + * @param {Either} spender - Account that wishes to spend `tokenId` + * @param {Uint<128>} tokenId - Token to spend + * @return {Boolean} - A boolean determining if `spender` may manage `tokenId` + */ + export circuit _isAuthorized( + owner: Either, + spender: Either, + tokenId: Uint<128> + ): Boolean { + Initializable_assertInitialized(); + return ( + !Utils_isKeyOrAddressZero(spender) && + (owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender) + ); + } + + /** + * @description Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. + * + * @circuitInfo k=11, rows=1181 + * + * Requirements: + * - The contract must have been initialized. + * - `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + * + * @param {Either} owner - Owner of the token + * @param {Either} spender - Account operating on `tokenId` + * @param {Uint<128>} tokenId - The token to spend + * @return {[]} - None. + */ + export circuit _checkAuthorized( + owner: Either, + spender: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + if (!_isAuthorized(owner, spender, tokenId)) { + assert !Utils_isKeyOrAddressZero(owner) "NonFungibleToken: Nonexistent Token"; + assert false "NonFungibleToken: Insufficient Approval"; + } + } + + /** + * @description Transfers `tokenId` from its current owner to `to`, or alternatively mints (or burns) if the current owner + * (or `to`) is the zero address. Returns the owner of the `tokenId` before the update. + * + * @circuitInfo k=12, rows=2049 + * + * Requirements: + * - The contract must have been initialized. + * - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * or approved to operate on the token (by the owner). + * + * @param {Either} to - The intended recipient of the token transfer + * @param {Uint<128>} tokenId - The token being transfered + * @param {Either} auth - An account authorized to transfer the token + * @return {Either} - Owner of the token before it was transfered + */ + circuit _update( + to: Either, + tokenId: Uint<128>, + auth: Either + ): Either { + Initializable_assertInitialized(); + const from = _ownerOf(tokenId); + + // Perform (optional) operator check + if (!Utils_isKeyOrAddressZero(auth)) { + _checkAuthorized(from, auth, tokenId); + } + + // Execute the update + if (!Utils_isKeyOrAddressZero(from)) { + // Clear approval. No need to re-authorize + _approve(burn_address(), tokenId, burn_address()); + const newBalance = _balances.lookup(from) - 1 as Uint<128>; + _balances.insert(from, newBalance); + } + + if (!Utils_isKeyOrAddressZero(to)) { + if (!_balances.member(to)) { + _balances.insert(to, 0); + } + const newBalance = _balances.lookup(to) + 1 as Uint<128>; + _balances.insert(to, newBalance); + } + + _owners.insert(tokenId, to); + + return from; + } + + /** + * @description Mints `tokenId` and transfers it to `to`. + * + * @circuitInfo k=11, rows=1073 + * + * Requirements: + * - The contract must have been initialized. + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * - `to` cannot be ContractAddress. + * + * @param {Either} to - The account receiving `tokenId` + * @param {Uint<128>} tokenId - The token to transfer + * @return {[]} - None. + */ + export circuit _mint( + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isContractAddress(to) "NonFungibleToken: Unsafe Transfer"; + + _unsafeMint(to, tokenId); + } + + /** + * @description Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress. + * + * @circuitInfo k=11, rows=1070 + * + * Requirements: + * - The contract must have been initialized. + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. + * + * @param {Either} to - The account receiving `tokenId` + * @param {Uint<128>} tokenId - The token to transfer + * @return {[]} - None. + */ + export circuit _unsafeMint( + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isKeyOrAddressZero(to) "NonFungibleToken: Invalid Receiver"; + + const previousOwner = _update(to, tokenId, burn_address()); + + assert Utils_isKeyOrAddressZero(previousOwner) "NonFungibleToken: Invalid Sender"; + } + + /** + * @description Destroys `tokenId`. + * The approval is cleared when the token is burned. + * This circuit does not check if the sender is authorized to operate on the token. + * + * @circuitInfo k=10, rows=509 + * + * Requirements: + * - The contract must have been initialized. + * - `tokenId` must exist. + * + * @param {Uint<128>} tokenId - The token to burn + * @return {[]} - None. + */ + export circuit _burn(tokenId: Uint<128>): [] { + Initializable_assertInitialized(); + const previousOwner = _update(burn_address(), tokenId, burn_address()); + assert !Utils_isKeyOrAddressZero(previousOwner) "NonFungibleToken: Invalid Sender"; + } + + /** + * @description Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on own_public_key(). + * + * @circuitInfo k=11, rows=1284 + * + * Requirements: + * - The contract must have been initialized. + * - `to` cannot be the zero address. + * - `to` cannot be ContractAddress. + * - `tokenId` token must be owned by `from`. + * + * @param {Either} from - The source account of the token transfer + * @param {Either} to - The target account of the token transfer + * @param {Uint<128>} tokenId - The token to transfer + * @return {[]} - None. + */ + export circuit _transfer( + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isContractAddress(to) "NonFungibleToken: Unsafe Transfer"; + + _unsafeTransfer(from, to, tokenId); + } + + /** + * @description Transfers `tokenId` from `from` to `to`. + * As opposed to {_unsafeTransferFrom}, this imposes no restrictions on own_public_key(). + * It does NOT check if the recipient is a ContractAddress. + * + * @circuitInfo k=11, rows=1281 + * + * Requirements: + * - The contract must have been initialized. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. All transfers to external contracts will be permanently "stuck" at the + * ContractAddress + * + * @param {Either} from - The source account of the token transfer + * @param {Either} to - The target account of the token transfer + * @param {Uint<128>} tokenId - The token to transfer + * @return {[]} - None. + */ + export circuit _unsafeTransfer( + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { + Initializable_assertInitialized(); + assert !Utils_isKeyOrAddressZero(to) "NonFungibleToken: Invalid Receiver"; + + const previousOwner = _update(to, tokenId, burn_address()); + + assert !Utils_isKeyOrAddressZero(previousOwner) "NonFungibleToken: Nonexistent Token"; + assert previousOwner == from "NonFungibleToken: Incorrect Owner"; + } + + /** + * @description Approve `to` to operate on `tokenId` + * + * @circuitInfo k=11, rows=1169 + * + * Requirements: + * - The contract must have been initialized. + * - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * or approved to operate on the token (by the owner). + * + * @param {Either} to - The target account to approve + * @param {Uint<128>} tokenId - The token to approve + * @param {Either} auth - An account authorized to operate on all tokens held by the owner the token + * @return {[]} - None. + */ + export circuit _approve( + to: Either, + tokenId: Uint<128>, + auth: Either + ): [] { + Initializable_assertInitialized(); + if (!Utils_isKeyOrAddressZero(auth)) { + const owner = _requireOwned(tokenId); + + // We do not use _isAuthorized because single-token approvals should not be able to call approve + assert (owner == auth || isApprovedForAll(owner, auth)) "NonFungibleToken: Invalid Approver"; + } + + _tokenApprovals.insert(tokenId, to); + } + + /** + * @description Approve `operator` to operate on all of `owner` tokens + * + * @circuitInfo k=10, rows=554 + * + * Requirements: + * - The contract must have been initialized. + * - `operator` can't be the address zero. + * + * @param {Either} owner - Owner of a token + * @param {Either} operator - The account to approve + * @param {Boolean} approved - A boolean determining if `operator` may operate on all of `owner` tokens + * @return {[]} - None. + */ + export circuit _setApprovalForAll( + owner: Either, + operator: Either, + approved: Boolean + ): [] { + Initializable_assertInitialized(); + assert !Utils_isKeyOrAddressZero(operator) "NonFungibleToken: Invalid Operator"; + + if (!_operatorApprovals.member(owner)) { + _operatorApprovals.insert( + owner, + default, Boolean>> + ); + } + + _operatorApprovals.lookup(owner).insert(operator, approved); + } + + /** + * @description Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). + * Returns the owner. + * + * @circuitInfo k=10, rows=318 + * + * Requirements: + * - The contract must have been initialized. + * - `tokenId` must exist. + * + * @param {Uint<128>} tokenId - The token that should be owned + * @return {Either} - The owner of `tokenId` + */ + export circuit _requireOwned(tokenId: Uint<128>): Either { + Initializable_assertInitialized(); + const owner = _ownerOf(tokenId); + + assert !Utils_isKeyOrAddressZero(owner) "NonFungibleToken: Nonexistent Token"; + return owner; + } +} diff --git a/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact b/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact new file mode 100644 index 00000000..29a91a1e --- /dev/null +++ b/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact @@ -0,0 +1,166 @@ +pragma language_version >= 0.15.0; + +import CompactStandardLibrary; + +import "../../NonFungibleToken" prefix NonFungibleToken_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + +/** + * @description `init` is a param for testing. + * If `init` is true, initialize the contract with `_name` and `_symbol`. + * Otherwise, the contract will not initialize and we can test the + * contract when it is not initialized properly. +*/ +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + init: Boolean +) { + if (init) { + NonFungibleToken_initialize(_name, _symbol); + } +} + +export circuit name(): Opaque<"string"> { + return NonFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return NonFungibleToken_symbol(); +} + +export circuit balanceOf(account: Either): Uint<128> { + return NonFungibleToken_balanceOf(account); +} + +export circuit ownerOf(tokenId: Uint<128>): Either { + return NonFungibleToken_ownerOf(tokenId); +} + +export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { + return NonFungibleToken_tokenURI(tokenId); +} + +export circuit approve( + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken_approve(to, tokenId); +} + +export circuit getApproved(tokenId: Uint<128>): Either { + return NonFungibleToken_getApproved(tokenId); +} + +export circuit setApprovalForAll( + operator: Either, + approved: Boolean +): [] { + return NonFungibleToken_setApprovalForAll(operator, approved); +} + +export circuit isApprovedForAll( + owner: Either, + operator: Either +): Boolean { + return NonFungibleToken_isApprovedForAll(owner, operator); +} + +export circuit transferFrom( + from: Either, + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken_transferFrom(from, to, tokenId); +} + +export circuit _requireOwned(tokenId: Uint<128>): Either { + return NonFungibleToken__requireOwned(tokenId); +} + +export circuit _ownerOf(tokenId: Uint<128>): Either { + return NonFungibleToken__ownerOf(tokenId); +} + +export circuit _approve( + to: Either, + tokenId: Uint<128>, + auth: Either +): [] { + return NonFungibleToken__approve(to, tokenId, auth); +} + +export circuit _checkAuthorized( + owner: Either, + spender: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__checkAuthorized(owner, spender, tokenId); +} + +export circuit _isAuthorized( + owner: Either, + spender: Either, + tokenId: Uint<128> +): Boolean { + return NonFungibleToken__isAuthorized(owner, spender, tokenId); +} + +export circuit _getApproved(tokenId: Uint<128>): Either { + return NonFungibleToken__getApproved(tokenId); +} + +export circuit _setApprovalForAll( + owner: Either, + operator: Either, + approved: Boolean +): [] { + return NonFungibleToken__setApprovalForAll(owner, operator, approved); +} + +export circuit _mint( + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__mint(to, tokenId); +} + +export circuit _burn(tokenId: Uint<128>): [] { + return NonFungibleToken__burn(tokenId); +} + +export circuit _transfer( + from: Either, + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__transfer(from, to, tokenId); +} + +export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] { + return NonFungibleToken__setTokenURI(tokenId, tokenURI); +} + +export circuit _unsafeTransferFrom( + from: Either, + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__unsafeTransferFrom(from, to, tokenId); +} + +export circuit _unsafeTransfer( + from: Either, + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__unsafeTransfer(from, to, tokenId); +} + +export circuit _unsafeMint( + to: Either, + tokenId: Uint<128> +): [] { + return NonFungibleToken__unsafeMint(to, tokenId); +} diff --git a/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts b/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts new file mode 100644 index 00000000..8fc5e6e9 --- /dev/null +++ b/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts @@ -0,0 +1,1136 @@ +import type { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { NonFungibleTokenSimulator } from './simulators/NonFungibleTokenSimulator.js'; +import { + ZERO_ADDRESS, + ZERO_KEY, + createEitherTestContractAddress, + createEitherTestUser, + toHexPadded, +} from './utils/address.js'; + +// Contract Metadata +const NAME = 'NAME'; +const SYMBOL = 'SYMBOL'; +const EMPTY_STRING = ''; +const INIT = true; +const BAD_INIT = false; + +// Token Metadata +const TOKENID_1: bigint = BigInt(1); +const TOKENID_2: bigint = BigInt(2); +const TOKENID_3: bigint = BigInt(3); +const NON_EXISTENT_TOKEN: bigint = BigInt(0xdead); +const SOME_URI = 'https://openzeppelin.example'; +const EMPTY_URI = ''; +const AMOUNT: bigint = BigInt(1); + +// Callers +const OWNER = toHexPadded('OWNER'); +const SPENDER = toHexPadded('SPENDER'); +const UNAUTHORIZED = toHexPadded('UNAUTHORIZED'); + +// Encoded PK/Addresses +const Z_OWNER = createEitherTestUser('OWNER'); +const Z_SPENDER = createEitherTestUser('SPENDER'); +const Z_RECIPIENT = createEitherTestUser('RECIPIENT'); +const Z_OTHER = createEitherTestUser('OTHER'); +const Z_UNAUTHORIZED = createEitherTestUser('UNAUTHORIZED'); +const SOME_CONTRACT = createEitherTestContractAddress('CONTRACT'); + +let token: NonFungibleTokenSimulator; +let _caller: CoinPublicKey; + +describe('NonFungibleToken', () => { + describe('initializer and metadata', () => { + it('should initialize metadata', () => { + token = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); + + expect(token.name()).toEqual(NAME); + expect(token.symbol()).toEqual(SYMBOL); + }); + + it('should initialize empty metadata', () => { + token = new NonFungibleTokenSimulator(EMPTY_STRING, EMPTY_STRING, INIT); + + expect(token.name()).toEqual(EMPTY_STRING); + expect(token.symbol()).toEqual(EMPTY_STRING); + }); + + it('should initialize metadata with whitespace', () => { + token = new NonFungibleTokenSimulator(' NAME ', ' SYMBOL ', INIT); + expect(token.name()).toEqual(' NAME '); + expect(token.symbol()).toEqual(' SYMBOL '); + }); + + it('should initialize metadata with special characters', () => { + token = new NonFungibleTokenSimulator('NAME!@#', 'SYMBOL$%^', INIT); + expect(token.name()).toEqual('NAME!@#'); + expect(token.symbol()).toEqual('SYMBOL$%^'); + }); + + it('should initialize metadata with very long strings', () => { + const longName = 'A'.repeat(1000); + const longSymbol = 'B'.repeat(1000); + token = new NonFungibleTokenSimulator(longName, longSymbol, INIT); + expect(token.name()).toEqual(longName); + expect(token.symbol()).toEqual(longSymbol); + }); + }); + + beforeEach(() => { + token = new NonFungibleTokenSimulator(NAME, SYMBOL, INIT); + }); + + describe('balanceOf', () => { + it('should return zero when requested account has no balance', () => { + expect(token.balanceOf(Z_OWNER)).toEqual(0n); + }); + + it('should return balance when requested account has tokens', () => { + token._mint(Z_OWNER, AMOUNT); + expect(token.balanceOf(Z_OWNER)).toEqual(AMOUNT); + }); + + it('should return correct balance for multiple tokens', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + expect(token.balanceOf(Z_OWNER)).toEqual(3n); + }); + + it('should return correct balance after burning multiple tokens', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + token._burn(TOKENID_1); + token._burn(TOKENID_2); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + }); + + it('should return correct balance after transferring multiple tokens', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + token._transfer(Z_OWNER, Z_RECIPIENT, TOKENID_1); + token._transfer(Z_OWNER, Z_RECIPIENT, TOKENID_2); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + expect(token.balanceOf(Z_RECIPIENT)).toEqual(2n); + }); + }); + + describe('ownerOf', () => { + it('should throw if token does not exist', () => { + expect(() => { + token.ownerOf(NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should throw if token has been burned', () => { + token._mint(Z_OWNER, TOKENID_1); + token._burn(TOKENID_1); + expect(() => { + token.ownerOf(TOKENID_1); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should return owner of token if it exists', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + }); + + it('should return correct owner for multiple tokens', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + expect(token.ownerOf(TOKENID_2)).toEqual(Z_OWNER); + expect(token.ownerOf(TOKENID_3)).toEqual(Z_OWNER); + }); + + it('should return correct owner after multiple transfers', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); + token._transfer(Z_OWNER, Z_OTHER, TOKENID_2); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + expect(token.ownerOf(TOKENID_2)).toEqual(Z_OTHER); + }); + + it('should return correct owner after multiple burns and mints', () => { + token._mint(Z_OWNER, TOKENID_1); + token._burn(TOKENID_1); + token._mint(Z_SPENDER, TOKENID_1); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + }); + + describe('tokenURI', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should throw if token does not exist', () => { + expect(() => { + token.tokenURI(NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should return the empty string for an unset tokenURI', () => { + expect(token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); + }); + + it('should return the empty string if tokenURI set as default value', () => { + token._setTokenURI(TOKENID_1, EMPTY_URI); + expect(token.tokenURI(TOKENID_1)).toEqual(EMPTY_URI); + }); + + it('should return some string if tokenURI is set', () => { + token._setTokenURI(TOKENID_1, SOME_URI); + expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + }); + + it('should return very long tokenURI', () => { + const longURI = 'A'.repeat(1000); + token._setTokenURI(TOKENID_1, longURI); + expect(token.tokenURI(TOKENID_1)).toEqual(longURI); + }); + + it('should return tokenURI with special characters', () => { + const specialURI = '!@#$%^&*()_+'; + token._setTokenURI(TOKENID_1, specialURI); + expect(token.tokenURI(TOKENID_1)).toEqual(specialURI); + }); + + it('should update tokenURI multiple times', () => { + token._setTokenURI(TOKENID_1, 'URI1'); + token._setTokenURI(TOKENID_1, 'URI2'); + token._setTokenURI(TOKENID_1, 'URI3'); + expect(token.tokenURI(TOKENID_1)).toEqual('URI3'); + }); + + it('should maintain tokenURI after token transfer', () => { + token._setTokenURI(TOKENID_1, SOME_URI); + token._transfer(Z_OWNER, Z_RECIPIENT, TOKENID_1); + expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + }); + }); + + describe('approve', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + + it('should throw if not owner', () => { + _caller = UNAUTHORIZED; + expect(() => { + token.approve(Z_SPENDER, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Invalid Approver'); + }); + + it('should approve spender', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should allow operator to approve', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, _caller); + _caller = SPENDER; + token.approve(Z_OTHER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_OTHER); + }); + + it('spender approved for only TOKENID_1 should not be able to approve', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + + _caller = SPENDER; + expect(() => { + token.approve(Z_OTHER, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Invalid Approver'); + }); + + it('should approve same address multiple times', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should approve after token transfer', () => { + _caller = OWNER; + token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); + + _caller = SPENDER; + token.approve(Z_OTHER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_OTHER); + }); + + it('should approve after token burn and remint', () => { + _caller = OWNER; + token._burn(TOKENID_1); + token._mint(Z_OWNER, TOKENID_1); + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should approve with very long token ID', () => { + _caller = OWNER; + const longTokenId = BigInt('18446744073709551615'); + token._mint(Z_OWNER, longTokenId); + token.approve(Z_SPENDER, longTokenId, _caller); + expect(token.getApproved(longTokenId)).toEqual(Z_SPENDER); + }); + }); + + describe('getApproved', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should throw if token does not exist', () => { + expect(() => { + token.getApproved(NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should throw if token has been burned', () => { + token._burn(TOKENID_1); + expect(() => { + token.getApproved(TOKENID_1); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should get current approved spender', () => { + _caller = OWNER; + token.approve(Z_OWNER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_OWNER); + }); + + it('should return zero key if approval not set', () => { + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); + + describe('setApprovalForAll', () => { + it('should not approve zero address', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token.setApprovalForAll(ZERO_KEY, true, _caller); + }).toThrow('NonFungibleToken: Invalid Operator'); + }); + + it('should set operator', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + + token.setApprovalForAll(Z_SPENDER, true, OWNER); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + }); + + it('should allow operator to manage owner tokens', () => { + token._mint(Z_OWNER, TOKENID_1); + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, _caller); + + _caller = SPENDER; + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + + token.approve(Z_OTHER, TOKENID_2, _caller); + expect(token.getApproved(TOKENID_2)).toEqual(Z_OTHER); + + token.approve(Z_SPENDER, TOKENID_3, _caller); + expect(token.getApproved(TOKENID_3)).toEqual(Z_SPENDER); + }); + + it('should revoke approval for all', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + + token.setApprovalForAll(Z_SPENDER, false, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false); + + _caller = SPENDER; + expect(() => { + token.approve(Z_SPENDER, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Invalid Approver'); + }); + + it('should set approval for all to same address multiple times', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, _caller); + token.setApprovalForAll(Z_SPENDER, true, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + }); + + it('should set approval for all after token transfer', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); + + _caller = SPENDER; + token.setApprovalForAll(Z_OTHER, true, _caller); + expect(token.isApprovedForAll(Z_SPENDER, Z_OTHER)).toBe(true); + }); + + it('should set approval for all with multiple operators', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, _caller); + token.setApprovalForAll(Z_OTHER, true, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + expect(token.isApprovedForAll(Z_OWNER, Z_OTHER)).toBe(true); + }); + + it('should set approval for all with very long token IDs', () => { + _caller = OWNER; + const longTokenId = BigInt('18446744073709551615'); + token._mint(Z_OWNER, longTokenId); + token.setApprovalForAll(Z_SPENDER, true, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + }); + }); + + describe('isApprovedForAll', () => { + it('should return false if approval not set', () => { + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false); + }); + + it('should return true if approval set', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, OWNER); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + }); + }); + + describe('transferFrom', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should not transfer to ContractAddress', () => { + expect(() => { + token.transferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1); + }).toThrow('NonFungibleToken: Unsafe Transfer'); + }); + + it('should not transfer to zero address', () => { + expect(() => { + token.transferFrom(Z_OWNER, ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should not transfer from zero address', () => { + expect(() => { + token.transferFrom(ZERO_KEY, Z_SPENDER, TOKENID_1); + }).toThrow('NonFungibleToken: Incorrect Owner'); + }); + + it('should not transfer from unauthorized', () => { + _caller = UNAUTHORIZED; + expect(() => { + token.transferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + + it('should not transfer token that has not been minted', () => { + _caller = OWNER; + expect(() => { + token.transferFrom(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN, _caller); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should transfer token without approvers or operators', () => { + _caller = OWNER; + token.transferFrom(Z_OWNER, Z_RECIPIENT, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_RECIPIENT); + }); + + it('should transfer token via approved operator', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, OWNER); + + _caller = SPENDER; + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should transfer token via approvedForAll operator', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, OWNER); + + _caller = SPENDER; + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should allow transfer to same address', () => { + _caller = OWNER; + token._approve(Z_SPENDER, TOKENID_1, Z_OWNER); + token._setApprovalForAll(Z_OWNER, Z_SPENDER, true); + + expect(() => { + token.transferFrom(Z_OWNER, Z_OWNER, TOKENID_1, _caller); + }).not.toThrow(); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + expect(token._isAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1)).toEqual(true); + }); + + it('should not transfer after approval revocation', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token.approve(ZERO_KEY, TOKENID_1, _caller); + + _caller = SPENDER; + expect(() => { + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + + it('should not transfer after approval for all revocation', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, _caller); + token.setApprovalForAll(Z_SPENDER, false, _caller); + + _caller = SPENDER; + expect(() => { + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + + it('should transfer multiple tokens in sequence', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + + token.approve(Z_SPENDER, TOKENID_1, _caller); + token.approve(Z_SPENDER, TOKENID_2, _caller); + token.approve(Z_SPENDER, TOKENID_3, _caller); + + _caller = SPENDER; + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2, _caller); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3, _caller); + + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + expect(token.ownerOf(TOKENID_2)).toEqual(Z_SPENDER); + expect(token.ownerOf(TOKENID_3)).toEqual(Z_SPENDER); + }); + + it('should transfer with very long token IDs', () => { + _caller = OWNER; + const longTokenId = BigInt('18446744073709551615'); + token._mint(Z_OWNER, longTokenId); + token.approve(Z_SPENDER, longTokenId, _caller); + + _caller = SPENDER; + token.transferFrom(Z_OWNER, Z_SPENDER, longTokenId, _caller); + expect(token.ownerOf(longTokenId)).toEqual(Z_SPENDER); + }); + + it('should revoke approval after transferFrom', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._setApprovalForAll(Z_OWNER, Z_SPENDER, true); + + token.transferFrom(Z_OWNER, Z_OTHER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + expect(token._isAuthorized(Z_OTHER, Z_SPENDER, TOKENID_1)).toBe(false); + + _caller = SPENDER; + expect(() => { + token.approve(Z_UNAUTHORIZED, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Invalid Approver'); + expect(() => { + token.transferFrom(Z_OTHER, Z_UNAUTHORIZED, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + }); + + describe('_requireOwned', () => { + it('should throw if token has not been minted', () => { + expect(() => { + token._requireOwned(TOKENID_1); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should throw if token has been burned', () => { + token._mint(Z_OWNER, TOKENID_1); + token._burn(TOKENID_1); + expect(() => { + token._requireOwned(TOKENID_1); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should return correct owner', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(token._requireOwned(TOKENID_1)).toEqual(Z_OWNER); + }); + }); + + describe('_ownerOf', () => { + it('should return zero address if token does not exist', () => { + expect(token._ownerOf(NON_EXISTENT_TOKEN)).toEqual(ZERO_KEY); + }); + + it('should return owner of token', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(token._ownerOf(TOKENID_1)).toEqual(Z_OWNER); + }); + }); + + describe('_approve', () => { + it('should approve if auth is owner', () => { + token._mint(Z_OWNER, TOKENID_1); + token._approve(Z_SPENDER, TOKENID_1, Z_OWNER); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should approve if auth is approved for all', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, _caller); + token._approve(Z_SPENDER, TOKENID_1, Z_SPENDER); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should throw if auth is unauthorized', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token._approve(Z_SPENDER, TOKENID_1, Z_UNAUTHORIZED); + }).toThrow('NonFungibleToken: Invalid Approver'); + }); + + it('should approve if auth is zero address', () => { + token._mint(Z_OWNER, TOKENID_1); + token._approve(Z_SPENDER, TOKENID_1, ZERO_KEY); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + }); + + describe('_checkAuthorized', () => { + it('should throw if token not minted', () => { + expect(() => { + token._checkAuthorized(ZERO_KEY, Z_OWNER, TOKENID_1); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should throw if unauthorized', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token._checkAuthorized(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + + it('should not throw if approved', () => { + token._mint(Z_OWNER, TOKENID_1); + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._checkAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1); + }); + + it('should not throw if approvedForAll', () => { + token._mint(Z_OWNER, TOKENID_1); + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, _caller); + token._checkAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1); + }); + }); + + describe('_isAuthorized', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should return true if spender is authorized', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token._isAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1)).toBe(true); + }); + + it('should return true if spender is authorized for all', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, _caller); + expect(token._isAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1)).toBe(true); + }); + + it('should return true if spender is owner', () => { + expect(token._isAuthorized(Z_OWNER, Z_OWNER, TOKENID_1)).toBe(true); + }); + + it('should return false if spender is zero address', () => { + expect(token._isAuthorized(Z_OWNER, ZERO_KEY, TOKENID_1)).toBe(false); + }); + + it('should return false for unauthorized', () => { + expect(token._isAuthorized(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1)).toBe( + false, + ); + }); + }); + + describe('_getApproved', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should return zero address if token is not minted', () => { + expect(token._getApproved(NON_EXISTENT_TOKEN)).toEqual(ZERO_KEY); + }); + + it('should return approved address', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token._getApproved(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should return zero address if no approvals', () => { + expect(token._getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); + + describe('_setApprovalForAll', () => { + it('should approve operator', () => { + token._mint(Z_OWNER, TOKENID_1); + token._setApprovalForAll(Z_OWNER, Z_SPENDER, true); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + }); + + it('should revoke operator approval', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.setApprovalForAll(Z_SPENDER, true, _caller); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); + + token._setApprovalForAll(Z_OWNER, Z_SPENDER, false); + expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false); + }); + + it('should throw if operator is zero address', () => { + expect(() => { + token._setApprovalForAll(Z_OWNER, ZERO_KEY, true); + }).toThrow('NonFungibleToken: Invalid Operator'); + }); + }); + + describe('_mint', () => { + it('should not mint to ContractAddress', () => { + expect(() => { + token._mint(SOME_CONTRACT, TOKENID_1); + }).toThrow('NonFungibleToken: Unsafe Transfer'); + }); + + it('should not mint to zero address', () => { + expect(() => { + token._mint(ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should not mint a token that already exists', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token._mint(Z_OWNER, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Sender'); + }); + + it('should mint token', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + expect(token.balanceOf(Z_OWNER)).toEqual(3n); + }); + + it('should mint multiple tokens in sequence', () => { + for (let i = 0; i < 10; i++) { + token._mint(Z_OWNER, TOKENID_1 + BigInt(i)); + } + expect(token.balanceOf(Z_OWNER)).toEqual(10n); + }); + + it('should mint with very long token IDs', () => { + const longTokenId = BigInt('18446744073709551615'); + token._mint(Z_OWNER, longTokenId); + expect(token.ownerOf(longTokenId)).toEqual(Z_OWNER); + }); + + it('should mint after burning', () => { + token._mint(Z_OWNER, TOKENID_1); + token._burn(TOKENID_1); + token._mint(Z_OWNER, TOKENID_1); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + }); + + it('should mint with special characters in metadata', () => { + token._mint(Z_OWNER, TOKENID_1); + token._setTokenURI(TOKENID_1, '!@#$%^&*()_+'); + expect(token.tokenURI(TOKENID_1)).toEqual('!@#$%^&*()_+'); + }); + }); + + describe('_burn', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should burn token', () => { + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + + token._burn(TOKENID_1); + expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_KEY); + expect(token.balanceOf(Z_OWNER)).toEqual(0n); + }); + + it('should not burn a token that does not exist', () => { + expect(() => { + token._burn(NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Invalid Sender'); + }); + + it('should clear approval when token is burned', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); + + token._burn(TOKENID_1); + expect(token._getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + + it('should burn multiple tokens in sequence', () => { + token._mint(Z_OWNER, TOKENID_2); + token._mint(Z_OWNER, TOKENID_3); + + token._burn(TOKENID_1); + token._burn(TOKENID_2); + token._burn(TOKENID_3); + expect(token.balanceOf(Z_OWNER)).toEqual(0n); + }); + + it('should burn with very long token IDs', () => { + const longTokenId = BigInt('18446744073709551615'); + token._mint(Z_OWNER, longTokenId); + token._burn(longTokenId); + expect(token._ownerOf(longTokenId)).toEqual(ZERO_KEY); + }); + + it('should burn after transfer', () => { + token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); + token._burn(TOKENID_1); + expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_KEY); + }); + + it('should burn after approval', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._burn(TOKENID_1); + expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_KEY); + expect(token._getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); + + describe('_transfer', () => { + it('should not transfer to ContractAddress', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token._transfer(Z_OWNER, SOME_CONTRACT, TOKENID_1); + }).toThrow('NonFungibleToken: Unsafe Transfer'); + }); + + it('should transfer token', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + expect(token.balanceOf(Z_SPENDER)).toEqual(0n); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + + token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); + expect(token.balanceOf(Z_OWNER)).toEqual(0n); + expect(token.balanceOf(Z_SPENDER)).toEqual(1n); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should not transfer to zero address', () => { + expect(() => { + token._transfer(Z_OWNER, ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should throw if from does not own token', () => { + token._mint(Z_OWNER, TOKENID_1); + expect(() => { + token._transfer(Z_UNAUTHORIZED, Z_SPENDER, TOKENID_1); + }).toThrow('NonFungibleToken: Incorrect Owner'); + }); + + it('should throw if token does not exist', () => { + expect(() => { + token._transfer(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should revoke approval after _transfer', () => { + _caller = OWNER; + token._mint(Z_OWNER, TOKENID_1); + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._transfer(Z_OWNER, Z_OTHER, TOKENID_1); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); + + describe('_setTokenURI', () => { + it('should throw if token does not exist', () => { + expect(() => { + token._setTokenURI(NON_EXISTENT_TOKEN, EMPTY_URI); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should set tokenURI', () => { + token._mint(Z_OWNER, TOKENID_1); + token._setTokenURI(TOKENID_1, SOME_URI); + expect(token.tokenURI(TOKENID_1)).toEqual(SOME_URI); + }); + }); + + describe('_unsafeMint', () => { + it('should mint to ContractAddress', () => { + expect(() => { + token._unsafeMint(SOME_CONTRACT, TOKENID_1); + }).not.toThrow(); + }); + + it('should not mint to zero address', () => { + expect(() => { + token._unsafeMint(ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + + expect(() => { + token._unsafeMint(ZERO_ADDRESS, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should not mint a token that already exists', () => { + token._unsafeMint(Z_OWNER, TOKENID_1); + expect(() => { + token._unsafeMint(Z_OWNER, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Sender'); + }); + + it('should mint token to public key', () => { + token._unsafeMint(Z_OWNER, TOKENID_1); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + + token._unsafeMint(Z_OWNER, TOKENID_2); + token._unsafeMint(Z_OWNER, TOKENID_3); + expect(token.balanceOf(Z_OWNER)).toEqual(3n); + }); + }); + + describe('_unsafeTransfer', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should transfer to ContractAddress', () => { + expect(() => { + token._unsafeTransfer(Z_OWNER, SOME_CONTRACT, TOKENID_1); + }).not.toThrow(); + }); + + it('should transfer token to public key', () => { + expect(token.balanceOf(Z_OWNER)).toEqual(1n); + expect(token.balanceOf(Z_SPENDER)).toEqual(0n); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); + + token._unsafeTransfer(Z_OWNER, Z_SPENDER, TOKENID_1); + expect(token.balanceOf(Z_OWNER)).toEqual(0n); + expect(token.balanceOf(Z_SPENDER)).toEqual(1n); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should not transfer to zero address', () => { + expect(() => { + token._unsafeTransfer(Z_OWNER, ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + + expect(() => { + token._unsafeTransfer(Z_OWNER, ZERO_ADDRESS, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should throw if from does not own token', () => { + expect(() => { + token._unsafeTransfer(Z_UNAUTHORIZED, Z_UNAUTHORIZED, TOKENID_1); + }).toThrow('NonFungibleToken: Incorrect Owner'); + }); + + it('should throw if token does not exist', () => { + expect(() => { + token._unsafeTransfer(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should revoke approval after _unsafeTransfer', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._unsafeTransfer(Z_OWNER, Z_OTHER, TOKENID_1); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); + + describe('_unsafeTransferFrom', () => { + beforeEach(() => { + token._mint(Z_OWNER, TOKENID_1); + }); + + it('should transfer to ContractAddress', () => { + expect(() => { + token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1); + }).not.toThrow(); + }); + + it('should not transfer to zero address', () => { + expect(() => { + token._unsafeTransferFrom(Z_OWNER, ZERO_KEY, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + + expect(() => { + token._unsafeTransferFrom(Z_OWNER, ZERO_ADDRESS, TOKENID_1); + }).toThrow('NonFungibleToken: Invalid Receiver'); + }); + + it('should not transfer from zero address', () => { + expect(() => { + token._unsafeTransferFrom(ZERO_KEY, Z_SPENDER, TOKENID_1); + }).toThrow('NonFungibleToken: Incorrect Owner'); + + expect(() => { + token._unsafeTransferFrom(ZERO_ADDRESS, Z_SPENDER, TOKENID_1); + }).toThrow('NonFungibleToken: Incorrect Owner'); + }); + + it('unapproved operator should not transfer', () => { + _caller = SPENDER; + expect(() => { + token._unsafeTransferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1, _caller); + }).toThrow('NonFungibleToken: Insufficient Approval'); + }); + + it('should not transfer token that has not been minted', () => { + _caller = OWNER; + expect(() => { + token._unsafeTransferFrom( + Z_OWNER, + Z_SPENDER, + NON_EXISTENT_TOKEN, + _caller, + ); + }).toThrow('NonFungibleToken: Nonexistent Token'); + }); + + it('should transfer token to spender via approved operator', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, OWNER); + + _caller = SPENDER; + token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should transfer token to ContractAddress via approved operator', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, OWNER); + + _caller = SPENDER; + token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + }); + + it('should transfer token to spender via approvedForAll operator', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, OWNER); + + _caller = SPENDER; + token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); + }); + + it('should transfer token to ContractAddress via approvedForAll operator', () => { + _caller = OWNER; + token.setApprovalForAll(Z_SPENDER, true, OWNER); + + _caller = SPENDER; + token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1, _caller); + expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); + }); + + it('should revoke approval after _unsafeTransferFrom', () => { + _caller = OWNER; + token.approve(Z_SPENDER, TOKENID_1, _caller); + token._unsafeTransferFrom(Z_OWNER, Z_OTHER, TOKENID_1, _caller); + expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); + }); + }); +}); + +type FailingCircuits = [ + method: keyof NonFungibleTokenSimulator, + args: unknown[], +]; // Circuit calls should fail before the args are used + +const circuitsToFail: FailingCircuits[] = [ + ['name', []], + ['symbol', []], + ['balanceOf', [Z_OWNER]], + ['ownerOf', [TOKENID_1]], + ['tokenURI', [TOKENID_1]], + ['approve', [Z_OWNER, TOKENID_1]], + ['getApproved', [TOKENID_1]], + ['setApprovalForAll', [Z_SPENDER, true]], + ['isApprovedForAll', [Z_OWNER, Z_SPENDER]], + ['transferFrom', [Z_OWNER, Z_RECIPIENT, TOKENID_1]], + ['_requireOwned', [TOKENID_1]], + ['_ownerOf', [TOKENID_1]], + ['_approve', [Z_OWNER, TOKENID_1, Z_SPENDER]], + ['_checkAuthorized', [Z_OWNER, Z_SPENDER, TOKENID_1]], + ['_isAuthorized', [Z_OWNER, Z_SPENDER, TOKENID_1]], + ['_getApproved', [TOKENID_1]], + ['_setApprovalForAll', [Z_OWNER, Z_SPENDER, true]], + ['_mint', [Z_OWNER, TOKENID_1]], + ['_burn', [TOKENID_1]], + ['_transfer', [Z_OWNER, Z_RECIPIENT, TOKENID_1]], + ['_setTokenURI', [TOKENID_1]], + ['_unsafeTransferFrom', [Z_OWNER, Z_RECIPIENT, TOKENID_1]], + ['_unsafeTransfer', [Z_OWNER, Z_RECIPIENT, TOKENID_1]], + ['_unsafeMint', [Z_OWNER, TOKENID_1]], +]; + +let uninitializedToken: NonFungibleTokenSimulator; + +describe('Uninitialized NonFungibleToken', () => { + beforeEach(() => { + uninitializedToken = new NonFungibleTokenSimulator(NAME, SYMBOL, BAD_INIT); + }); + + it.each(circuitsToFail)('%s should fail', (circuitName, args) => { + expect(() => { + (uninitializedToken[circuitName] as (...args: unknown[]) => unknown)( + ...args, + ); + }).toThrow('Initializable: contract not initialized'); + }); +}); diff --git a/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts b/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts new file mode 100644 index 00000000..dd5e2146 --- /dev/null +++ b/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts @@ -0,0 +1,612 @@ +import { + type CircuitContext, + type CoinPublicKey, + type ContractState, + QueryContext, + constructorContext, + emptyZswapLocalState, +} from '@midnight-ntwrk/compact-runtime'; +import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import { + type ContractAddress, + type Either, + type Ledger, + Contract as MockNonFungibleToken, + type ZswapCoinPublicKey, + ledger, +} from '../../artifacts/MockNonFungibleToken/contract/index.cjs'; // Combined imports +import { + type NonFungibleTokenPrivateState, + NonFungibleTokenWitnesses, +} from '../../witnesses/NonFungibleTokenWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; + +/** + * @description A simulator implementation of an nonFungibleToken contract for testing purposes. + * @template P - The private state type, fixed to NonFungibleTokenPrivateState. + * @template L - The ledger type, fixed to Contract.Ledger. + */ +export class NonFungibleTokenSimulator + implements IContractSimulator +{ + /** @description The underlying contract instance managing contract logic. */ + readonly contract: MockNonFungibleToken; + + /** @description The deployed address of the contract. */ + readonly contractAddress: string; + + /** @description The current circuit context, updated by contract operations. */ + circuitContext: CircuitContext; + + /** + * @description Initializes the mock contract. + */ + constructor(name: string, symbol: string, init: boolean) { + this.contract = new MockNonFungibleToken( + NonFungibleTokenWitnesses, + ); + const { + currentPrivateState, + currentContractState, + currentZswapLocalState, + } = this.contract.initialState( + constructorContext({}, '0'.repeat(64)), + name, + symbol, + init, + ); + this.circuitContext = { + currentPrivateState, + currentZswapLocalState, + originalState: currentContractState, + transactionContext: new QueryContext( + currentContractState.data, + sampleContractAddress(), + ), + }; + this.contractAddress = this.circuitContext.transactionContext.address; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getCurrentPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); + } + + /** + * @description Retrieves the current private state of the contract. + * @returns The private state of type NonFungibleTokenPrivateState. + */ + public getCurrentPrivateState(): NonFungibleTokenPrivateState { + return this.circuitContext.currentPrivateState; + } + + /** + * @description Retrieves the current contract state. + * @returns The contract state object. + */ + public getCurrentContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * @description Returns the token name. + * @returns The token name. + */ + public name(): string { + return this.contract.impureCircuits.name(this.circuitContext).result; + } + + /** + * @description Returns the symbol of the token. + * @returns The token name. + */ + public symbol(): string { + return this.contract.impureCircuits.symbol(this.circuitContext).result; + } + + /** + * @description Returns the number of tokens in `account`'s account. + * @param account The public key to query. + * @return The number of tokens in `account`'s account. + */ + public balanceOf( + account: Either, + ): bigint { + return this.contract.impureCircuits.balanceOf(this.circuitContext, account) + .result; + } + + /** + * @description Returns the owner of the `tokenId` token. + * @param tokenId The identifier for a token. + * @return The public key that owns the token. + */ + public ownerOf(tokenId: bigint): Either { + return this.contract.impureCircuits.ownerOf(this.circuitContext, tokenId) + .result; + } + + /** + * @description Returns the token URI for the given `tokenId`. + * @notice Since Midnight does not support native strings and string operations + * within the Compact language, concatenating a base URI + token ID is not possible + * like in other NFT implementations. Therefore, we propose the URI storage + * approach; whereby, NFTs may or may not have unique "base" URIs. + * It's up to the implementation to decide on how to handle this. + * @param tokenId The identifier for a token. + * @returns The token id's URI. + */ + public tokenURI(tokenId: bigint): string { + return this.contract.impureCircuits.tokenURI(this.circuitContext, tokenId) + .result; + } + + /** + * @description Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * @param to The account receiving the approval + * @param tokenId The token `to` may be permitted to transfer + * @return None. + */ + public approve( + to: Either, + tokenId: bigint, + sender?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.approve( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + to, + tokenId, + ); + + this.circuitContext = res.context; + } + + /** + * @description Returns the account approved for `tokenId` token. + * @param tokenId The token an account may be approved to manage + * @return The account approved to manage the token + */ + public getApproved( + tokenId: bigint, + ): Either { + return this.contract.impureCircuits.getApproved( + this.circuitContext, + tokenId, + ).result; + } + + /** + * @description Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the address zero. + * + * @param operator An operator to manage the caller's tokens + * @param approved A boolean determining if `operator` may manage all tokens of the caller + * @return None. + */ + public setApprovalForAll( + operator: Either, + approved: boolean, + sender?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.setApprovalForAll( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + operator, + approved, + ); + + this.circuitContext = res.context; + } + + /** + * @description Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * @param owner The owner of a token + * @param operator An account that may operate on `owner`'s tokens + * @return A boolean determining if `operator` is allowed to manage all of the tokens of `owner` + */ + public isApprovedForAll( + owner: Either, + operator: Either, + ): boolean { + return this.contract.impureCircuits.isApprovedForAll( + this.circuitContext, + owner, + operator, + ).result; + } + + /** + * @description Transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * @param {Either} from - The source account from which the token is being transfered + * @param {Either} to - The target account to transfer token to + * @param {TokenId} tokenId - The token being transfered + * @return {[]} - None. + */ + public transferFrom( + from: Either, + to: Either, + tokenId: bigint, + sender?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits.transferFrom( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + from, + to, + tokenId, + ); + + this.circuitContext = res.context; + } + + /** + * @description Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). + * Returns the owner. + * + * Overrides to ownership logic should be done to {_ownerOf}. + * + * @param tokenId The token that should be owned + * @return The owner of `tokenId` + */ + public _requireOwned( + tokenId: bigint, + ): Either { + return this.contract.impureCircuits._requireOwned( + this.circuitContext, + tokenId, + ).result; + } + + /** + * @description Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist + * + * @param tokenId The target token of the owner query + * @return The owner of the token + */ + public _ownerOf( + tokenId: bigint, + ): Either { + return this.contract.impureCircuits._ownerOf(this.circuitContext, tokenId) + .result; + } + + /** + * @description Approve `to` to operate on `tokenId` + * + * The `auth` argument is optional. If the value passed is non 0, then this function will check that `auth` is + * either the owner of the token, or approved to operate on all tokens held by this owner. + * + * @param to The target account to approve + * @param tokenId The token to approve + * @param auth An account authorized to operate on all tokens held by the owner the token + * @return None. + */ + public _approve( + to: Either, + tokenId: bigint, + auth: Either, + ) { + this.circuitContext = this.contract.impureCircuits._approve( + this.circuitContext, + to, + tokenId, + auth, + ).context; + } + + /** + * @description Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. + * Reverts if: + * - `spender` does not have approval from `owner` for `tokenId`. + * - `spender` does not have approval to manage all of `owner`'s assets. + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + * + * @param owner Owner of the token + * @param spender Account operating on `tokenId` + * @param tokenId The token to spend + * @return None. + */ + public _checkAuthorized( + owner: Either, + spender: Either, + tokenId: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._checkAuthorized( + this.circuitContext, + owner, + spender, + tokenId, + ).context; + } + + /** + * @description Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in + * particular (ignoring whether it is owned by `owner`). + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + * + * @param owner Owner of the token + * @param spender Account that wishes to spend `tokenId` + * @param tokenId Token to spend + * @return A boolean determining if `spender` may manage `tokenId` + */ + public _isAuthorized( + owner: Either, + spender: Either, + tokenId: bigint, + ): boolean { + return this.contract.impureCircuits._isAuthorized( + this.circuitContext, + owner, + spender, + tokenId, + ).result; + } + + /** + * @description Returns the approved address for `tokenId`. Returns 0 if `tokenId` is not minted. + * + * @param tokenId The token to query + * @return An account approved to spend `tokenId` + */ + public _getApproved( + tokenId: bigint, + ): Either { + return this.contract.impureCircuits._getApproved( + this.circuitContext, + tokenId, + ).result; + } + + /** + * @description Approve `operator` to operate on all of `owner` tokens + * + * Requirements: + * + * - operator can't be the address zero. + * + * @param owner Owner of a token + * @param operator The account to approve + * @param approved A boolean determining if `operator` may operate on all of `owner` tokens + * @return None. + */ + public _setApprovalForAll( + owner: Either, + operator: Either, + approved: boolean, + ) { + this.circuitContext = this.contract.impureCircuits._setApprovalForAll( + this.circuitContext, + owner, + operator, + approved, + ).context; + } + + /** + * @description Mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * @param to The account receiving `tokenId` + * @param tokenId The token to transfer + * @return None. + */ + public _mint( + to: Either, + tokenId: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._mint( + this.circuitContext, + to, + tokenId, + ).context; + } + + /** + * @description Destroys `tokenId`. + * The approval is cleared when the token is burned. + * This is an internal function that does not check if the sender is authorized to operate on the token. + * + * Requirements: + * + * - `tokenId` must exist. + * + * @param tokenId The token to burn + * @return None. + */ + public _burn(tokenId: bigint) { + this.circuitContext = this.contract.impureCircuits._burn( + this.circuitContext, + tokenId, + ).context; + } + + /** + * @description Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on own_public_key(). + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * @param from The source account of the token transfer + * @param to The target account of the token transfer + * @param tokenId The token to transfer + * @return None. + */ + public _transfer( + from: Either, + to: Either, + tokenId: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._transfer( + this.circuitContext, + from, + to, + tokenId, + ).context; + } + + /** + * @description Sets the the URI as `tokenURI` for the given `tokenId`. + * The `tokenId` must exist. + * + * @notice The URI for a given NFT is usually set when the NFT is minted. + * + * @param tokenId The identifier of the token. + * @param tokenURI The URI of `tokenId`. + * @return None + */ + public _setTokenURI(tokenId: bigint, tokenURI: string) { + this.circuitContext = this.contract.impureCircuits._setTokenURI( + this.circuitContext, + tokenId, + tokenURI, + ).context; + } + + /** + * @description Transfers `tokenId` token from `from` to `to`. It does NOT check if the recipient is a ContractAddress. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. All transfers to external contracts will be permanently "stuck" at the + * ContractAddress + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * @param {Either} from - The source account from which the token is being transfered + * @param {Either} to - The target account to transfer token to + * @param {TokenId} tokenId - The token being transfered + * @return {[]} - None. + */ + public _unsafeTransferFrom( + from: Either, + to: Either, + tokenId: bigint, + sender?: CoinPublicKey, + ) { + const res = this.contract.impureCircuits._unsafeTransferFrom( + { + ...this.circuitContext, + currentZswapLocalState: sender + ? emptyZswapLocalState(sender) + : this.circuitContext.currentZswapLocalState, + }, + from, + to, + tokenId, + ); + + this.circuitContext = res.context; + } + + /** + * @description Transfers `tokenId` from `from` to `to`. + * As opposed to {_unsafeTransferFrom}, this imposes no restrictions on own_public_key(). + * It does NOT check if the recipient is a ContractAddress. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. All transfers to external contracts will be permanently "stuck" at the + * ContractAddress + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * @param {Either} from - The source account of the token transfer + * @param {Either} to - The target account of the token transfer + * @param {TokenId} tokenId - The token to transfer + * @return {[]} - None. + */ + public _unsafeTransfer( + from: Either, + to: Either, + tokenId: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._unsafeTransfer( + this.circuitContext, + from, + to, + tokenId, + ).context; + } + + /** + * @description Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress. + * + * @notice External smart contracts cannot call the token contract at this time, so any transfers to external contracts + * may result in a permanent loss of the token. All transfers to external contracts will be permanently "stuck" at the + * ContractAddress + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * @param {Either} to - The account receiving `tokenId` + * @param {TokenId} tokenId - The token to transfer + * @return {[]} - None. + */ + public _unsafeMint( + to: Either, + tokenId: bigint, + ) { + this.circuitContext = this.contract.impureCircuits._unsafeMint( + this.circuitContext, + to, + tokenId, + ).context; + } +} diff --git a/contracts/nonFungibleToken/src/test/types/test.ts b/contracts/nonFungibleToken/src/test/types/test.ts new file mode 100644 index 00000000..7a909543 --- /dev/null +++ b/contracts/nonFungibleToken/src/test/types/test.ts @@ -0,0 +1,26 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; + +/** + * Generic interface for mock contract implementations. + * @template P - The type of the contract's private state. + * @template L - The type of the contract's ledger (public state). + */ +export interface IContractSimulator { + /** The contract's deployed address. */ + readonly contractAddress: string; + + /** The current circuit context. */ + circuitContext: CircuitContext

; + + /** Retrieves the current ledger state. */ + getCurrentPublicState(): L; + + /** Retrieves the current private state. */ + getCurrentPrivateState(): P; + + /** Retrieves the current contract state. */ + getCurrentContractState(): ContractState; +} diff --git a/contracts/nonFungibleToken/src/test/utils/address.ts b/contracts/nonFungibleToken/src/test/utils/address.ts new file mode 100644 index 00000000..af4ed548 --- /dev/null +++ b/contracts/nonFungibleToken/src/test/utils/address.ts @@ -0,0 +1,81 @@ +import { + convert_bigint_to_Uint8Array, + encodeCoinPublicKey, +} from '@midnight-ntwrk/compact-runtime'; +import { encodeContractAddress } from '@midnight-ntwrk/ledger'; +import type * as Compact from '../../artifacts/MockNonFungibleToken/contract/index.cjs'; + +const PREFIX_ADDRESS = '0200'; + +/** + * @description Converts an ASCII string to its hexadecimal representation, + * left-padded with zeros to a specified length. Useful for generating + * fixed-size hex strings for encoding. + * @param str ASCII string to convert. + * @param len Total desired length of the resulting hex string. Defaults to 64. + * @returns Hexadecimal string representation of `str`, padded to `length` characters. + */ +export const toHexPadded = (str: string, len = 64) => + Buffer.from(str, 'ascii').toString('hex').padStart(len, '0'); + +/** + * @description Generates ZswapCoinPublicKey from `str` for testing purposes. + * @param str String to hexify and encode. + * @returns Encoded `ZswapCoinPublicKey`. + */ +export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { + const toHex = Buffer.from(str, 'ascii').toString('hex'); + return { bytes: encodeCoinPublicKey(String(toHex).padStart(64, '0')) }; +}; + +/** + * @description Generates ContractAddress from `str` for testing purposes. + * Prepends 32-byte hex with PREFIX_ADDRESS before encoding. + * @param str String to hexify and encode. + * @returns Encoded `ZswapCoinPublicKey`. + */ +export const encodeToAddress = (str: string): Compact.ContractAddress => { + const toHex = Buffer.from(str, 'ascii').toString('hex'); + const fullAddress = PREFIX_ADDRESS + String(toHex).padStart(64, '0'); + return { bytes: encodeContractAddress(fullAddress) }; +}; + +/** + * @description Generates an Either object for ZswapCoinPublicKey for testing. + * For use when an Either argument is expected. + * @param str String to hexify and encode. + * @returns Defined Either object for ZswapCoinPublicKey. + */ +export const createEitherTestUser = (str: string) => { + return { + is_left: true, + left: encodeToPK(str), + right: encodeToAddress(''), + }; +}; + +/** + * @description Generates an Either object for ContractAddress for testing. + * For use when an Either argument is expected. + * @param str String to hexify and encode. + * @returns Defined Either object for ContractAddress. + */ +export const createEitherTestContractAddress = (str: string) => { + return { + is_left: false, + left: encodeToPK(''), + right: encodeToAddress(str), + }; +}; + +export const ZERO_KEY = { + is_left: true, + left: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, + right: encodeToAddress(''), +}; + +export const ZERO_ADDRESS = { + is_left: false, + left: encodeToPK(''), + right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, +}; diff --git a/contracts/nonFungibleToken/src/witnesses/NonFungibleTokenWitnesses.ts b/contracts/nonFungibleToken/src/witnesses/NonFungibleTokenWitnesses.ts new file mode 100644 index 00000000..4ff84e7f --- /dev/null +++ b/contracts/nonFungibleToken/src/witnesses/NonFungibleTokenWitnesses.ts @@ -0,0 +1,3 @@ +// This is how we type an empty object. +export type NonFungibleTokenPrivateState = Record; +export const NonFungibleTokenWitnesses = {}; diff --git a/contracts/nonFungibleToken/tsconfig.build.json b/contracts/nonFungibleToken/tsconfig.build.json new file mode 100644 index 00000000..f1132509 --- /dev/null +++ b/contracts/nonFungibleToken/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/test/**/*.ts"], + "compilerOptions": {} +} diff --git a/contracts/nonFungibleToken/tsconfig.json b/contracts/nonFungibleToken/tsconfig.json new file mode 100644 index 00000000..0ff1d3e9 --- /dev/null +++ b/contracts/nonFungibleToken/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["src/**/*.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "lib": ["ES2022"], + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strict": true, + "isolatedModules": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/contracts/nonFungibleToken/turbo.json b/contracts/nonFungibleToken/turbo.json new file mode 100644 index 00000000..5a61a218 --- /dev/null +++ b/contracts/nonFungibleToken/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build", "compact"] + } + } +} diff --git a/contracts/nonFungibleToken/vitest.config.ts b/contracts/nonFungibleToken/vitest.config.ts new file mode 100644 index 00000000..785b792e --- /dev/null +++ b/contracts/nonFungibleToken/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/test/**/*.test.ts'], + reporters: 'verbose', + }, +}); diff --git a/contracts/utils/src/Utils.compact b/contracts/utils/src/Utils.compact index 8e703879..90ad8986 100644 --- a/contracts/utils/src/Utils.compact +++ b/contracts/utils/src/Utils.compact @@ -67,4 +67,13 @@ module Utils { export pure circuit isContractAddress(keyOrAddress: Either): Boolean { return !keyOrAddress.is_left; } + + /** + * @description A helper function that returns the empty string: "" + * + * @return {Opaque<"string">} - The empty string: "" + */ + export pure circuit emptyString(): Opaque<"string"> { + return default>; + } } diff --git a/contracts/utils/src/test/Initializable.test.ts b/contracts/utils/src/test/Initializable.test.ts index 125d253b..a3bb2eba 100644 --- a/contracts/utils/src/test/Initializable.test.ts +++ b/contracts/utils/src/test/Initializable.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { InitializableSimulator } from './simulators/InitializableSimulator'; +import { InitializableSimulator } from './simulators/InitializableSimulator.js'; let initializable: InitializableSimulator; diff --git a/contracts/utils/src/test/Pausable.test.ts b/contracts/utils/src/test/Pausable.test.ts index 0a16e444..94662b0c 100644 --- a/contracts/utils/src/test/Pausable.test.ts +++ b/contracts/utils/src/test/Pausable.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { PausableSimulator } from './simulators/PausableSimulator'; +import { PausableSimulator } from './simulators/PausableSimulator.js'; let pausable: PausableSimulator; diff --git a/contracts/utils/src/test/mocks/MockUtils.compact b/contracts/utils/src/test/mocks/MockUtils.compact index 71ad1606..eaeb3d18 100644 --- a/contracts/utils/src/test/mocks/MockUtils.compact +++ b/contracts/utils/src/test/mocks/MockUtils.compact @@ -7,20 +7,24 @@ import "../../Utils" prefix Utils_; export { ZswapCoinPublicKey, ContractAddress, Either }; export pure circuit isKeyOrAddressZero(keyOrAddress: Either): Boolean { - return Utils_isKeyOrAddressZero(keyOrAddress); + return Utils_isKeyOrAddressZero(keyOrAddress); } export pure circuit isKeyOrAddressEqual( keyOrAddress: Either, other: Either ): Boolean { - return Utils_isKeyOrAddressEqual(keyOrAddress, other); + return Utils_isKeyOrAddressEqual(keyOrAddress, other); } export pure circuit isKeyZero(key: ZswapCoinPublicKey): Boolean { - return Utils_isKeyZero(key); + return Utils_isKeyZero(key); } export pure circuit isContractAddress(keyOrAddress: Either): Boolean { - return Utils_isContractAddress(keyOrAddress); + return Utils_isContractAddress(keyOrAddress); +} + +export pure circuit emptyString(): Opaque<"string"> { + return Utils_emptyString(); } diff --git a/contracts/utils/src/test/simulators/InitializableSimulator.ts b/contracts/utils/src/test/simulators/InitializableSimulator.ts index fa8ad2f2..e2cfa376 100644 --- a/contracts/utils/src/test/simulators/InitializableSimulator.ts +++ b/contracts/utils/src/test/simulators/InitializableSimulator.ts @@ -13,8 +13,8 @@ import { import { type InitializablePrivateState, InitializableWitnesses, -} from '../../witnesses/InitializableWitnesses'; -import type { IContractSimulator } from '../types/test'; +} from '../../witnesses/InitializableWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of an utils contract for testing purposes. diff --git a/contracts/utils/src/test/simulators/PausableSimulator.ts b/contracts/utils/src/test/simulators/PausableSimulator.ts index 034d84a4..cbec7204 100644 --- a/contracts/utils/src/test/simulators/PausableSimulator.ts +++ b/contracts/utils/src/test/simulators/PausableSimulator.ts @@ -13,8 +13,8 @@ import { import { type PausablePrivateState, PausableWitnesses, -} from '../../witnesses/PausableWitnesses'; -import type { IContractSimulator } from '../types/test'; +} from '../../witnesses/PausableWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of an utils contract for testing purposes. diff --git a/contracts/utils/src/test/simulators/UtilsSimulator.ts b/contracts/utils/src/test/simulators/UtilsSimulator.ts index 6b7618cb..4a607c7a 100644 --- a/contracts/utils/src/test/simulators/UtilsSimulator.ts +++ b/contracts/utils/src/test/simulators/UtilsSimulator.ts @@ -16,8 +16,8 @@ import { import { type UtilsPrivateState, UtilsWitnesses, -} from '../../witnesses/UtilsWitnesses'; -import type { IContractSimulator } from '../types/test'; +} from '../../witnesses/UtilsWitnesses.js'; +import type { IContractSimulator } from '../types/test.js'; /** * @description A simulator implementation of an utils contract for testing purposes. @@ -138,4 +138,12 @@ export class UtilsSimulator keyOrAddress, ).result; } + + /** + * @description A helper function that returns the empty string: "" + * @returns The empty string: "" + */ + public emptyString(): string { + return this.contract.circuits.emptyString(this.circuitContext).result; + } } diff --git a/contracts/utils/src/test/utils.test.ts b/contracts/utils/src/test/utils.test.ts index f767ec3c..1398d4d9 100644 --- a/contracts/utils/src/test/utils.test.ts +++ b/contracts/utils/src/test/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { UtilsSimulator } from './simulators/UtilsSimulator'; -import * as contractUtils from './utils/address'; +import { UtilsSimulator } from './simulators/UtilsSimulator.js'; +import * as contractUtils from './utils/address.js'; const Z_SOME_KEY = contractUtils.createEitherTestUser('SOME_KEY'); const Z_OTHER_KEY = contractUtils.createEitherTestUser('OTHER_KEY'); @@ -9,6 +9,8 @@ const SOME_CONTRACT = const OTHER_CONTRACT = contractUtils.createEitherTestContractAddress('OTHER_CONTRACT'); +const EMPTY_STRING = ''; + let contract: UtilsSimulator; describe('Utils', () => { @@ -54,6 +56,15 @@ describe('Utils', () => { false, ); }); + + it('should return false for two different address types of equal value', () => { + expect( + contract.isKeyOrAddressEqual( + contractUtils.ZERO_KEY, + contractUtils.ZERO_ADDRESS, + ), + ).toBe(false); + }); }); describe('isKeyZero', () => { @@ -75,4 +86,10 @@ describe('Utils', () => { expect(contract.isContractAddress(Z_SOME_KEY)).toBe(false); }); }); + + describe('emptyString', () => { + it('should return the empty string', () => { + expect(contract.emptyString()).toBe(EMPTY_STRING); + }); + }); }); diff --git a/contracts/utils/src/test/utils/address.ts b/contracts/utils/src/test/utils/address.ts index d4ac78a7..f288ae82 100644 --- a/contracts/utils/src/test/utils/address.ts +++ b/contracts/utils/src/test/utils/address.ts @@ -7,16 +7,16 @@ import type * as Compact from '../../artifacts/MockUtils/contract/index.cjs'; const PREFIX_ADDRESS = '0200'; -export const pad = (s: string, n: number): Uint8Array => { - const encoder = new TextEncoder(); - const utf8Bytes = encoder.encode(s); - if (n < utf8Bytes.length) { - throw new Error(`The padded length n must be at least ${utf8Bytes.length}`); - } - const paddedArray = new Uint8Array(n); - paddedArray.set(utf8Bytes); - return paddedArray; -}; +/** + * @description Converts an ASCII string to its hexadecimal representation, + * left-padded with zeros to a specified length. Useful for generating + * fixed-size hex strings for encoding. + * @param str ASCII string to convert. + * @param len Total desired length of the resulting hex string. Defaults to 64. + * @returns Hexadecimal string representation of `str`, padded to `length` characters. + */ +export const toHexPadded = (str: string, len = 64) => + Buffer.from(str, 'ascii').toString('hex').padStart(len, '0'); /** * @description Generates ZswapCoinPublicKey from `str` for testing purposes. diff --git a/contracts/utils/src/test/utils/test.ts b/contracts/utils/src/test/utils/test.ts index d467e572..9fd2d4f6 100644 --- a/contracts/utils/src/test/utils/test.ts +++ b/contracts/utils/src/test/utils/test.ts @@ -6,7 +6,7 @@ import { QueryContext, emptyZswapLocalState, } from '@midnight-ntwrk/compact-runtime'; -import type { IContractSimulator } from '../types/test'; +import type { IContractSimulator } from '../types/test.js'; /** * Constructs a `CircuitContext` from the given state and sender information. diff --git a/contracts/utils/tsconfig.json b/contracts/utils/tsconfig.json index 3e90b0a9..4ae082c4 100644 --- a/contracts/utils/tsconfig.json +++ b/contracts/utils/tsconfig.json @@ -1,13 +1,17 @@ { - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts" + ], "compilerOptions": { "rootDir": "src", "outDir": "dist", "declaration": true, - "lib": ["ESNext"], + "lib": [ + "ES2022" + ], "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "allowJs": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": true, diff --git a/yarn.lock b/yarn.lock index c63ddd18..77adaf31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -428,6 +428,18 @@ __metadata: languageName: unknown linkType: soft +"@openzeppelin-midnight/non-fungible-token@workspace:contracts/nonFungibleToken": + version: 0.0.0-use.local + resolution: "@openzeppelin-midnight/non-fungible-token@workspace:contracts/nonFungibleToken" + dependencies: + "@openzeppelin-midnight/compact": "workspace:^" + "@types/node": "npm:22.14.0" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.2.2" + vitest: "npm:^3.1.3" + languageName: unknown + linkType: soft + "@openzeppelin-midnight/utils@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-midnight/utils@workspace:contracts/utils" @@ -466,142 +478,142 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.43.0" +"@rollup/rollup-android-arm-eabi@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.44.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-android-arm64@npm:4.43.0" +"@rollup/rollup-android-arm64@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-android-arm64@npm:4.44.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.43.0" +"@rollup/rollup-darwin-arm64@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.44.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.43.0" +"@rollup/rollup-darwin-x64@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.44.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.43.0" +"@rollup/rollup-freebsd-arm64@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.44.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.43.0" +"@rollup/rollup-freebsd-x64@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.44.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.43.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.44.1" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.43.0" +"@rollup/rollup-linux-arm-musleabihf@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.44.1" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.43.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.44.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.43.0" +"@rollup/rollup-linux-arm64-musl@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.44.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.43.0" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.44.1" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.43.0" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.44.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.43.0" +"@rollup/rollup-linux-riscv64-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.44.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.43.0" +"@rollup/rollup-linux-riscv64-musl@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.44.1" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.43.0" +"@rollup/rollup-linux-s390x-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.44.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.43.0" +"@rollup/rollup-linux-x64-gnu@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.44.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.43.0" +"@rollup/rollup-linux-x64-musl@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.44.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.43.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.44.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.43.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.44.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.43.0": - version: 4.43.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.43.0" +"@rollup/rollup-win32-x64-msvc@npm:4.44.1": + version: 4.44.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.44.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -650,14 +662,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.7": - version: 1.0.7 - resolution: "@types/estree@npm:1.0.7" - checksum: 10/419c845ece767ad4b21171e6e5b63dabb2eb46b9c0d97361edcd9cabbf6a95fcadb91d89b5fa098d1336fa0b8fceaea82fca97a2ef3971f5c86e53031e157b21 - languageName: node - linkType: hard - -"@types/estree@npm:^1.0.0": +"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 @@ -674,11 +679,11 @@ __metadata: linkType: hard "@types/node@npm:^22": - version: 22.15.32 - resolution: "@types/node@npm:22.15.32" + version: 22.15.33 + resolution: "@types/node@npm:22.15.33" dependencies: undici-types: "npm:~6.21.0" - checksum: 10/10b4c106d0c512a1d35ec08142bd7fb5cf2e1df93fc5627b3c69dd843dec4be07a47f1fa7ede232ad84762d75a372ea35028b79ee1e753b6f2adecd0b2cb2f71 + checksum: 10/5734cbca7fc363f3d6ad191e1be645cc9885d642e9f90688892459f10629cf663d2206e7ed7b255dd476baaa86fb011aa09647e77520958b5993b391f793856f languageName: node linkType: hard @@ -1922,9 +1927,9 @@ __metadata: linkType: hard "pathval@npm:^2.0.0": - version: 2.0.0 - resolution: "pathval@npm:2.0.0" - checksum: 10/b91575bf9cdf01757afd7b5e521eb8a0b874a49bc972d08e0047cfea0cd3c019f5614521d4bc83d2855e3fcc331db6817dfd533dd8f3d90b16bc76fad2450fc1 + version: 2.0.1 + resolution: "pathval@npm:2.0.1" + checksum: 10/f5e8b82f6b988a5bba197970af050268fd800780d0f9ee026e6f0b544ac4b17ab52bebeabccb790d63a794530a1641ae399ad07ecfc67ad337504c85dc9e5693 languageName: node linkType: hard @@ -1949,7 +1954,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.5.5": +"postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -2011,30 +2016,30 @@ __metadata: linkType: hard "rollup@npm:^4.40.0": - version: 4.43.0 - resolution: "rollup@npm:4.43.0" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.43.0" - "@rollup/rollup-android-arm64": "npm:4.43.0" - "@rollup/rollup-darwin-arm64": "npm:4.43.0" - "@rollup/rollup-darwin-x64": "npm:4.43.0" - "@rollup/rollup-freebsd-arm64": "npm:4.43.0" - "@rollup/rollup-freebsd-x64": "npm:4.43.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.43.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.43.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.43.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.43.0" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.43.0" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.43.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.43.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.43.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.43.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.43.0" - "@rollup/rollup-linux-x64-musl": "npm:4.43.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.43.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.43.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.43.0" - "@types/estree": "npm:1.0.7" + version: 4.44.1 + resolution: "rollup@npm:4.44.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.44.1" + "@rollup/rollup-android-arm64": "npm:4.44.1" + "@rollup/rollup-darwin-arm64": "npm:4.44.1" + "@rollup/rollup-darwin-x64": "npm:4.44.1" + "@rollup/rollup-freebsd-arm64": "npm:4.44.1" + "@rollup/rollup-freebsd-x64": "npm:4.44.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.44.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.44.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.44.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.44.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.44.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.44.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.44.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.44.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.44.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.44.1" + "@rollup/rollup-linux-x64-musl": "npm:4.44.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.44.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.44.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.44.1" + "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -2081,7 +2086,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/c7f436880dfd5bd54e9ac579625b5355be58b5437ebb386eb88d709d6bed733a4411673cc80fd64dc5514cd71794544bc83775842108c86ed2b51827e11b33b8 + checksum: 10/4130fcc4fb7df4364bfbdf78f277c0c2afc881812b3d01bd498b709da180ce69ff359af003d187d7c554576956dbc66d85468f4fc62b4b42b87839cd095ee9fd languageName: node linkType: hard @@ -2524,14 +2529,14 @@ __metadata: linkType: hard "vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": - version: 7.0.0-beta.2 - resolution: "vite@npm:7.0.0-beta.2" + version: 7.0.0 + resolution: "vite@npm:7.0.0" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.4.6" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.2" - postcss: "npm:^8.5.5" + postcss: "npm:^8.5.6" rollup: "npm:^4.40.0" tinyglobby: "npm:^0.2.14" peerDependencies: @@ -2574,7 +2579,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/01245969939849d2a1fbfc6bba95b80079ecaf2a181bf530a35718bc8e093b49f92c0d228e64e7cf8d1976fdf77da5ca4ff0fd8d8e1df6bd81830c51c79e3b98 + checksum: 10/2501b706dc481529efb16c6241794a66d68ea7a074d49f22e45b701769fbeeccc721c58272c9fce743d3b1472a3de497f85ca18cb059b1b8b906b2b295e524dc languageName: node linkType: hard