diff --git a/packages/erc721-watcher/.gitignore b/packages/erc721-watcher/.gitignore new file mode 100644 index 000000000..d3a728b90 --- /dev/null +++ b/packages/erc721-watcher/.gitignore @@ -0,0 +1,3 @@ +#Hardhat files +cache +artifacts diff --git a/packages/erc721-watcher/README.md b/packages/erc721-watcher/README.md index 528a7fb10..dfda3232e 100644 --- a/packages/erc721-watcher/README.md +++ b/packages/erc721-watcher/README.md @@ -8,12 +8,6 @@ yarn ``` -* Run the IPFS (go-ipfs version 0.12.2) daemon: - - ```bash - ipfs daemon - ``` - * Create a postgres12 database for the watcher: ```bash @@ -41,13 +35,169 @@ erc721-watcher-job-queue=# exit ``` +* The following core services should be setup and running on localhost: + + * `vulcanize/go-ethereum` [v1.10.18-statediff-3.2.2](https://github.com/vulcanize/go-ethereum/releases/tag/v1.10.18-statediff-3.2.2) on port 8545 + + * `vulcanize/ipld-eth-server` [v3.2.2](https://github.com/vulcanize/ipld-eth-server/releases/tag/v3.2.2) with native GQL API enabled, on port 8082 + * In the [config file](./environments/local.toml): * Update the database connection settings. * Update the `upstream` config and provide the `ipld-eth-server` GQL API endpoint. - * Update the `server` config with state checkpoint settings and provide the IPFS API address. +## Demo + +* Deploy an ERC721 token: + + ```bash + yarn nft:deploy + # NFT deployed to: NFT_ADDRESS + ``` + + Export the address of the deployed token to a shell variable for later use: + + ```bash + export NFT_ADDRESS="" + ``` + +* Open `http://localhost:3006/graphql` (GraphQL Playground) in a browser window + +* Connect MetaMask to `http://localhost:8545` (with chain ID `41337`) + +* Add a second account to Metamask and export the account address to a shell variable for later use: + + ```bash + export RECIPIENT_ADDRESS="" + ``` + +* To get the current block hash at any time, run: + + ```bash + yarn block:latest + ``` + +* Run the following GQL query (`eth_call`) in generated watcher graphql endpoint http://127.0.0.1:3006/graphql + + ```graphql + query { + name( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + ) { + value + proof { + data + } + } + symbol( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + ) { + value + proof { + data + } + } + balanceOf( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + owner: "0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc" + ) { + value + proof { + data + } + } + } + ``` + +* Run the following GQL query (`storage`) in generated watcher graphql endpoint http://127.0.0.1:3006/graphql + + ```graphql + query { + _name( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + ) { + value + proof { + data + } + } + _symbol( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + ) { + value + proof { + data + } + } + _balances( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + key0: "0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc" + ) { + value + proof { + data + } + } + } + ``` + +* Mint token + + ```bash + yarn nft:mint --nft $NFT_ADDRESS --to 0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc --token-id 1 + ``` + +* Get the latest blockHash and run the following query for `balanceOf` and `ownerOf` (`eth_call`): + + ```graphql + query { + fromBalanceOf: balanceOf( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + owner: "0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc" + ) { + value + proof { + data + } + } + toBalanceOf: balanceOf( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + owner: "RECIPIENT_ADDRESS" + ) { + value + proof { + data + } + } + ownerOf( + blockHash: "LATEST_BLOCK_HASH" + contractAddress: "NFT_ADDRESS" + tokenId: 1 + ) { + value + proof { + data + } + } + } + ``` + + * Transfer token + + ```bash + yarn nft:transfer --nft $NFT_ADDRESS --from 0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc --to $RECIPIENT_ADDRESS --token-id 1 + ``` + + * Get the latest blockHash and replace the blockHash in the above query. The result should be different and the token should be transferred to the recipient. ## Customize @@ -99,7 +249,7 @@ GQL console: http://localhost:3006/graphql Watch a contract with its address and checkpointing on: ```bash - yarn watch:contract --address 0x1F78641644feB8b64642e833cE4AFE93DD6e7833 --kind ERC20 --checkpoint true + yarn watch:contract --address 0x1F78641644feB8b64642e833cE4AFE93DD6e7833 --kind ERC721 --checkpoint true ``` Watch a contract with its identifier and checkpointing on: diff --git a/packages/erc721-watcher/hardhat.config.ts b/packages/erc721-watcher/hardhat.config.ts new file mode 100644 index 000000000..5d885b127 --- /dev/null +++ b/packages/erc721-watcher/hardhat.config.ts @@ -0,0 +1,28 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import '@nomiclabs/hardhat-waffle'; + +import './test/tasks/nft-deploy'; +import './test/tasks/nft-mint'; +import './test/tasks/nft-transfer'; +import './test/tasks/block-latest'; + +// You need to export an object to set up your config +// Go to https://hardhat.org/config/ to learn more + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +export default { + solidity: '0.8.0', + networks: { + docker: { + url: 'http://geth:8545' + } + }, + paths: { + sources: './test/contracts' + } +}; diff --git a/packages/erc721-watcher/package.json b/packages/erc721-watcher/package.json index 4068fb3d7..9192d8454 100644 --- a/packages/erc721-watcher/package.json +++ b/packages/erc721-watcher/package.json @@ -15,7 +15,11 @@ "checkpoint": "DEBUG=vulcanize:* ts-node src/cli/checkpoint.ts", "export-state": "DEBUG=vulcanize:* ts-node src/cli/export-state.ts", "import-state": "DEBUG=vulcanize:* ts-node src/cli/import-state.ts", - "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts" + "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts", + "nft:deploy": "hardhat --network localhost nft-deploy", + "nft:mint": "hardhat --network localhost nft-mint", + "nft:transfer": "hardhat --network localhost nft-transfer", + "block:latest": "hardhat --network localhost block-latest" }, "repository": { "type": "git", @@ -50,6 +54,8 @@ }, "devDependencies": { "@ethersproject/abi": "^5.3.0", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/express": "^4.17.11", "@types/yargs": "^17.0.0", "@typescript-eslint/eslint-plugin": "^4.25.0", @@ -62,6 +68,7 @@ "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^5.0.0", "ts-node": "^10.0.0", - "typescript": "^4.3.2" + "typescript": "^4.3.2", + "hardhat": "^2.3.0" } } diff --git a/packages/erc721-watcher/src/indexer.ts b/packages/erc721-watcher/src/indexer.ts index e6fbb34d9..38f671ea4 100644 --- a/packages/erc721-watcher/src/indexer.ts +++ b/packages/erc721-watcher/src/indexer.ts @@ -132,10 +132,8 @@ export class Indexer implements IPLDIndexerInterface { this._contractMap.set(KIND_ERC721, new ethers.utils.Interface(ERC721ABI)); this._entityTypesMap = new Map(); - this._populateEntityTypesMap(); this._relationsMap = new Map(); - this._populateRelationsMap(); } async init (): Promise { diff --git a/packages/erc721-watcher/src/types.ts b/packages/erc721-watcher/src/types.ts deleted file mode 100644 index c4562172f..000000000 --- a/packages/erc721-watcher/src/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -// -// Copyright 2021 Vulcanize, Inc. -// diff --git a/packages/erc721-watcher/test/contracts/TestNFT.sol b/packages/erc721-watcher/test/contracts/TestNFT.sol new file mode 100644 index 000000000..0ebf59f69 --- /dev/null +++ b/packages/erc721-watcher/test/contracts/TestNFT.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract TestNFT is ERC721 { + constructor() ERC721("TestNFT", "TNFT") { + } + + function mint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } +} diff --git a/packages/erc721-watcher/test/tasks/block-latest.ts b/packages/erc721-watcher/test/tasks/block-latest.ts new file mode 100644 index 000000000..0ba58bc7f --- /dev/null +++ b/packages/erc721-watcher/test/tasks/block-latest.ts @@ -0,0 +1,18 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { task } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; + +task( + 'block-latest', + 'Prints the current block info', + async (_, { ethers }) => { + const blockNumber = await ethers.provider.getBlockNumber(); + const block = await ethers.provider.getBlock(blockNumber); + + console.log('Block Number:', blockNumber); + console.log('Block Hash:', block.hash); + } +); diff --git a/packages/erc721-watcher/test/tasks/nft-deploy.ts b/packages/erc721-watcher/test/tasks/nft-deploy.ts new file mode 100644 index 000000000..77838419f --- /dev/null +++ b/packages/erc721-watcher/test/tasks/nft-deploy.ts @@ -0,0 +1,15 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { task } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; + +task('nft-deploy', 'Deploys NFT') + .setAction(async (args, hre) => { + await hre.run('compile'); + const NFT = await hre.ethers.getContractFactory('TestNFT'); + const nft = await NFT.deploy(); + + console.log('NFT deployed to:', nft.address); + }); diff --git a/packages/erc721-watcher/test/tasks/nft-mint.ts b/packages/erc721-watcher/test/tasks/nft-mint.ts new file mode 100644 index 000000000..032794915 --- /dev/null +++ b/packages/erc721-watcher/test/tasks/nft-mint.ts @@ -0,0 +1,33 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { task, types } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import { ContractTransaction } from 'ethers'; + +task('nft-mint', 'Mint NFT') + .addParam('nft', 'Contract address', undefined, types.string) + .addParam('tokenId', 'Token ID', undefined, types.string) + .addParam('to', 'Transfer recipient address', undefined, types.string) + .setAction(async (args, hre) => { + const { tokenId, to, nft: contractAddress } = args; + await hre.run('compile'); + const NFT = await hre.ethers.getContractFactory('TestNFT'); + const nft = NFT.attach(contractAddress); + + const transaction: ContractTransaction = await nft.mint(to, tokenId); + + const receipt = await transaction.wait(); + + if (receipt.events) { + const TransferEvent = receipt.events.find(el => el.event === 'Transfer'); + + if (TransferEvent && TransferEvent.args) { + console.log('Transfer Event'); + console.log('from:', TransferEvent.args.from.toString()); + console.log('to:', TransferEvent.args.to.toString()); + console.log('tokenId:', TransferEvent.args.tokenId.toString()); + } + } + }); diff --git a/packages/erc721-watcher/test/tasks/nft-transfer.ts b/packages/erc721-watcher/test/tasks/nft-transfer.ts new file mode 100644 index 000000000..abca34841 --- /dev/null +++ b/packages/erc721-watcher/test/tasks/nft-transfer.ts @@ -0,0 +1,34 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +import { task, types } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import { ContractTransaction } from 'ethers'; + +task('nft-transfer', 'Move tokens to recipient') + .addParam('nft', 'Contract address', undefined, types.string) + .addParam('from', 'Transfer from address', undefined, types.string) + .addParam('to', 'Transfer recipient address', undefined, types.string) + .addParam('tokenId', 'Token ID to transfer', undefined, types.string) + .setAction(async (args, hre) => { + const { nft: contractAddress, from, to, tokenId } = args; + await hre.run('compile'); + const NFT = await hre.ethers.getContractFactory('TestNFT'); + const nft = NFT.attach(contractAddress); + + const transaction: ContractTransaction = await nft['safeTransferFrom(address,address,uint256)'](from, to, tokenId); + + const receipt = await transaction.wait(); + + if (receipt.events) { + const TransferEvent = receipt.events.find(el => el.event === 'Transfer'); + + if (TransferEvent && TransferEvent.args) { + console.log('Transfer Event'); + console.log('from:', TransferEvent.args.from.toString()); + console.log('to:', TransferEvent.args.to.toString()); + console.log('tokenId:', TransferEvent.args.tokenId.toString()); + } + } + });