From 79501feb7a4820a83d435cedf08260a7a236ab06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernani=20S=C3=A3o=20Thiago?= Date: Thu, 16 Nov 2023 11:36:55 -0300 Subject: [PATCH 1/2] Support Cronos (#13) add support for cronos network --- .github/workflows/all.yaml | 9 +++++---- .github/workflows/master.yaml | 8 ++++---- .github/workflows/qa.yaml | 10 ++++++---- README.md | 6 +++--- config/cronos-mainnet.json | 11 +++++++++++ config/goerli.json | 10 ---------- package.json | 5 ++--- subgraph.template.yaml | 5 ++++- 8 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 config/cronos-mainnet.json delete mode 100644 config/goerli.json diff --git a/.github/workflows/all.yaml b/.github/workflows/all.yaml index 939cc82..d75bf0e 100644 --- a/.github/workflows/all.yaml +++ b/.github/workflows/all.yaml @@ -23,11 +23,12 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GHB_TOKEN }} - name: Lint run: npm run lint - - name: Build Goerli - run: npm run build:goerli + - name: Test + run: npm run test - name: Build Mumbai run: npm run build:mumbai + - name: Build Cronos + run: npm run build:cronos-mainnet - name: Build Polygon run: npm run build:polygon - - name: Test - run: npm run test + diff --git a/.github/workflows/master.yaml b/.github/workflows/master.yaml index 66da709..521008a 100644 --- a/.github/workflows/master.yaml +++ b/.github/workflows/master.yaml @@ -25,14 +25,14 @@ jobs: run: npm run lint - name: Subgraph Auth run: yarn graph:auth -- ${{ secrets.THEGRAPH_API_KEY }} - - name: Build Goerli - run: npm run build:goerli - - name: Deploy Goerli Hosted Service - run: npm run deploy:goerli - name: Build Mumbai run: npm run build:mumbai - name: Deploy Mumbai Hosted Service run: npm run deploy:mumbai + - name: Build Cronos + run: npm run build:cronos-mainnet + - name: Deploy Self-hosted Cronos + run: npx graph deploy --node ${{ secrets.SELF_HOSTED_SUBGRAPH_URL }} --version-label ${{ github.sha }} cronos-mainnet_prod_nft-roles - name: Build Polygon run: npm run build:polygon - name: Test diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml index 2ff34e4..8e67a95 100644 --- a/.github/workflows/qa.yaml +++ b/.github/workflows/qa.yaml @@ -23,14 +23,16 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GHB_TOKEN }} - name: Lint run: npm run lint - - name: Build Goerli - run: npm run build:goerli + - name: Test + run: npm run test - name: Build Mumbai run: npm run build:mumbai + - name: Build Cronos + run: npm run build:cronos-mainnet + - name: Deploy Self-hosted Cronos QA + run: npx graph deploy --node ${{ secrets.SELF_HOSTED_SUBGRAPH_URL }} --version-label qa cronos-mainnet_qa_nft-roles - name: Build Polygon run: npm run build:polygon - - name: Test - run: npm run test - name: Delete Satsuma Polygon QA run: | curl -X DELETE https://subgraphs.alchemy.com/api/subgraphs/8c268d3e8b83112a7d0c732a9b88ba1c732da600bffaf68790171b9a0b5d5394/polygon-roles-registry/qa/delete -H 'x-api-key: ${{ secrets.SATSUMA_DEPLOY_KEY }}' diff --git a/README.md b/README.md index 7e12bd6..c32ed6c 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,16 @@ Get started by installing dependencies, building the project and running the tes ```shell npm ci -npm run build:goerli +npm run build:mumbai npm test ``` ## Build Project Building subgraphs consist in generating the code and building the project against a manifest file. This repository -provides a subgraph manifest for each network supported. You can build the project for **Goerli** with the following +provides a subgraph manifest for each network supported. You can build the project for **Mumbai** with the following command: ```shell -cp subgraph-goerli.yaml subgraph.yaml && graph codegen subgraph.yaml +npm run build:mumbai ``` diff --git a/config/cronos-mainnet.json b/config/cronos-mainnet.json new file mode 100644 index 0000000..ca7806d --- /dev/null +++ b/config/cronos-mainnet.json @@ -0,0 +1,11 @@ +{ + "isSelfHosted": true, + "network": "cronos-mainnet", + "ERC721": [ + { + "name": "BoredCandyCity", + "address": "0xe1049178296ce004996afb16b0816c5a95ac8482", + "startBlock": 5203166 + } + ] +} diff --git a/config/goerli.json b/config/goerli.json deleted file mode 100644 index d06c149..0000000 --- a/config/goerli.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "network": "goerli", - "ERC721": [ - { - "name": "ChronosTraveler", - "address": "0x450c91d1fe9f3d57b91218f6ff96f7994eec4d32", - "startBlock": 8099655 - } - ] -} \ No newline at end of file diff --git a/package.json b/package.json index 7ff6eef..53b10a1 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,12 @@ }, "scripts": { "graph:auth": "graph auth --product hosted-service", - "build:goerli": "node mustache.config.js goerli && graph codegen subgraph.yaml && graph build subgraph.yaml", "build:mumbai": "node mustache.config.js mumbai && graph codegen subgraph.yaml && graph build subgraph.yaml", "build:polygon": "node mustache.config.js polygon && graph codegen subgraph.yaml && graph build subgraph.yaml", - "deploy:goerli": "graph deploy --node https://api.thegraph.com/deploy/ orium-network/nft-roles-goerli", + "build:cronos-mainnet": "node mustache.config.js cronos-mainnet && graph codegen subgraph.yaml && graph build subgraph.yaml", "deploy:mumbai": "graph deploy --node https://api.thegraph.com/deploy/ orium-network/nft-roles-mumbai", "deploy:polygon": "graph deploy --node https://api.thegraph.com/deploy/ orium-network/nft-roles-polygon", - "test": "graph test", + "test": "node mustache.config.js polygon && graph codegen subgraph.yaml && graph test", "coverage": "graph test -- -c", "lint": "npx eslint src/", "lint:fix": "npx eslint src/ --fix && npx prettier --write .", diff --git a/subgraph.template.yaml b/subgraph.template.yaml index 1203296..216c436 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -58,6 +58,9 @@ dataSources: network: {{network}} source: abi: ERC7432 +{{#isSelfHosted}} + startBlock: 1 +{{/isSelfHosted}} mapping: kind: ethereum/events apiVersion: 0.0.6 @@ -79,4 +82,4 @@ dataSources: handler: handleRoleRevoked - event: RoleApprovalForAll(indexed address,indexed address,bool) handler: handleRoleApprovalForAll - file: ./src/erc7432/index.ts \ No newline at end of file + file: ./src/erc7432/index.ts From e3eaa82fea02887b676a52ff3e6e7543df59216d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernani=20S=C3=A3o=20Thiago?= Date: Fri, 17 Nov 2023 17:47:28 -0500 Subject: [PATCH 2/2] implemented tokenIdCount for ERC-721 and ERC-1155 NFTs --- .github/workflows/all.yaml | 1 - abis/ERC1155.json | 648 +++++++++--------- config/cronos-mainnet.json | 18 +- config/mumbai.json | 42 +- config/polygon.json | 52 +- schema.graphql | 7 + src/erc1155/transfer-batch-handler.ts | 3 +- src/erc1155/transfer-single-handler.ts | 3 +- src/erc721/index.ts | 12 +- tests/erc1155/transfer-batch-handler.test.ts | 21 +- tests/erc1155/transfer-single-handler.test.ts | 28 +- tests/erc721/transfer-handler.test.ts | 93 ++- utils/constants/index.ts | 5 + utils/entities/index.ts | 1 + utils/entities/nft/erc-721.ts | 3 +- utils/entities/nft/index.ts | 96 +++ 16 files changed, 635 insertions(+), 398 deletions(-) diff --git a/.github/workflows/all.yaml b/.github/workflows/all.yaml index d75bf0e..be0d47b 100644 --- a/.github/workflows/all.yaml +++ b/.github/workflows/all.yaml @@ -31,4 +31,3 @@ jobs: run: npm run build:cronos-mainnet - name: Build Polygon run: npm run build:polygon - diff --git a/abis/ERC1155.json b/abis/ERC1155.json index a82d8ce..f9c9047 100644 --- a/abis/ERC1155.json +++ b/abis/ERC1155.json @@ -1,325 +1,325 @@ [ - { - "inputs": [ - { - "internalType": "string", - "name": "uri_", - "type": "string" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": false, - "internalType": "bool", - "name": "approved", - "type": "bool" - } - ], - "name": "ApprovalForAll", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "ids", - "type": "uint256[]" - }, - { - "indexed": false, - "internalType": "uint256[]", - "name": "values", - "type": "uint256[]" - } - ], - "name": "TransferBatch", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "id", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "TransferSingle", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "value", - "type": "string" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "id", - "type": "uint256" - } - ], - "name": "URI", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "uint256", - "name": "id", - "type": "uint256" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "accounts", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "ids", - "type": "uint256[]" - } - ], - "name": "balanceOfBatch", - "outputs": [ - { - "internalType": "uint256[]", - "name": "", - "type": "uint256[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "address", - "name": "operator", - "type": "address" - } - ], - "name": "isApprovedForAll", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256[]", - "name": "ids", - "type": "uint256[]" - }, - { - "internalType": "uint256[]", - "name": "amounts", - "type": "uint256[]" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "safeBatchTransferFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "safeTransferFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "internalType": "bool", - "name": "approved", - "type": "bool" - } - ], - "name": "setApprovalForAll", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes4", - "name": "interfaceId", - "type": "bytes4" - } - ], - "name": "supportsInterface", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "uri", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - } - ] \ No newline at end of file + { + "inputs": [ + { + "internalType": "string", + "name": "uri_", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/config/cronos-mainnet.json b/config/cronos-mainnet.json index ca7806d..98ec364 100644 --- a/config/cronos-mainnet.json +++ b/config/cronos-mainnet.json @@ -1,11 +1,11 @@ { - "isSelfHosted": true, - "network": "cronos-mainnet", - "ERC721": [ - { - "name": "BoredCandyCity", - "address": "0xe1049178296ce004996afb16b0816c5a95ac8482", - "startBlock": 5203166 - } - ] + "isSelfHosted": true, + "network": "cronos-mainnet", + "ERC721": [ + { + "name": "BoredCandyCity", + "address": "0xe1049178296ce004996afb16b0816c5a95ac8482", + "startBlock": 5203166 + } + ] } diff --git a/config/mumbai.json b/config/mumbai.json index 9ea758b..789bf33 100644 --- a/config/mumbai.json +++ b/config/mumbai.json @@ -1,22 +1,22 @@ { - "network": "mumbai", - "ERC721": [ - { - "name": "AavegotchiGotchi", - "address": "0x83e73D9CF22dFc3A767EA1cE0611F7f50306622e", - "startBlock": 34467860 - }, - { - "name": "AavegotchiParcel", - "address": "0xBcCf68d104aCEa36b1EA20BBE8f06ceD12CaC008", - "startBlock": 38134604 - } - ], - "ERC1155": [ - { - "name": "AavegotchiWearable", - "address": "0x1b1bcB49A744a09aEd636CDD9893508BdF1431A8", - "startBlock": 34467877 - } - ] -} \ No newline at end of file + "network": "mumbai", + "ERC721": [ + { + "name": "AavegotchiGotchi", + "address": "0x83e73D9CF22dFc3A767EA1cE0611F7f50306622e", + "startBlock": 34467860 + }, + { + "name": "AavegotchiParcel", + "address": "0xBcCf68d104aCEa36b1EA20BBE8f06ceD12CaC008", + "startBlock": 38134604 + } + ], + "ERC1155": [ + { + "name": "AavegotchiWearable", + "address": "0x1b1bcB49A744a09aEd636CDD9893508BdF1431A8", + "startBlock": 34467877 + } + ] +} diff --git a/config/polygon.json b/config/polygon.json index ae863a6..a340f8f 100644 --- a/config/polygon.json +++ b/config/polygon.json @@ -1,27 +1,27 @@ { - "network": "matic", - "ERC721": [ - { - "name": "AavegotchiGotchi", - "address": "0x86935f11c86623dec8a25696e1c19a8659cbf95d", - "startBlock": 11516320 - }, - { - "name": "AavegotchiParcel", - "address": "0x1D0360BaC7299C86Ec8E99d0c1C9A95FEfaF2a11", - "startBlock": 20667840 - }, - { - "name": "ChronosTravaler", - "address": "0xa03c4e40d1fcaa826591007a17ca929ef8adbf1c", - "startBlock": 36796276 - } - ], - "ERC1155": [ - { - "name": "AavegotchiWearable", - "address": "0x58de9aabcaeec0f69883c94318810ad79cc6a44f", - "startBlock": 35999793 - } - ] -} \ No newline at end of file + "network": "matic", + "ERC721": [ + { + "name": "AavegotchiGotchi", + "address": "0x86935f11c86623dec8a25696e1c19a8659cbf95d", + "startBlock": 11516320 + }, + { + "name": "AavegotchiParcel", + "address": "0x1D0360BaC7299C86Ec8E99d0c1C9A95FEfaF2a11", + "startBlock": 20667840 + }, + { + "name": "ChronosTravaler", + "address": "0xa03c4e40d1fcaa826591007a17ca929ef8adbf1c", + "startBlock": 36796276 + } + ], + "ERC1155": [ + { + "name": "AavegotchiWearable", + "address": "0x58de9aabcaeec0f69883c94318810ad79cc6a44f", + "startBlock": 35999793 + } + ] +} diff --git a/schema.graphql b/schema.graphql index 8140c10..f54ddaf 100644 --- a/schema.graphql +++ b/schema.graphql @@ -13,6 +13,13 @@ type Nft @entity { roles: [Role!] @derivedFrom(field: "nft") } +type NftCollection @entity { + id: ID! # tokenAddress + type: NftType! + tokenIds: [BigInt!]! + tokenIdCount: BigInt! +} + type Account @entity { id: ID! # address nfts: [Nft!] @derivedFrom(field: "owner") diff --git a/src/erc1155/transfer-batch-handler.ts b/src/erc1155/transfer-batch-handler.ts index d07e757..013517d 100644 --- a/src/erc1155/transfer-batch-handler.ts +++ b/src/erc1155/transfer-batch-handler.ts @@ -1,6 +1,6 @@ import { log } from '@graphprotocol/graph-ts' import { TransferBatch } from '../../generated/ERC1155/ERC1155' -import { upsertERC1155Nft } from '../../utils' +import { NftType, upsertERC1155Nft, upsertNftCollection } from '../../utils' /** @dev This handler is called when a token is transferred. @@ -30,6 +30,7 @@ export function handleTransferBatch(event: TransferBatch): void { const amount = amounts[i] const nft = upsertERC1155Nft(tokenAddress, tokenId, amount, fromAddress, toAddress) + upsertNftCollection(NftType.ERC1155, tokenAddress, tokenId, fromAddress, toAddress) log.warning('[erc1155][handleTransferBatch] NFT {} amount {} transferred from {} to {} tx {}', [ nft.id, diff --git a/src/erc1155/transfer-single-handler.ts b/src/erc1155/transfer-single-handler.ts index 73b8425..131f072 100644 --- a/src/erc1155/transfer-single-handler.ts +++ b/src/erc1155/transfer-single-handler.ts @@ -1,6 +1,6 @@ import { log } from '@graphprotocol/graph-ts' import { TransferSingle } from '../../generated/ERC1155/ERC1155' -import { upsertERC1155Nft } from '../../utils' +import { NftType, upsertERC1155Nft, upsertNftCollection } from '../../utils' /** @dev This handler is called when a token is transferred. @@ -17,6 +17,7 @@ export function handleTransferSingle(event: TransferSingle): void { const amount = event.params.value const nft = upsertERC1155Nft(tokenAddress, tokenId, amount, fromAddress, toAddress) + upsertNftCollection(NftType.ERC1155, tokenAddress, tokenId, fromAddress, toAddress) log.warning('[erc1155][handleTransferSingle] NFT {} amount {} transferred from {} to {} tx {}', [ nft.id, diff --git a/src/erc721/index.ts b/src/erc721/index.ts index 768bdb7..d7f6fc8 100644 --- a/src/erc721/index.ts +++ b/src/erc721/index.ts @@ -1,18 +1,20 @@ import { Transfer } from '../../generated/ERC721/ERC721' -import { upsertERC721Nft } from '../../utils' +import { NftType, upsertERC721Nft, upsertNftCollection } from '../../utils' import { log } from '@graphprotocol/graph-ts' export function handleTransfer(event: Transfer): void { + const from = event.params.from.toHex() + const to = event.params.to.toHex() const tokenAddress = event.address.toHex() const tokenId = event.params.tokenId - const accountAddress = event.params.to.toHex() - upsertERC721Nft(tokenAddress, tokenId, accountAddress) + upsertERC721Nft(tokenAddress, tokenId, to) + upsertNftCollection(NftType.ERC721, tokenAddress, tokenId, from, to) log.warning('[erc721][handleTransfer] NFT {} transferred from {} to {} tx {}', [ tokenId.toString(), - event.params.from.toHex(), - event.params.to.toHex(), + from, + to, event.transaction.hash.toHex(), ]) } diff --git a/tests/erc1155/transfer-batch-handler.test.ts b/tests/erc1155/transfer-batch-handler.test.ts index feba0f0..20a27e7 100644 --- a/tests/erc1155/transfer-batch-handler.test.ts +++ b/tests/erc1155/transfer-batch-handler.test.ts @@ -1,4 +1,4 @@ -import { assert, describe, test, clearStore, afterEach } from 'matchstick-as' +import { assert, describe, test, clearStore, afterEach, afterAll } from 'matchstick-as' import { NftType, generateERC1155NftId } from '../../utils' import { createTransferBatchEvent } from '../helpers/events' import { Addresses, Amounts, TokenIds, ZERO_ADDRESS } from '../helpers/contants' @@ -99,3 +99,22 @@ describe('ERC-1155 Transfer Batch Handler', () => { }) }) }) + +describe('ERC-1155 Collection', () => { + afterAll(() => { + clearStore() + }) + + test('should create NftCollection when TransferBatch is emitted', () => { + assert.entityCount('NftCollection', 0) + + const tokenAddress = ZERO_ADDRESS + const event = createTransferBatchEvent(Addresses[0], Addresses[0], Addresses[1], TokenIds, Amounts, ZERO_ADDRESS) + handleTransferBatch(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC1155) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '3') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', `[${TokenIds.join(', ')}]`) + }) +}) diff --git a/tests/erc1155/transfer-single-handler.test.ts b/tests/erc1155/transfer-single-handler.test.ts index 1a6b7f2..1de107f 100644 --- a/tests/erc1155/transfer-single-handler.test.ts +++ b/tests/erc1155/transfer-single-handler.test.ts @@ -1,4 +1,4 @@ -import { assert, describe, test, clearStore, afterEach } from 'matchstick-as' +import { assert, describe, test, clearStore, afterEach, afterAll } from 'matchstick-as' import { NftType, generateERC1155NftId } from '../../utils' import { createTransferSingleEvent } from '../helpers/events' import { Addresses, Amounts, TokenIds, ZERO_ADDRESS } from '../helpers/contants' @@ -119,3 +119,29 @@ describe('ERC-1155 Transfer Single Handler', () => { }) }) }) + +describe('ERC-1155 TransferSingle Collection', () => { + afterAll(() => { + clearStore() + }) + + test('should create NftCollection when TransferSingle is emitted', () => { + assert.entityCount('NftCollection', 0) + + const tokenAddress = ZERO_ADDRESS + const event = createTransferSingleEvent( + Addresses[0], + Addresses[0], + Addresses[1], + TokenIds[0], + Amounts[1], + tokenAddress, + ) + handleTransferSingle(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC1155) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', `[${TokenIds[0]}]`) + }) +}) diff --git a/tests/erc721/transfer-handler.test.ts b/tests/erc721/transfer-handler.test.ts index fda360c..89fb366 100644 --- a/tests/erc721/transfer-handler.test.ts +++ b/tests/erc721/transfer-handler.test.ts @@ -3,8 +3,10 @@ import { handleTransfer } from '../../src/erc721' import { generateERC721NftId } from '../../utils' import { createTransferEvent } from '../helpers/events' import { Addresses, ZERO_ADDRESS } from '../helpers/contants' +import { NftType } from '../../utils' -const tokenId = '123' +const tokenIds = ['123', '456', '789'] +const tokenAddress = ZERO_ADDRESS describe('ERC-721 Transfer Handler', () => { afterAll(() => { @@ -14,48 +16,125 @@ describe('ERC-721 Transfer Handler', () => { test('should create NFT and Account when NFT and Account does not exist', () => { assert.entityCount('Nft', 0) assert.entityCount('Account', 0) + assert.entityCount('NftCollection', 0) - const event = createTransferEvent(Addresses[0], Addresses[1], tokenId, ZERO_ADDRESS) + const event = createTransferEvent(Addresses[0], Addresses[1], tokenIds[0], ZERO_ADDRESS) handleTransfer(event) assert.entityCount('Nft', 1) assert.entityCount('Account', 1) + assert.entityCount('NftCollection', 1) const _id = generateERC721NftId(event.address.toHexString(), event.params.tokenId) assert.fieldEquals('Nft', _id, 'tokenAddress', ZERO_ADDRESS) - assert.fieldEquals('Nft', _id, 'tokenId', tokenId) + assert.fieldEquals('Nft', _id, 'tokenId', tokenIds[0]) assert.fieldEquals('Nft', _id, 'owner', Addresses[1]) + + const collectionId = event.address.toHexString() + assert.fieldEquals('NftCollection', collectionId, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', collectionId, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', collectionId, 'tokenIds', `[${tokenIds[0]}]`) }) test('should transfer NFT and create Account when NFT exist Account does not', () => { assert.entityCount('Nft', 1) assert.entityCount('Account', 1) + assert.entityCount('NftCollection', 1) - const event = createTransferEvent(Addresses[1], Addresses[2], tokenId, ZERO_ADDRESS) + const event = createTransferEvent(Addresses[1], Addresses[2], tokenIds[0], ZERO_ADDRESS) handleTransfer(event) assert.entityCount('Nft', 1) assert.entityCount('Account', 2) + assert.entityCount('NftCollection', 1) const _id = generateERC721NftId(event.address.toHexString(), event.params.tokenId) assert.fieldEquals('Nft', _id, 'tokenAddress', ZERO_ADDRESS) - assert.fieldEquals('Nft', _id, 'tokenId', tokenId) + assert.fieldEquals('Nft', _id, 'tokenId', tokenIds[0]) assert.fieldEquals('Nft', _id, 'owner', Addresses[2]) + + const collectionId = event.address.toHexString() + assert.fieldEquals('NftCollection', collectionId, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', collectionId, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', collectionId, 'tokenIds', `[${tokenIds[0]}]`) }) test('should only transfer NFT when NFT and Account exist', () => { assert.entityCount('Nft', 1) assert.entityCount('Account', 2) + assert.entityCount('NftCollection', 1) - const event = createTransferEvent(Addresses[0], Addresses[2], tokenId, ZERO_ADDRESS) + const event = createTransferEvent(Addresses[0], Addresses[2], tokenIds[0], ZERO_ADDRESS) handleTransfer(event) assert.entityCount('Nft', 1) assert.entityCount('Account', 2) + assert.entityCount('NftCollection', 1) const _id = generateERC721NftId(event.address.toHexString(), event.params.tokenId) assert.fieldEquals('Nft', _id, 'tokenAddress', ZERO_ADDRESS) - assert.fieldEquals('Nft', _id, 'tokenId', tokenId) + assert.fieldEquals('Nft', _id, 'tokenId', tokenIds[0]) assert.fieldEquals('Nft', _id, 'owner', Addresses[2]) + + const collectionId = event.address.toHexString() + assert.fieldEquals('NftCollection', collectionId, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', collectionId, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', collectionId, 'tokenIds', `[${tokenIds[0]}]`) + }) +}) + +describe('ERC-721 Nft Collection', () => { + afterAll(() => { + clearStore() + }) + + test('should create NftCollection with zero data when from and to are zero address', () => { + assert.entityCount('NftCollection', 0) + + const event = createTransferEvent(ZERO_ADDRESS, ZERO_ADDRESS, tokenIds[0], tokenAddress) + handleTransfer(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '0') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', '[]') + }) + + test('should add tokenId to NftCollection when mint event', () => { + const event = createTransferEvent(ZERO_ADDRESS, Addresses[0], tokenIds[0], tokenAddress) + handleTransfer(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', `[${tokenIds[0]}]`) + }) + + test('should add tokenId to NftCollection when transfer between accounts', () => { + const event = createTransferEvent(Addresses[0], Addresses[1], tokenIds[1], tokenAddress) + handleTransfer(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '2') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', `[${tokenIds[0]}, ${tokenIds[1]}]`) + }) + + test('should remove tokenId from NftCollection when burn event', () => { + let event = createTransferEvent(Addresses[0], ZERO_ADDRESS, tokenIds[1], tokenAddress) + handleTransfer(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '1') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', `[${tokenIds[0]}]`) + + event = createTransferEvent(Addresses[0], ZERO_ADDRESS, tokenIds[0], tokenAddress) + handleTransfer(event) + + assert.entityCount('NftCollection', 1) + assert.fieldEquals('NftCollection', tokenAddress, 'type', NftType.ERC721) + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIdCount', '0') + assert.fieldEquals('NftCollection', tokenAddress, 'tokenIds', '[]') }) }) diff --git a/utils/constants/index.ts b/utils/constants/index.ts index 78840d3..7b0fab2 100644 --- a/utils/constants/index.ts +++ b/utils/constants/index.ts @@ -1 +1,6 @@ +import { BigInt } from '@graphprotocol/graph-ts' + export { ONE_ETHER, ONE_HUNDRED_ETHER } from './wei' + +export const ONE = BigInt.fromI32(1) +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/utils/entities/index.ts b/utils/entities/index.ts index 63ef636..716adc6 100644 --- a/utils/entities/index.ts +++ b/utils/entities/index.ts @@ -5,6 +5,7 @@ export { generateERC1155NftId, upsertERC1155Nft, findOrCreateERC1155Nft, + upsertNftCollection, } from './nft' export { generateRoleAssignmentId, findOrCreateRoleAssignment } from './role-assignment' export { generateRoleApprovalId, findOrCreateRoleApproval, deleteRoleApproval } from './role-approval' diff --git a/utils/entities/nft/erc-721.ts b/utils/entities/nft/erc-721.ts index c917c6a..a693dd9 100644 --- a/utils/entities/nft/erc-721.ts +++ b/utils/entities/nft/erc-721.ts @@ -1,11 +1,12 @@ +import { BigInt } from '@graphprotocol/graph-ts' import { Nft } from '../../../generated/schema' import { NftType } from '../../enums' import { findOrCreateAccount } from '../account' -import { BigInt } from '@graphprotocol/graph-ts' export function generateERC721NftId(tokenAddress: string, tokenId: BigInt): string { return tokenAddress + '-' + tokenId.toString() } + export function upsertERC721Nft(tokenAddress: string, tokenId: BigInt, to: string): Nft { const nftId = generateERC721NftId(tokenAddress, tokenId) diff --git a/utils/entities/nft/index.ts b/utils/entities/nft/index.ts index 9d43c10..921d0e0 100644 --- a/utils/entities/nft/index.ts +++ b/utils/entities/nft/index.ts @@ -1,2 +1,98 @@ +import { BigInt } from '@graphprotocol/graph-ts' +import { NftCollection } from '../../../generated/schema' +import { ONE, ZERO_ADDRESS } from '../../constants' +import { NftType } from '../../enums' + export { generateERC721NftId, upsertERC721Nft } from './erc-721' export { generateERC1155NftId, upsertERC1155Nft, findOrCreateERC1155Nft } from './erc-1155' + +export function upsertNftCollection( + nftType: string, + tokenAddress: string, + tokenId: BigInt, + from: string, + to: string, +): NftCollection { + let collection = NftCollection.load(tokenAddress) + if (!collection) { + collection = new NftCollection(tokenAddress) + collection.type = nftType + collection.tokenIdCount = BigInt.zero() + collection.tokenIds = [] + } + + if (nftType == NftType.ERC721) { + // ERC-721 might burn tokens and remove them from the tokenIds list + collection = updateERC721Collection(collection, from, to, tokenId) + } else if (nftType == NftType.ERC1155) { + // ERC-1155 may also burn tokens, but it would not make sense to remove them from the list + collection = updateERC1155Collection(collection, tokenId) + } + + collection.save() + return collection +} + +function updateERC1155Collection(collection: NftCollection, tokenId: BigInt): NftCollection { + if (!collection.tokenIds.includes(tokenId)) { + // if it does not include tokenId + // add tokenId to list + const tokenIds = collection.tokenIds + tokenIds.push(tokenId) + collection.tokenIds = tokenIds + // increment tokenIdCount + collection.tokenIdCount = collection.tokenIdCount.plus(ONE) + } + return collection +} + +function updateERC721Collection(collection: NftCollection, from: string, to: string, tokenId: BigInt): NftCollection { + if (from == ZERO_ADDRESS) { + if (to == ZERO_ADDRESS) { + // mint and burn event + // do nothing + return collection + } else if (!collection.tokenIds.includes(tokenId)) { + // mint event + // if it does not include tokenId + // add tokenId to list + const tokenIds = collection.tokenIds + tokenIds.push(tokenId) + collection.tokenIds = tokenIds + // increment tokenIdCount + collection.tokenIdCount = collection.tokenIdCount.plus(ONE) + } + } else if (to == ZERO_ADDRESS) { + // burn event + if (collection.tokenIds.includes(tokenId)) { + // if it does include tokenId + // remove tokenId from list + collection.tokenIds = removeAllOccurrencesOf(collection.tokenIds, tokenId) + // decrement tokenIdCount + const newAmount = collection.tokenIdCount.minus(ONE) + collection.tokenIdCount = newAmount.lt(BigInt.zero()) ? BigInt.zero() : newAmount + } + } else if (!collection.tokenIds.includes(tokenId)) { + // transfer from one user to another + // if it does not include tokenId + // add tokenId to list + const tokenIds = collection.tokenIds + tokenIds.push(tokenId) + collection.tokenIds = tokenIds + // increment tokenIdCount + collection.tokenIdCount = collection.tokenIdCount.plus(ONE) + } + return collection +} + +function removeAllOccurrencesOf(array: Array, value: BigInt): Array { + let index = array.indexOf(value) + while (index > -1) { + if (array.length > 1) { + array[index] = array[array.length - 1] + } + array.pop() + index = array.indexOf(value) + } + return array +}