From db41f9fea7b63cab4e19e06b7319ef2815de3200 Mon Sep 17 00:00:00 2001 From: cedoor Date: Wed, 10 Jan 2024 13:21:05 +0000 Subject: [PATCH] chore(subgraph): move subgraph to main repo Former-commit-id: 3d5710598231e10424eb162d7ce77a9165ed19dd --- .eslintignore | 4 + .github/workflows/production.yml | 36 ++ .github/workflows/pull-requests.yml | 8 +- .prettierignore | 4 + apps/subgraph/.gitignore | 5 + apps/subgraph/README.md | 137 +++++ apps/subgraph/abis/Semaphore.json | 626 ++++++++++++++++++++++ apps/subgraph/docker-compose-graph.yml | 60 +++ apps/subgraph/matchstick.yaml | 1 + apps/subgraph/networks.json | 44 ++ apps/subgraph/package.json | 26 + apps/subgraph/schema.graphql | 35 ++ apps/subgraph/scripts/generateSubgraph.js | 10 + apps/subgraph/src/semaphore.ts | 191 +++++++ apps/subgraph/src/utils.ts | 25 + apps/subgraph/subgraph.template.yaml | 35 ++ apps/subgraph/tests/.bin/semaphore.wasm | Bin 0 -> 50093 bytes apps/subgraph/tests/semaphore-utils.ts | 160 ++++++ apps/subgraph/tests/semaphore.test.ts | 156 ++++++ apps/subgraph/tsconfig.json | 4 + package.json | 10 +- packages/contracts/package.json | 2 +- yarn.lock.REMOVED.git-id | 2 +- 23 files changed, 1574 insertions(+), 7 deletions(-) create mode 100644 apps/subgraph/.gitignore create mode 100644 apps/subgraph/README.md create mode 100644 apps/subgraph/abis/Semaphore.json create mode 100644 apps/subgraph/docker-compose-graph.yml create mode 100644 apps/subgraph/matchstick.yaml create mode 100644 apps/subgraph/networks.json create mode 100644 apps/subgraph/package.json create mode 100644 apps/subgraph/schema.graphql create mode 100644 apps/subgraph/scripts/generateSubgraph.js create mode 100644 apps/subgraph/src/semaphore.ts create mode 100644 apps/subgraph/src/utils.ts create mode 100644 apps/subgraph/subgraph.template.yaml create mode 100644 apps/subgraph/tests/.bin/semaphore.wasm create mode 100644 apps/subgraph/tests/semaphore-utils.ts create mode 100644 apps/subgraph/tests/semaphore.test.ts create mode 100644 apps/subgraph/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 1d32fe2d..3cf5385d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -24,6 +24,10 @@ dist build /docs +# subgraph +subgraph.template.yaml +generated + # Docusaurus cache and generated files .docusaurus .cache-loader diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 7c4956b8..f1d3a513 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -43,12 +43,48 @@ jobs: - name: Build libraries run: yarn build:libraries + - name: Build subgraph + run: yarn build:subgraph + - name: Run Prettier run: yarn prettier - name: Run Eslint run: yarn lint + test-subgraph: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn config get cacheFolder)" + + - name: Restore yarn cache + uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn + + - name: Build subgraph + run: yarn build:subgraph + + - name: Test subgraph + run: yarn test:subgraph + test: runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 1a94ec74..9339ef1f 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -41,6 +41,9 @@ jobs: - name: Build libraries run: yarn build:libraries + - name: Build subgraph + run: yarn build:subgraph + - name: Run Prettier run: yarn prettier @@ -77,5 +80,8 @@ jobs: - name: Build libraries run: yarn build:libraries - - name: Test contracts and libraries + - name: Build subgraph + run: yarn build:subgraph + + - name: Test contracts, libraries and subgraph run: yarn test diff --git a/.prettierignore b/.prettierignore index 5b70d8a1..1b01a7f1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,6 +27,10 @@ dist build /docs +# subgraph +subgraph.template.yaml +generated + # github .github/ISSUE_TEMPLATE diff --git a/apps/subgraph/.gitignore b/apps/subgraph/.gitignore new file mode 100644 index 00000000..cc96ad5c --- /dev/null +++ b/apps/subgraph/.gitignore @@ -0,0 +1,5 @@ +# Generate output +generated + +# The Graph +subgraph.yaml diff --git a/apps/subgraph/README.md b/apps/subgraph/README.md new file mode 100644 index 00000000..0fa25361 --- /dev/null +++ b/apps/subgraph/README.md @@ -0,0 +1,137 @@ +

+ Semaphore Subgraph +

+ +

+ + + + + Github license + + + Linter eslint + + + Code style prettier + +

+ +
+

+ + 👥 Contributing + +   |   + + 🤝 Code of conduct + +   |   + + 🔎 Issues + +   |   + + 🗣️ Chat & Support + +

