// SPDX-License-Identifier: MIT // MyFun Contracts v0.1.4 (token/NonFungibleToken.compact) pragma language_version >= 0.16.0; /** * @module NonFungibleToken * @description An unshielded Non-Fungible Token library. * * @notice One notable difference regarding this implementation and the EIP721 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 "../security/Initializable" prefix Initializable_; import "../utils/Utils" prefix Utils_; /// 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. * * This MUST be called in the implementing contract's constructor. * Failure to do so can lead to an irreparable contract. * * @circuitInfo k=10, rows=65 * * Requirements: * * - Contract is not initialized. * * @param {Opaque<"string">} name_ - The name of the token. * @param {Opaque<"string">} symbol_ - The symbol of the token. * @return {[]} - Empty tuple. */ export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">): [] { Initializable_initialize(); _name = disclose(name_); _symbol = disclose(symbol_); } /** * @description Returns the number of tokens in `owner`'s account. * * @circuitInfo k=10, rows=309 * * Requirements: * * - The contract is 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(disclose(owner))) { return 0; } return _balances.lookup(disclose(owner)); } /** * @description Returns the owner of the `tokenId` token. * * @circuitInfo k=10, rows=290 * * Requirements: * * - The contract is 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 is 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 is 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=296 * * Requirements: * * - The contract is initialized. * - The `tokenId` must exist. * * @notice Native strings and string operations aren't supported within the Compact language, * e.g. 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 {Uint<128>} tokenId - The identifier for a token. * @return {Opaque<"string">} - the token id's URI. */ export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { Initializable_assertInitialized(); _requireOwned(tokenId); if (!_tokenURIs.member(disclose(tokenId))) { return Utils_emptyString(); } return _tokenURIs.lookup(disclose(tokenId)); } /** * @description Sets the the URI as `tokenURI` for the given `tokenId`. * * @circuitInfo k=10, rows=253 * * Requirements: * * - The contract is 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 {[]} - Empty tuple. */ export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] { Initializable_assertInitialized(); _requireOwned(tokenId); return _tokenURIs.insert(disclose(tokenId), disclose(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=966 * * Requirements: * * - The contract is 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 {[]} - Empty tuple. */ export circuit approve( to: Either, tokenId: Uint<128> ): [] { Initializable_assertInitialized(); const auth = left(ownPublicKey()); _approve( to, tokenId, auth ); } /** * @description Returns the account approved for `tokenId` token. * * @circuitInfo k=10, rows=409 * * Requirements: * * - The contract is 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=409 * * Requirements: * * - The contract is 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 {[]} - Empty tuple. */ export circuit setApprovalForAll( operator: Either, approved: Boolean ): [] { Initializable_assertInitialized(); const owner = left(ownPublicKey()); _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 is 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(disclose(owner)) && _operatorApprovals.lookup(owner).member(disclose(operator))) { return _operatorApprovals.lookup(owner).lookup(disclose(operator)); } else { return false; } } /** * @description Transfers `tokenId` token from `from` to `to`. * * @notice Transfers to contract addresses are currently disallowed until contract-to-contract interactions * are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot * currently handle token receipt. * * @circuitInfo k=11, rows=1966 * * Requirements: * * - The contract is initialized. * - `from` is not the zero address. * - `to` is not the zero address. * - `to` is not a 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 {[]} - Empty tuple. */ 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. * * WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract calls * are not currently supported. Tokens sent to a contract address may become irretrievable. * Once contract-to-contract calls are supported, this circuit may be deprecated. * * @circuitInfo k=11, rows=1963 * * Requirements: * * - The contract is initialized. * - `from` is not the zero address. * - `to` is not 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 {Uint<128>} tokenId - The token being transfered * @return {[]} - Empty tuple. */ 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(ownPublicKey()); 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 is 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(disclose(tokenId))) { return burnAddress(); } return _owners.lookup(disclose(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 is 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(disclose(tokenId))) { return burnAddress(); } return _tokenApprovals.lookup(disclose(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=1098 * * Requirements: * * - The contract is 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(disclose(spender)) && (disclose(owner) == disclose(spender) || isApprovedForAll(owner, spender) || _getApproved(tokenId) == disclose(spender)) ); } /** * @description Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. * * @circuitInfo k=11, rows=1121 * * Requirements: * * - The contract is 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 {[]} - Empty tuple. */ 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=11, rows=1959 * * Requirements: * * - The contract is 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(disclose(auth))) { _checkAuthorized(from, auth, tokenId); } // Execute the update if (!Utils_isKeyOrAddressZero(disclose(from))) { // Clear approval. No need to re-authorize _approve(burnAddress(), tokenId, burnAddress()); const newBalance = _balances.lookup(disclose(from)) - 1 as Uint<128>; _balances.insert(disclose(from), disclose(newBalance)); } if (!Utils_isKeyOrAddressZero(disclose(to))) { if (!_balances.member(disclose(to))) { _balances.insert(disclose(to), 0); } const newBalance = _balances.lookup(disclose(to)) + 1 as Uint<128>; _balances.insert(disclose(to), disclose(newBalance)); } _owners.insert(disclose(tokenId), disclose(to)); return from; } /** * @description Mints `tokenId` and transfers it to `to`. * * @circuitInfo k=11, rows=1013 * * Requirements: * * - The contract is initialized. * - `tokenId` must not exist. * - `to` is not the zero address. * - `to` is not a ContractAddress. * * @param {Either} to - The account receiving `tokenId` * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ 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=1010 * * Requirements: * * - The contract is initialized. * - `tokenId` must not exist. * - `to` is not the zero address. * * WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract * calls are not currently supported. Tokens sent to a contract address may become irretrievable. * Once contract-to-contract calls are supported, this circuit may be deprecated. * * @param {Either} to - The account receiving `tokenId` * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ export circuit _unsafeMint( to: Either, tokenId: Uint<128> ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); const previousOwner = _update(to, tokenId, burnAddress()); 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=479 * * Requirements: * * - The contract is initialized. * - `tokenId` must exist. * * @param {Uint<128>} tokenId - The token to burn * @return {[]} - Empty tuple. */ export circuit _burn(tokenId: Uint<128>): [] { Initializable_assertInitialized(); const previousOwner = _update(burnAddress(), tokenId, burnAddress()); assert(!Utils_isKeyOrAddressZero(previousOwner), "NonFungibleToken: Invalid Sender"); } /** * @description Transfers `tokenId` from `from` to `to`. * As opposed to {transferFrom}, this imposes no restrictions on ownPublicKey(). * * @notice Transfers to contract addresses are currently disallowed until contract-to-contract * interactions are supported in Compact. This restriction prevents assets from being inadvertently * locked in contracts that cannot currently handle token receipt. * * @circuitInfo k=11, rows=1224 * * Requirements: * * - The contract is initialized. * - `to` is not the zero address. * - `to` is not a 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 {[]} - Empty tuple. */ 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 ownPublicKey(). * It does NOT check if the recipient is a ContractAddress. * * @circuitInfo k=11, rows=1221 * * Requirements: * * - The contract is initialized. * - `to` is not the zero address. * - `tokenId` token must be owned by `from`. * * WARNING: Transfers to contract addresses are considered unsafe because contract-to-contract * calls are not currently supported. Tokens sent to a contract address may become irretrievable. * Once contract-to-contract calls are supported, this circuit may be deprecated. * * @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 {[]} - Empty tuple. */ export circuit _unsafeTransfer( from: Either, to: Either, tokenId: Uint<128> ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); const previousOwner = _update(to, tokenId, burnAddress()); assert(!Utils_isKeyOrAddressZero(previousOwner), "NonFungibleToken: Nonexistent Token"); assert(previousOwner == from, "NonFungibleToken: Incorrect Owner"); } /** * @description Approve `to` to operate on `tokenId` * * @circuitInfo k=11, rows=1109 * * Requirements: * * - The contract is 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 {[]} - Empty tuple. */ export circuit _approve( to: Either, tokenId: Uint<128>, auth: Either ): [] { Initializable_assertInitialized(); if (!Utils_isKeyOrAddressZero(disclose(auth))) { const owner = _requireOwned(tokenId); // We do not use _isAuthorized because single-token approvals should not be able to call approve assert((owner == disclose(auth) || isApprovedForAll(owner, auth)), "NonFungibleToken: Invalid Approver"); } _tokenApprovals.insert(disclose(tokenId), disclose(to)); } /** * @description Approve `operator` to operate on all of `owner` tokens * * @circuitInfo k=10, rows=524 * * Requirements: * * - The contract is initialized. * - `operator` is not 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 {[]} - Empty tuple. */ export circuit _setApprovalForAll( owner: Either, operator: Either, approved: Boolean ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(operator), "NonFungibleToken: Invalid Operator"); if (!_operatorApprovals.member(disclose(owner))) { _operatorApprovals.insert( disclose(owner), default, Boolean>> ); } _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(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=288 * * Requirements: * * - The contract is 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; } }