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/README.md b/README.md index ec3108b3..b6c8ea06 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,27 @@ Usage: compactc.bin ... > - [turbo](https://turborepo.com/docs/getting-started/installation) > - [compact](https://docs.midnight.network/develop/tutorial/building/#midnight-compact-compiler) +Make sure you have [nvm](https://github.com/nvm-sh/nvm) and [yarn](https://yarnpkg.com/getting-started/install) installed on your machine. + +Follow Midnight's [compact installation guide](https://docs.midnight.network/develop/tutorial/building/#midnight-compact-compiler) and confirm that `compactc` is in the `PATH` env variable. + +```bash +$ compactc + +Compactc version: 0.23.0 +Usage: compactc.bin ... + --help displays detailed usage information +``` + +## Set up the project + +> ### Requirements +> +> - [node](https://nodejs.org/) +> - [yarn](https://yarnpkg.com/getting-started/install) +> - [turbo](https://turborepo.com/docs/getting-started/installation) +> - [compact](https://docs.midnight.network/develop/tutorial/building/#midnight-compact-compiler) + Clone the repository: ```bash diff --git a/SECURITY.md b/SECURITY.md index 68902d38..5985dd0e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,4 @@ Security vulnerabilities should be disclosed to the project maintainers by email ## Legal -Blockchain is a nascent technology and carries a high level of risk and uncertainty. OpenZeppelin makes certain software available under open source licenses, which disclaim all warranties in relation to the project and which limits the liability of OpenZeppelin. Subject to any particular licensing terms, your use of the project is governed by the terms found at [www.openzeppelin.com/tos](https://www.openzeppelin.com/tos) (the "Terms"). As set out in the Terms, you are solely responsible for any use of the project and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an ongoing duty by any contributor, including OpenZeppelin, to correct any issues or vulnerabilities or alert you to all or any of the risks of utilizing the project. \ No newline at end of file +Blockchain is a nascent technology and carries a high level of risk and uncertainty. OpenZeppelin makes certain software available under open source licenses, which disclaim all warranties in relation to the project and which limits the liability of OpenZeppelin. Subject to any particular licensing terms, your use of the project is governed by the terms found at [www.openzeppelin.com/tos](https://www.openzeppelin.com/tos) (the "Terms"). As set out in the Terms, you are solely responsible for any use of the project and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an ongoing duty by any contributor, including OpenZeppelin, to correct any issues or vulnerabilities or alert you to all or any of the risks of utilizing the project. diff --git a/biome.json b/biome.json index 5e4b9777..10c26977 100644 --- a/biome.json +++ b/biome.json @@ -21,7 +21,9 @@ "formatter": { "enabled": true, "indentStyle": "space", - "ignore": ["package.json"] + "ignore": [ + "package.json" + ] }, "organizeImports": { "enabled": true @@ -87,4 +89,4 @@ "indentStyle": "space" } } -} +} \ No newline at end of file diff --git a/compact/package.json b/compact/package.json index 8e4541ed..786f6b0c 100644 --- a/compact/package.json +++ b/compact/package.json @@ -22,6 +22,9 @@ "scripts": { "build": "tsc -p .", "types": "tsc -p tsconfig.json --noEmit", + "fmt-and-lint": "biome check . --changed", + "fmt-and-lint:fix": "biome check . --changed --fix", + "fmt-and-lint:ci": "biome ci --changed --no-errors-on-unmatched", "clean": "git clean -fXd" }, "devDependencies": { diff --git a/compact/tsconfig.json b/compact/tsconfig.json index 9c283308..55676612 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" + ] +} \ No newline at end of file diff --git a/compact/turbo.json b/compact/turbo.json index 5d208997..3133543c 100644 --- a/compact/turbo.json +++ b/compact/turbo.json @@ -1,12 +1,21 @@ { "$schema": "https://turbo.build/schema.json", - "extends": ["//"], + "extends": [ + "//" + ], "tasks": { "build": { - "outputs": ["dist/**"], - "inputs": ["src/**/*.ts", "tsconfig.json"], - "env": ["COMPACT_HOME"], + "outputs": [ + "dist/**" + ], + "inputs": [ + "src/**/*.ts", + "tsconfig.json" + ], + "env": [ + "COMPACT_HOME" + ], "cache": true } } -} +} \ No newline at end of file diff --git a/contracts/fungibleToken/package.json b/contracts/fungibleToken/package.json index 68b33631..335abf5d 100644 --- a/contracts/fungibleToken/package.json +++ b/contracts/fungibleToken/package.json @@ -18,6 +18,9 @@ "build": "compact-builder && tsc", "test": "vitest run", "types": "tsc -p tsconfig.json --noEmit", + "fmt-and-lint": "biome check . --changed", + "fmt-and-lint:fix": "biome check . --changed --fix", + "fmt-and-lint:ci": "biome ci --changed --no-errors-on-unmatched", "clean": "git clean -fXd" }, "dependencies": { diff --git a/contracts/fungibleToken/src/FungibleToken.compact b/contracts/fungibleToken/src/FungibleToken.compact index 7c53ad02..ec6387cf 100644 --- a/contracts/fungibleToken/src/FungibleToken.compact +++ b/contracts/fungibleToken/src/FungibleToken.compact @@ -66,7 +66,6 @@ module FungibleToken { export sealed ledger _name: Opaque<"string">; export sealed ledger _symbol: Opaque<"string">; export sealed ledger _decimals: Uint<8>; - /** * @description Initializes the contract by setting the name, symbol, and decimals. * @dev This MUST be called in the implementing contract's constructor. Failure to do so 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..7fa793b8 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. @@ -27,8 +27,7 @@ import type { IContractSimulator } from '../types/test'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class FungibleTokenSimulator - implements IContractSimulator -{ + implements IContractSimulator { /** @description The underlying contract instance managing contract logic. */ readonly contract: MockFungibleToken; 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..b6b69fc7 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, @@ -18,4 +22,4 @@ "esModuleInterop": true, "skipLibCheck": true } -} +} \ No newline at end of file diff --git a/contracts/fungibleToken/turbo.json b/contracts/fungibleToken/turbo.json new file mode 100644 index 00000000..5a61a218 --- /dev/null +++ b/contracts/fungibleToken/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build", "compact"] + } + } +} diff --git a/contracts/nonFungibleToken/package.json b/contracts/nonFungibleToken/package.json new file mode 100644 index 00000000..c0735a42 --- /dev/null +++ b/contracts/nonFungibleToken/package.json @@ -0,0 +1,36 @@ +{ + "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", + "fmt-and-lint": "biome check . --changed", + "fmt-and-lint:fix": "biome check . --changed --fix", + "fmt-and-lint:ci": "biome ci --changed --no-errors-on-unmatched", + "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-midnight/compact": "workspace:^" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@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..d6c626dc --- /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 + } +} \ No newline at end of file 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/package.json b/contracts/utils/package.json index 377b7707..4d358015 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -18,6 +18,9 @@ "build": "compact-builder && tsc", "test": "vitest run", "types": "tsc -p tsconfig.json --noEmit", + "fmt-and-lint": "biome check . --changed", + "fmt-and-lint:fix": "biome check . --changed --fix", + "fmt-and-lint:ci": "biome ci --changed --no-errors-on-unmatched", "clean": "git clean -fXd" }, "dependencies": { 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..bdc89da2 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. @@ -22,8 +22,7 @@ import type { IContractSimulator } from '../types/test'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class InitializableSimulator - implements IContractSimulator -{ + implements IContractSimulator { /** @description The underlying contract instance managing contract logic. */ readonly contract: MockInitializable; diff --git a/contracts/utils/src/test/simulators/PausableSimulator.ts b/contracts/utils/src/test/simulators/PausableSimulator.ts index 034d84a4..7ff2e401 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. @@ -22,8 +22,7 @@ import type { IContractSimulator } from '../types/test'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class PausableSimulator - implements IContractSimulator -{ + implements IContractSimulator { /** @description The underlying contract instance managing contract logic. */ readonly contract: MockPausable; diff --git a/contracts/utils/src/test/simulators/UtilsSimulator.ts b/contracts/utils/src/test/simulators/UtilsSimulator.ts index 6b7618cb..04a9df12 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. @@ -25,8 +25,7 @@ import type { IContractSimulator } from '../types/test'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class UtilsSimulator - implements IContractSimulator -{ + implements IContractSimulator { /** @description The underlying contract instance managing contract logic. */ readonly contract: MockUtils; @@ -138,4 +137,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..0ff1d3e9 100644 --- a/contracts/utils/tsconfig.json +++ b/contracts/utils/tsconfig.json @@ -4,10 +4,10 @@ "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/utils/turbo.json b/contracts/utils/turbo.json new file mode 100644 index 00000000..5a61a218 --- /dev/null +++ b/contracts/utils/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build", "compact"] + } + } +} diff --git a/package.json b/package.json index 3fe48c95..da56058b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@biomejs/biome": "1.9.4", "@midnight-ntwrk/ledger": "^4.0.0", "@midnight-ntwrk/zswap": "^4.0.0", - "@types/node": "^22", + "@types/node": "22.14.0", "fast-check": "^3.15.0", "ts-node": "^10.9.2", "turbo": "^2.5.1", diff --git a/turbo.json b/turbo.json index d19f046b..d00de0d1 100644 --- a/turbo.json +++ b/turbo.json @@ -2,20 +2,37 @@ "$schema": "https://turbo.build/schema.json", "tasks": { "compact": { - "dependsOn": ["^build"], - "env": ["COMPACT_HOME"], - "inputs": ["src/**/*.compact"], + "dependsOn": [ + "^build" + ], + "env": [ + "COMPACT_HOME" + ], + "inputs": [ + "src/**/*.compact" + ], "outputLogs": "new-only", - "outputs": ["src/artifacts/**", "src/gen/**", "gen/**"] + "outputs": [ + "src/artifacts/**", + "src/gen/**", + "gen/**" + ] }, "test": { - "dependsOn": ["^build", "compact"], + "dependsOn": [ + "^build", + "compact" + ], "outputs": [], "cache": false }, "build": { - "dependsOn": ["^build"], - "env": ["COMPACT_HOME"], + "dependsOn": [ + "^build" + ], + "env": [ + "COMPACT_HOME" + ], "inputs": [ "src/**/*.ts", "!src/**/*.test.ts", @@ -24,10 +41,14 @@ "tsconfig.build.json", ".env" ], - "outputs": ["dist/**"] + "outputs": [ + "dist/**" + ] }, "types": { - "dependsOn": ["compact"], + "dependsOn": [ + "compact" + ], "outputs": [], "cache": false }, @@ -49,4 +70,4 @@ "cache": false } } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 621e3b2c..27bb2468 100644 --- a/yarn.lock +++ b/yarn.lock @@ -416,6 +416,19 @@ __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: + "@biomejs/biome": "npm:1.9.4" + "@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" @@ -454,142 +467,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 @@ -638,14 +651,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 @@ -661,15 +667,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22": - version: 22.15.32 - resolution: "@types/node@npm:22.15.32" - dependencies: - undici-types: "npm:~6.21.0" - checksum: 10/10b4c106d0c512a1d35ec08142bd7fb5cf2e1df93fc5627b3c69dd843dec4be07a47f1fa7ede232ad84762d75a372ea35028b79ee1e753b6f2adecd0b2cb2f71 - languageName: node - linkType: hard - "@types/object-inspect@npm:^1.8.1": version: 1.13.0 resolution: "@types/object-inspect@npm:1.13.0" @@ -1813,7 +1810,7 @@ __metadata: "@midnight-ntwrk/compact-runtime": "npm:^0.8.1" "@midnight-ntwrk/ledger": "npm:^4.0.0" "@midnight-ntwrk/zswap": "npm:^4.0.0" - "@types/node": "npm:^22" + "@types/node": "npm:22.14.0" fast-check: "npm:^3.15.0" ts-node: "npm:^10.9.2" turbo: "npm:^2.5.1" @@ -1910,9 +1907,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 @@ -1937,7 +1934,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: @@ -1999,30 +1996,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": @@ -2069,7 +2066,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/c7f436880dfd5bd54e9ac579625b5355be58b5437ebb386eb88d709d6bed733a4411673cc80fd64dc5514cd71794544bc83775842108c86ed2b51827e11b33b8 + checksum: 10/4130fcc4fb7df4364bfbdf78f277c0c2afc881812b3d01bd498b709da180ce69ff359af003d187d7c554576956dbc66d85468f4fc62b4b42b87839cd095ee9fd languageName: node linkType: hard @@ -2512,14 +2509,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: @@ -2562,7 +2559,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/01245969939849d2a1fbfc6bba95b80079ecaf2a181bf530a35718bc8e093b49f92c0d228e64e7cf8d1976fdf77da5ca4ff0fd8d8e1df6bd81830c51c79e3b98 + checksum: 10/2501b706dc481529efb16c6241794a66d68ea7a074d49f22e45b701769fbeeccc721c58272c9fce743d3b1472a3de497f85ca18cb059b1b8b906b2b295e524dc languageName: node linkType: hard