├── .eslintrc.json ├── .vscode ├── extensions.json └── settings.json ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── postcss.config.js ├── .gitmodules ├── next.config.js ├── .env ├── contracts ├── foundry.toml ├── .gitignore └── src │ ├── helpers │ └── ByteHasher.sol │ ├── interfaces │ └── IWorldID.sol │ └── Contract.sol ├── .prettierrc.js ├── src ├── pages │ ├── _document.tsx │ ├── _app.tsx │ └── index.tsx ├── styles │ └── globals.css ├── lib │ └── config.ts └── abi │ └── ContractAbi.json ├── .gitignore ├── tailwind.config.js ├── tsconfig.json ├── .github └── workflows │ └── test.yml ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "juanblanco.solidity" 4 | ] 5 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/world-id-onchain-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/forge-std"] 2 | path = contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | WORLD_ID_ROUTER=0x11cA3127182f7583EfC416a8771BD4d11Fae4334 2 | NEXT_PUBLIC_APP_ID= 3 | NEXT_PUBLIC_ACTION= 4 | NEXT_PUBLIC_WALLETCONNECT_ID= 5 | NEXT_PUBLIC_CONTRACT_ADDRESS= -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 7 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "connectkit", 4 | "consts", 5 | "predev", 6 | "Sepolia", 7 | "tanstack", 8 | "viem", 9 | "Wagmi", 10 | "WALLETCONNECT", 11 | "zustand" 12 | ], 13 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | printWidth: 120, 5 | tabWidth: 4, 6 | trailingComma: 'es5', 7 | useTabs: true, 8 | bracketSpacing: true, 9 | arrowParens: 'avoid', 10 | plugins: [require('prettier-plugin-sort-imports-desc')], 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /contracts/src/helpers/ByteHasher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | library ByteHasher { 5 | /// @dev Creates a keccak256 hash of a bytestring. 6 | /// @param value The bytestring to hash 7 | /// @return The hash of the specified value 8 | /// @dev `>> 8` makes sure that the result is included in our field 9 | function hashToField(bytes memory value) internal pure returns (uint256) { 10 | return uint256(keccak256(abi.encodePacked(value))) >> 8; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { WagmiProvider } from 'wagmi' 4 | import { ConnectKitProvider } from 'connectkit' 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 6 | import { config } from '@/lib/config' 7 | 8 | const queryClient = new QueryClient() 9 | 10 | export default function App({ Component, pageProps }: AppProps) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowImportingTsExtensions": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /contracts/src/interfaces/IWorldID.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | interface IWorldID { 5 | /// @notice Reverts if the zero-knowledge proof is invalid. 6 | /// @param root The of the Merkle tree 7 | /// @param groupId The id of the Semaphore group 8 | /// @param signalHash A keccak256 hash of the Semaphore signal 9 | /// @param nullifierHash The nullifier hash 10 | /// @param externalNullifierHash A keccak256 hash of the external nullifier 11 | /// @param proof The zero-knowledge proof 12 | /// @dev Note that a double-signaling check is not included here, and should be carried by the caller. 13 | function verifyProof( 14 | uint256 root, 15 | uint256 groupId, 16 | uint256 signalHash, 17 | uint256 nullifierHash, 18 | uint256 externalNullifierHash, 19 | uint256[8] calldata proof 20 | ) external view; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { createConfig, http } from 'wagmi' 2 | import { type Chain } from 'viem' 3 | import { fallback, injected, unstable_connector } from '@wagmi/core'; 4 | import { getDefaultConfig } from 'connectkit'; 5 | 6 | export const chain: Chain = { 7 | id: 11155420, 8 | name: "Optimism Sepolia Anvil Fork", 9 | nativeCurrency: { 10 | decimals: 18, 11 | name: "Optimism Sepolia Anvil Fork Ether", 12 | symbol: "SETH", 13 | }, 14 | rpcUrls: { 15 | default: { http: ["http://127.0.0.1:8545/"] }, 16 | }, 17 | testnet: true, 18 | }; 19 | 20 | export const config = createConfig( 21 | getDefaultConfig({ 22 | chains: [chain], 23 | transports: { 24 | [chain.id]: fallback([ 25 | unstable_connector(injected), 26 | http(chain.rpcUrls.default.http[0]), 27 | ]) 28 | }, 29 | walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_ID!, 30 | appName: "World ID Onchain Template", 31 | }), 32 | ) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "world-id-onchain-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "predev": "rm src/abi/ContractAbi.json && cd contracts && forge inspect Contract abi >> ../src/abi/ContractAbi.json", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@tanstack/react-query": "^5.28.6", 14 | "@types/node": "20.1.2", 15 | "@types/react": "18.2.6", 16 | "@types/react-dom": "18.2.4", 17 | "@wagmi/core": "^2.6.9", 18 | "@worldcoin/idkit": "^1.1.4", 19 | "autoprefixer": "10.4.14", 20 | "connectkit": "^1.7.2", 21 | "eslint": "8.40.0", 22 | "eslint-config-next": "13.4.1", 23 | "next": "13.4.1", 24 | "postcss": "8.4.23", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "tailwindcss": "3.3.2", 28 | "typescript": "5.0.4", 29 | "viem": "^2.8.16", 30 | "wagmi": "^2.5.11" 31 | }, 32 | "devDependencies": { 33 | "prettier-plugin-sort-imports-desc": "^1.0.0" 34 | }, 35 | "pnpm": { 36 | "overrides": { 37 | "@worldcoin/idkit>zustand": "^4.5" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/abi/ContractAbi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "inputs": [ 5 | { 6 | "name": "_worldId", 7 | "type": "address", 8 | "internalType": "contract IWorldID" 9 | }, 10 | { 11 | "name": "_appId", 12 | "type": "string", 13 | "internalType": "string" 14 | }, 15 | { 16 | "name": "_actionId", 17 | "type": "string", 18 | "internalType": "string" 19 | } 20 | ], 21 | "stateMutability": "nonpayable" 22 | }, 23 | { 24 | "type": "function", 25 | "name": "verifyAndExecute", 26 | "inputs": [ 27 | { 28 | "name": "signal", 29 | "type": "address", 30 | "internalType": "address" 31 | }, 32 | { 33 | "name": "root", 34 | "type": "uint256", 35 | "internalType": "uint256" 36 | }, 37 | { 38 | "name": "nullifierHash", 39 | "type": "uint256", 40 | "internalType": "uint256" 41 | }, 42 | { 43 | "name": "proof", 44 | "type": "uint256[8]", 45 | "internalType": "uint256[8]" 46 | } 47 | ], 48 | "outputs": [], 49 | "stateMutability": "nonpayable" 50 | }, 51 | { 52 | "type": "event", 53 | "name": "verified", 54 | "inputs": [ 55 | { 56 | "name": "nullifierHash", 57 | "type": "uint256", 58 | "indexed": false, 59 | "internalType": "uint256" 60 | } 61 | ], 62 | "anonymous": false 63 | }, 64 | { 65 | "type": "error", 66 | "name": "DuplicateNullifier", 67 | "inputs": [ 68 | { 69 | "name": "nullifierHash", 70 | "type": "uint256", 71 | "internalType": "uint256" 72 | } 73 | ] 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import abi from '@/abi/ContractAbi.json' 2 | import { ConnectKitButton } from 'connectkit' 3 | import { IDKitWidget, ISuccessResult, useIDKit } from '@worldcoin/idkit' 4 | import { useAccount, useWriteContract, useWaitForTransactionReceipt, type BaseError } from 'wagmi' 5 | import { decodeAbiParameters, parseAbiParameters } from 'viem' 6 | import { useState } from 'react' 7 | 8 | export default function Home() { 9 | const account = useAccount() 10 | const { setOpen } = useIDKit() 11 | const [done, setDone] = useState(false) 12 | const { data: hash, isPending, error, writeContractAsync } = useWriteContract() 13 | const { isLoading: isConfirming, isSuccess: isConfirmed } = 14 | useWaitForTransactionReceipt({ 15 | hash, 16 | }) 17 | 18 | const submitTx = async (proof: ISuccessResult) => { 19 | try { 20 | await writeContractAsync({ 21 | address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x${string}`, 22 | account: account.address!, 23 | abi, 24 | functionName: 'verifyAndExecute', 25 | args: [ 26 | account.address!, 27 | BigInt(proof!.merkle_root), 28 | BigInt(proof!.nullifier_hash), 29 | decodeAbiParameters( 30 | parseAbiParameters('uint256[8]'), 31 | proof!.proof as `0x${string}` 32 | )[0], 33 | ], 34 | }) 35 | setDone(true) 36 | } catch (error) {throw new Error((error as BaseError).shortMessage)} 37 | } 38 | 39 | return ( 40 |
41 | 42 | {account.isConnected && (<> 43 | 50 | 51 | {!done && } 52 | 53 | {hash &&

Transaction Hash: {hash}

} 54 | {isConfirming &&

Waiting for confirmation...

} 55 | {isConfirmed &&

Transaction confirmed.

} 56 | {error &&

Error: {(error as BaseError).message}

} 57 | )} 58 |
59 | ) 60 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # World ID On-Chain Template 2 | 3 | Template repository for a World ID On-Chain Integration. 4 | 5 | ## Local Development 6 | 7 | ### Prerequisites 8 | 9 | Create a staging on-chain app in the [Worldcoin Developer Portal](https://developer.worldcoin.org). 10 | 11 | Ensure you have installed [Foundry](https://book.getfoundry.sh/getting-started/installation), [NodeJS](https://nodejs.org/en/download), and [pnpm](https://pnpm.io/installation). 12 | 13 | ### Local Testnet Setup 14 | 15 | Start a local node forked from Optimism Sepolia, replacing `$YOUR_API_KEY` with your Alchemy API key: 16 | 17 | ```bash 18 | # leave this running in the background 19 | anvil -f https://opt-sepolia.g.alchemy.com/v2/$YOUR_API_KEY 20 | ``` 21 | 22 | In another shell, deploy the contract, replacing `$WORLD_ID_ROUTER` with the [World ID Router address](https://docs.worldcoin.org/reference/address-book) for your selected chain, `$NEXT_PUBLIC_APP_ID` with the app ID as configured in the [Worldcoin Developer Portal](https://developer.worldcoin.org), and `$NEXT_PUBLIC_ACTION` with the action ID as configured in the Worldcoin Developer Portal: 23 | 24 | ```bash 25 | cd contracts 26 | forge create --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/Contract.sol:Contract --constructor-args $WORLD_ID_ROUTER $NEXT_PUBLIC_APP_ID $NEXT_PUBLIC_ACTION 27 | ``` 28 | 29 | Note the `Deployed to:` address from the output. 30 | 31 | ### Local Web Setup 32 | 33 | In a new shell, install project dependencies: 34 | 35 | ```bash 36 | pnpm i 37 | ``` 38 | 39 | Set up your environment variables in the `.env` file. You will need to set the following variables: 40 | - `NEXT_PUBLIC_APP_ID`: The app ID as configured in the [Worldcoin Developer Portal](https://developer.worldcoin.org). 41 | - `NEXT_PUBLIC_ACTION`: The action ID as configured in the Worldcoin Developer Portal. 42 | - `NEXT_PUBLIC_WALLETCONNECT_ID`: Your WalletConnect ID. 43 | - `NEXT_PUBLIC_CONTRACT_ADDRESS`: The address of the contract deployed in the previous step. 44 | 45 | Start the development server: 46 | 47 | ```bash 48 | pnpm dev 49 | ``` 50 | 51 | The Contract ABI will be automatically re-generated and saved to `src/abi/ContractAbi.json` on each run of `pnpm dev`. 52 | 53 | ### Iterating 54 | 55 | After making changes to the contract, you should: 56 | - re-run the `forge create` command from above 57 | - replace the `NEXT_PUBLIC_CONTRACT_ADDRESS` environment variable with the new contract address 58 | - if your contract ABI has changed, restart the local web server 59 | 60 | ### Testing 61 | 62 | You'll need to import the private keys on the local testnet into your wallet used for local development. The default development seed phrase is `test test test test test test test test test test test junk`. 63 | 64 | > [!CAUTION] 65 | > This is only for local development. Do not use this seed phrase on mainnet or any public testnet. 66 | 67 | When connecting your wallet to the local development environment, you will be prompted to add the network to your wallet. 68 | 69 | Use the [Worldcoin Simulator](https://simulator.worldcoin.org) in place of World App to scan the IDKit QR codes and generate the zero-knowledge proofs. -------------------------------------------------------------------------------- /contracts/src/Contract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import { ByteHasher } from './helpers/ByteHasher.sol'; 5 | import { IWorldID } from './interfaces/IWorldID.sol'; 6 | 7 | contract Contract { 8 | using ByteHasher for bytes; 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | /// ERRORS /// 12 | ////////////////////////////////////////////////////////////////////////////// 13 | 14 | /// @notice Thrown when attempting to reuse a nullifier 15 | error DuplicateNullifier(uint256 nullifierHash); 16 | 17 | /// @dev The World ID instance that will be used for verifying proofs 18 | IWorldID internal immutable worldId; 19 | 20 | /// @dev The contract's external nullifier hash 21 | uint256 internal immutable externalNullifier; 22 | 23 | /// @dev The World ID group ID (always 1) 24 | uint256 internal immutable groupId = 1; 25 | 26 | /// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person 27 | mapping(uint256 => bool) internal nullifierHashes; 28 | 29 | /// @param nullifierHash The nullifier hash for the verified proof 30 | /// @dev A placeholder event that is emitted when a user successfully verifies with World ID 31 | event Verified(uint256 nullifierHash); 32 | 33 | /// @param _worldId The WorldID router that will verify the proofs 34 | /// @param _appId The World ID app ID 35 | /// @param _actionId The World ID action ID 36 | constructor(IWorldID _worldId, string memory _appId, string memory _actionId) { 37 | worldId = _worldId; 38 | externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField(); 39 | } 40 | 41 | /// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details) 42 | /// @param root The root of the Merkle tree (returned by the JS widget). 43 | /// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget). 44 | /// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget). 45 | /// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past. 46 | function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public { 47 | // First, we make sure this person hasn't done this before 48 | if (nullifierHashes[nullifierHash]) revert DuplicateNullifier(nullifierHash); 49 | 50 | // We now verify the provided proof is valid and the user is verified by World ID 51 | worldId.verifyProof( 52 | root, 53 | groupId, 54 | abi.encodePacked(signal).hashToField(), 55 | nullifierHash, 56 | externalNullifier, 57 | proof 58 | ); 59 | 60 | // We now record the user has done this, so they can't do it again (proof of uniqueness) 61 | nullifierHashes[nullifierHash] = true; 62 | 63 | // Finally, execute your logic here, for example issue a token, NFT, etc... 64 | // Make sure to emit some kind of event afterwards! 65 | 66 | emit Verified(nullifierHash); 67 | } 68 | } 69 | --------------------------------------------------------------------------------