+
+ +| The Graph is an indexing protocol for querying networks like Ethereum and IPFS. Our subgraphs allow you to get data from the [`Semaphore.sol`](https://github.com/semaphore-protocol/semaphore/blob/main/contracts/Semaphore.sol) smart contract. | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + +## Networks + +| Semaphore version | Sepolia | Goerli | Mumbai | Optimism Goerli | Arbitrum Goerli | Arbitrum One | +| ----------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| v2.0 | N/A | N/A | N/A | N/A | N/A | [semaphore-protocol/arbitrum](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/arbitrum) | +| v2.5 | N/A | [semaphore-protocol/goerli](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/goerli) | N/A | N/A | N/A | N/A | +| v2.6 | N/A | [semaphore-protocol/goerli-5259d3](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/goerli-5259d3) | N/A | N/A | N/A | [semaphore-protocol/arbitrum-86337c](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/arbitrum-86337c) | +| v3.0 - v3.1 | N/A | [semaphore-protocol/goerli-89490c](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/goerli-89490c) | N/A | N/A | N/A | [semaphore-protocol/arbitrum-72dca3](https://thegraph.com/hosted-service/subgraph/semaphore-protocol/arbitrum-72dca3) | +| >= v3.2 | [semaphore-sepolia](https://api.studio.thegraph.com/query/14377/semaphore-sepolia/v3.6.1) | [semaphore-goerli](https://api.studio.thegraph.com/query/14377/semaphore-goerli/v3.6.1) | [semaphore-mumbai](https://api.studio.thegraph.com/query/14377/semaphore-mumbai/v3.6.1) | [semaphore-optimism-goerli](https://api.studio.thegraph.com/query/14377/semaphore-optimism-goerli/v3.6.1) | [semaphore-arbitrum-goerli](https://api.studio.thegraph.com/query/14377/semaphore-arbitrum-goerli/v3.6.1) | [semaphore-arbitrum](https://api.studio.thegraph.com/query/14377/semaphore-arbitrum/v3.6.1) | + +## 🛠 Install + +Clone this repository: + +```bash +git clone https://github.com/semaphore-protocol/semaphore.git +``` + +and install the dependencies: + +```bash +cd semaphore/apps/subgraph && yarn +``` + +## Usage + +The subgraph definition consists of a few files: + +- `subgraph.template.yaml`: a YAML file containing the subgraph manifest, +- `schema.graphql`: a GraphQL schema that defines what data is stored for the subgraph, and how to query it via GraphQL, +- `src/semaphore.ts`: AssemblyScript code that translates from the event data to the entities defined in the schema. + +### Code generation + +Generate AssemblyScript types for the subgraph (required every time the schema changes): + +```bash +yarn codegen +``` + +It also generates a `subgraph.yaml` file for your specific network. + +### Testing + +After generating the types and `subgraph.yaml` file, test your subgraph: + +```bash +yarn test +``` + +### Deployment + +#### TheGraph Studio + +Set the authorization code that links your account on thegraph.com: + +```bash +yarn auth +``` + +Deploy the subgraph to the [TheGraph Studio](https://thegraph.com/studio/): + +```bash +yarn deploy +``` + +#### Local + +Start services required for TheGraph node by running: + +```bash +docker compose -f docker-compose-graph.yml up +``` + +Start a local Hardhat node and deploy the [Semaphore contract](https://github.com/semaphore-protocol/semaphore/tree/main/packages/contracts): + +```bash +# CWD = /semaphore/packages/contracts +yarn start +yarn deploy:semaphore --network localhost +``` + +Create the `subgraph.yaml` file for your local network and create/deploy your subgraph: + +```bash +yarn codegen localhost +yarn create-local +yarn deploy-local +``` + +Once the subgraph is published it will start indexing. You can query the subgraph using the following GraphQL endpoint: + +``` +http://127.0.0.1:8000/subgraphs/name/sempahore/graphql +``` diff --git a/apps/subgraph/abis/Semaphore.json b/apps/subgraph/abis/Semaphore.json new file mode 100644 index 00000000..5a052415 --- /dev/null +++ b/apps/subgraph/abis/Semaphore.json @@ -0,0 +1,626 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Semaphore", + "sourceName": "contracts/Semaphore.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract ISemaphoreVerifier", + "name": "_verifier", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "Semaphore__CallerIsNotTheGroupAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__GroupAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__GroupDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__MerkleTreeDepthIsNotSupported", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__MerkleTreeRootIsExpired", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__MerkleTreeRootIsNotPartOfTheGroup", + "type": "error" + }, + { + "inputs": [], + "name": "Semaphore__YouAreUsingTheSameNillifierTwice", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "GroupAdminUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "merkleTreeDepth", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "zeroValue", + "type": "uint256" + } + ], + "name": "GroupCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldMerkleTreeDuration", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMerkleTreeDuration", + "type": "uint256" + } + ], + "name": "GroupMerkleTreeDurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "merkleTreeRoot", + "type": "uint256" + } + ], + "name": "MemberAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "merkleTreeRoot", + "type": "uint256" + } + ], + "name": "MemberRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newIdentityCommitment", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "merkleTreeRoot", + "type": "uint256" + } + ], + "name": "MemberUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "merkleTreeRoot", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nullifierHash", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "externalNullifier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "signal", + "type": "uint256" + } + ], + "name": "ProofVerified", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + } + ], + "name": "addMember", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "identityCommitments", + "type": "uint256[]" + } + ], + "name": "addMembers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "merkleTreeDepth", + "type": "uint256" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "merkleTreeDuration", + "type": "uint256" + } + ], + "name": "createGroup", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "merkleTreeDepth", + "type": "uint256" + }, + { + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "createGroup", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + } + ], + "name": "getMerkleTreeDepth", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + } + ], + "name": "getMerkleTreeRoot", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + } + ], + "name": "getNumberOfMerkleTreeLeaves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "groups", + "outputs": [ + { + "internalType": "address", + "name": "admin", + "type": "address" + }, + { + "internalType": "uint256", + "name": "merkleTreeDuration", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "proofSiblings", + "type": "uint256[]" + }, + { + "internalType": "uint8[]", + "name": "proofPathIndices", + "type": "uint8[]" + } + ], + "name": "removeMember", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "updateGroupAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newMerkleTreeDuration", + "type": "uint256" + } + ], + "name": "updateGroupMerkleTreeDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "identityCommitment", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newIdentityCommitment", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "proofSiblings", + "type": "uint256[]" + }, + { + "internalType": "uint8[]", + "name": "proofPathIndices", + "type": "uint8[]" + } + ], + "name": "updateMember", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "verifier", + "outputs": [ + { + "internalType": "contract ISemaphoreVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "merkleTreeRoot", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "signal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nullifierHash", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "externalNullifier", + "type": "uint256" + }, + { + "internalType": "uint256[8]", + "name": "proof", + "type": "uint256[8]" + } + ], + "name": "verifyProof", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b506040516200214a3803806200214a833981810160405281019062000037919062000096565b80600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505062000124565b60008151905062000090816200010a565b92915050565b600060208284031215620000a957600080fd5b6000620000b9848285016200007f565b91505092915050565b6000620000cf82620000ea565b9050919050565b6000620000e382620000c2565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6200011581620000d6565b81146200012157600080fd5b50565b61201680620001346000396000f3fe608060405234801561001057600080fd5b50600436106100ea5760003560e01c80636389e1071161008c5780639c112141116100665780639c11214114610246578063dabc4d5114610262578063ec45622a14610292578063fcf0b6ec146102ae576100ea565b80636389e107146101c957806365e54f83146101f957806396324bd414610215576100ea565b80633bc778e3116100c85780633bc778e31461014557806343989f8514610161578063568ee8261461017d578063638480be14610199576100ea565b806304245371146100ef5780631783efc31461010b5780632b7ac3f314610127575b600080fd5b61010960048036038101906101049190611655565b6102ca565b005b610125600480360381019061012091906116ad565b610410565b005b61012f6104fc565b60405161013c9190611b24565b60405180910390f35b61015f600480360381019061015a91906118e6565b610522565b005b61017b6004803603810190610176919061179b565b6107cf565b005b61019760048036038101906101929190611619565b6108c3565b005b6101b360048036038101906101ae91906115f0565b610a22565b6040516101c09190611c5b565b60405180910390f35b6101e360048036038101906101de91906115f0565b610a41565b6040516101f09190611c5b565b60405180910390f35b610213600480360381019061020e9190611738565b610a60565b005b61022f600480360381019061022a91906115f0565b610b84565b60405161023d929190611afb565b60405180910390f35b610260600480360381019061025b91906116e9565b610bc8565b005b61027c600480360381019061027791906115f0565b610ced565b6040516102899190611c5b565b60405180910390f35b6102ac60048036038101906102a79190611836565b610d0c565b005b6102c860048036038101906102c391906116ad565b610e02565b005b826102d3610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161461036d576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60005b838390508110156103cf576103c4858585848181106103b8577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020135610f23565b806001019050610370565b5060006103db85610ced565b905042600260008781526020019081526020016000206002016000838152602001908152602001600020819055505050505050565b81610419610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146104b3576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6104bd8383610f23565b60006104c884610ced565b9050426002600086815260200190815260200160002060020160008381526020019081526020016000208190555050505050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600061052d87610a41565b9050600081141561056a576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600061057588610ced565b9050808714610648576000600260008a815260200190815260200160002060020160008981526020019081526020016000205490506000600260008b81526020019081526020016000206001015490506000821415610600576040517f4d32958600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b808261060c9190611dce565b421115610645576040517f9581a99000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50505b60026000898152602001908152602001600020600301600086815260200190815260200160002060009054906101000a900460ff16156106b4576040517f948d067400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16639aca8f2a8887898888886040518763ffffffff1660e01b815260040161071996959493929190611d1b565b60006040518083038186803b15801561073157600080fd5b505afa158015610745573d6000803e3d6000fd5b505050506001600260008a8152602001908152602001600020600301600087815260200190815260200160002060006101000a81548160ff0219169083151502179055508387897f48950129900df26c2140187532df49c8af343c3daf74f1e99e562e1b2be07adc888a6040516107bd929190611c76565b60405180910390a45050505050505050565b856107d8610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610872576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61088087878787878761104a565b600061088b88610ced565b905042600260008a81526020019081526020016000206002016000838152602001908152602001600020819055505050505050505050565b816108cc610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610966576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b816002600085815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff166109da610f1b565b73ffffffffffffffffffffffffffffffffffffffff16847f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a4505050565b6000806000838152602001908152602001600020600201549050919050565b6000806000838152602001908152602001600020600001549050919050565b826010811080610a705750602081115b15610aa7576040517fecf64f1200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610ab18585611172565b826002600087815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508160026000878152602001908152602001600020600101819055508273ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff16867f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a45050505050565b60026020528060005260406000206000915090508060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010154905082565b816010811080610bd85750602081115b15610c0f576040517fecf64f1200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610c198484611172565b816002600086815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610e1060026000868152602001908152602001600020600101819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff16857f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a450505050565b6000806000838152602001908152602001600020600101549050919050565b86610d15610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610daf576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610dbe888888888888886112a3565b6000610dc989610ced565b905042600260008b8152602001908152602001600020600201600083815260200190815260200160002081905550505050505050505050565b81610e0b610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610ea5576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600060026000858152602001908152602001600020600101549050826002600086815260200190815260200160002060010181905550837f264b2a8f6763c084235fe832ba903482b2ef1a521336881fc75b987c2dfd29c58285604051610f0d929190611c76565b60405180910390a250505050565b600033905090565b6000610f2e83610a41565b1415610f66576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008083815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__63168703fa9091836040518363ffffffff1660e01b8152600401610fb2929190611b3f565b60006040518083038186803b158015610fca57600080fd5b505af4158015610fde573d6000803e3d6000fd5b505050506000610fed83610ced565b905060006001610ffc85610a22565b6110069190611e7e565b9050837f19239b3f93cd10558aaf11423af70c77763bf54f52bcc75bfa74d4d13548cde982858560405161103c93929190611c9f565b60405180910390a250505050565b600061105587610a41565b141561108d576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008087815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__630629596f909187878787876040518763ffffffff1660e01b81526004016110e196959493929190611b68565b60006040518083038186803b1580156110f957600080fd5b505af415801561110d573d6000803e3d6000fd5b50505050600061111c87610ced565b9050600061112a84846113d0565b9050877f3108849c053c77b8073a11256dffb5ffd5b55e93e105a355e1c9061db890d87182898560405161116093929190611c9f565b60405180910390a25050505050505050565b600061117d83610a41565b146111b4576040517f8121725b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006008836040516020016111c99190611ae0565b6040516020818303038152906040528051906020012060001c901c905060008084815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__631095fbb4909184846040518463ffffffff1660e01b815260040161123493929190611bbf565b60006040518083038186803b15801561124c57600080fd5b505af4158015611260573d6000803e3d6000fd5b50505050827f0d000126c26c1bbe400fd2332187f75d58b69306f9ec47b408686189d3a008338383604051611296929190611c76565b60405180910390a2505050565b60006112ae88610a41565b14156112e6576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008088815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__63a547882790918888888888886040518863ffffffff1660e01b815260040161133c9796959493929190611bf6565b60006040518083038186803b15801561135457600080fd5b505af4158015611368573d6000803e3d6000fd5b50505050600061137788610ced565b9050600061138584846113d0565b9050887fea3588e4a2a0c93d6a0e69dfeaf7496f43ccccf02ad9ce0a5b7627cbca4b61b1828a8a866040516113bd9493929190611cd6565b60405180910390a2505050505050505050565b6000806000905060008484905090505b60008160ff1611156114f457600082118061145b5750600085856001846114079190611eb2565b60ff16818110611440577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020160208101906114559190611970565b60ff1614155b156114e85760028261146d9190611e24565b9150600185856001846114809190611eb2565b60ff168181106114b9577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020160208101906114ce9190611970565b60ff1614156114e7576001826114e49190611dce565b91505b5b806001900390506113e0565b508091505092915050565b60008135905061150e81611f9b565b92915050565b60008190508260206008028201111561152c57600080fd5b92915050565b60008083601f84011261154457600080fd5b8235905067ffffffffffffffff81111561155d57600080fd5b60208301915083602082028301111561157557600080fd5b9250929050565b60008083601f84011261158e57600080fd5b8235905067ffffffffffffffff8111156115a757600080fd5b6020830191508360208202830111156115bf57600080fd5b9250929050565b6000813590506115d581611fb2565b92915050565b6000813590506115ea81611fc9565b92915050565b60006020828403121561160257600080fd5b6000611610848285016115c6565b91505092915050565b6000806040838503121561162c57600080fd5b600061163a858286016115c6565b925050602061164b858286016114ff565b9150509250929050565b60008060006040848603121561166a57600080fd5b6000611678868287016115c6565b935050602084013567ffffffffffffffff81111561169557600080fd5b6116a186828701611532565b92509250509250925092565b600080604083850312156116c057600080fd5b60006116ce858286016115c6565b92505060206116df858286016115c6565b9150509250929050565b6000806000606084860312156116fe57600080fd5b600061170c868287016115c6565b935050602061171d868287016115c6565b925050604061172e868287016114ff565b9150509250925092565b6000806000806080858703121561174e57600080fd5b600061175c878288016115c6565b945050602061176d878288016115c6565b935050604061177e878288016114ff565b925050606061178f878288016115c6565b91505092959194509250565b600080600080600080608087890312156117b457600080fd5b60006117c289828a016115c6565b96505060206117d389828a016115c6565b955050604087013567ffffffffffffffff8111156117f057600080fd5b6117fc89828a01611532565b9450945050606087013567ffffffffffffffff81111561181b57600080fd5b61182789828a0161157c565b92509250509295509295509295565b600080600080600080600060a0888a03121561185157600080fd5b600061185f8a828b016115c6565b97505060206118708a828b016115c6565b96505060406118818a828b016115c6565b955050606088013567ffffffffffffffff81111561189e57600080fd5b6118aa8a828b01611532565b9450945050608088013567ffffffffffffffff8111156118c957600080fd5b6118d58a828b0161157c565b925092505092959891949750929550565b6000806000806000806101a0878903121561190057600080fd5b600061190e89828a016115c6565b965050602061191f89828a016115c6565b955050604061193089828a016115c6565b945050606061194189828a016115c6565b935050608061195289828a016115c6565b92505060a061196389828a01611514565b9150509295509295509295565b60006020828403121561198257600080fd5b6000611990848285016115db565b91505092915050565b60006119a58383611ad1565b60208301905092915050565b6119ba81611ee6565b82525050565b6119cd6101008383611f53565b5050565b60006119dd8385611d95565b93507f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831115611a0c57600080fd5b602083029250611a1d838584611f53565b82840190509392505050565b6000611a358385611da6565b9350611a4082611d7e565b8060005b85811015611a7957611a568284611db7565b611a608882611999565b9750611a6b83611d88565b925050600181019050611a44565b5085925050509392505050565b611a8f81611f2f565b82525050565b8082525050565b611aa581611f18565b82525050565b611ab481611f18565b82525050565b611acb611ac682611f18565b611f62565b82525050565b611ada81611f22565b82525050565b6000611aec8284611aba565b60208201915081905092915050565b6000604082019050611b1060008301856119b1565b611b1d6020830184611a9c565b9392505050565b6000602082019050611b396000830184611a86565b92915050565b6000604082019050611b546000830185611a95565b611b616020830184611aab565b9392505050565b6000608082019050611b7d6000830189611a95565b611b8a6020830188611aab565b8181036040830152611b9d8186886119d1565b90508181036060830152611bb2818486611a29565b9050979650505050505050565b6000606082019050611bd46000830186611a95565b611be16020830185611aab565b611bee6040830184611aab565b949350505050565b600060a082019050611c0b600083018a611a95565b611c186020830189611aab565b611c256040830188611aab565b8181036060830152611c388186886119d1565b90508181036080830152611c4d818486611a29565b905098975050505050505050565b6000602082019050611c706000830184611a9c565b92915050565b6000604082019050611c8b6000830185611a9c565b611c986020830184611a9c565b9392505050565b6000606082019050611cb46000830186611a9c565b611cc16020830185611a9c565b611cce6040830184611a9c565b949350505050565b6000608082019050611ceb6000830187611a9c565b611cf86020830186611a9c565b611d056040830185611a9c565b611d126060830184611a9c565b95945050505050565b60006101a082019050611d316000830189611a9c565b611d3e6020830188611a9c565b611d4b6040830187611a9c565b611d586060830186611a9c565b611d6560808301856119c0565b611d73610180830184611a9c565b979650505050505050565b6000819050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b6000611dc660208401846115db565b905092915050565b6000611dd982611f18565b9150611de483611f18565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115611e1957611e18611f6c565b5b828201905092915050565b6000611e2f82611f18565b9150611e3a83611f18565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615611e7357611e72611f6c565b5b828202905092915050565b6000611e8982611f18565b9150611e9483611f18565b925082821015611ea757611ea6611f6c565b5b828203905092915050565b6000611ebd82611f22565b9150611ec883611f22565b925082821015611edb57611eda611f6c565b5b828203905092915050565b6000611ef182611ef8565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b6000611f3a82611f41565b9050919050565b6000611f4c82611ef8565b9050919050565b82818337600083830152505050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b611fa481611ee6565b8114611faf57600080fd5b50565b611fbb81611f18565b8114611fc657600080fd5b50565b611fd281611f22565b8114611fdd57600080fd5b5056fea2646970667358221220a6777edc645a16d8f81beacbb5ec133f19452c578127e38e7f40d5540508d08464736f6c63430008040033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100ea5760003560e01c80636389e1071161008c5780639c112141116100665780639c11214114610246578063dabc4d5114610262578063ec45622a14610292578063fcf0b6ec146102ae576100ea565b80636389e107146101c957806365e54f83146101f957806396324bd414610215576100ea565b80633bc778e3116100c85780633bc778e31461014557806343989f8514610161578063568ee8261461017d578063638480be14610199576100ea565b806304245371146100ef5780631783efc31461010b5780632b7ac3f314610127575b600080fd5b61010960048036038101906101049190611655565b6102ca565b005b610125600480360381019061012091906116ad565b610410565b005b61012f6104fc565b60405161013c9190611b24565b60405180910390f35b61015f600480360381019061015a91906118e6565b610522565b005b61017b6004803603810190610176919061179b565b6107cf565b005b61019760048036038101906101929190611619565b6108c3565b005b6101b360048036038101906101ae91906115f0565b610a22565b6040516101c09190611c5b565b60405180910390f35b6101e360048036038101906101de91906115f0565b610a41565b6040516101f09190611c5b565b60405180910390f35b610213600480360381019061020e9190611738565b610a60565b005b61022f600480360381019061022a91906115f0565b610b84565b60405161023d929190611afb565b60405180910390f35b610260600480360381019061025b91906116e9565b610bc8565b005b61027c600480360381019061027791906115f0565b610ced565b6040516102899190611c5b565b60405180910390f35b6102ac60048036038101906102a79190611836565b610d0c565b005b6102c860048036038101906102c391906116ad565b610e02565b005b826102d3610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161461036d576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60005b838390508110156103cf576103c4858585848181106103b8577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020135610f23565b806001019050610370565b5060006103db85610ced565b905042600260008781526020019081526020016000206002016000838152602001908152602001600020819055505050505050565b81610419610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146104b3576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6104bd8383610f23565b60006104c884610ced565b9050426002600086815260200190815260200160002060020160008381526020019081526020016000208190555050505050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600061052d87610a41565b9050600081141561056a576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600061057588610ced565b9050808714610648576000600260008a815260200190815260200160002060020160008981526020019081526020016000205490506000600260008b81526020019081526020016000206001015490506000821415610600576040517f4d32958600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b808261060c9190611dce565b421115610645576040517f9581a99000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50505b60026000898152602001908152602001600020600301600086815260200190815260200160002060009054906101000a900460ff16156106b4576040517f948d067400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16639aca8f2a8887898888886040518763ffffffff1660e01b815260040161071996959493929190611d1b565b60006040518083038186803b15801561073157600080fd5b505afa158015610745573d6000803e3d6000fd5b505050506001600260008a8152602001908152602001600020600301600087815260200190815260200160002060006101000a81548160ff0219169083151502179055508387897f48950129900df26c2140187532df49c8af343c3daf74f1e99e562e1b2be07adc888a6040516107bd929190611c76565b60405180910390a45050505050505050565b856107d8610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610872576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61088087878787878761104a565b600061088b88610ced565b905042600260008a81526020019081526020016000206002016000838152602001908152602001600020819055505050505050505050565b816108cc610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610966576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b816002600085815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff166109da610f1b565b73ffffffffffffffffffffffffffffffffffffffff16847f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a4505050565b6000806000838152602001908152602001600020600201549050919050565b6000806000838152602001908152602001600020600001549050919050565b826010811080610a705750602081115b15610aa7576040517fecf64f1200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610ab18585611172565b826002600087815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508160026000878152602001908152602001600020600101819055508273ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff16867f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a45050505050565b60026020528060005260406000206000915090508060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16908060010154905082565b816010811080610bd85750602081115b15610c0f576040517fecf64f1200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610c198484611172565b816002600086815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610e1060026000868152602001908152602001600020600101819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff16857f0ba83579a0e79193ef649b9f5a8759d35af086ba62a3e207b52e4a8ae30d49e360405160405180910390a450505050565b6000806000838152602001908152602001600020600101549050919050565b86610d15610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610daf576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610dbe888888888888886112a3565b6000610dc989610ced565b905042600260008b8152602001908152602001600020600201600083815260200190815260200160002081905550505050505050505050565b81610e0b610f1b565b73ffffffffffffffffffffffffffffffffffffffff166002600083815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610ea5576040517fbb9bf27800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600060026000858152602001908152602001600020600101549050826002600086815260200190815260200160002060010181905550837f264b2a8f6763c084235fe832ba903482b2ef1a521336881fc75b987c2dfd29c58285604051610f0d929190611c76565b60405180910390a250505050565b600033905090565b6000610f2e83610a41565b1415610f66576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008083815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__63168703fa9091836040518363ffffffff1660e01b8152600401610fb2929190611b3f565b60006040518083038186803b158015610fca57600080fd5b505af4158015610fde573d6000803e3d6000fd5b505050506000610fed83610ced565b905060006001610ffc85610a22565b6110069190611e7e565b9050837f19239b3f93cd10558aaf11423af70c77763bf54f52bcc75bfa74d4d13548cde982858560405161103c93929190611c9f565b60405180910390a250505050565b600061105587610a41565b141561108d576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008087815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__630629596f909187878787876040518763ffffffff1660e01b81526004016110e196959493929190611b68565b60006040518083038186803b1580156110f957600080fd5b505af415801561110d573d6000803e3d6000fd5b50505050600061111c87610ced565b9050600061112a84846113d0565b9050877f3108849c053c77b8073a11256dffb5ffd5b55e93e105a355e1c9061db890d87182898560405161116093929190611c9f565b60405180910390a25050505050505050565b600061117d83610a41565b146111b4576040517f8121725b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006008836040516020016111c99190611ae0565b6040516020818303038152906040528051906020012060001c901c905060008084815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__631095fbb4909184846040518463ffffffff1660e01b815260040161123493929190611bbf565b60006040518083038186803b15801561124c57600080fd5b505af4158015611260573d6000803e3d6000fd5b50505050827f0d000126c26c1bbe400fd2332187f75d58b69306f9ec47b408686189d3a008338383604051611296929190611c76565b60405180910390a2505050565b60006112ae88610a41565b14156112e6576040517f029f057900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008088815260200190815260200160002073__$0c6eb7207c37accf1552a1c47686411ac0$__63a547882790918888888888886040518863ffffffff1660e01b815260040161133c9796959493929190611bf6565b60006040518083038186803b15801561135457600080fd5b505af4158015611368573d6000803e3d6000fd5b50505050600061137788610ced565b9050600061138584846113d0565b9050887fea3588e4a2a0c93d6a0e69dfeaf7496f43ccccf02ad9ce0a5b7627cbca4b61b1828a8a866040516113bd9493929190611cd6565b60405180910390a2505050505050505050565b6000806000905060008484905090505b60008160ff1611156114f457600082118061145b5750600085856001846114079190611eb2565b60ff16818110611440577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020160208101906114559190611970565b60ff1614155b156114e85760028261146d9190611e24565b9150600185856001846114809190611eb2565b60ff168181106114b9577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020160208101906114ce9190611970565b60ff1614156114e7576001826114e49190611dce565b91505b5b806001900390506113e0565b508091505092915050565b60008135905061150e81611f9b565b92915050565b60008190508260206008028201111561152c57600080fd5b92915050565b60008083601f84011261154457600080fd5b8235905067ffffffffffffffff81111561155d57600080fd5b60208301915083602082028301111561157557600080fd5b9250929050565b60008083601f84011261158e57600080fd5b8235905067ffffffffffffffff8111156115a757600080fd5b6020830191508360208202830111156115bf57600080fd5b9250929050565b6000813590506115d581611fb2565b92915050565b6000813590506115ea81611fc9565b92915050565b60006020828403121561160257600080fd5b6000611610848285016115c6565b91505092915050565b6000806040838503121561162c57600080fd5b600061163a858286016115c6565b925050602061164b858286016114ff565b9150509250929050565b60008060006040848603121561166a57600080fd5b6000611678868287016115c6565b935050602084013567ffffffffffffffff81111561169557600080fd5b6116a186828701611532565b92509250509250925092565b600080604083850312156116c057600080fd5b60006116ce858286016115c6565b92505060206116df858286016115c6565b9150509250929050565b6000806000606084860312156116fe57600080fd5b600061170c868287016115c6565b935050602061171d868287016115c6565b925050604061172e868287016114ff565b9150509250925092565b6000806000806080858703121561174e57600080fd5b600061175c878288016115c6565b945050602061176d878288016115c6565b935050604061177e878288016114ff565b925050606061178f878288016115c6565b91505092959194509250565b600080600080600080608087890312156117b457600080fd5b60006117c289828a016115c6565b96505060206117d389828a016115c6565b955050604087013567ffffffffffffffff8111156117f057600080fd5b6117fc89828a01611532565b9450945050606087013567ffffffffffffffff81111561181b57600080fd5b61182789828a0161157c565b92509250509295509295509295565b600080600080600080600060a0888a03121561185157600080fd5b600061185f8a828b016115c6565b97505060206118708a828b016115c6565b96505060406118818a828b016115c6565b955050606088013567ffffffffffffffff81111561189e57600080fd5b6118aa8a828b01611532565b9450945050608088013567ffffffffffffffff8111156118c957600080fd5b6118d58a828b0161157c565b925092505092959891949750929550565b6000806000806000806101a0878903121561190057600080fd5b600061190e89828a016115c6565b965050602061191f89828a016115c6565b955050604061193089828a016115c6565b945050606061194189828a016115c6565b935050608061195289828a016115c6565b92505060a061196389828a01611514565b9150509295509295509295565b60006020828403121561198257600080fd5b6000611990848285016115db565b91505092915050565b60006119a58383611ad1565b60208301905092915050565b6119ba81611ee6565b82525050565b6119cd6101008383611f53565b5050565b60006119dd8385611d95565b93507f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831115611a0c57600080fd5b602083029250611a1d838584611f53565b82840190509392505050565b6000611a358385611da6565b9350611a4082611d7e565b8060005b85811015611a7957611a568284611db7565b611a608882611999565b9750611a6b83611d88565b925050600181019050611a44565b5085925050509392505050565b611a8f81611f2f565b82525050565b8082525050565b611aa581611f18565b82525050565b611ab481611f18565b82525050565b611acb611ac682611f18565b611f62565b82525050565b611ada81611f22565b82525050565b6000611aec8284611aba565b60208201915081905092915050565b6000604082019050611b1060008301856119b1565b611b1d6020830184611a9c565b9392505050565b6000602082019050611b396000830184611a86565b92915050565b6000604082019050611b546000830185611a95565b611b616020830184611aab565b9392505050565b6000608082019050611b7d6000830189611a95565b611b8a6020830188611aab565b8181036040830152611b9d8186886119d1565b90508181036060830152611bb2818486611a29565b9050979650505050505050565b6000606082019050611bd46000830186611a95565b611be16020830185611aab565b611bee6040830184611aab565b949350505050565b600060a082019050611c0b600083018a611a95565b611c186020830189611aab565b611c256040830188611aab565b8181036060830152611c388186886119d1565b90508181036080830152611c4d818486611a29565b905098975050505050505050565b6000602082019050611c706000830184611a9c565b92915050565b6000604082019050611c8b6000830185611a9c565b611c986020830184611a9c565b9392505050565b6000606082019050611cb46000830186611a9c565b611cc16020830185611a9c565b611cce6040830184611a9c565b949350505050565b6000608082019050611ceb6000830187611a9c565b611cf86020830186611a9c565b611d056040830185611a9c565b611d126060830184611a9c565b95945050505050565b60006101a082019050611d316000830189611a9c565b611d3e6020830188611a9c565b611d4b6040830187611a9c565b611d586060830186611a9c565b611d6560808301856119c0565b611d73610180830184611a9c565b979650505050505050565b6000819050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b6000611dc660208401846115db565b905092915050565b6000611dd982611f18565b9150611de483611f18565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115611e1957611e18611f6c565b5b828201905092915050565b6000611e2f82611f18565b9150611e3a83611f18565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615611e7357611e72611f6c565b5b828202905092915050565b6000611e8982611f18565b9150611e9483611f18565b925082821015611ea757611ea6611f6c565b5b828203905092915050565b6000611ebd82611f22565b9150611ec883611f22565b925082821015611edb57611eda611f6c565b5b828203905092915050565b6000611ef182611ef8565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b6000611f3a82611f41565b9050919050565b6000611f4c82611ef8565b9050919050565b82818337600083830152505050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b611fa481611ee6565b8114611faf57600080fd5b50565b611fbb81611f18565b8114611fc657600080fd5b50565b611fd281611f22565b8114611fdd57600080fd5b5056fea2646970667358221220a6777edc645a16d8f81beacbb5ec133f19452c578127e38e7f40d5540508d08464736f6c63430008040033", + "linkReferences": { + "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol": { + "IncrementalBinaryTree": [ + { + "length": 20, + "start": 4270 + }, + { + "length": 20, + "start": 4565 + }, + { + "length": 20, + "start": 4910 + }, + { + "length": 20, + "start": 5166 + } + ] + } + }, + "deployedLinkReferences": { + "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol": { + "IncrementalBinaryTree": [ + { + "length": 20, + "start": 3962 + }, + { + "length": 20, + "start": 4257 + }, + { + "length": 20, + "start": 4602 + }, + { + "length": 20, + "start": 4858 + } + ] + } + } +} diff --git a/apps/subgraph/docker-compose-graph.yml b/apps/subgraph/docker-compose-graph.yml new file mode 100644 index 00000000..332efbc5 --- /dev/null +++ b/apps/subgraph/docker-compose-graph.yml @@ -0,0 +1,60 @@ +version: "3.9" + +services: + graph-node: + image: graphprotocol/graph-node + ports: + - "8000:8000" + - "8001:8001" + - "8020:8020" + - "8030:8030" + - "8040:8040" + depends_on: + - ipfs + - postgres + environment: + postgres_host: postgres + postgres_user: graph-node + postgres_pass: let-me-in + postgres_db: graph-node + postgres_port: 5432 + ipfs: "ipfs:5001" + ethereum: "localhost:http://host.docker.internal:8545" # Should use the `localhost` as network name in subgraph.yml + # ethereum: 'goerli:https://goerli.infura.io/v3/YOUR-API-KEY' + GRAPH_LOG: info + GRAPH_ALLOW_NON_DETERMINISTIC_IPFS: 1 + networks: + - the-graph + + ipfs: + image: ipfs/go-ipfs:v0.4.23 + ports: + - "5001:5001" + volumes: + - graph-ipfs:/data/ipfs + networks: + - the-graph + + postgres: + image: postgres + ports: + - "5433:5432" + command: ["postgres", "-cshared_preload_libraries=pg_stat_statements"] + environment: + POSTGRES_USER: graph-node + POSTGRES_PASSWORD: let-me-in + POSTGRES_DB: graph-node + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + networks: + - the-graph + volumes: + - graph-postgres:/var/lib/postgresql/data + +networks: + the-graph: + internal: false + driver: bridge + +volumes: + graph-postgres: + graph-ipfs: diff --git a/apps/subgraph/matchstick.yaml b/apps/subgraph/matchstick.yaml new file mode 100644 index 00000000..8851578c --- /dev/null +++ b/apps/subgraph/matchstick.yaml @@ -0,0 +1 @@ +libsFolder: ../../node_modules diff --git a/apps/subgraph/networks.json b/apps/subgraph/networks.json new file mode 100644 index 00000000..2066652d --- /dev/null +++ b/apps/subgraph/networks.json @@ -0,0 +1,44 @@ +{ + "sepolia": { + "Semaphore": { + "address": "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131", + "startBlock": 3231111 + } + }, + "goerli": { + "Semaphore": { + "address": "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131", + "startBlock": 8777695 + } + }, + "mumbai": { + "Semaphore": { + "address": "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131", + "startBlock": 33995010 + } + }, + "optimism-goerli": { + "Semaphore": { + "address": "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131", + "startBlock": 7632846 + } + }, + "arbitrum-goerli": { + "Semaphore": { + "address": "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131", + "startBlock": 15174410 + } + }, + "arbitrum-one": { + "Semaphore": { + "address": "0xc60E0Ee1a2770d5F619858C641f14FC4a6401520", + "startBlock": 77278430 + } + }, + "localhost": { + "Semaphore": { + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "startBlock": 0 + } + } +} diff --git a/apps/subgraph/package.json b/apps/subgraph/package.json new file mode 100644 index 00000000..1cff22ed --- /dev/null +++ b/apps/subgraph/package.json @@ -0,0 +1,26 @@ +{ + "name": "semaphore-subgraph", + "type": "module", + "description": "Semaphore subgraph definition (The Graph).", + "license": "MIT", + "private": true, + "scripts": { + "codegen": "node scripts/generateSubgraph.js ${0} && graph codegen", + "build": "graph build", + "auth": "graph auth --studio", + "deploy": "graph deploy --node https://api.studio.thegraph.com/deploy/ ${0}", + "create-local": "graph create --node http://localhost:8020/ semaphore", + "remove-local": "graph remove --node http://localhost:8020/ semaphore", + "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 semaphore", + "test": "graph test Semaphore -v 0.5.0" + }, + "dependencies": { + "@graphprotocol/graph-cli": "0.56.0", + "@graphprotocol/graph-ts": "^0.31.0" + }, + "devDependencies": { + "@types/mustache": "^4.2.2", + "matchstick-as": "0.5.0", + "mustache": "^4.2.0" + } +} diff --git a/apps/subgraph/schema.graphql b/apps/subgraph/schema.graphql new file mode 100644 index 00000000..f8d238b2 --- /dev/null +++ b/apps/subgraph/schema.graphql @@ -0,0 +1,35 @@ +type MerkleTree @entity { + id: ID! + depth: BigInt! + root: BigInt + zeroValue: BigInt! + numberOfLeaves: Int! + group: Group! +} + +type Group @entity { + id: ID! + merkleTree: MerkleTree! + timestamp: BigInt! + admin: Bytes + members: [Member!] @derivedFrom(field: "group") + verifiedProofs: [VerifiedProof!] @derivedFrom(field: "group") +} + +type Member @entity { + id: ID! + identityCommitment: BigInt! + timestamp: BigInt! + index: Int! + group: Group! +} + +type VerifiedProof @entity { + id: ID! + signal: BigInt! + merkleTreeRoot: BigInt! + nullifierHash: BigInt! + externalNullifier: BigInt! + timestamp: BigInt! + group: Group! +} diff --git a/apps/subgraph/scripts/generateSubgraph.js b/apps/subgraph/scripts/generateSubgraph.js new file mode 100644 index 00000000..8b867969 --- /dev/null +++ b/apps/subgraph/scripts/generateSubgraph.js @@ -0,0 +1,10 @@ +import Mustache from "mustache" +import { readFileSync, writeFileSync } from "fs" + +const network = process.argv.at(2) + +const template = readFileSync("./subgraph.template.yaml", "utf-8") +const networks = JSON.parse(readFileSync("./networks.json", "utf-8")) +const subgraph = Mustache.render(template, { network, ...networks[network].Semaphore }) + +writeFileSync("./subgraph.yaml", subgraph) diff --git a/apps/subgraph/src/semaphore.ts b/apps/subgraph/src/semaphore.ts new file mode 100644 index 00000000..1bc56436 --- /dev/null +++ b/apps/subgraph/src/semaphore.ts @@ -0,0 +1,191 @@ +import { ByteArray, log } from "@graphprotocol/graph-ts" +import { + GroupAdminUpdated, + GroupCreated, + MemberAdded, + MemberRemoved, + MemberUpdated, + ProofVerified +} from "../generated/Semaphore/Semaphore" +import { Member, Group, VerifiedProof, MerkleTree } from "../generated/schema" +import { concat, hash } from "./utils" + +/** + * Creates a new group. + * @param event Ethereum event emitted when a group is created. + */ +export function createGroup(event: GroupCreated): void { + log.debug(`GroupCreated event block: {}`, [event.block.number.toString()]) + + const group = new Group(event.params.groupId.toString()) + const merkleTree = new MerkleTree(event.params.groupId.toString()) + + log.info("Creating group '{}'", [group.id]) + + merkleTree.depth = event.params.merkleTreeDepth + merkleTree.zeroValue = event.params.zeroValue + merkleTree.numberOfLeaves = 0 + merkleTree.group = group.id + + group.timestamp = event.block.timestamp + group.merkleTree = merkleTree.id + + merkleTree.save() + group.save() + + log.info("Group '{}' has been created", [group.id]) +} + +/** + * Updates the admin of a group. + * @param event Ethereum event emitted when a group admin is updated. + */ +export function updateGroupAdmin(event: GroupAdminUpdated): void { + log.debug(`GroupAdminUpdated event block: {}`, [event.block.number.toString()]) + + const group = Group.load(event.params.groupId.toString()) + + if (group) { + log.info("Updating admin '{}' in the group '{}'", [event.params.newAdmin.toString(), group.id]) + + group.admin = event.params.newAdmin + + group.save() + + log.info("Admin '{}' of the group '{}' has been updated ", [group.admin!.toString(), group.id]) + } +} + +/** + * Adds a member to a group. + * @param event Ethereum event emitted when a member is added to a group. + */ +export function addMember(event: MemberAdded): void { + log.debug(`MemberAdded event block {}`, [event.block.number.toString()]) + + const merkleTree = MerkleTree.load(event.params.groupId.toString()) + + if (merkleTree) { + const memberId = hash( + concat(ByteArray.fromBigInt(event.params.index), ByteArray.fromBigInt(event.params.groupId)) + ) + const member = new Member(memberId) + + log.info("Adding member '{}' in the onchain group '{}'", [member.id, merkleTree.group]) + + member.group = merkleTree.group + member.identityCommitment = event.params.identityCommitment + member.timestamp = event.block.timestamp + member.index = merkleTree.numberOfLeaves + + member.save() + + merkleTree.root = event.params.merkleTreeRoot + merkleTree.numberOfLeaves += 1 + + merkleTree.save() + + log.info("Member '{}' of the onchain group '{}' has been added", [member.id, merkleTree.id]) + } +} + +/** + * Updates a member in a group. + * @param event Ethereum event emitted when a member is removed from a group. + */ +export function updateMember(event: MemberUpdated): void { + log.debug(`MemberUpdated event block {}`, [event.block.number.toString()]) + + const merkleTree = MerkleTree.load(event.params.groupId.toString()) + + if (merkleTree) { + const memberId = hash( + concat(ByteArray.fromBigInt(event.params.index), ByteArray.fromBigInt(event.params.groupId)) + ) + const member = Member.load(memberId) + + if (member) { + log.info("Updating member '{}' from the onchain group '{}'", [member.id, merkleTree.group]) + + member.identityCommitment = event.params.newIdentityCommitment + + member.save() + + merkleTree.root = event.params.merkleTreeRoot + + merkleTree.save() + + log.info("Member '{}' of the onchain group '{}' has been removed", [member.id, merkleTree.group]) + } + } +} + +/** + * Removes a member from a group. + * @param event Ethereum event emitted when a member is removed from a group. + */ +export function removeMember(event: MemberRemoved): void { + log.debug(`MemberRemoved event block {}`, [event.block.number.toString()]) + + const merkleTree = MerkleTree.load(event.params.groupId.toString()) + + if (merkleTree) { + const memberId = hash( + concat(ByteArray.fromBigInt(event.params.index), ByteArray.fromBigInt(event.params.groupId)) + ) + const member = Member.load(memberId) + + if (member) { + log.info("Removing member '{}' from the onchain group '{}'", [member.id, merkleTree.group]) + + member.identityCommitment = merkleTree.zeroValue + + member.save() + + merkleTree.root = event.params.merkleTreeRoot + + merkleTree.save() + + log.info("Member '{}' of the onchain group '{}' has been removed", [member.id, merkleTree.group]) + } + } +} + +/** + * Adds a verified proof in a group. + * @param event Ethereum event emitted when a proof has been verified. + */ +export function addVerifiedProof(event: ProofVerified): void { + log.debug(`ProofVerified event block {}`, [event.block.number.toString()]) + + const group = Group.load(event.params.groupId.toString()) + + if (group) { + const verifiedProofId = hash( + concat(ByteArray.fromBigInt(event.params.nullifierHash), ByteArray.fromBigInt(event.params.groupId)) + ) + + const verifiedProof = new VerifiedProof(verifiedProofId) + + log.info("Adding verified proof with signal '{}' in the onchain group '{}'", [ + event.params.signal.toHexString(), + group.id + ]) + + verifiedProof.group = group.id + verifiedProof.signal = event.params.signal + verifiedProof.merkleTreeRoot = event.params.merkleTreeRoot + verifiedProof.externalNullifier = event.params.externalNullifier + verifiedProof.nullifierHash = event.params.nullifierHash + verifiedProof.timestamp = event.block.timestamp + + verifiedProof.save() + + group.save() + + log.info("Verified proof with signal '{}' in the onchain group '{}' has been added", [ + event.params.signal.toHexString(), + group.id + ]) + } +} diff --git a/apps/subgraph/src/utils.ts b/apps/subgraph/src/utils.ts new file mode 100644 index 00000000..0c3549fd --- /dev/null +++ b/apps/subgraph/src/utils.ts @@ -0,0 +1,25 @@ +import { ByteArray, crypto } from "@graphprotocol/graph-ts" + +/** + * Concatenates two byte arrays. + * @param a First byte array. + * @param b Second byte array. + * @returns Final concatenated byte array. + */ +export function concat(a: ByteArray, b: ByteArray): ByteArray { + const c = new ByteArray(a.length + b.length) + + c.set(a) + c.set(b, a.length) + + return c +} + +/** + * Creates a Keccak256 hash. + * @param message Message to hash. + * @returns Hexadecimal string of the Keccak256 hash. + */ +export function hash(message: ByteArray): string { + return crypto.keccak256(message).toHexString() +} diff --git a/apps/subgraph/subgraph.template.yaml b/apps/subgraph/subgraph.template.yaml new file mode 100644 index 00000000..434a461a --- /dev/null +++ b/apps/subgraph/subgraph.template.yaml @@ -0,0 +1,35 @@ +specVersion: 0.0.5 +schema: + file: ./schema.graphql +dataSources: + - kind: ethereum + name: Semaphore + network: {{ network }} + source: + address: "{{ address }}" + abi: Semaphore + startBlock: {{ startBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - Group + - Member + abis: + - name: Semaphore + file: ./abis/Semaphore.json + eventHandlers: + - event: GroupCreated(indexed uint256,uint256,uint256) + handler: createGroup + - event: GroupAdminUpdated(indexed uint256,indexed address,indexed address) + handler: updateGroupAdmin + - event: MemberAdded(indexed uint256,uint256,uint256,uint256) + handler: addMember + - event: MemberUpdated(indexed uint256,uint256,uint256,uint256,uint256) + handler: updateMember + - event: MemberRemoved(indexed uint256,uint256,uint256,uint256) + handler: removeMember + - event: ProofVerified(indexed uint256,indexed uint256,uint256,indexed uint256,uint256) + handler: addVerifiedProof + file: ./src/semaphore.ts diff --git a/apps/subgraph/tests/.bin/semaphore.wasm b/apps/subgraph/tests/.bin/semaphore.wasm new file mode 100644 index 0000000000000000000000000000000000000000..30bf3dc8c1b59f7173d9c35117cb8343c1cca3ed GIT binary patch literal 50093 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 literal 0 HcmV?d00001 diff --git a/apps/subgraph/tests/semaphore-utils.ts b/apps/subgraph/tests/semaphore-utils.ts new file mode 100644 index 00000000..38b963f1 --- /dev/null +++ b/apps/subgraph/tests/semaphore-utils.ts @@ -0,0 +1,160 @@ +import { newMockEvent } from "matchstick-as" +import { ethereum, BigInt, Address } from "@graphprotocol/graph-ts" +import { + GroupAdminUpdated, + GroupCreated, + GroupMerkleTreeDurationUpdated, + MemberAdded, + MemberRemoved, + MemberUpdated, + ProofVerified +} from "../generated/Semaphore/Semaphore" + +export function createGroupCreatedEvent(groupId: BigInt, merkleTreeDepth: BigInt, zeroValue: BigInt): GroupCreated { + const groupCreatedEvent = changetype(newMockEvent()) + + groupCreatedEvent.parameters = [] + + groupCreatedEvent.parameters.push(new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId))) + groupCreatedEvent.parameters.push( + new ethereum.EventParam("merkleTreeDepth", ethereum.Value.fromUnsignedBigInt(merkleTreeDepth)) + ) + groupCreatedEvent.parameters.push( + new ethereum.EventParam("zeroValue", ethereum.Value.fromUnsignedBigInt(zeroValue)) + ) + + return groupCreatedEvent +} + +export function createGroupAdminUpdatedEvent(groupId: BigInt, oldAdmin: Address, newAdmin: Address): GroupAdminUpdated { + const groupAdminUpdatedEvent = changetype(newMockEvent()) + + groupAdminUpdatedEvent.parameters = [] + + groupAdminUpdatedEvent.parameters.push( + new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId)) + ) + groupAdminUpdatedEvent.parameters.push(new ethereum.EventParam("oldAdmin", ethereum.Value.fromAddress(oldAdmin))) + groupAdminUpdatedEvent.parameters.push(new ethereum.EventParam("newAdmin", ethereum.Value.fromAddress(newAdmin))) + + return groupAdminUpdatedEvent +} + +export function createGroupMerkleTreeDurationUpdatedEvent( + groupId: BigInt, + oldMerkleTreeDuration: BigInt, + newMerkleTreeDuration: BigInt +): GroupMerkleTreeDurationUpdated { + const groupMerkleTreeDurationUpdatedEvent = changetype(newMockEvent()) + + groupMerkleTreeDurationUpdatedEvent.parameters = [] + + groupMerkleTreeDurationUpdatedEvent.parameters.push( + new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId)) + ) + groupMerkleTreeDurationUpdatedEvent.parameters.push( + new ethereum.EventParam("oldMerkleTreeDuration", ethereum.Value.fromUnsignedBigInt(oldMerkleTreeDuration)) + ) + groupMerkleTreeDurationUpdatedEvent.parameters.push( + new ethereum.EventParam("newMerkleTreeDuration", ethereum.Value.fromUnsignedBigInt(newMerkleTreeDuration)) + ) + + return groupMerkleTreeDurationUpdatedEvent +} + +export function createMemberAddedEvent( + groupId: BigInt, + index: BigInt, + identityCommitment: BigInt, + merkleTreeRoot: BigInt +): MemberAdded { + const memberAddedEvent = changetype(newMockEvent()) + + memberAddedEvent.parameters = [] + + memberAddedEvent.parameters.push(new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId))) + memberAddedEvent.parameters.push(new ethereum.EventParam("index", ethereum.Value.fromUnsignedBigInt(index))) + memberAddedEvent.parameters.push( + new ethereum.EventParam("identityCommitment", ethereum.Value.fromUnsignedBigInt(identityCommitment)) + ) + memberAddedEvent.parameters.push( + new ethereum.EventParam("merkleTreeRoot", ethereum.Value.fromUnsignedBigInt(merkleTreeRoot)) + ) + + return memberAddedEvent +} + +export function createMemberRemovedEvent( + groupId: BigInt, + index: BigInt, + identityCommitment: BigInt, + merkleTreeRoot: BigInt +): MemberRemoved { + const memberRemovedEvent = changetype(newMockEvent()) + + memberRemovedEvent.parameters = [] + + memberRemovedEvent.parameters.push(new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId))) + memberRemovedEvent.parameters.push(new ethereum.EventParam("index", ethereum.Value.fromUnsignedBigInt(index))) + memberRemovedEvent.parameters.push( + new ethereum.EventParam("identityCommitment", ethereum.Value.fromUnsignedBigInt(identityCommitment)) + ) + memberRemovedEvent.parameters.push( + new ethereum.EventParam("merkleTreeRoot", ethereum.Value.fromUnsignedBigInt(merkleTreeRoot)) + ) + + return memberRemovedEvent +} + +export function createMemberUpdatedEvent( + groupId: BigInt, + index: BigInt, + identityCommitment: BigInt, + newIdentityCommitment: BigInt, + merkleTreeRoot: BigInt +): MemberUpdated { + const memberUpdatedEvent = changetype(newMockEvent()) + + memberUpdatedEvent.parameters = [] + + memberUpdatedEvent.parameters.push(new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId))) + memberUpdatedEvent.parameters.push(new ethereum.EventParam("index", ethereum.Value.fromUnsignedBigInt(index))) + memberUpdatedEvent.parameters.push( + new ethereum.EventParam("identityCommitment", ethereum.Value.fromUnsignedBigInt(identityCommitment)) + ) + memberUpdatedEvent.parameters.push( + new ethereum.EventParam("newIdentityCommitment", ethereum.Value.fromUnsignedBigInt(newIdentityCommitment)) + ) + memberUpdatedEvent.parameters.push( + new ethereum.EventParam("merkleTreeRoot", ethereum.Value.fromUnsignedBigInt(merkleTreeRoot)) + ) + + return memberUpdatedEvent +} + +export function createProofVerifiedEvent( + groupId: BigInt, + merkleTreeRoot: BigInt, + externalNullifier: BigInt, + nullifierHash: BigInt, + signal: BigInt +): ProofVerified { + const proofVerifiedEvent = changetype(newMockEvent()) + + proofVerifiedEvent.parameters = [] + + proofVerifiedEvent.parameters.push(new ethereum.EventParam("groupId", ethereum.Value.fromUnsignedBigInt(groupId))) + proofVerifiedEvent.parameters.push( + new ethereum.EventParam("merkleTreeRoot", ethereum.Value.fromUnsignedBigInt(merkleTreeRoot)) + ) + proofVerifiedEvent.parameters.push( + new ethereum.EventParam("nullifierHash", ethereum.Value.fromUnsignedBigInt(nullifierHash)) + ) + proofVerifiedEvent.parameters.push( + new ethereum.EventParam("externalNullifier", ethereum.Value.fromUnsignedBigInt(externalNullifier)) + ) + + proofVerifiedEvent.parameters.push(new ethereum.EventParam("signal", ethereum.Value.fromUnsignedBigInt(signal))) + + return proofVerifiedEvent +} diff --git a/apps/subgraph/tests/semaphore.test.ts b/apps/subgraph/tests/semaphore.test.ts new file mode 100644 index 00000000..64e6d2ef --- /dev/null +++ b/apps/subgraph/tests/semaphore.test.ts @@ -0,0 +1,156 @@ +import { Address, BigInt, ByteArray } from "@graphprotocol/graph-ts" +import { afterAll, assert, clearStore, describe, test } from "matchstick-as/assembly/index" +import { + addMember, + addVerifiedProof, + createGroup, + removeMember, + updateGroupAdmin, + updateMember +} from "../src/semaphore" +import { concat, hash } from "../src/utils" +import { + createGroupAdminUpdatedEvent, + createGroupCreatedEvent, + createMemberAddedEvent, + createMemberRemovedEvent, + createMemberUpdatedEvent, + createProofVerifiedEvent +} from "./semaphore-utils" + +// https://thegraph.com/docs/en/developer/matchstick +describe("Semaphore subgraph", () => { + afterAll(() => { + clearStore() + }) + + describe("# createGroup", () => { + test("Should have created a group", () => { + const groupId = BigInt.fromI32(234) + const merkleTreeDepth = BigInt.fromI32(20) + const zeroValue = BigInt.fromI32(0) + const oldAdmin = Address.fromString("0x0000000000000000000000000000000000000000") + const newAdmin = Address.fromString("0x0000000000000000000000000000000000000001") + + const event1 = createGroupCreatedEvent(groupId, merkleTreeDepth, zeroValue) + const event2 = createGroupAdminUpdatedEvent(groupId, oldAdmin, newAdmin) + + createGroup(event1) + updateGroupAdmin(event2) + + assert.entityCount("Group", 1) + assert.entityCount("MerkleTree", 1) + + assert.fieldEquals("Group", groupId.toString(), "admin", "0x0000000000000000000000000000000000000001") + assert.fieldEquals("Group", groupId.toString(), "merkleTree", groupId.toString()) + + assert.fieldEquals("MerkleTree", groupId.toString(), "depth", "20") + assert.fieldEquals("MerkleTree", groupId.toString(), "zeroValue", "0") + assert.fieldEquals("MerkleTree", groupId.toString(), "numberOfLeaves", "0") + assert.fieldEquals("MerkleTree", groupId.toString(), "group", groupId.toString()) + }) + }) + + describe("# updateGroupAdmin", () => { + test("Should have updated a group admin", () => { + const groupId = BigInt.fromI32(234) + const oldAdmin = Address.fromString("0x0000000000000000000000000000000000000001") + const newAdmin = Address.fromString("0x0000000000000000000000000000000000000002") + + const event = createGroupAdminUpdatedEvent(groupId, oldAdmin, newAdmin) + + updateGroupAdmin(event) + + assert.fieldEquals("Group", groupId.toString(), "admin", "0x0000000000000000000000000000000000000002") + }) + }) + + describe("# addMember", () => { + test("Should have added a group member", () => { + const groupId = BigInt.fromI32(234) + const index = BigInt.fromI32(0) + const identityCommitment = BigInt.fromI32(123) + const merkleTreeRoot = BigInt.fromI32(999) + const id = hash(concat(ByteArray.fromBigInt(index), ByteArray.fromBigInt(groupId))) + + const event = createMemberAddedEvent(groupId, index, identityCommitment, merkleTreeRoot) + + addMember(event) + + assert.entityCount("Member", 1) + + assert.fieldEquals("Member", id, "index", "0") + assert.fieldEquals("Member", id, "identityCommitment", "123") + assert.fieldEquals("Member", id, "group", groupId.toString()) + + assert.fieldEquals("MerkleTree", groupId.toString(), "root", "999") + assert.fieldEquals("MerkleTree", groupId.toString(), "numberOfLeaves", "1") + }) + }) + + describe("# updateMember", () => { + test("Should have added a group member", () => { + const groupId = BigInt.fromI32(234) + const index = BigInt.fromI32(0) + const identityCommitment = BigInt.fromI32(123) + const newIdentityCommitment = BigInt.fromI32(124) + const merkleTreeRoot = BigInt.fromI32(1000) + const id = hash(concat(ByteArray.fromBigInt(index), ByteArray.fromBigInt(groupId))) + + const event = createMemberUpdatedEvent( + groupId, + index, + identityCommitment, + newIdentityCommitment, + merkleTreeRoot + ) + + updateMember(event) + + assert.fieldEquals("Member", id, "identityCommitment", "124") + + assert.fieldEquals("MerkleTree", groupId.toString(), "root", "1000") + }) + }) + + describe("# removeMember", () => { + test("Should have removed a group member", () => { + const groupId = BigInt.fromI32(234) + const index = BigInt.fromI32(0) + const identityCommitment = BigInt.fromI32(123) + const merkleTreeRoot = BigInt.fromI32(1001) + const id = hash(concat(ByteArray.fromBigInt(index), ByteArray.fromBigInt(groupId))) + + const event = createMemberRemovedEvent(groupId, index, identityCommitment, merkleTreeRoot) + + removeMember(event) + + assert.fieldEquals("Member", id, "identityCommitment", "0") + + assert.fieldEquals("MerkleTree", groupId.toString(), "root", "1001") + }) + }) + + describe("# addVerifiedProof", () => { + test("Should have added a proof", () => { + const groupId = BigInt.fromI32(234) + const merkleTreeRoot = BigInt.fromI32(1001) + const externalNullifier = BigInt.fromI32(1) + const nullifierHash = BigInt.fromI32(666) + const signal = BigInt.fromI32(2) + const id = hash(concat(ByteArray.fromBigInt(nullifierHash), ByteArray.fromBigInt(groupId))) + + const event = createProofVerifiedEvent(groupId, merkleTreeRoot, externalNullifier, nullifierHash, signal) + + addVerifiedProof(event) + + assert.entityCount("VerifiedProof", 1) + + assert.fieldEquals("VerifiedProof", id, "merkleTreeRoot", "1001") + assert.fieldEquals("VerifiedProof", id, "externalNullifier", "1") + assert.fieldEquals("VerifiedProof", id, "nullifierHash", "666") + assert.fieldEquals("VerifiedProof", id, "signal", "2") + assert.fieldEquals("VerifiedProof", id, "group", groupId.toString()) + }) + }) +}) diff --git a/apps/subgraph/tsconfig.json b/apps/subgraph/tsconfig.json new file mode 100644 index 00000000..1c33f88a --- /dev/null +++ b/apps/subgraph/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json", + "include": ["src", "scripts", "tests"] +} diff --git a/package.json b/package.json index 3dd2b642..7f5851ba 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "private": true, "scripts": { "build:libraries": "yarn workspaces foreach -t --no-private run build", - "compile:contracts": "yarn workspace contracts compile", + "build:subgraph": "yarn workspace semaphore-subgraph codegen goerli && yarn workspace semaphore-subgraph build", + "compile:contracts": "yarn workspace semaphore-contracts compile", "download:snark-artifacts": "rimraf snark-artifacts && ts-node scripts/download-snark-artifacts.ts", "remove:template-files": "ts-node scripts/remove-template-files.ts", - "test": "yarn test:libraries && yarn test:contracts", + "test": "yarn test:libraries && yarn test:contracts && yarn test:subgraph", "test:libraries": "jest --coverage", - "test:contracts": "yarn workspace contracts test:coverage", - "lint": "eslint . --ext .js,.ts,.tsx && yarn workspace contracts lint", + "test:subgraph": "yarn workspace semaphore-subgraph test", + "test:contracts": "yarn workspace semaphore-contracts test:coverage", + "lint": "eslint . --ext .js,.ts,.tsx && yarn workspace semaphore-contracts lint", "prettier": "prettier -c .", "prettier:write": "prettier -w .", "docs": "typedoc --cname js.semaphore.pse.dev --githubPages true", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 783d8027..9eeb023c 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,5 +1,5 @@ { - "name": "contracts", + "name": "semaphore-contracts", "private": true, "scripts": { "start": "hardhat node", diff --git a/yarn.lock.REMOVED.git-id b/yarn.lock.REMOVED.git-id index 97f0b603..50a16600 100644 --- a/yarn.lock.REMOVED.git-id +++ b/yarn.lock.REMOVED.git-id @@ -1 +1 @@ -27e13e856d828fdf7ba52e07404ed16ca3a0b6f3 \ No newline at end of file +90c3d691290740e8dfd7b75dd1a43eca7d45a826 \ No newline at end of file