diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34fbfa1..7ae4578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: uses: actions/checkout@v3 - name: Install dependencies run: npm ci + - name: Prepare CI + run: npm run ci-prepare - name: Build run: npm run build - name: Test diff --git a/.gitignore b/.gitignore index ecfc811..6fee291 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /types /artifacts /cache +.secrets.json ################################################################################ diff --git a/README.md b/README.md index 3d526d6..cd2284d 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,37 @@ npm test ``` npm run build ``` + +## Verification + +Set up a `.secrets.json` file that looks similar to `example.secrets.json`: + +After deploying your contract, you can verify it by running the following. + +### Rinkeby +``` +npx hardhat verify --network rinkeby CONTRACT_ADDRESS +``` + +For a proxy contract, you'll need to pass the arguments to the proxy contract, and pass the contract as well: + +``` +npx hardhat verify --network rinkeby \ + --contract contracts/common/UpgradeableProxy.sol:UpgradeableProxy \ + --constructor-args examples/parcelNFTConstructorParams.js \ + CONTRACT_ADDRESS +``` + +### Mainnet +``` +npx hardhat verify --network rinkeby CONTRACT_ADDRESS +``` + +``` +npx hardhat verify --network mainnet \ + --contract contracts/common/UpgradeableProxy.sol:UpgradeableProxy \ + --constructor-args examples/parcelNFTConstructorParams.js \ + CONTRACT_ADDRESS +``` + +See [hardhat-etherscan](https://hardhat.org/plugins/nomiclabs-hardhat-etherscan.html) for more examples diff --git a/contracts/ParcelNFT.sol b/contracts/ParcelNFT.sol index 20a4bdf..c571293 100644 --- a/contracts/ParcelNFT.sol +++ b/contracts/ParcelNFT.sol @@ -5,14 +5,16 @@ pragma solidity ^0.8.9; import '@gnus.ai/contracts-upgradeable-diamond/access/AccessControlUpgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/proxy/utils/UUPSUpgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/security/PausableUpgradeable.sol'; -import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol'; +import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/ERC721RoyaltyUpgradeable.sol'; -import './ParcelNFTStorage.sol'; +import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol'; import './common/RoyaltyEventSupport.sol'; +import './common/TokenUriStorage.sol'; import './Roles.sol'; contract ParcelNFT is UUPSUpgradeable, + ERC721EnumerableUpgradeable, ERC721URIStorageUpgradeable, RoyaltyEventSupport, ERC721RoyaltyUpgradeable, @@ -27,6 +29,7 @@ contract ParcelNFT is function initialize(InitParams memory initParams) public initializer { __ERC721_init(initParams.name, initParams.symbol); + __ERC721Enumerable_init_unchained(); __ERC721URIStorage_init_unchained(); __ERC2981_init_unchained(); __ERC721Royalty_init_unchained(); @@ -48,21 +51,34 @@ contract ParcelNFT is public view virtual - override(AccessControlUpgradeable, ERC721RoyaltyUpgradeable, ERC721Upgradeable, ERC2981Upgradeable) + override( + AccessControlUpgradeable, + ERC721EnumerableUpgradeable, + ERC721RoyaltyUpgradeable, + ERC721Upgradeable, + ERC2981Upgradeable + ) returns (bool) { return super.supportsInterface(interfaceId); } /** - * @notice Sets `baseURI` as the base URI for all tokens. Used when explicit tokenURI not set. + * @notice Gets base URI for all tokens, if set. */ - function setBaseURI(string calldata baseURI) external onlyRole(Roles.PARCEL_MANAGER) { - ParcelNFTStorage.setBaseURI(baseURI); + function baseURI() public view returns (string memory) { + return _baseURI(); } function _baseURI() internal view virtual override returns (string memory) { - return ParcelNFTStorage.baseURI(); + return TokenUriStorage.baseURI(); + } + + /** + * @notice Sets `baseURI` as the base URI for all tokens. Used when explicit tokenURI not set. + */ + function setBaseURI(string calldata __baseURI) external onlyRole(Roles.PARCEL_MANAGER) { + TokenUriStorage.setBaseURI(__baseURI); } /** @@ -190,10 +206,22 @@ contract ParcelNFT is _unpause(); } - function _burn(uint256 tokenId) internal virtual override(ERC721URIStorageUpgradeable, ERC721RoyaltyUpgradeable) { + function _burn(uint256 tokenId) + internal + virtual + override(ERC721URIStorageUpgradeable, ERC721RoyaltyUpgradeable, ERC721Upgradeable) + { super._burn(tokenId); } + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override(ERC721EnumerableUpgradeable, ERC721Upgradeable) { + super._beforeTokenTransfer(from, to, tokenId); + } + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address newImplementation) internal virtual override onlyRole(Roles.UPGRADER) {} } diff --git a/contracts/ParcelNFTStorage.sol b/contracts/common/TokenUriStorage.sol similarity index 91% rename from contracts/ParcelNFTStorage.sol rename to contracts/common/TokenUriStorage.sol index b445488..23ad8d5 100644 --- a/contracts/ParcelNFTStorage.sol +++ b/contracts/common/TokenUriStorage.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.9; -library ParcelNFTStorage { +library TokenUriStorage { struct Layout { // storage for base URI string baseURI; } - bytes32 internal constant STORAGE_SLOT = keccak256('citydao.contracts.storage.ParcelNFT'); + bytes32 internal constant STORAGE_SLOT = keccak256('citydao.contracts.storage.TokenUri'); //noinspection NoReturn function layout() internal pure returns (Layout storage _layout) { diff --git a/contracts/common/UpgradeableProxy.sol b/contracts/common/UpgradeableProxy.sol new file mode 100644 index 0000000..03f5d6f --- /dev/null +++ b/contracts/common/UpgradeableProxy.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.9; + +import '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol'; + +// needed to be able to use the proxy for verification +contract UpgradeableProxy is ERC1967Proxy { + // solhint-disable-next-line no-empty-blocks + constructor(address _logic, bytes memory _data) payable ERC1967Proxy(_logic, _data) {} +} diff --git a/contracts/test/ERC165IdCalc.sol b/contracts/test/ERC165IdCalc.sol index 1c83521..74f6241 100644 --- a/contracts/test/ERC165IdCalc.sol +++ b/contracts/test/ERC165IdCalc.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.9; import '@gnus.ai/contracts-upgradeable-diamond/access/IAccessControlUpgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/interfaces/IERC2981Upgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/IERC721Upgradeable.sol'; +import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/IERC721EnumerableUpgradeable.sol'; import '@gnus.ai/contracts-upgradeable-diamond/token/ERC721/extensions/IERC721MetadataUpgradeable.sol'; library ERC165IdCalc { @@ -20,6 +21,10 @@ library ERC165IdCalc { return type(IERC721Upgradeable).interfaceId; } + function calcERC721EnumerableInterfaceId() external pure returns (bytes4) { + return type(IERC721EnumerableUpgradeable).interfaceId; + } + function calcERC721MetadataInterfaceId() external pure returns (bytes4) { return type(IERC721MetadataUpgradeable).interfaceId; } diff --git a/example.secrets.json b/example.secrets.json new file mode 100644 index 0000000..1b999c8 --- /dev/null +++ b/example.secrets.json @@ -0,0 +1,8 @@ +{ + "networks": { + "rinkeby": { + "url": "https://rinkeby.infura.io/v3/YOUR_PROJECT_ID" + } + }, + "etherscanApiKey": "YOUR_API_KEY" +} diff --git a/examples/parcelNFTConstructorParams.js b/examples/parcelNFTConstructorParams.js new file mode 100644 index 0000000..1b3e50e --- /dev/null +++ b/examples/parcelNFTConstructorParams.js @@ -0,0 +1,8 @@ +const { buildParcelNFTInitFunction } = require('../dist/src/contracts/parcelNFT'); +module.exports = [ + '0xc0cA359c8ce6De21B98fC6c7921a08703f453Fe9', + buildParcelNFTInitFunction({ + name: 'ParcelNFT Test 2022-05-07-02', + symbol: 'PT050702', + }), +]; diff --git a/hardhat.config.ts b/hardhat.config.ts index b49bb79..967b9a0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,7 +1,9 @@ import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-etherscan'; import '@nomiclabs/hardhat-waffle'; import '@typechain/hardhat'; import { HardhatUserConfig } from 'hardhat/config'; +import { etherscanApiKey, networks } from './.secrets.json'; const hardhatConfig: HardhatUserConfig = { solidity: { @@ -19,6 +21,12 @@ const hardhatConfig: HardhatUserConfig = { outDir: 'types/contracts', target: 'ethers-v5', }, + + networks, + + etherscan: { + apiKey: etherscanApiKey, + }, }; export default hardhatConfig; diff --git a/package-lock.json b/package-lock.json index 9514dd8..8467d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@nomiclabs/hardhat-ethers": "2.0.5", + "@nomiclabs/hardhat-etherscan": "3.0.3", "@nomiclabs/hardhat-waffle": "2.0.3", "@typechain/ethers-v5": "9.0.0", "@typechain/hardhat": "4.0.0", @@ -31,6 +32,7 @@ "eslint-plugin-prettier": "4.0.0", "ethereum-waffle": "3.4.4", "hardhat": "2.9.3", + "hardhat-etherscan": "1.0.1", "husky": "7.0.4", "merkletreejs": "0.2.31", "mocha": "9.2.2", @@ -1614,6 +1616,56 @@ "hardhat": "^2.0.0" } }, + "node_modules/@nomiclabs/hardhat-etherscan": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.3.tgz", + "integrity": "sha512-OfNtUKc/ZwzivmZnnpwWREfaYncXteKHskn3yDnz+fPBZ6wfM4GR+d5RwjREzYFWE+o5iR9ruXhWw/8fejWM9g==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^5.0.2", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "semver": "^6.3.0", + "undici": "^4.14.1" + }, + "peerDependencies": { + "hardhat": "^2.0.4" + } + }, + "node_modules/@nomiclabs/hardhat-etherscan/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@nomiclabs/hardhat-etherscan/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@nomiclabs/hardhat-etherscan/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nomiclabs/hardhat-waffle": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz", @@ -2965,6 +3017,19 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "node_modules/cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/chai": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", @@ -15283,6 +15348,12 @@ "node": "^12.0.0 || ^14.0.0 || ^16.0.0" } }, + "node_modules/hardhat-etherscan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hardhat-etherscan/-/hardhat-etherscan-1.0.1.tgz", + "integrity": "sha512-FShbbcrmxLK0n1JJfRO0xq6CfEsbynt0HiNHQYvY9GOQ9UDTHRAtZT4+P20cn/yAqfyQl4bgw3URxvvNN85HXA==", + "dev": true + }, "node_modules/hardhat/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -17711,6 +17782,15 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -22501,6 +22581,49 @@ "dev": true, "requires": {} }, + "@nomiclabs/hardhat-etherscan": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-etherscan/-/hardhat-etherscan-3.0.3.tgz", + "integrity": "sha512-OfNtUKc/ZwzivmZnnpwWREfaYncXteKHskn3yDnz+fPBZ6wfM4GR+d5RwjREzYFWE+o5iR9ruXhWw/8fejWM9g==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^5.0.2", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "semver": "^6.3.0", + "undici": "^4.14.1" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "@nomiclabs/hardhat-waffle": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.3.tgz", @@ -23582,6 +23705,16 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "requires": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + } + }, "chai": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", @@ -33324,6 +33457,12 @@ } } }, + "hardhat-etherscan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hardhat-etherscan/-/hardhat-etherscan-1.0.1.tgz", + "integrity": "sha512-FShbbcrmxLK0n1JJfRO0xq6CfEsbynt0HiNHQYvY9GOQ9UDTHRAtZT4+P20cn/yAqfyQl4bgw3URxvvNN85HXA==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -34979,6 +35118,12 @@ "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", "dev": true }, + "nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", diff --git a/package.json b/package.json index 41c0d08..0e5bd58 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "lint": "solhint 'contracts/**/*.sol' && eslint", "pretty": "pretty-quick", "release": "standard-version", - "prepublishOnly": "npm run rebuild && npm run release" + "prepublishOnly": "npm run rebuild && npm run release", + "ci-prepare": "cp example.secrets.json .secrets.json" }, "husky": { "hooks": { @@ -46,6 +47,7 @@ }, "devDependencies": { "@nomiclabs/hardhat-ethers": "2.0.5", + "@nomiclabs/hardhat-etherscan": "3.0.3", "@nomiclabs/hardhat-waffle": "2.0.3", "@typechain/ethers-v5": "9.0.0", "@typechain/hardhat": "4.0.0", @@ -62,6 +64,7 @@ "eslint-plugin-prettier": "4.0.0", "ethereum-waffle": "3.4.4", "hardhat": "2.9.3", + "hardhat-etherscan": "1.0.1", "husky": "7.0.4", "merkletreejs": "0.2.31", "mocha": "9.2.2", diff --git a/src/constants/interfaces.ts b/src/constants/interfaces.ts index dfcaa17..ee3ac2f 100644 --- a/src/constants/interfaces.ts +++ b/src/constants/interfaces.ts @@ -6,7 +6,27 @@ export type Erc165InterfaceId = string; export const toErc165InterfaceId = (value: BytesLike | Hexable | number): Erc165InterfaceId => toByte4String(value); +/** + * @description Access Control List (ACL) + */ export const ACCESS_CONTROL_INTERFACE_ID = toErc165InterfaceId(0x7965db0b); + +/** + * @description Royalty standard + */ export const ERC2981_INTERFACE_ID = toErc165InterfaceId(0x2a55205a); + +/** + * @description NFT standard + */ export const ERC721_INTERFACE_ID = toErc165InterfaceId(0x80ac58cd); + +/** + * @description Supports totalSupply, tokenOfOwnerByIndex, and tokenByIndex + */ +export const ERC721_ENUMERABLE_INTERFACE_ID = toErc165InterfaceId(0x780e9d63); + +/** + * @description Supports name, symbol, and tokenURI + */ export const ERC721_METADATA_INTERFACE_ID = toErc165InterfaceId(0x5b5e139f); diff --git a/src/contracts/parcelNFT.ts b/src/contracts/parcelNFT.ts index 29fecc2..14fdc6f 100644 --- a/src/contracts/parcelNFT.ts +++ b/src/contracts/parcelNFT.ts @@ -1,7 +1,8 @@ +import { TransactionRequest } from '@ethersproject/providers'; import { Signer } from 'ethers'; import { ParcelNFT, ParcelNFT__factory } from '../../types/contracts'; import { ContractAddress, EthereumAddress, ZERO_ADDRESS } from '../constants/accounts'; -import { deployUpgradeableProxy } from './upgradeableProxy'; +import { buildDeployUpgradeableProxyTransactionRequest, deployUpgradeableProxy } from './upgradeableProxy'; export interface ParcelNFTInitParams { // the name of the token @@ -39,3 +40,25 @@ export const createParcelNFT = async ( const proxy = await deployUpgradeableProxy(signer, parcelNFTLogicAddress, initFunction); return ParcelNFT__factory.connect(proxy.address, signer); }; + +/** + * Builds a transaction request to create and initialize an upgradeable Parcel NFT contract using the already-deployed + * ParcelNFT contract for implementation + * + * @param parcelNFTLogicAddress the deployed parcel contract that will be used for logic + * @param initParams (optional) the initialization parameters + */ +export const buildCreateParcelNFTTransactionRequest = ( + parcelNFTLogicAddress: ContractAddress, + initParams: Partial = {}, +): TransactionRequest => + buildDeployUpgradeableProxyTransactionRequest(parcelNFTLogicAddress, buildParcelNFTInitFunction(initParams)); + +/** + * Builds the initialization function for the ParcelNFT contract with the given inputs + * @param initParams (optional) the initialization parameters + */ +export const buildParcelNFTInitFunction = (initParams: Partial = {}) => + ParcelNFT__factory.createInterface().encodeFunctionData('initialize', [ + { ...defaultParcelNFTInitParams, ...initParams }, + ]); diff --git a/src/contracts/upgradeableProxy.ts b/src/contracts/upgradeableProxy.ts index ed137f3..44b5a0c 100644 --- a/src/contracts/upgradeableProxy.ts +++ b/src/contracts/upgradeableProxy.ts @@ -1,6 +1,7 @@ +import { TransactionRequest } from '@ethersproject/providers'; import { Signer } from 'ethers'; import { BytesLike } from 'ethers/lib/utils'; -import { ERC1967Proxy__factory } from '../../types/contracts'; +import { UpgradeableProxy__factory } from '../../types/contracts'; import { ContractAddress } from '../constants/accounts'; /** @@ -15,18 +16,16 @@ export const deployUpgradeableProxy = async ( signer: Signer, logicAddress: ContractAddress, initFunction: BytesLike = '0x', -) => new ERC1967Proxy__factory(signer).deploy(logicAddress, initFunction); +) => new UpgradeableProxy__factory(signer).deploy(logicAddress, initFunction); /** * Build a transaction to deploy an upgradeable proxy contract with the given contract address * to be used for the logic and initialization function * - * @param signer The signer to use for deployment * @param logicAddress the address of the contract to use for the implementation logic * @param initFunction (optional) the initialization function, abi-encoded. Defaults to empty */ -export const buildUpgradeableProxyDeployment = async ( - signer: Signer, +export const buildDeployUpgradeableProxyTransactionRequest = ( logicAddress: ContractAddress, initFunction: BytesLike = '0x', -) => new ERC1967Proxy__factory().getDeployTransaction(logicAddress, initFunction); +): TransactionRequest => new UpgradeableProxy__factory().getDeployTransaction(logicAddress, initFunction); diff --git a/test/contracts/access/access.spec.ts b/test/contracts/access/access.spec.ts index 62d0e13..1d3ad4a 100644 --- a/test/contracts/access/access.spec.ts +++ b/test/contracts/access/access.spec.ts @@ -7,7 +7,7 @@ import { createParcelNFT } from '../../helpers/contracts/ParcelNFTHelper'; import { shouldSupportInterface } from '../../helpers/ERC165Helper'; import { ROLE1, ROLE2 } from '../../helpers/Roles'; -describe('supportsInterface', () => { +describe('IAccessControl', () => { shouldSupportInterface('IAccessControl', () => createParcelNFT(), ACCESS_CONTROL_INTERFACE_ID); }); diff --git a/test/contracts/erc165/interfaceCalc.spec.ts b/test/contracts/erc165/interfaceCalc.spec.ts index 54d395e..a6ee847 100644 --- a/test/contracts/erc165/interfaceCalc.spec.ts +++ b/test/contracts/erc165/interfaceCalc.spec.ts @@ -3,6 +3,7 @@ import { ACCESS_CONTROL_INTERFACE_ID, Erc165InterfaceId, ERC2981_INTERFACE_ID, + ERC721_ENUMERABLE_INTERFACE_ID, ERC721_INTERFACE_ID, ERC721_METADATA_INTERFACE_ID, } from '../../../src/constants/interfaces'; @@ -31,6 +32,11 @@ const interfaceTests: InterfaceTest[] = [ interfaceId: ERC721_INTERFACE_ID, calcInterfaceId: (idCalc) => idCalc.calcERC721InterfaceId(), }, + { + name: 'ERC721Enumerable', + interfaceId: ERC721_ENUMERABLE_INTERFACE_ID, + calcInterfaceId: (idCalc) => idCalc.calcERC721EnumerableInterfaceId(), + }, { name: 'ERC721Metadata', interfaceId: ERC721_METADATA_INTERFACE_ID, @@ -38,7 +44,7 @@ const interfaceTests: InterfaceTest[] = [ }, ]; -describe('calculations', () => { +describe('ERC165 calculations', () => { interfaceTests.forEach(({ name, interfaceId, calcInterfaceId }) => it(`should match ${name} interface id`, async () => { const idCalc = await new ERC165IdCalc__factory(INITIALIZER).deploy(); diff --git a/test/contracts/erc721/erc721.ts b/test/contracts/erc721/erc721.ts index 5a6480c..2226aa1 100644 --- a/test/contracts/erc721/erc721.ts +++ b/test/contracts/erc721/erc721.ts @@ -1,8 +1,110 @@ -import { ERC721_INTERFACE_ID, ERC721_METADATA_INTERFACE_ID } from '../../../src/constants/interfaces'; +import { expect } from 'chai'; +import { + ERC721_ENUMERABLE_INTERFACE_ID, + ERC721_INTERFACE_ID, + ERC721_METADATA_INTERFACE_ID, +} from '../../../src/constants/interfaces'; +import { PARCEL_MANAGER_ROLE } from '../../../src/constants/roles'; +import { USER1, USER2 } from '../../helpers/Accounts'; import { createParcelNFT } from '../../helpers/contracts/ParcelNFTHelper'; import { shouldSupportInterface } from '../../helpers/ERC165Helper'; -describe('supportsInterface', () => { +describe('ERC721', () => { shouldSupportInterface('IERC721', () => createParcelNFT(), ERC721_INTERFACE_ID); + shouldSupportInterface('IERC721Enumerable', () => createParcelNFT(), ERC721_ENUMERABLE_INTERFACE_ID); shouldSupportInterface('IERC721Metadata', () => createParcelNFT(), ERC721_METADATA_INTERFACE_ID); }); + +// these tests are cursory just to confirm that the implementation is included +describe('ERC721Enumerable', () => { + // totalSupply, tokenOfOwnerByIndex, and tokenByIndex + describe('totalSupply', () => { + it('should return the totalSupply', async () => { + const parcelNFT = await createParcelNFT(); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER1.address); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER2.address); + + expect(await parcelNFT.totalSupply()).to.eq(0); + + await parcelNFT.connect(USER1).mint(100); + expect(await parcelNFT.totalSupply()).to.eq(1); + + await parcelNFT.connect(USER1).mint(101); + await parcelNFT.connect(USER2).mint(102); + await parcelNFT.connect(USER1).mint(103); + expect(await parcelNFT.totalSupply()).to.eq(4); + }); + }); + + describe('tokenOfOwnerByIndex', () => { + it('should return the token at the given index for the given user', async () => { + const parcelNFT = await createParcelNFT(); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER1.address); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER2.address); + + await parcelNFT.connect(USER1).mint(100); + expect(await parcelNFT.tokenOfOwnerByIndex(USER1.address, 0)).to.eq(100); + + await parcelNFT.connect(USER2).mint(101); + expect(await parcelNFT.tokenOfOwnerByIndex(USER1.address, 0)).to.eq(100); + expect(await parcelNFT.tokenOfOwnerByIndex(USER2.address, 0)).to.eq(101); + + await parcelNFT.connect(USER1).mint(102); + await parcelNFT.connect(USER1).mint(103); + expect(await parcelNFT.tokenOfOwnerByIndex(USER1.address, 0)).to.eq(100); + expect(await parcelNFT.tokenOfOwnerByIndex(USER1.address, 1)).to.eq(102); + expect(await parcelNFT.tokenOfOwnerByIndex(USER1.address, 2)).to.eq(103); + expect(await parcelNFT.tokenOfOwnerByIndex(USER2.address, 0)).to.eq(101); + }); + }); + + describe('tokenByIndex', () => { + it('should return the token at the given index', async () => { + const parcelNFT = await createParcelNFT(); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER1.address); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER2.address); + + await parcelNFT.connect(USER1).mint(100); + expect(await parcelNFT.tokenByIndex(0)).to.eq(100); + + await parcelNFT.connect(USER2).mint(101); + expect(await parcelNFT.tokenByIndex(0)).to.eq(100); + expect(await parcelNFT.tokenByIndex(1)).to.eq(101); + + await parcelNFT.connect(USER1).mint(102); + await parcelNFT.connect(USER1).mint(103); + expect(await parcelNFT.tokenByIndex(0)).to.eq(100); + expect(await parcelNFT.tokenByIndex(1)).to.eq(101); + expect(await parcelNFT.tokenByIndex(2)).to.eq(102); + expect(await parcelNFT.tokenByIndex(3)).to.eq(103); + }); + }); +}); + +describe('ERC721Metadata', () => { + describe('name', () => { + it('should return the name', async () => { + const parcelNFT = await createParcelNFT({ name: 'The Name' }); + expect(await parcelNFT.name()).to.eq('The Name'); + }); + }); + + describe('symbol', () => { + it('should return the symbol', async () => { + const parcelNFT = await createParcelNFT({ symbol: 'The Symbol' }); + expect(await parcelNFT.symbol()).to.eq('The Symbol'); + }); + }); + + describe('tokenURI', () => { + it('should return the token URI', async () => { + const parcelNFT = await createParcelNFT(); + await parcelNFT.grantRole(PARCEL_MANAGER_ROLE, USER1.address); + + await parcelNFT.connect(USER1).mint(100); + + await parcelNFT.connect(USER1).setBaseURI('https://the.base.uri/'); + expect(await parcelNFT.tokenURI(100)).to.eq('https://the.base.uri/100'); + }); + }); +}); diff --git a/test/contracts/erc721/tokenURI.spec.ts b/test/contracts/erc721/tokenURI.spec.ts index 268f9e2..16a0495 100644 --- a/test/contracts/erc721/tokenURI.spec.ts +++ b/test/contracts/erc721/tokenURI.spec.ts @@ -14,6 +14,7 @@ describe('setBaseURI', () => { await parcelNFT.connect(USER1).setBaseURI('the-base/'); + expect(await parcelNFT.baseURI()).to.eq('the-base/'); expect(await parcelNFT.tokenURI(100)).to.eq('the-base/100'); }); diff --git a/test/contracts/royalty/royalty.spec.ts b/test/contracts/royalty/royalty.spec.ts index 7775f89..b8d546b 100644 --- a/test/contracts/royalty/royalty.spec.ts +++ b/test/contracts/royalty/royalty.spec.ts @@ -7,7 +7,7 @@ import { INITIALIZER, USER1, USER2 } from '../../helpers/Accounts'; import { createParcelNFT } from '../../helpers/contracts/ParcelNFTHelper'; import { shouldSupportInterface } from '../../helpers/ERC165Helper'; -describe('supportsInterface', () => { +describe('IERC2981', () => { shouldSupportInterface('IERC2981', () => createParcelNFT(), ERC2981_INTERFACE_ID); }); diff --git a/tsconfig.json b/tsconfig.json index db8d84e..dc87235 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "strictNullChecks": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "outDir": "dist" }, "include": ["./src/**/*.ts"],