├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── README.md ├── package.json ├── packages ├── frontend │ ├── .babelrc │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .prettierignore │ ├── .prettierrc │ ├── components │ │ ├── Balance.tsx │ │ ├── ConnectWallet.tsx │ │ ├── Error.tsx │ │ ├── api │ │ │ ├── RequestBuilder.tsx │ │ │ └── index.ts │ │ ├── feeds │ │ │ ├── PriceFeed.tsx │ │ │ ├── ProofOfReserve.tsx │ │ │ ├── SelectFeed.tsx │ │ │ └── index.ts │ │ ├── layout │ │ │ ├── Head.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Section.tsx │ │ │ └── index.ts │ │ └── vrf │ │ │ ├── RandomNFT │ │ │ ├── ExternalLink.tsx │ │ │ └── index.tsx │ │ │ ├── RandomNumber.tsx │ │ │ └── index.ts │ ├── conf │ │ └── config.ts │ ├── contracts │ │ ├── external.ts │ │ └── hardhat_contracts.json │ ├── hooks │ │ ├── useContract.ts │ │ ├── useContractCall.ts │ │ └── useContractConfig.ts │ ├── jest.config.js │ ├── lib │ │ ├── connectors.ts │ │ └── utils.ts │ ├── next-env.d.ts │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── automation.tsx │ │ ├── external-api.tsx │ │ ├── feeds.tsx │ │ ├── index.tsx │ │ └── vrf.tsx │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── github.svg │ │ │ ├── logo-metamask.png │ │ │ ├── logo-walletconnect.svg │ │ │ └── social-preview.png │ │ └── vercel.svg │ ├── test │ │ ├── __mocks__ │ │ │ └── fileMock.js │ │ └── pages │ │ │ └── __snapshots__ │ │ │ └── index.test.tsx.snap │ └── tsconfig.json ├── hardhat │ ├── .env.example │ ├── .prettierignore │ ├── .prettierrc │ ├── contracts │ │ ├── APIConsumer.sol │ │ ├── FeedRegistryConsumer.sol │ │ ├── Multicall.sol │ │ ├── PriceConsumerV3.sol │ │ ├── RandomNumberConsumer.sol │ │ ├── RandomSVG.sol │ │ ├── interfaces │ │ │ └── FeedRegistryInterface.sol │ │ └── mocks │ │ │ ├── LinkToken.sol │ │ │ ├── MockFeedRegistry.sol │ │ │ ├── MockOracle.sol │ │ │ ├── MockV3Aggregator.sol │ │ │ ├── VRFCoordinatorV2Mock.sol │ │ │ └── VRFV2Wrapper.sol │ ├── decs.d.ts │ ├── deploy │ │ ├── 00_Deploy_Multicall.ts │ │ ├── 01_Deploy_Mocks.ts │ │ ├── 02_Deploy_FeedRegistryConsumer.ts │ │ ├── 03_Deploy_PriceConsumerV3.ts │ │ ├── 04_Deploy_RandomNumberConsumer.ts │ │ ├── 05_Deploy_RandomSVG.ts │ │ ├── 06_Deploy_APIConsumer.ts │ │ └── 07_Setup_Contracts.ts │ ├── hardhat.config.ts │ ├── helper-hardhat-config.ts │ ├── package.json │ ├── tasks │ │ ├── accounts.ts │ │ ├── fund-link.ts │ │ └── withdraw-link.ts │ ├── test │ │ ├── data │ │ │ └── randomSVG.txt │ │ ├── integration │ │ │ ├── APIConsumer.test.ts │ │ │ ├── PriceConsumerV3.test.ts │ │ │ └── RandomNumberConsumer.test.ts │ │ └── unit │ │ │ ├── APIConsumer.test.ts │ │ │ ├── FeedRegistryConsumer.test.ts │ │ │ ├── PriceConsumerV3.test.ts │ │ │ ├── RandomNumberConsumer.test.ts │ │ │ └── RandomSVG.test.ts │ ├── tsconfig.json │ └── utils.ts └── types │ └── package.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: ["push", "pull_request"] 3 | 4 | jobs: 5 | test: 6 | name: Test contracts 7 | runs-on: ubuntu-latest 8 | env: 9 | REPORT_GAS: true 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: 14 15 | 16 | - name: Init submodules 17 | run: git submodule init 18 | 19 | - name: Update submodules 20 | run: git submodule update 21 | 22 | - name: Install dependencies 23 | run: yarn install --frozen-lockfile 24 | 25 | - name: Compile contracts 26 | run: yarn compile 27 | 28 | - name: Run tests 29 | run: yarn test:contracts --network hardhat 30 | 31 | - name: Coverage 32 | run: yarn coverage:contracts 33 | 34 | - name: Coveralls 35 | uses: codecov/codecov-action@v2 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | -------------------------------------------------------------------------------- /.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 | # misc 9 | .DS_Store 10 | *.pem 11 | 12 | # debug 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # local env files 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | .env 24 | 25 | # vercel 26 | .vercel 27 | 28 | node_modules 29 | 30 | ## Hardhat files ## 31 | packages/hardhat/*.txt 32 | packages/hardhat/cache 33 | packages/hardhat/artifacts 34 | packages/hardhat/deployments 35 | packages/hardhat/coverage 36 | packages/hardhat/coverage.json 37 | 38 | ## Frontend Files ## 39 | # testing 40 | packages/frontend/coverage 41 | packages/react-app 42 | packages/subgraph 43 | # next.js 44 | packages/frontend/.next/ 45 | packages/frontend/out/ 46 | # production 47 | packages/frontend/build 48 | # contracts 49 | # packages/frontend/artifacts/* 50 | 51 | # Typechain Files ## 52 | packages/types/typechain 53 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/hardhat/vendor/openzeppelin"] 2 | path = packages/hardhat/vendor/openzeppelin 3 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chainlink Fullstack Demo App 2 | 3 | [![codecov](https://codecov.io/gh/hackbg/chainlink-fullstack/branch/main/graph/badge.svg?token=60ZDWLHB53)](https://codecov.io/gh/hackbg/chainlink-fullstack) 4 | 5 | [LIVE DEMO](https://chainlink-demo.app) 6 | 7 | End-to-end implementation of the following Chainlink features using Hardhat development environment and Next.js frontend framework: 8 | 9 | - [Request & Receive data](https://docs.chain.link/docs/request-and-receive-data/) 10 | - [Chainlink Price Feeds](https://docs.chain.link/docs/using-chainlink-reference-contracts/) 11 | - [Chainlink VRF](https://docs.chain.link/docs/chainlink-vrf/) 12 | 13 | Built with: 14 | 15 | - [Next.js](https://nextjs.org) 16 | - [TypeScript](https://www.typescriptlang.org) 17 | - [Hardhat](https://hardhat.org) 18 | - [TypeChain](https://github.com/dethcrypto/TypeChain) 19 | - [Ethers.js](https://docs.ethers.io/v5/) 20 | - [useDApp](https://usedapp.io) 21 | - [Chakra UI](https://chakra-ui.com) 22 | - Linting with [ESLint](https://eslint.org) 23 | - Formatting with [Prettier](https://prettier.io) 24 | 25 | ## Requirements 26 | 27 | - [Node](https://nodejs.org/en/download/) 28 | - [Yarn](https://classic.yarnpkg.com/en/docs/install/#mac-stable) 29 | - [Git](https://git-scm.com/downloads) 30 | 31 | In order to use the frontend portion of the demo application you will need: 32 | 33 | - A crypto wallet such as [Metamask](https://metamask.io/) or [Coinbase Wallet](https://www.coinbase.com/wallet) 34 | - Test $LINK for the relevant testnet. You can get some at the [Chainlink Faucets](https://faucets.chain.link/) page. 35 | - Test $ETH to pay for gas costs. You can get some at the [Chainlink Faucets](https://faucets.chain.link/) page. 36 | 37 | ## Quick Start 38 | 39 | Clone the repo and install all dependencies: 40 | 41 | ```bash 42 | git clone https://github.com/smartcontractkit/chainlink-fullstack 43 | cd chainlink-fullstack 44 | 45 | git submodule init 46 | git submodule update 47 | 48 | yarn install 49 | ``` 50 | 51 | Start up the local Hardhat network and deploy all contracts: 52 | 53 | ```bash 54 | yarn chain 55 | ``` 56 | 57 | In a second terminal start up the local development server run the front-end app: 58 | 59 | ```bash 60 | yarn dev 61 | ``` 62 | 63 | To interact with the local network, follow this step-by-step guide on how to use [MetaMask with a Hardhat node](https://support.chainstack.com/hc/en-us/articles/4408642503449-Using-MetaMask-with-a-Hardhat-node). 64 | 65 | If you've set the mnemonic from MetaMask the first 20 accounts will be funded with ETH. 66 | 67 | ## Environment Variables 68 | 69 | To make setting environment variables easier there are `.env.example` files in the `hardhat` and `frontend` workspaces. You can copy them to new `.env` files and replace the values with your own. 70 | 71 | #### Hardhat 72 | 73 | | Name | Description | 74 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 75 | | `NETWORK_RPC_URL` | Required to deploy to public networks. Obtain from [Infura's site](https://infura.io). | 76 | | `MNEMONIC` | Used to derive accounts from wallet seed phrase, ie Metamask. The first account must have enough ETH to deploy the contracts, as well as LINK which can be obtained from [Chainlink's faucets](https://faucets.chain.link). | 77 | | `PRIVATE_KEY` | Alternative to using mnemonic. Some changes are required in `hardhat.config.js` | 78 | | `ETHERSCAN_API_KEY` | Verify contract code on Etherscan. | 79 | 80 | #### Front-end 81 | 82 | | Name | Description | 83 | | ------------------------ | --------------------------------- | 84 | | `NEXT_PUBLIC_INFURA_KEY` | Read-only mode and WalletConnect. | 85 | 86 | ## Deploy Contracts 87 | 88 | This will run the deploy scripts to a local Hardhat network: 89 | 90 | ```bash 91 | yarn deploy 92 | ``` 93 | 94 | To deploy on a public network: 95 | 96 | ```bash 97 | yarn deploy --network goerli 98 | ``` 99 | 100 | Before deploying `RandomSVG` contract on a public network, an ID of a prefunded VRF subscription must be set in [`helper-hardhat-config.ts`](/packages/hardhat/helper-hardhat-config.ts). 101 | 102 | See how to [Create and Fund a Subscription](https://docs.chain.link/docs/vrf/v2/subscription/ui/). 103 | 104 | ## Auto-Funding 105 | 106 | The Hardhat project will attempt to auto-fund any newly deployed contract that uses Any-API or VRF, which otherwise has to be done manually. 107 | 108 | The amount in LINK to send as part of this process can be modified in this [Hardhat Config](https://github.com/hackbg/chainlink-fullstack/blob/main/packages/hardhat/helper-hardhat-config.ts), and are configurable per network. 109 | 110 | | Parameter | Description | Default Value | 111 | | ---------- | :------------------------------------------------ | :------------ | 112 | | fundAmount | Amount of LINK to transfer when funding contracts | 5 LINK | 113 | 114 | If you wish to deploy the smart contracts without performing the auto-funding, run the following command when doing your deployment: 115 | 116 | ```bash 117 | yarn deploy --tags main 118 | ``` 119 | 120 | ## Test 121 | 122 | If the test command is executed without a specified network it will run locally and only perform the unit tests: 123 | 124 | ```bash 125 | yarn test:contracts 126 | ``` 127 | 128 | Integration tests must be run on a public testnet that has Chainlink oracles responding: 129 | 130 | ```bash 131 | yarn test:contracts --network goerli 132 | ``` 133 | 134 | For coverage report: 135 | 136 | ```bash 137 | yarn coverage:contracts 138 | ``` 139 | 140 | ## Verify on Etherscan 141 | 142 | You'll need an `ETHERSCAN_API_KEY` environment variable. You can get one from the [Etherscan API site.](https://etherscan.io/apis) 143 | 144 | ```bash 145 | npx hardhat verify --network 146 | ``` 147 | 148 | example: 149 | 150 | ```bash 151 | npx hardhat verify --network goerli 0x9279791897f112a41FfDa267ff7DbBC46b96c296 "0x9326BFA02ADD2366b30bacB125260Af641031331" 152 | ``` 153 | 154 | ## Format 155 | 156 | Fix formatting according to prettier config in the respective workspace: 157 | 158 | ```bash 159 | yarn format:frontend 160 | yarn format:hardhat 161 | ``` 162 | 163 | ## Lint 164 | 165 | ```bash 166 | yarn lint:frontend 167 | ``` 168 | 169 | ## Testnet Contracts 170 | 171 | This repo includes deployed and verified contracts on Goerli so the front-end can run without the need to deploy them. 172 | 173 | Once the `deploy` command is executed on any network the contracts config will be overwritten and you can start from scratch with your own deployments. 174 | 175 | #### Sepolia 176 | 177 | | Name | Address | 178 | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- | 179 | | `PriceConsumerV3` | [0xf37F9826f60870894190B5Ffe89138f3ef10079C](https://sepolia.etherscan.io/address/0xf37F9826f60870894190B5Ffe89138f3ef10079C) | 180 | | `APIConsumer` | [0x8fEa7488314D44776C7960B3149258827B8ADa31](https://sepolia.etherscan.io/address/0x8fEa7488314D44776C7960B3149258827B8ADa31) | 181 | | `RandomNumberConsumer` | [0xBcFd34a46C2Da1E10568B4691ab2678cB24265db](https://sepolia.etherscan.io/address/0xBcFd34a46C2Da1E10568B4691ab2678cB24265db) | 182 | | `RandomSVG` | [0xc055B4DA31b7895f60c6335276f47EbD817F98E1](https://sepolia.etherscan.io/address/0xc055B4DA31b7895f60c6335276f47EbD817F98E1) | 183 | 184 | #### Goerli 185 | 186 | | Name | Address | 187 | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | 188 | | `PriceConsumerV3` | [0x46b73aca4AF8D060355beAb7f3C941B214ba0E1F](https://goerli.etherscan.io/address/0x46b73aca4AF8D060355beAb7f3C941B214ba0E1F) | 189 | | `APIConsumer` | [0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b](https://goerli.etherscan.io/address/0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b) | 190 | | `RandomNumberConsumer` | [0x35ea06342a82e091040CbF415cc899228DB4C936](https://goerli.etherscan.io/address/0x35ea06342a82e091040CbF415cc899228DB4C936) | 191 | | `RandomSVG` | [0xa652548CDAb898d9d885896f464Fd4a07F353aBc](https://goerli.etherscan.io/address/0xa652548CDAb898d9d885896f464Fd4a07F353aBc) | 192 | 193 | #### Kovan (deprecated) 194 | 195 | | Name | Address | 196 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------- | 197 | | `PriceConsumerV3` | [0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00](https://kovan.etherscan.io/address/0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00) | 198 | | `FeedRegistryConsumer` | [0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50](https://kovan.etherscan.io/address/0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50) | 199 | | `APIConsumer` | [0x14005AB90bc520E20Ffd7815Cae64372abb6b04d](https://kovan.etherscan.io/address/0x14005AB90bc520E20Ffd7815Cae64372abb6b04d) | 200 | | `RandomNumberConsumer` | [0xF9556187bf86823Cf0D7081625F97391642Fc242](https://kovan.etherscan.io/address/0xF9556187bf86823Cf0D7081625F97391642Fc242) | 201 | | `RandomSVG` | [0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76](https://kovan.etherscan.io/address/0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76) | 202 | 203 | #### Rinkeby (deprecated) 204 | 205 | | Name | Address | 206 | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- | 207 | | `PriceConsumerV3` | [0x4998Bd433216bBc56976BCb4Fe5AA240bA766763](https://rinkeby.etherscan.io/address/0x4998Bd433216bBc56976BCb4Fe5AA240bA766763) | 208 | | `APIConsumer` | [0x43a87559277fd5F6F1AdC6e6331998899634e9Aa](https://rinkeby.etherscan.io/address/0x43a87559277fd5F6F1AdC6e6331998899634e9Aa) | 209 | | `RandomNumberConsumer` | [0xA0e617aaA36Ff4A6bf61C4Ce2Ed66822B1e24726](https://rinkeby.etherscan.io/address/0xA0e617aaA36Ff4A6bf61C4Ce2Ed66822B1e24726) | 210 | | `RandomSVG` | [0xeC6CcE025e538D12E52D8C90181849B099a776A3](https://rinkeby.etherscan.io/address/0xeC6CcE025e538D12E52D8C90181849B099a776A3) | 211 | 212 | ## References 213 | 214 | - [Chainlink Docs](https://docs.chain.link) 215 | - [Chainlink Hardhat Box](https://github.com/smartcontractkit/hardhat-starter-kit) 216 | - [Scaffold ETH](https://github.com/scaffold-eth/scaffold-eth/blob/nextjs-typescript) 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainlink-fullstack", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "yarn workspace chainlink-fullstack-frontend dev", 7 | "build": "yarn workspace chainlink-fullstack-frontend build", 8 | "export": "yarn workspace chainlink-fullstack-frontend export", 9 | "start": "yarn workspace chainlink-fullstack-frontend start", 10 | "chain": "yarn workspace chainlink-fullstack-hardhat chain", 11 | "deploy": "yarn workspace chainlink-fullstack-hardhat deploy", 12 | "compile": "yarn workspace chainlink-fullstack-hardhat compile", 13 | "test:contracts": "yarn workspace chainlink-fullstack-hardhat test", 14 | "test:frontend": "yarn workspace chainlink-fullstack-frontend test", 15 | "coverage:contracts": "yarn workspace chainlink-fullstack-hardhat coverage", 16 | "lint:frontend": "yarn workspace chainlink-fullstack-frontend lint", 17 | "format:frontend": "yarn workspace chainlink-fullstack-frontend format", 18 | "format:hardhat": "yarn workspace chainlink-fullstack-hardhat format" 19 | }, 20 | "workspaces": { 21 | "packages": [ 22 | "packages/*" 23 | ], 24 | "nohoist": [ 25 | "**/hardhat", 26 | "**/hardhat/**", 27 | "**" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/frontend/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_INFURA_KEY='your-api-key' 2 | NEXT_PUBLIC_GTM_ID='your-gtm-id' -------------------------------------------------------------------------------- /packages/frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/out/* 3 | **/.next/* 4 | -------------------------------------------------------------------------------- /packages/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react-hooks/recommended" 9 | // Uncomment the following lines to enable eslint-config-prettier 10 | // Is not enabled right now to avoid issues with the Next.js repo 11 | // "prettier", 12 | ], 13 | "env": { 14 | "es6": true, 15 | "browser": true, 16 | "jest": true, 17 | "node": true 18 | }, 19 | "settings": { 20 | "react": { 21 | "version": "detect" 22 | } 23 | }, 24 | "rules": { 25 | "react/react-in-jsx-scope": 0, 26 | "react/display-name": 0, 27 | "react/prop-types": 0, 28 | "@typescript-eslint/ban-ts-comment": 0, 29 | "@typescript-eslint/explicit-function-return-type": 0, 30 | "@typescript-eslint/explicit-member-accessibility": 0, 31 | "@typescript-eslint/indent": 0, 32 | "@typescript-eslint/member-delimiter-style": 0, 33 | "@typescript-eslint/no-explicit-any": 0, 34 | "@typescript-eslint/no-var-requires": 0, 35 | "@typescript-eslint/no-use-before-define": 0, 36 | "@typescript-eslint/no-unused-vars": [ 37 | 2, 38 | { 39 | "argsIgnorePattern": "^_" 40 | } 41 | ], 42 | "no-console": [ 43 | 2, 44 | { 45 | "allow": ["warn", "error"] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public 6 | -------------------------------------------------------------------------------- /packages/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /packages/frontend/components/Balance.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@chakra-ui/react' 2 | import { useEtherBalance, useEthers } from '@usedapp/core' 3 | import { utils } from 'ethers' 4 | 5 | /** 6 | * Component 7 | */ 8 | export function Balance(): JSX.Element { 9 | const { account } = useEthers() 10 | const etherBalance = useEtherBalance(account) 11 | const finalBalance = etherBalance ? utils.formatEther(etherBalance) : '' 12 | 13 | return {finalBalance} ETH 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/components/ConnectWallet.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Image, 5 | Modal, 6 | ModalBody, 7 | ModalCloseButton, 8 | ModalContent, 9 | ModalHeader, 10 | ModalOverlay, 11 | useDisclosure, 12 | } from '@chakra-ui/react' 13 | import { useEthers } from '@usedapp/core' 14 | import React from 'react' 15 | import { walletconnect } from '../lib/connectors' 16 | 17 | export function ConnectWallet(): JSX.Element { 18 | const { activate, activateBrowserWallet } = useEthers() 19 | 20 | const { onOpen, isOpen, onClose } = useDisclosure() 21 | 22 | return ( 23 | <> 24 | 28 | 31 | 32 | 33 | 34 | 35 | Connect to a wallet 36 | 37 | 38 | 58 | 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /packages/frontend/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Alert, 4 | AlertDescription, 5 | AlertIcon, 6 | AlertTitle, 7 | } from '@chakra-ui/react' 8 | 9 | /** 10 | * Prop Types 11 | */ 12 | interface ErrorProps { 13 | message: string 14 | } 15 | 16 | /** 17 | * Component 18 | */ 19 | export function Error({ message }: ErrorProps): JSX.Element { 20 | return ( 21 | 22 | 23 | Error: 24 | {message} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/components/api/RequestBuilder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { 3 | Button, 4 | Input, 5 | Text, 6 | Tooltip, 7 | FormControl, 8 | FormLabel, 9 | FormErrorMessage, 10 | } from '@chakra-ui/react' 11 | import { BigNumber } from 'ethers' 12 | import { formatFixed } from '@ethersproject/bignumber' 13 | import { useContractFunction, useEthers } from '@usedapp/core' 14 | import { Error } from '../../components/Error' 15 | import { useContract } from '../../hooks/useContract' 16 | import { getRequestStatus, getContractError } from '../../lib/utils' 17 | // @ts-ignore 18 | import { APIConsumer } from 'types/typechain' 19 | 20 | const DEFAULT_MULTIPLIER = '1000000000000000000' 21 | const DEFAULT_URL = 22 | 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD' 23 | const DEFAULT_PATH = 'RAW,ETH,USD,VOLUME24HOUR' 24 | const URL_REGEX = 25 | /^(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/ 26 | const PATH_REGEX = /^[a-zA-Z_][\w]*(?:,[\w]+)*$/ 27 | const MULTIPLIER_REGEX = /^(1(0)*)$/ 28 | 29 | export function RequestBuilder(): JSX.Element { 30 | const { account, error } = useEthers() 31 | 32 | const [url, setURL] = useState(DEFAULT_URL) 33 | const [path, setPath] = useState(DEFAULT_PATH) 34 | const [multiplier, setMultiplier] = useState(DEFAULT_MULTIPLIER) 35 | const [requestId, setRequestId] = useState('') 36 | const [data, setData] = useState('') 37 | 38 | const apiConsumer = useContract('APIConsumer') 39 | 40 | const { send, state, events } = useContractFunction( 41 | apiConsumer, 42 | 'requestData', 43 | { transactionName: 'External API Request' } 44 | ) 45 | 46 | const requestData = async () => { 47 | setRequestId('') 48 | await send(url, path, BigNumber.from(multiplier)) 49 | setData(null) 50 | } 51 | 52 | const readData = useCallback(async () => { 53 | const res = await apiConsumer.data() 54 | const data = formatFixed( 55 | res, 56 | multiplier.split('').filter((e) => e === '0').length 57 | ) 58 | setData(data) 59 | }, [apiConsumer, multiplier]) 60 | 61 | useEffect(() => { 62 | if (events) { 63 | const event = events.find((e) => e.name === 'ChainlinkRequested') 64 | if (event) { 65 | setRequestId(event.args.id) 66 | } 67 | } 68 | }, [events]) 69 | 70 | useEffect(() => { 71 | if (apiConsumer && requestId) { 72 | apiConsumer.on('ChainlinkFulfilled', (id: string) => { 73 | if (requestId === id) { 74 | readData() 75 | apiConsumer.removeAllListeners() 76 | } 77 | }) 78 | } 79 | }, [apiConsumer, requestId, readData]) 80 | 81 | const isLoading = 82 | state.status === 'Mining' || (state.status === 'Success' && !data) 83 | 84 | const hasError = state.status === 'Exception' 85 | 86 | const isInvalidUrl = !URL_REGEX.test(url) 87 | const isInvalidPath = !PATH_REGEX.test(path) 88 | const isInvalidMultiplier = !MULTIPLIER_REGEX.test(multiplier) 89 | const isInvalid = isInvalidUrl || isInvalidPath || isInvalidMultiplier 90 | 91 | return ( 92 | <> 93 | {hasError && } 94 | 95 | Data Source 96 | 102 | setURL(event.target.value)} 108 | /> 109 | 110 | {isInvalidUrl && URL is not valid.} 111 | 112 | 113 | 114 | Path to Number 115 | 121 | setPath(event.target.value)} 127 | /> 128 | 129 | {isInvalidPath && ( 130 | Path is not valid. 131 | )} 132 | 133 | 134 | 135 | Multiplier 136 | 143 | setMultiplier(event.target.value)} 148 | /> 149 | 150 | {isInvalidMultiplier && ( 151 | Multiplier is not valid. 152 | )} 153 | 154 | 155 | 165 | {data && ( 166 | 167 | Result: {data} 168 | 169 | )} 170 | 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /packages/frontend/components/api/index.ts: -------------------------------------------------------------------------------- 1 | export { RequestBuilder } from './RequestBuilder' 2 | -------------------------------------------------------------------------------- /packages/frontend/components/feeds/PriceFeed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HStack, Spinner, Text } from '@chakra-ui/react' 3 | import { BigNumber } from 'ethers' 4 | import { useContractCall } from '../../hooks/useContractCall' 5 | import { formatUsd } from '../../lib/utils' 6 | 7 | export function PriceFeed(): JSX.Element { 8 | const result = useContractCall('PriceConsumerV3', 'getLatestPrice') 9 | 10 | return ( 11 | 12 | ETH/USD: 13 | {!result && } 14 | {result && {formatUsd(result)}} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/frontend/components/feeds/ProofOfReserve.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { HStack, Spinner, Text } from '@chakra-ui/react' 3 | import { ChainId } from '@usedapp/core' 4 | import { BigNumber, ethers } from 'ethers' 5 | import { formatBtc } from '../../lib/utils' 6 | import config, { WbtcPorAddress } from '../../conf/config' 7 | import { AggregatorV3InterfaceABI } from '../../contracts/external' 8 | // @ts-ignore 9 | import { AggregatorV3Interface } from 'types/typechain' 10 | 11 | const providerMainnet = new ethers.providers.JsonRpcProvider( 12 | config.readOnlyUrls[ChainId.Mainnet].toString() 13 | ) 14 | 15 | const wbtcPorAggregator = new ethers.Contract( 16 | WbtcPorAddress, 17 | AggregatorV3InterfaceABI, 18 | providerMainnet 19 | ) as AggregatorV3Interface 20 | 21 | export function ProofOfReserve(): JSX.Element { 22 | const [result, setResult] = useState() 23 | 24 | const fetchData = useCallback(async () => { 25 | const data = await wbtcPorAggregator.latestRoundData() 26 | setResult(data.answer) 27 | }, []) 28 | 29 | useEffect(() => { 30 | fetchData() 31 | }, [fetchData]) 32 | 33 | return ( 34 | 35 | WBTC PoR: 36 | {!result && } 37 | {result && {formatBtc(result)}} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/frontend/components/feeds/SelectFeed.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react' 2 | import { Box, HStack, Select, Spinner, Text } from '@chakra-ui/react' 3 | import { BigNumber, ethers } from 'ethers' 4 | import { ChainId } from '@usedapp/core' 5 | import { formatUsd } from '../../lib/utils' 6 | import config, { 7 | Denominations, 8 | LinkTokenAddress, 9 | FeedRegistryAddress, 10 | } from '../../conf/config' 11 | import { FeedRegistryABI } from '../../contracts/external' 12 | // @ts-ignore 13 | import { FeedRegistryInterface } from 'types/typechain' 14 | 15 | const providerMainnet = new ethers.providers.JsonRpcProvider( 16 | config.readOnlyUrls[ChainId.Mainnet].toString() 17 | ) 18 | 19 | const feedRegistryAggregator = new ethers.Contract( 20 | FeedRegistryAddress, 21 | FeedRegistryABI, 22 | providerMainnet 23 | ) as FeedRegistryInterface 24 | 25 | export function SelectFeed(): JSX.Element { 26 | const [base, setBase] = useState(LinkTokenAddress) 27 | const [result, setResult] = useState() 28 | 29 | const fetchData = useCallback(async (base: string) => { 30 | const data = await feedRegistryAggregator.latestRoundData( 31 | base, 32 | Denominations.USD 33 | ) 34 | setResult(data.answer) 35 | }, []) 36 | 37 | useEffect(() => { 38 | setResult(undefined) 39 | fetchData(base) 40 | }, [fetchData, base]) 41 | 42 | return ( 43 | <> 44 | 45 | 46 | 54 | 55 | / USD: 56 | {!result && } 57 | {result && {formatUsd(result)}} 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/frontend/components/feeds/index.ts: -------------------------------------------------------------------------------- 1 | export { SelectFeed } from './SelectFeed' 2 | export { PriceFeed } from './PriceFeed' 3 | export { ProofOfReserve } from './ProofOfReserve' 4 | -------------------------------------------------------------------------------- /packages/frontend/components/layout/Head.tsx: -------------------------------------------------------------------------------- 1 | import NextHead from 'next/head' 2 | import { useRouter } from 'next/router' 3 | import React from 'react' 4 | 5 | /** 6 | * Constants & Helpers 7 | */ 8 | export const WEBSITE_HOST_URL = 'https://chainlink-demo.app' 9 | 10 | /** 11 | * Prop Types 12 | */ 13 | export interface MetaProps { 14 | description?: string 15 | image?: string 16 | title: string 17 | type?: string 18 | } 19 | 20 | /** 21 | * Component 22 | */ 23 | export const Head = ({ 24 | customMeta, 25 | }: { 26 | customMeta?: MetaProps 27 | }): JSX.Element => { 28 | const router = useRouter() 29 | const meta: MetaProps = { 30 | title: 'Chainlink Demo App', 31 | description: 'Full stack starter project showcasing Chainlink products on Ethereum', 32 | image: `${WEBSITE_HOST_URL}/images/social-preview.png`, 33 | type: 'website', 34 | ...customMeta, 35 | } 36 | 37 | return ( 38 | 39 | {meta.title} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/frontend/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | AlertDescription, 4 | AlertIcon, 5 | AlertTitle, 6 | Text, 7 | Box, 8 | Button, 9 | Container, 10 | Flex, 11 | HStack, 12 | Image, 13 | Link, 14 | Menu, 15 | MenuButton, 16 | MenuItem, 17 | MenuList, 18 | SimpleGrid, 19 | } from '@chakra-ui/react' 20 | import { useEthers, useNotifications } from '@usedapp/core' 21 | import blockies from 'blockies-ts' 22 | import NextLink from 'next/link' 23 | import TagManager from 'react-gtm-module' 24 | import React, { useEffect } from 'react' 25 | import { getErrorMessage } from '../../lib/utils' 26 | import { Balance } from '../Balance' 27 | import { ConnectWallet } from '../ConnectWallet' 28 | import { Head, MetaProps } from './Head' 29 | import { Error } from '../Error' 30 | 31 | // Extends `window` to add `ethereum`. 32 | declare global { 33 | interface Window { 34 | ethereum: any 35 | } 36 | } 37 | 38 | /** 39 | * Constants & Helpers 40 | */ 41 | 42 | // Title text for the various transaction notifications. 43 | const TRANSACTION_TYPE_TITLES = { 44 | transactionStarted: 'Started', 45 | transactionSucceed: 'Completed', 46 | } 47 | 48 | const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID 49 | 50 | // Takes a long hash string and truncates it. 51 | function truncateHash(hash: string, length = 38): string { 52 | return hash.replace(hash.substring(6, length), '...') 53 | } 54 | 55 | /** 56 | * Prop Types 57 | */ 58 | interface LayoutProps { 59 | children: React.ReactNode 60 | customMeta?: MetaProps 61 | } 62 | 63 | /** 64 | * Component 65 | */ 66 | export const Layout = ({ children, customMeta }: LayoutProps): JSX.Element => { 67 | const { account, deactivate, error } = useEthers() 68 | const { notifications } = useNotifications() 69 | 70 | useEffect(() => { 71 | if (GTM_ID) { 72 | TagManager.initialize({ gtmId: GTM_ID }) 73 | } 74 | }, []) 75 | 76 | let blockieImageSrc 77 | if (typeof window !== 'undefined') { 78 | blockieImageSrc = blockies.create({ seed: account }).toDataURL() 79 | } 80 | 81 | return ( 82 | <> 83 | 84 |
85 | 86 | 92 | 93 | 94 | 95 | Home 96 | 97 | 98 | 99 | 100 | Data Feeds 101 | 102 | 103 | 104 | 105 | Randomness 106 | 107 | 108 | 109 | 110 | External API 111 | 112 | 113 | 114 | 115 | Automation 116 | 117 | 118 | 119 | {account ? ( 120 | 125 | 126 | blockie 127 | 128 | 129 | {truncateHash(account)} 130 | 131 | 132 | { 134 | deactivate() 135 | }} 136 | > 137 | Disconnect 138 | 139 | 140 | 141 | 142 | ) : ( 143 | 144 | )} 145 | 146 | 147 |
148 |
149 | 150 | {error && } 151 | {children} 152 | {notifications.map((notification) => { 153 | if (notification.type === 'walletConnected') { 154 | return null 155 | } 156 | return ( 157 | 165 | 166 | 167 | 168 | {notification.transactionName}{' '} 169 | {TRANSACTION_TYPE_TITLES[notification.type]} 170 | 171 | {'transaction' in notification && ( 172 | 173 | Transaction Hash: 174 | {truncateHash(notification.transaction.hash, 61)} 175 | 176 | )} 177 | 178 | 179 | ) 180 | })} 181 | 182 |
183 |
184 | 185 | 186 | 187 | 188 | GitHub 189 | 190 | 191 | 192 |
193 | 194 | ) 195 | } 196 | -------------------------------------------------------------------------------- /packages/frontend/components/layout/Section.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from '@chakra-ui/react' 3 | 4 | interface SectionProps { 5 | children: React.ReactNode 6 | } 7 | 8 | export const Section = (props: SectionProps): JSX.Element => ( 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /packages/frontend/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout' 2 | export { Head } from './Head' 3 | export { Section } from './Section' 4 | -------------------------------------------------------------------------------- /packages/frontend/components/vrf/RandomNFT/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Badge, HStack, Link } from '@chakra-ui/react' 3 | import { ExternalLinkIcon } from '@chakra-ui/icons' 4 | import { ChainId, useEthers } from '@usedapp/core' 5 | import { BigNumber } from '@ethersproject/bignumber' 6 | import { useContractConfig } from '../../../hooks/useContractConfig' 7 | import { OpenSeaUrl } from '../../../conf/config' 8 | 9 | /** 10 | * Prop Types 11 | */ 12 | export interface Props { 13 | tokenId: BigNumber 14 | } 15 | 16 | /** 17 | * Component 18 | */ 19 | export function ExternalLink({ tokenId }: Props): JSX.Element { 20 | const { chainId } = useEthers() 21 | 22 | const contract = useContractConfig('RandomSVG') 23 | 24 | const active = chainId === ChainId.Goerli 25 | 26 | const url = active 27 | ? `${OpenSeaUrl}/assets/${contract.address}/${tokenId}` 28 | : undefined 29 | 30 | return ( 31 | 32 | 33 | See on OpenSea Testnet Marketplace 34 | 35 | {!active && Goerli only} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/frontend/components/vrf/RandomNFT/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useEffect } from 'react' 2 | import { Text, Image, Button, Stack, Tooltip } from '@chakra-ui/react' 3 | import { useContractFunction, useEthers } from '@usedapp/core' 4 | import { BigNumber } from '@ethersproject/bignumber' 5 | import { getRequestStatus, getContractError } from '../../../lib/utils' 6 | import { useContract } from '../../../hooks/useContract' 7 | import { ExternalLink } from './ExternalLink' 8 | import { Error } from '../../Error' 9 | // @ts-ignore 10 | import { RandomSVG } from '../../../../types/typechain' 11 | 12 | /** 13 | * Constants & Helpers 14 | */ 15 | const parseMetadata = (encoded: string): Metadata => 16 | JSON.parse(atob(encoded.split(',')[1])) 17 | 18 | /** 19 | * Types 20 | */ 21 | type Metadata = { 22 | name: string 23 | description: string 24 | image: string 25 | } 26 | 27 | /** 28 | * Component 29 | */ 30 | export function RandomNFT(): JSX.Element { 31 | const { account, error } = useEthers() 32 | 33 | const [pending, setPending] = useState(false) 34 | const [fulfilled, setFulfilled] = useState(false) 35 | const [tokenId, setTokenId] = useState() 36 | const [metadata, setMetadata] = useState() 37 | 38 | const randomSvg = useContract('RandomSVG') 39 | 40 | const { 41 | send: create, 42 | state: createState, 43 | events: createEvents, 44 | } = useContractFunction(randomSvg, 'create', { 45 | transactionName: 'NFT Request', 46 | }) 47 | 48 | const { 49 | send: finish, 50 | state: finishState, 51 | events: finishEvents, 52 | } = useContractFunction(randomSvg, 'finishMint', { 53 | transactionName: 'NFT Mint Finish', 54 | }) 55 | 56 | const createRequest = async () => { 57 | await create() 58 | setTokenId(undefined) 59 | setFulfilled(false) 60 | } 61 | 62 | const getMetadata = useCallback(async () => { 63 | const result = await randomSvg.tokenURI(tokenId) 64 | return parseMetadata(result) 65 | }, [randomSvg, tokenId]) 66 | 67 | useEffect(() => { 68 | if (createEvents) { 69 | const event = createEvents.find((e) => e.name === 'requestedRandomSVG') 70 | if (event) { 71 | setTokenId(event.args.tokenId) 72 | } 73 | } 74 | }, [createEvents]) 75 | 76 | useEffect(() => { 77 | if (randomSvg && tokenId) { 78 | randomSvg.on('CreatedUnfinishedRandomSVG', (id: BigNumber) => { 79 | if (tokenId.eq(id)) { 80 | setFulfilled(true) 81 | setPending(true) 82 | randomSvg.removeAllListeners() 83 | } 84 | }) 85 | } 86 | }, [randomSvg, tokenId]) 87 | 88 | useEffect(() => { 89 | if (finishEvents) { 90 | if (finishEvents.find((e) => e.name === 'CreatedRandomSVG')) { 91 | getMetadata().then((result) => { 92 | setMetadata(result) 93 | setPending(false) 94 | }) 95 | } 96 | } 97 | }, [finishEvents, getMetadata]) 98 | 99 | const isCreating = 100 | createState.status === 'Mining' || 101 | (createState.status === 'Success' && !fulfilled) 102 | 103 | const isFinishing = 104 | finishState.status === 'Mining' || 105 | (finishState.status === 'Success' && !metadata) 106 | 107 | const hasError = 108 | createState.status === 'Exception' || finishState.status === 'Exception' 109 | 110 | const errorMessage = createState.errorMessage || finishState.errorMessage 111 | 112 | return ( 113 | <> 114 | {hasError && } 115 | {!pending && ( 116 | 122 | 131 | 132 | )} 133 | {pending && ( 134 | 141 | 150 | 151 | )} 152 | {metadata && ( 153 | 154 | Result 155 | Random SVG 163 | 164 | 165 | )} 166 | 167 | ) 168 | } 169 | -------------------------------------------------------------------------------- /packages/frontend/components/vrf/RandomNumber.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { Text, Button, Code, Stack } from '@chakra-ui/react' 3 | import { useContractFunction, useEthers } from '@usedapp/core' 4 | import { BigNumber } from 'ethers' 5 | import { getRequestStatus, getContractError } from '../../lib/utils' 6 | import { useContract } from '../../hooks/useContract' 7 | import { Error } from '../Error' 8 | // @ts-ignore 9 | import { RandomNumberConsumer } from '../../../types/typechain' 10 | 11 | export function RandomNumber(): JSX.Element { 12 | const { account, error } = useEthers() 13 | 14 | const [requestId, setRequestId] = useState() 15 | const [randomNumber, setRandomNumber] = useState('') 16 | 17 | const randomNumberConsumer = useContract( 18 | 'RandomNumberConsumer' 19 | ) 20 | 21 | const { send, state, events } = useContractFunction( 22 | randomNumberConsumer, 23 | 'getRandomNumber', 24 | { transactionName: 'Randomness Request', gasLimitBufferPercentage: 250 } 25 | ) 26 | 27 | const requestRandomNumber = async () => { 28 | await send() 29 | setRandomNumber('') 30 | } 31 | 32 | const readRandomNumber = useCallback(async () => { 33 | const result = await randomNumberConsumer.randomResult() 34 | setRandomNumber(String(result)) 35 | }, [randomNumberConsumer]) 36 | 37 | useEffect(() => { 38 | if (events) { 39 | const event = events.find((e) => e.name === 'RequestedRandomness') 40 | if (event) { 41 | setRequestId(event.args.requestId) 42 | } 43 | } 44 | }, [events]) 45 | 46 | useEffect(() => { 47 | if (randomNumberConsumer && requestId) { 48 | randomNumberConsumer.on('FulfilledRandomness', (id: BigNumber) => { 49 | if (requestId.eq(id)) { 50 | readRandomNumber() 51 | randomNumberConsumer.removeAllListeners() 52 | } 53 | }) 54 | } 55 | }, [randomNumberConsumer, requestId, readRandomNumber]) 56 | 57 | const isLoading = 58 | state.status === 'Mining' || (state.status === 'Success' && !randomNumber) 59 | 60 | const hasError = state.status === 'Exception' 61 | 62 | return ( 63 | <> 64 | {hasError && } 65 | 74 | {randomNumber && ( 75 | 76 | Result 77 | 78 | {randomNumber} 79 | 80 | 81 | )} 82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /packages/frontend/components/vrf/index.ts: -------------------------------------------------------------------------------- 1 | export { RandomNumber } from './RandomNumber' 2 | export { RandomNFT } from './RandomNFT' 3 | -------------------------------------------------------------------------------- /packages/frontend/conf/config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | Config, 4 | Sepolia, 5 | Goerli, 6 | Mainnet, 7 | Hardhat, 8 | } from '@usedapp/core' 9 | import deployedContracts from '../contracts/hardhat_contracts.json' 10 | 11 | const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY 12 | 13 | const config: Config = { 14 | readOnlyChainId: ChainId.Sepolia, 15 | readOnlyUrls: { 16 | [ChainId.Goerli]: `https://goerli.infura.io/v3/${INFURA_KEY}`, 17 | [ChainId.Sepolia]: `https://sepolia.infura.io/v3/${INFURA_KEY}`, 18 | [ChainId.Mainnet]: `https://mainnet.infura.io/v3/${INFURA_KEY}`, 19 | [ChainId.Hardhat]: 'http://127.0.0.1:8545', 20 | }, 21 | networks: [Sepolia, Goerli, Hardhat], 22 | multicallAddresses: { 23 | [ChainId.Hardhat]: 24 | deployedContracts[ChainId.Hardhat][0].contracts.Multicall.address, 25 | [ChainId.Mainnet]: Mainnet.multicallAddress, 26 | }, 27 | } 28 | 29 | export const WbtcPorAddress = '0xa81FE04086865e63E12dD3776978E49DEEa2ea4e' 30 | 31 | export const FeedRegistryAddress = '0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf' 32 | 33 | export const LinkTokenAddress = '0x514910771AF9Ca656af840dff83E8264EcF986CA' 34 | 35 | export enum Denominations { 36 | ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', 37 | BTC = '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 38 | USD = '0x0000000000000000000000000000000000000348', 39 | } 40 | 41 | export const OpenSeaUrl = 'https://testnets.opensea.io' 42 | 43 | export default config 44 | -------------------------------------------------------------------------------- /packages/frontend/contracts/external.ts: -------------------------------------------------------------------------------- 1 | export const AggregatorV3InterfaceABI = [ 2 | { 3 | inputs: [], 4 | name: 'decimals', 5 | outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], 6 | stateMutability: 'view', 7 | type: 'function', 8 | }, 9 | { 10 | inputs: [], 11 | name: 'description', 12 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 13 | stateMutability: 'view', 14 | type: 'function', 15 | }, 16 | { 17 | inputs: [{ internalType: 'uint80', name: '_roundId', type: 'uint80' }], 18 | name: 'getRoundData', 19 | outputs: [ 20 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 21 | { internalType: 'int256', name: 'answer', type: 'int256' }, 22 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 23 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 24 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 25 | ], 26 | stateMutability: 'view', 27 | type: 'function', 28 | }, 29 | { 30 | inputs: [], 31 | name: 'latestRoundData', 32 | outputs: [ 33 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 34 | { internalType: 'int256', name: 'answer', type: 'int256' }, 35 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 36 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 37 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 38 | ], 39 | stateMutability: 'view', 40 | type: 'function', 41 | }, 42 | { 43 | inputs: [], 44 | name: 'version', 45 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 46 | stateMutability: 'view', 47 | type: 'function', 48 | }, 49 | ] 50 | 51 | export const FeedRegistryABI = [ 52 | { 53 | anonymous: false, 54 | inputs: [ 55 | { 56 | indexed: true, 57 | internalType: 'address', 58 | name: 'accessController', 59 | type: 'address', 60 | }, 61 | { 62 | indexed: true, 63 | internalType: 'address', 64 | name: 'sender', 65 | type: 'address', 66 | }, 67 | ], 68 | name: 'AccessControllerSet', 69 | type: 'event', 70 | }, 71 | { 72 | anonymous: false, 73 | inputs: [ 74 | { 75 | indexed: true, 76 | internalType: 'address', 77 | name: 'asset', 78 | type: 'address', 79 | }, 80 | { 81 | indexed: true, 82 | internalType: 'address', 83 | name: 'denomination', 84 | type: 'address', 85 | }, 86 | { 87 | indexed: true, 88 | internalType: 'address', 89 | name: 'latestAggregator', 90 | type: 'address', 91 | }, 92 | { 93 | indexed: false, 94 | internalType: 'address', 95 | name: 'previousAggregator', 96 | type: 'address', 97 | }, 98 | { 99 | indexed: false, 100 | internalType: 'uint16', 101 | name: 'nextPhaseId', 102 | type: 'uint16', 103 | }, 104 | { 105 | indexed: false, 106 | internalType: 'address', 107 | name: 'sender', 108 | type: 'address', 109 | }, 110 | ], 111 | name: 'FeedConfirmed', 112 | type: 'event', 113 | }, 114 | { 115 | anonymous: false, 116 | inputs: [ 117 | { 118 | indexed: true, 119 | internalType: 'address', 120 | name: 'asset', 121 | type: 'address', 122 | }, 123 | { 124 | indexed: true, 125 | internalType: 'address', 126 | name: 'denomination', 127 | type: 'address', 128 | }, 129 | { 130 | indexed: true, 131 | internalType: 'address', 132 | name: 'proposedAggregator', 133 | type: 'address', 134 | }, 135 | { 136 | indexed: false, 137 | internalType: 'address', 138 | name: 'currentAggregator', 139 | type: 'address', 140 | }, 141 | { 142 | indexed: false, 143 | internalType: 'address', 144 | name: 'sender', 145 | type: 'address', 146 | }, 147 | ], 148 | name: 'FeedProposed', 149 | type: 'event', 150 | }, 151 | { 152 | anonymous: false, 153 | inputs: [ 154 | { indexed: true, internalType: 'address', name: 'from', type: 'address' }, 155 | { indexed: true, internalType: 'address', name: 'to', type: 'address' }, 156 | ], 157 | name: 'OwnershipTransferRequested', 158 | type: 'event', 159 | }, 160 | { 161 | anonymous: false, 162 | inputs: [ 163 | { indexed: true, internalType: 'address', name: 'from', type: 'address' }, 164 | { indexed: true, internalType: 'address', name: 'to', type: 'address' }, 165 | ], 166 | name: 'OwnershipTransferred', 167 | type: 'event', 168 | }, 169 | { 170 | inputs: [], 171 | name: 'acceptOwnership', 172 | outputs: [], 173 | stateMutability: 'nonpayable', 174 | type: 'function', 175 | }, 176 | { 177 | inputs: [ 178 | { internalType: 'address', name: 'base', type: 'address' }, 179 | { internalType: 'address', name: 'quote', type: 'address' }, 180 | { internalType: 'address', name: 'aggregator', type: 'address' }, 181 | ], 182 | name: 'confirmFeed', 183 | outputs: [], 184 | stateMutability: 'nonpayable', 185 | type: 'function', 186 | }, 187 | { 188 | inputs: [ 189 | { internalType: 'address', name: 'base', type: 'address' }, 190 | { internalType: 'address', name: 'quote', type: 'address' }, 191 | ], 192 | name: 'decimals', 193 | outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], 194 | stateMutability: 'view', 195 | type: 'function', 196 | }, 197 | { 198 | inputs: [ 199 | { internalType: 'address', name: 'base', type: 'address' }, 200 | { internalType: 'address', name: 'quote', type: 'address' }, 201 | ], 202 | name: 'description', 203 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 204 | stateMutability: 'view', 205 | type: 'function', 206 | }, 207 | { 208 | inputs: [], 209 | name: 'getAccessController', 210 | outputs: [ 211 | { 212 | internalType: 'contract AccessControllerInterface', 213 | name: '', 214 | type: 'address', 215 | }, 216 | ], 217 | stateMutability: 'view', 218 | type: 'function', 219 | }, 220 | { 221 | inputs: [ 222 | { internalType: 'address', name: 'base', type: 'address' }, 223 | { internalType: 'address', name: 'quote', type: 'address' }, 224 | { internalType: 'uint256', name: 'roundId', type: 'uint256' }, 225 | ], 226 | name: 'getAnswer', 227 | outputs: [{ internalType: 'int256', name: 'answer', type: 'int256' }], 228 | stateMutability: 'view', 229 | type: 'function', 230 | }, 231 | { 232 | inputs: [ 233 | { internalType: 'address', name: 'base', type: 'address' }, 234 | { internalType: 'address', name: 'quote', type: 'address' }, 235 | ], 236 | name: 'getCurrentPhaseId', 237 | outputs: [ 238 | { internalType: 'uint16', name: 'currentPhaseId', type: 'uint16' }, 239 | ], 240 | stateMutability: 'view', 241 | type: 'function', 242 | }, 243 | { 244 | inputs: [ 245 | { internalType: 'address', name: 'base', type: 'address' }, 246 | { internalType: 'address', name: 'quote', type: 'address' }, 247 | ], 248 | name: 'getFeed', 249 | outputs: [ 250 | { 251 | internalType: 'contract AggregatorV2V3Interface', 252 | name: 'aggregator', 253 | type: 'address', 254 | }, 255 | ], 256 | stateMutability: 'view', 257 | type: 'function', 258 | }, 259 | { 260 | inputs: [ 261 | { internalType: 'address', name: 'base', type: 'address' }, 262 | { internalType: 'address', name: 'quote', type: 'address' }, 263 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 264 | ], 265 | name: 'getNextRoundId', 266 | outputs: [{ internalType: 'uint80', name: 'nextRoundId', type: 'uint80' }], 267 | stateMutability: 'view', 268 | type: 'function', 269 | }, 270 | { 271 | inputs: [ 272 | { internalType: 'address', name: 'base', type: 'address' }, 273 | { internalType: 'address', name: 'quote', type: 'address' }, 274 | { internalType: 'uint16', name: 'phaseId', type: 'uint16' }, 275 | ], 276 | name: 'getPhase', 277 | outputs: [ 278 | { 279 | components: [ 280 | { internalType: 'uint16', name: 'phaseId', type: 'uint16' }, 281 | { 282 | internalType: 'uint80', 283 | name: 'startingAggregatorRoundId', 284 | type: 'uint80', 285 | }, 286 | { 287 | internalType: 'uint80', 288 | name: 'endingAggregatorRoundId', 289 | type: 'uint80', 290 | }, 291 | ], 292 | internalType: 'struct FeedRegistryInterface.Phase', 293 | name: 'phase', 294 | type: 'tuple', 295 | }, 296 | ], 297 | stateMutability: 'view', 298 | type: 'function', 299 | }, 300 | { 301 | inputs: [ 302 | { internalType: 'address', name: 'base', type: 'address' }, 303 | { internalType: 'address', name: 'quote', type: 'address' }, 304 | { internalType: 'uint16', name: 'phaseId', type: 'uint16' }, 305 | ], 306 | name: 'getPhaseFeed', 307 | outputs: [ 308 | { 309 | internalType: 'contract AggregatorV2V3Interface', 310 | name: 'aggregator', 311 | type: 'address', 312 | }, 313 | ], 314 | stateMutability: 'view', 315 | type: 'function', 316 | }, 317 | { 318 | inputs: [ 319 | { internalType: 'address', name: 'base', type: 'address' }, 320 | { internalType: 'address', name: 'quote', type: 'address' }, 321 | { internalType: 'uint16', name: 'phaseId', type: 'uint16' }, 322 | ], 323 | name: 'getPhaseRange', 324 | outputs: [ 325 | { internalType: 'uint80', name: 'startingRoundId', type: 'uint80' }, 326 | { internalType: 'uint80', name: 'endingRoundId', type: 'uint80' }, 327 | ], 328 | stateMutability: 'view', 329 | type: 'function', 330 | }, 331 | { 332 | inputs: [ 333 | { internalType: 'address', name: 'base', type: 'address' }, 334 | { internalType: 'address', name: 'quote', type: 'address' }, 335 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 336 | ], 337 | name: 'getPreviousRoundId', 338 | outputs: [ 339 | { internalType: 'uint80', name: 'previousRoundId', type: 'uint80' }, 340 | ], 341 | stateMutability: 'view', 342 | type: 'function', 343 | }, 344 | { 345 | inputs: [ 346 | { internalType: 'address', name: 'base', type: 'address' }, 347 | { internalType: 'address', name: 'quote', type: 'address' }, 348 | ], 349 | name: 'getProposedFeed', 350 | outputs: [ 351 | { 352 | internalType: 'contract AggregatorV2V3Interface', 353 | name: 'proposedAggregator', 354 | type: 'address', 355 | }, 356 | ], 357 | stateMutability: 'view', 358 | type: 'function', 359 | }, 360 | { 361 | inputs: [ 362 | { internalType: 'address', name: 'base', type: 'address' }, 363 | { internalType: 'address', name: 'quote', type: 'address' }, 364 | { internalType: 'uint80', name: '_roundId', type: 'uint80' }, 365 | ], 366 | name: 'getRoundData', 367 | outputs: [ 368 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 369 | { internalType: 'int256', name: 'answer', type: 'int256' }, 370 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 371 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 372 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 373 | ], 374 | stateMutability: 'view', 375 | type: 'function', 376 | }, 377 | { 378 | inputs: [ 379 | { internalType: 'address', name: 'base', type: 'address' }, 380 | { internalType: 'address', name: 'quote', type: 'address' }, 381 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 382 | ], 383 | name: 'getRoundFeed', 384 | outputs: [ 385 | { 386 | internalType: 'contract AggregatorV2V3Interface', 387 | name: 'aggregator', 388 | type: 'address', 389 | }, 390 | ], 391 | stateMutability: 'view', 392 | type: 'function', 393 | }, 394 | { 395 | inputs: [ 396 | { internalType: 'address', name: 'base', type: 'address' }, 397 | { internalType: 'address', name: 'quote', type: 'address' }, 398 | { internalType: 'uint256', name: 'roundId', type: 'uint256' }, 399 | ], 400 | name: 'getTimestamp', 401 | outputs: [{ internalType: 'uint256', name: 'timestamp', type: 'uint256' }], 402 | stateMutability: 'view', 403 | type: 'function', 404 | }, 405 | { 406 | inputs: [{ internalType: 'address', name: 'aggregator', type: 'address' }], 407 | name: 'isFeedEnabled', 408 | outputs: [{ internalType: 'bool', name: '', type: 'bool' }], 409 | stateMutability: 'view', 410 | type: 'function', 411 | }, 412 | { 413 | inputs: [ 414 | { internalType: 'address', name: 'base', type: 'address' }, 415 | { internalType: 'address', name: 'quote', type: 'address' }, 416 | ], 417 | name: 'latestAnswer', 418 | outputs: [{ internalType: 'int256', name: 'answer', type: 'int256' }], 419 | stateMutability: 'view', 420 | type: 'function', 421 | }, 422 | { 423 | inputs: [ 424 | { internalType: 'address', name: 'base', type: 'address' }, 425 | { internalType: 'address', name: 'quote', type: 'address' }, 426 | ], 427 | name: 'latestRound', 428 | outputs: [{ internalType: 'uint256', name: 'roundId', type: 'uint256' }], 429 | stateMutability: 'view', 430 | type: 'function', 431 | }, 432 | { 433 | inputs: [ 434 | { internalType: 'address', name: 'base', type: 'address' }, 435 | { internalType: 'address', name: 'quote', type: 'address' }, 436 | ], 437 | name: 'latestRoundData', 438 | outputs: [ 439 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 440 | { internalType: 'int256', name: 'answer', type: 'int256' }, 441 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 442 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 443 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 444 | ], 445 | stateMutability: 'view', 446 | type: 'function', 447 | }, 448 | { 449 | inputs: [ 450 | { internalType: 'address', name: 'base', type: 'address' }, 451 | { internalType: 'address', name: 'quote', type: 'address' }, 452 | ], 453 | name: 'latestTimestamp', 454 | outputs: [{ internalType: 'uint256', name: 'timestamp', type: 'uint256' }], 455 | stateMutability: 'view', 456 | type: 'function', 457 | }, 458 | { 459 | inputs: [], 460 | name: 'owner', 461 | outputs: [{ internalType: 'address', name: '', type: 'address' }], 462 | stateMutability: 'view', 463 | type: 'function', 464 | }, 465 | { 466 | inputs: [ 467 | { internalType: 'address', name: 'base', type: 'address' }, 468 | { internalType: 'address', name: 'quote', type: 'address' }, 469 | { internalType: 'address', name: 'aggregator', type: 'address' }, 470 | ], 471 | name: 'proposeFeed', 472 | outputs: [], 473 | stateMutability: 'nonpayable', 474 | type: 'function', 475 | }, 476 | { 477 | inputs: [ 478 | { internalType: 'address', name: 'base', type: 'address' }, 479 | { internalType: 'address', name: 'quote', type: 'address' }, 480 | { internalType: 'uint80', name: 'roundId', type: 'uint80' }, 481 | ], 482 | name: 'proposedGetRoundData', 483 | outputs: [ 484 | { internalType: 'uint80', name: 'id', type: 'uint80' }, 485 | { internalType: 'int256', name: 'answer', type: 'int256' }, 486 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 487 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 488 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 489 | ], 490 | stateMutability: 'view', 491 | type: 'function', 492 | }, 493 | { 494 | inputs: [ 495 | { internalType: 'address', name: 'base', type: 'address' }, 496 | { internalType: 'address', name: 'quote', type: 'address' }, 497 | ], 498 | name: 'proposedLatestRoundData', 499 | outputs: [ 500 | { internalType: 'uint80', name: 'id', type: 'uint80' }, 501 | { internalType: 'int256', name: 'answer', type: 'int256' }, 502 | { internalType: 'uint256', name: 'startedAt', type: 'uint256' }, 503 | { internalType: 'uint256', name: 'updatedAt', type: 'uint256' }, 504 | { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }, 505 | ], 506 | stateMutability: 'view', 507 | type: 'function', 508 | }, 509 | { 510 | inputs: [ 511 | { 512 | internalType: 'contract AccessControllerInterface', 513 | name: '_accessController', 514 | type: 'address', 515 | }, 516 | ], 517 | name: 'setAccessController', 518 | outputs: [], 519 | stateMutability: 'nonpayable', 520 | type: 'function', 521 | }, 522 | { 523 | inputs: [{ internalType: 'address', name: 'to', type: 'address' }], 524 | name: 'transferOwnership', 525 | outputs: [], 526 | stateMutability: 'nonpayable', 527 | type: 'function', 528 | }, 529 | { 530 | inputs: [], 531 | name: 'typeAndVersion', 532 | outputs: [{ internalType: 'string', name: '', type: 'string' }], 533 | stateMutability: 'pure', 534 | type: 'function', 535 | }, 536 | { 537 | inputs: [ 538 | { internalType: 'address', name: 'base', type: 'address' }, 539 | { internalType: 'address', name: 'quote', type: 'address' }, 540 | ], 541 | name: 'version', 542 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 543 | stateMutability: 'view', 544 | type: 'function', 545 | }, 546 | ] 547 | -------------------------------------------------------------------------------- /packages/frontend/hooks/useContract.ts: -------------------------------------------------------------------------------- 1 | import { useEthers } from '@usedapp/core' 2 | import { Contract, ethers } from 'ethers' 3 | import { useMemo } from 'react' 4 | import { useContractConfig } from './useContractConfig' 5 | import { JsonRpcProvider } from '@ethersproject/providers' 6 | 7 | export function useContract( 8 | name: string 9 | ): T | null { 10 | const { library } = useEthers() 11 | 12 | const contract = useContractConfig(name) 13 | 14 | return useMemo(() => { 15 | if (!library) return null 16 | if (!contract?.address) return null 17 | if (!(library instanceof JsonRpcProvider)) { 18 | return null 19 | } 20 | 21 | return new ethers.Contract( 22 | contract.address, 23 | contract.abi, 24 | library.getSigner() 25 | ) as T 26 | }, [contract, library]) 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/hooks/useContractCall.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { useCall } from '@usedapp/core' 3 | import { useContractConfig } from './useContractConfig' 4 | 5 | export function useContractCall( 6 | name: string, 7 | method: string, 8 | args: any[] = [] 9 | ): T | undefined { 10 | const contract = useContractConfig(name) 11 | 12 | const { value } = 13 | useCall( 14 | contract && { 15 | contract: new Contract(contract.address, contract.abi), 16 | method, 17 | args, 18 | } 19 | ) ?? {} 20 | 21 | return value?.[0] 22 | } 23 | -------------------------------------------------------------------------------- /packages/frontend/hooks/useContractConfig.ts: -------------------------------------------------------------------------------- 1 | import { useEthers } from '@usedapp/core' 2 | import deployedContracts from '../contracts/hardhat_contracts.json' 3 | 4 | type DeployedHardhatContractsJson = { 5 | [chainId: string]: [ 6 | { 7 | name: string 8 | chainId: string 9 | contracts: { 10 | [contractName: string]: { 11 | address: string 12 | abi?: any[] 13 | } 14 | } 15 | } 16 | ] 17 | } 18 | 19 | const contractsConfig = deployedContracts as unknown as DeployedHardhatContractsJson 20 | 21 | export function useContractConfig(name: string) { 22 | const { chainId } = useEthers() 23 | 24 | return ( 25 | chainId && 26 | contractsConfig[chainId] && 27 | Object.values(contractsConfig[chainId]).find( 28 | (c) => c.chainId === String(chainId) 29 | )?.contracts?.[name] 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'], 4 | testPathIgnorePatterns: ['[/\\\\](node_modules|.next)[/\\\\]'], 5 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'], 6 | transform: { 7 | '^.+\\.(ts|tsx)$': 'babel-jest', 8 | }, 9 | watchPlugins: [ 10 | 'jest-watch-typeahead/filename', 11 | 'jest-watch-typeahead/testname', 12 | ], 13 | moduleNameMapper: { 14 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 15 | '\\.(gif|ttf|eot|svg|png)$': '/test/__mocks__/fileMock.js', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/frontend/lib/connectors.ts: -------------------------------------------------------------------------------- 1 | import { WalletConnectConnector } from '@web3-react/walletconnect-connector' 2 | 3 | const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY 4 | 5 | const RPC_URLS: { [chainId: number]: string } = { 6 | 1: `https://mainnet.infura.io/v3/${INFURA_KEY}`, 7 | 5: `https://goerli.infura.io/v3/${INFURA_KEY}`, 8 | 11155111: `https://sepolia.infura.io/v3/${INFURA_KEY}`, 9 | } 10 | export const walletconnect = new WalletConnectConnector({ 11 | rpc: { 1: RPC_URLS[1], 5: RPC_URLS[5] }, 12 | qrcode: true, 13 | }) 14 | -------------------------------------------------------------------------------- /packages/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Currency, FiatCurrency, TransactionState } from '@usedapp/core' 2 | import { BigNumber } from 'ethers' 3 | 4 | // From https://github.com/NoahZinsmeister/web3-react/blob/v6/example/pages/index.tsx 5 | // Parses the possible errors provided by web3-react 6 | export function getErrorMessage(error: Error): string { 7 | if (error.message.includes("No injected provider available") ) { 8 | return 'No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile.' 9 | } else if (error.name === "ChainIdError") { 10 | return "You're connected to an unsupported network. Please switch to Goerli or Sepolia." 11 | } else if ( 12 | error.message.includes("The user rejected the request") || 13 | error.message.includes("User rejected the request") 14 | ) { 15 | return 'Please authorize this website to access your Ethereum account.' 16 | } else { 17 | console.error(error) 18 | return 'An unknown error occurred. Check the console for more details.' 19 | } 20 | } 21 | 22 | export function getContractError(msg: string): string { 23 | if (msg.includes('The execution failed due to an exception.')) { 24 | return `${msg} Please check if the contract has enough LINK to pay the oracle.` 25 | } else { 26 | return msg 27 | } 28 | } 29 | 30 | const btcFormatter = new Currency('Bitcoin', 'BTC', 8, { 31 | fixedPrecisionDigits: 2, 32 | useFixedPrecision: true, 33 | }) 34 | 35 | export const formatBtc = (value: BigNumber): string => 36 | btcFormatter.format(value.toString()) 37 | 38 | const usdFormatter = new FiatCurrency('United States Dollar', 'USD', 8, { 39 | fixedPrecisionDigits: 2, 40 | }) 41 | 42 | export const formatUsd = (value: BigNumber): string => 43 | usdFormatter.format(value.toString()) 44 | 45 | export const getRequestStatus = (status: TransactionState): string => 46 | (status === 'Mining' && 'Mining Request') || 47 | (status === 'Success' && 'Fulfilling Request') 48 | -------------------------------------------------------------------------------- /packages/frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainlink-fullstack-frontend", 3 | "author": "@hackbg", 4 | "license": "MIT", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "export": "next export", 10 | "start": "next start", 11 | "type-check": "tsc --pretty --noEmit", 12 | "format": "prettier --write .", 13 | "lint": "eslint . --ext ts --ext tsx --ext js", 14 | "test": "jest", 15 | "test-all": "yarn lint && yarn type-check && yarn test" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged", 20 | "pre-push": "yarn run type-check" 21 | } 22 | }, 23 | "lint-staged": { 24 | "*.@(ts|tsx)": [ 25 | "yarn lint" 26 | ] 27 | }, 28 | "dependencies": { 29 | "@chakra-ui/icons": "^2.0.4", 30 | "@chakra-ui/react": "^2.2.4", 31 | "@emotion/react": "^11", 32 | "@emotion/styled": "^11", 33 | "@usedapp/core": "1.2.7", 34 | "@web3-react/core": "^6.1.9", 35 | "@web3-react/injected-connector": "^6.0.7", 36 | "@web3-react/walletconnect-connector": "^6.2.13", 37 | "blockies-ts": "^1.0.0", 38 | "deepmerge": "^4.2.2", 39 | "ethers": "5.6.9", 40 | "framer-motion": "^6", 41 | "lodash": "4.17.21", 42 | "next": "^12.2.3", 43 | "react": "^18.2.0", 44 | "react-dom": "^18.2.0", 45 | "react-gtm-module": "^2.0.11" 46 | }, 47 | "devDependencies": { 48 | "@testing-library/react": "^13.3.0", 49 | "@types/jest": "^28.1.6", 50 | "@types/node": "^18.6.2", 51 | "@types/react": "^18.0.15", 52 | "@typescript-eslint/eslint-plugin": "^5.31.0", 53 | "@typescript-eslint/parser": "^5.31.0", 54 | "babel-jest": "^28.1.3", 55 | "eslint": "^8.20.0", 56 | "eslint-config-next": "^12.2.3", 57 | "eslint-config-prettier": "^8.5.0", 58 | "eslint-plugin-react": "^7.30.1", 59 | "eslint-plugin-react-hooks": "^4.6.0", 60 | "husky": "^8.0.1", 61 | "identity-obj-proxy": "^3.0.0", 62 | "jest": "^28.1.3", 63 | "jest-watch-typeahead": "^2.0.0", 64 | "lint-staged": "^13.0.3", 65 | "prettier": "^2.7.1", 66 | "types": "0.1.1", 67 | "typescript": "^4.7.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChakraProvider } from '@chakra-ui/react' 3 | import { DAppProvider } from '@usedapp/core' 4 | import type { AppProps } from 'next/app' 5 | import { Layout } from '../components/layout' 6 | import config from '../conf/config' 7 | 8 | const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default MyApp 21 | -------------------------------------------------------------------------------- /packages/frontend/pages/automation.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Text, Link } from '@chakra-ui/react' 2 | import { ExternalLinkIcon } from '@chakra-ui/icons' 3 | import { Section } from '../components/layout' 4 | 5 | function HomeIndex(): JSX.Element { 6 | return ( 7 | <> 8 | 9 | Automation 10 | 11 | 12 | Reliably execute smart contract functions using a variety of triggers. 13 | 14 |
15 | 16 | Batch NFT Demo App 17 | 18 | 19 | Create batch-revealed NFT collections powered by Chainlink Automation 20 | & VRF. 21 | 22 | 23 | Go to Demo 24 | 25 |
26 | 27 | ) 28 | } 29 | 30 | export default HomeIndex 31 | -------------------------------------------------------------------------------- /packages/frontend/pages/external-api.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Heading, Text, Link } from '@chakra-ui/react' 3 | import { ExternalLinkIcon } from '@chakra-ui/icons' 4 | import { RequestBuilder } from '../components/api' 5 | import { Section } from '../components/layout' 6 | 7 | function ExternalAPI(): JSX.Element { 8 | 9 | return ( 10 | <> 11 | 12 | External API 13 | 14 | 15 | Request & Receive data from any API in your smart contracts. 16 | 17 |
18 | 19 | 20 | Consume data from any API via HTTP GET request, through 21 | Chainlink's decentralized oracle network. It provides smart 22 | contracts with the ability to push and pull data, facilitating the 23 | interoperability between on-chain and off-chain applications. 24 | 25 | 29 | Learn More 30 | 31 |
32 | 33 | ) 34 | } 35 | 36 | export default ExternalAPI 37 | -------------------------------------------------------------------------------- /packages/frontend/pages/feeds.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Code, Heading, HStack, Link, Text } from '@chakra-ui/react' 3 | import { ExternalLinkIcon } from '@chakra-ui/icons' 4 | import { Section } from '../components/layout' 5 | import { SelectFeed, PriceFeed, ProofOfReserve } from '../components/feeds' 6 | 7 | function Feeds(): JSX.Element { 8 | return ( 9 | <> 10 | 11 | Data Feeds 12 | 13 | 14 | Retrieve the latest prices and data points of assets in your smart 15 | contracts. 16 | 17 |
18 | 19 | 20 | Consuming price feed by address via AggregatorV3Interface 21 | . 22 | 23 | 24 | 28 | Learn More 29 | 30 | 34 | Contract Addresses 35 | 36 | 37 |
38 |
39 | 40 | 41 | Feed Registry is an on-chain mapping of assets to feeds. It enables 42 | you to query Chainlink data feeds from asset addresses directly, 43 | without needing to know the feed contract addresses. 44 | 45 | 46 | Learn More 47 | 48 |
49 |
50 | 51 | 52 | Proof of Reserve enables the reliable and timely monitoring of reserve 53 | assets using automated audits based on cryptographic truth. 54 | 55 | 56 | Learn More 57 | 58 |
59 | 60 | ) 61 | } 62 | 63 | export default Feeds 64 | -------------------------------------------------------------------------------- /packages/frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | import { Heading, Text, Link } from '@chakra-ui/react' 3 | import { ArrowForwardIcon } from '@chakra-ui/icons' 4 | import { Section } from '../components/layout' 5 | 6 | function HomeIndex(): JSX.Element { 7 | return ( 8 | <> 9 | 10 | Welcome to the Chainlink Demo App 11 | 12 | 13 | Full stack starter project showcasing Chainlink products on Ethereum 14 | (EVM). 15 | 16 |
17 | 18 | Data Feeds 19 | 20 | 21 | Retrieve the latest prices and data points of assets in your smart 22 | contracts. 23 | 24 | 25 | 26 | Go to Demo 27 | 28 | 29 |
30 |
31 | 32 | Randomness (VRF) 33 | 34 | 35 | Use VRF (Verifiable Random Function) to consume randomness in your 36 | smart contracts. 37 | 38 | 39 | 40 | Go to Demo 41 | 42 | 43 |
44 |
45 | 46 | Call External API 47 | 48 | 49 | Request & Receive data from any API in your smart contracts. 50 | 51 | 52 | 53 | Go to Demo 54 | 55 | 56 |
57 |
58 | 59 | Automation 60 | 61 | 62 | Reliably execute smart contract functions using a variety of triggers. 63 | 64 | 65 | 66 | Go to Demo 67 | 68 | 69 |
70 | 71 | ) 72 | } 73 | 74 | export default HomeIndex 75 | -------------------------------------------------------------------------------- /packages/frontend/pages/vrf.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Heading, Text, Link } from '@chakra-ui/react' 3 | import { ExternalLinkIcon } from '@chakra-ui/icons' 4 | import { Section } from '../components/layout' 5 | import { RandomNFT, RandomNumber } from '../components/vrf' 6 | 7 | function VRF(): JSX.Element { 8 | return ( 9 | <> 10 | 11 | Randomness 12 | 13 | 14 | Use VRF (Verifiable Random Function) to consume randomness in your smart 15 | contracts. 16 | 17 |
18 | 19 | 20 | With every new request for randomness, Chainlink VRF generates a 21 | random number and cryptographic proof of how that number was 22 | determined. VRF enables smart contracts to access randomness without 23 | compromising on security or usability. 24 | 25 | 29 | Learn More 30 | 31 |
32 |
33 | 34 | 35 | 100% on-chain generated NFT using VRF as randomness source. Each 36 | request creates and stores an unique Scalable Vector Graphic (SVG). 37 | 38 | 39 | Learn More 40 | 41 |
42 | 43 | ) 44 | } 45 | 46 | export default VRF 47 | -------------------------------------------------------------------------------- /packages/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-fullstack/e060cda28468833c36fac5a12acb7912552a8c13/packages/frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/frontend/public/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/frontend/public/images/logo-metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-fullstack/e060cda28468833c36fac5a12acb7912552a8c13/packages/frontend/public/images/logo-metamask.png -------------------------------------------------------------------------------- /packages/frontend/public/images/logo-walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/frontend/public/images/social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-fullstack/e060cda28468833c36fac5a12acb7912552a8c13/packages/frontend/public/images/social-preview.png -------------------------------------------------------------------------------- /packages/frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /packages/frontend/test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /packages/frontend/test/pages/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Home page matches snapshot 1`] = ` 4 | 5 | 136 | 137 | `; 138 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | ".next", 25 | "out" 26 | ], 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "**/*.js" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/hardhat/.env.example: -------------------------------------------------------------------------------- 1 | GOERLI_RPC_URL='https://goerli.infura.io/v3/your-api-key' 2 | SEPOLIA_RPC_URL='https://sepolia.infura.io/v3/your-api-key' 3 | ETHERSCAN_API_KEY='your etherscan api key' 4 | PRIVATE_KEY='your private key' 5 | MNEMONIC='your mnemonic' -------------------------------------------------------------------------------- /packages/hardhat/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cache 3 | artifacts 4 | vendor -------------------------------------------------------------------------------- /packages/hardhat/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "overrides": [ 5 | { 6 | "files": "*.sol", 7 | "options": { 8 | "printWidth": 120, 9 | "tabWidth": 2, 10 | "useTabs": false, 11 | "singleQuote": false, 12 | "bracketSpacing": false, 13 | "explicitTypes": "always" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/APIConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.6; 3 | 4 | import "@chainlink/contracts/src/v0.6/ChainlinkClient.sol"; 5 | import "@chainlink/contracts/src/v0.6/vendor/Ownable.sol"; 6 | 7 | contract APIConsumer is ChainlinkClient, Ownable { 8 | uint256 public data; 9 | string public text; 10 | 11 | address private oracle; 12 | bytes32 private jobId; 13 | uint256 private fee; 14 | 15 | /** 16 | * Network: Sepolia 17 | * Oracle: 0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD 18 | * Job ID: ca98366cc7314957b8c012c72f05aeeb 19 | * Fee: 0.1 LINK 20 | */ 21 | constructor( 22 | address _oracle, 23 | string memory _jobId, 24 | uint256 _fee, 25 | address _link 26 | ) public { 27 | if (_link == address(0)) { 28 | setPublicChainlinkToken(); 29 | } else { 30 | setChainlinkToken(_link); 31 | } 32 | // oracle = 0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD; 33 | // jobId = "ca98366cc7314957b8c012c72f05aeeb"; 34 | // fee = 0.1 * 10 ** 18; // 0.1 LINK 35 | oracle = _oracle; 36 | jobId = stringToBytes32(_jobId); 37 | fee = _fee; 38 | } 39 | 40 | /** 41 | * Create a Chainlink request to retrieve API response, find the target 42 | * data, then multiply by timesAmount (to remove decimal places from data). 43 | */ 44 | function requestData( 45 | string memory url, 46 | string memory path, 47 | int256 timesAmount 48 | ) public returns (bytes32 requestId) { 49 | Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector); 50 | 51 | // Set the URL to perform the GET request on 52 | request.add("get", url); 53 | 54 | // Set the path to find the desired data in the API response, where the response format is: 55 | // {"RAW": 56 | // {"ETH": 57 | // {"USD": 58 | // { 59 | // "VOLUME24HOUR": xxx.xxx, 60 | // } 61 | // } 62 | // } 63 | // } 64 | request.add("path", path); 65 | 66 | // Multiply the result by timesAmount to remove decimals 67 | request.addInt("times", timesAmount); 68 | 69 | // Sends the request 70 | return sendChainlinkRequestTo(oracle, request, fee); 71 | } 72 | 73 | /** 74 | * Receive the response in the form of uint256 75 | */ 76 | function fulfill(bytes32 _requestId, uint256 _data) public recordChainlinkFulfillment(_requestId) { 77 | data = _data; 78 | } 79 | 80 | /** 81 | * Withdraw LINK from this contract 82 | * 83 | */ 84 | function withdrawLink() external onlyOwner { 85 | LinkTokenInterface linkToken = LinkTokenInterface(chainlinkTokenAddress()); 86 | require(linkToken.transfer(msg.sender, linkToken.balanceOf(address(this))), "Unable to transfer"); 87 | } 88 | 89 | function stringToBytes32(string memory source) public pure returns (bytes32 result) { 90 | bytes memory tempEmptyStringTest = bytes(source); 91 | if (tempEmptyStringTest.length == 0) { 92 | return 0x0; 93 | } 94 | 95 | assembly { 96 | result := mload(add(source, 32)) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/FeedRegistryConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.3; 3 | 4 | import "@chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol"; 5 | import "@chainlink/contracts/src/v0.8/Denominations.sol"; 6 | 7 | contract FeedRegistryConsumer { 8 | FeedRegistryInterface internal registry; 9 | 10 | /** 11 | * Network: Ethereum Mainnet 12 | * Feed Registry: 0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf 13 | */ 14 | constructor(address _registry) { 15 | registry = FeedRegistryInterface(_registry); 16 | } 17 | 18 | /** 19 | * Returns the ETH / USD price 20 | */ 21 | function getEthUsdPrice() public view returns (int256) { 22 | (, int256 price, , , ) = registry.latestRoundData(Denominations.ETH, Denominations.USD); 23 | return price; 24 | } 25 | 26 | /** 27 | * Returns the latest price 28 | */ 29 | function getPrice(address base, address quote) public view returns (int256) { 30 | (, int256 price, , , ) = registry.latestRoundData(base, quote); 31 | return price; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/Multicall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | /// @title Multicall - Aggregate results from multiple read-only function calls 6 | /// @author Michael Elliot 7 | /// @author Joshua Levine 8 | /// @author Nick Johnson 9 | 10 | contract Multicall { 11 | struct Call { 12 | address target; 13 | bytes callData; 14 | } 15 | 16 | function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { 17 | blockNumber = block.number; 18 | returnData = new bytes[](calls.length); 19 | for (uint256 i = 0; i < calls.length; i++) { 20 | (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); 21 | require(success); 22 | returnData[i] = ret; 23 | } 24 | } 25 | 26 | // Helper functions 27 | function getEthBalance(address addr) public view returns (uint256 balance) { 28 | balance = addr.balance; 29 | } 30 | 31 | function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { 32 | blockHash = blockhash(blockNumber); 33 | } 34 | 35 | function getLastBlockHash() public view returns (bytes32 blockHash) { 36 | blockHash = blockhash(block.number - 1); 37 | } 38 | 39 | function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { 40 | timestamp = block.timestamp; 41 | } 42 | 43 | function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { 44 | difficulty = block.difficulty; 45 | } 46 | 47 | function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { 48 | gaslimit = block.gaslimit; 49 | } 50 | 51 | function getCurrentBlockCoinbase() public view returns (address coinbase) { 52 | coinbase = block.coinbase; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/PriceConsumerV3.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.6; 2 | 3 | import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; 4 | 5 | contract PriceConsumerV3 { 6 | AggregatorV3Interface internal priceFeed; 7 | 8 | /** 9 | * Network: Sepolia 10 | * Aggregator: ETH/USD 11 | * Address: 0x694AA1769357215DE4FAC081bf1f309aDC325306 12 | */ 13 | constructor(address _priceFeed) public { 14 | priceFeed = AggregatorV3Interface(_priceFeed); 15 | } 16 | 17 | /** 18 | * Returns the latest price 19 | */ 20 | function getLatestPrice() public view returns (int256) { 21 | (, int256 price, , , ) = priceFeed.latestRoundData(); 22 | return price; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/RandomNumberConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol"; 5 | 6 | contract RandomNumberConsumer is VRFV2WrapperConsumerBase { 7 | uint256 public randomResult; 8 | 9 | uint32 internal callbackGasLimit; 10 | // The default is 3, but you can set this higher. 11 | uint16 internal requestConfirmations = 3; 12 | // For this example, retrieve 2 random values in one request. 13 | // Cannot exceed VRFV2Wrapper.getConfig().maxNumWords. 14 | uint32 internal numWords = 1; 15 | 16 | event RequestedRandomness(uint256 requestId); 17 | event FulfilledRandomness(uint256 requestId); 18 | 19 | /** 20 | * Constructor inherits VRFConsumerBase 21 | * 22 | * Network: Sepolia 23 | * Chainlink VRF Wrapper address: 0xab18414CD93297B0d12ac29E63Ca20f515b3DB46 24 | * LINK token address: 0x779877A7B0D9E8603169DdbD7836e478b4624789 25 | */ 26 | constructor( 27 | address _wrapperAddress, 28 | address _link, 29 | uint32 _callbackGasLimit 30 | ) 31 | VRFV2WrapperConsumerBase( 32 | _link, // LINK Token 33 | _wrapperAddress // VRF Wrapper 34 | ) 35 | { 36 | callbackGasLimit = _callbackGasLimit; 37 | } 38 | 39 | /** 40 | * Requests randomness 41 | */ 42 | function getRandomNumber() public returns (uint256 requestId) { 43 | requestId = requestRandomness(callbackGasLimit, requestConfirmations, numWords); 44 | emit RequestedRandomness(requestId); 45 | } 46 | 47 | /** 48 | * Callback function used by VRF Coordinator 49 | */ 50 | function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { 51 | randomResult = _randomWords[0]; 52 | emit FulfilledRandomness(_requestId); 53 | } 54 | 55 | /** 56 | * Withdraw LINK from this contract 57 | * 58 | * DO NOT USE THIS IN PRODUCTION AS IT CAN BE CALLED BY ANY ADDRESS. 59 | * THIS IS PURELY FOR EXAMPLE PURPOSES. 60 | */ 61 | function withdrawLink() external { 62 | require(LINK.transfer(msg.sender, LINK.balanceOf(address(this))), "Unable to transfer"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/RandomSVG.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.4; 3 | 4 | import "../vendor/openzeppelin/contracts/access/Ownable.sol"; 5 | import "../vendor/openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; 6 | import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; 7 | import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; 8 | import "base64-sol/base64.sol"; 9 | 10 | contract RandomSVG is ERC721URIStorage, VRFConsumerBaseV2, Ownable { 11 | // MUTABLE STORAGE 12 | 13 | uint256 public tokenCounter; 14 | uint256 public maxNumberOfPaths; 15 | uint256 public maxNumberOfPathCommands; 16 | uint256 public size; 17 | string[] public pathCommands; 18 | string[] public colors; 19 | mapping(uint256 => address) public requestIdToSender; 20 | mapping(uint256 => uint256) public tokenIdToRandomNumber; 21 | mapping(uint256 => uint256) public requestIdToTokenId; 22 | 23 | // VRF CONSTANTS & IMMUTABLE 24 | 25 | uint16 private constant vrfRequestConfirmations = 3; 26 | uint32 private constant vrfNumWords = 1; 27 | VRFCoordinatorV2Interface private immutable vrfCoordinatorV2; 28 | uint64 private immutable vrfSubscriptionId; 29 | bytes32 private immutable vrfGasLane; 30 | uint32 private immutable vrfCallbackGasLimit; 31 | 32 | // EVENTS 33 | 34 | event CreatedRandomSVG(uint256 indexed tokenId, string tokenURI); 35 | event CreatedUnfinishedRandomSVG(uint256 indexed tokenId, uint256 randomNumber); 36 | event requestedRandomSVG(uint256 indexed requestId, uint256 indexed tokenId); 37 | 38 | constructor( 39 | address _vrfCoordinatorV2, 40 | uint64 _vrfSubscriptionId, 41 | bytes32 _vrfGasLane, 42 | uint32 _vrfCallbackGasLimit 43 | ) VRFConsumerBaseV2(_vrfCoordinatorV2) ERC721("RandomSVG", "rsNFT") { 44 | vrfCoordinatorV2 = VRFCoordinatorV2Interface(_vrfCoordinatorV2); 45 | vrfSubscriptionId = _vrfSubscriptionId; 46 | vrfGasLane = _vrfGasLane; 47 | vrfCallbackGasLimit = _vrfCallbackGasLimit; 48 | 49 | maxNumberOfPaths = 10; 50 | maxNumberOfPathCommands = 5; 51 | size = 500; 52 | pathCommands = ["M", "L"]; 53 | colors = ["red", "blue", "green", "yellow", "black", "white"]; 54 | } 55 | 56 | // ACTIONS 57 | 58 | function create() public { 59 | uint256 requestId = vrfCoordinatorV2.requestRandomWords( 60 | vrfGasLane, 61 | vrfSubscriptionId, 62 | vrfRequestConfirmations, 63 | vrfCallbackGasLimit, 64 | vrfNumWords 65 | ); 66 | requestIdToSender[requestId] = msg.sender; 67 | uint256 tokenId = tokenCounter; 68 | requestIdToTokenId[requestId] = tokenId; 69 | tokenCounter = tokenCounter + 1; 70 | emit requestedRandomSVG(requestId, tokenId); 71 | } 72 | 73 | function finishMint(uint256 tokenId) public { 74 | require(bytes(tokenURI(tokenId)).length <= 0, "tokenURI is already set!"); 75 | require(tokenCounter > tokenId, "TokenId has not been minted yet!"); 76 | require(tokenIdToRandomNumber[tokenId] > 0, "Need to wait for the Chainlink node to respond!"); 77 | uint256 randomNumber = tokenIdToRandomNumber[tokenId]; 78 | string memory svg = generateSVG(randomNumber); 79 | string memory imageURI = svgToImageURI(svg); 80 | _setTokenURI(tokenId, formatTokenURI(imageURI)); 81 | emit CreatedRandomSVG(tokenId, svg); 82 | } 83 | 84 | function withdraw() public payable onlyOwner { 85 | payable(owner()).transfer(address(this).balance); 86 | } 87 | 88 | // VRF 89 | 90 | function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { 91 | address nftOwner = requestIdToSender[requestId]; 92 | uint256 tokenId = requestIdToTokenId[requestId]; 93 | _safeMint(nftOwner, tokenId); 94 | tokenIdToRandomNumber[tokenId] = randomWords[0]; 95 | emit CreatedUnfinishedRandomSVG(tokenId, randomWords[0]); 96 | } 97 | 98 | // GETTERS 99 | 100 | function formatTokenURI(string memory imageURI) public pure returns (string memory) { 101 | return 102 | string( 103 | abi.encodePacked( 104 | "data:application/json;base64,", 105 | Base64.encode( 106 | bytes( 107 | abi.encodePacked( 108 | '{"name":"', 109 | "SVG NFT", // You can add whatever name here 110 | '", "description":"An NFT based on SVG!", "attributes":"", "image":"', 111 | imageURI, 112 | '"}' 113 | ) 114 | ) 115 | ) 116 | ) 117 | ); 118 | } 119 | 120 | // HELPERS 121 | 122 | function generateSVG(uint256 _randomness) public view returns (string memory finalSvg) { 123 | // We will only use the path element, with stroke and d elements 124 | uint256 numberOfPaths = (_randomness % maxNumberOfPaths) + 1; 125 | finalSvg = string( 126 | abi.encodePacked( 127 | "" 132 | ) 133 | ); 134 | for (uint256 i = 0; i < numberOfPaths; i++) { 135 | // we get a new random number for each path 136 | string memory pathSvg = generatePath(uint256(keccak256(abi.encode(_randomness, i)))); 137 | finalSvg = string(abi.encodePacked(finalSvg, pathSvg)); 138 | } 139 | finalSvg = string(abi.encodePacked(finalSvg, "")); 140 | } 141 | 142 | function generatePath(uint256 _randomness) public view returns (string memory pathSvg) { 143 | uint256 numberOfPathCommands = (_randomness % maxNumberOfPathCommands) + 1; 144 | pathSvg = "")); 151 | } 152 | 153 | function generatePathCommand(uint256 _randomness) public view returns (string memory pathCommand) { 154 | pathCommand = pathCommands[_randomness % pathCommands.length]; 155 | uint256 parameterOne = uint256(keccak256(abi.encode(_randomness, size * 2))) % size; 156 | uint256 parameterTwo = uint256(keccak256(abi.encode(_randomness, size * 2 + 1))) % size; 157 | pathCommand = string(abi.encodePacked(pathCommand, " ", uint2str(parameterOne), " ", uint2str(parameterTwo))); 158 | } 159 | 160 | // From: https://stackoverflow.com/a/65707309/11969592 161 | function uint2str(uint256 _i) internal pure returns (string memory _uintAsString) { 162 | if (_i == 0) { 163 | return "0"; 164 | } 165 | uint256 j = _i; 166 | uint256 len; 167 | while (j != 0) { 168 | len++; 169 | j /= 10; 170 | } 171 | bytes memory bstr = new bytes(len); 172 | uint256 k = len; 173 | while (_i != 0) { 174 | k = k - 1; 175 | uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); 176 | bytes1 b1 = bytes1(temp); 177 | bstr[k] = b1; 178 | _i /= 10; 179 | } 180 | return string(bstr); 181 | } 182 | 183 | // You could also just upload the raw SVG and have solildity convert it! 184 | function svgToImageURI(string memory svg) public pure returns (string memory) { 185 | // example: 186 | // 187 | // data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNTAwJyBoZWlnaHQ9JzUwMCcgdmlld0JveD0nMCAwIDI4NSAzNTAnIGZpbGw9J25vbmUnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZmlsbD0nYmxhY2snIGQ9J00xNTAsMCxMNzUsMjAwLEwyMjUsMjAwLFonPjwvcGF0aD48L3N2Zz4= 188 | string memory baseURL = "data:image/svg+xml;base64,"; 189 | string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked(svg)))); 190 | return string(abi.encodePacked(baseURL, svgBase64Encoded)); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/interfaces/FeedRegistryInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.3; 3 | 4 | import "@chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol"; 5 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/LinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.6; 3 | 4 | import "@chainlink/token/contracts/v0.6/LinkToken.sol"; 5 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/MockFeedRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.3; 3 | 4 | contract MockFeedRegistry { 5 | uint8 public decimals; 6 | int256 public latestAnswer; 7 | uint256 public latestTimestamp; 8 | uint256 public latestRound; 9 | 10 | mapping(uint256 => int256) public getAnswer; 11 | mapping(uint256 => uint256) public getTimestamp; 12 | mapping(uint256 => uint256) private getStartedAt; 13 | 14 | function updateAnswer(int256 _answer) public { 15 | latestAnswer = _answer; 16 | latestTimestamp = block.timestamp; 17 | latestRound++; 18 | getAnswer[latestRound] = _answer; 19 | getTimestamp[latestRound] = block.timestamp; 20 | getStartedAt[latestRound] = block.timestamp; 21 | } 22 | 23 | function latestRoundData(address, address) 24 | external 25 | view 26 | returns ( 27 | uint80 roundId, 28 | int256 answer, 29 | uint256 startedAt, 30 | uint256 updatedAt, 31 | uint80 answeredInRound 32 | ) 33 | { 34 | return ( 35 | uint80(latestRound), 36 | getAnswer[latestRound], 37 | getStartedAt[latestRound], 38 | getTimestamp[latestRound], 39 | uint80(latestRound) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.6; 3 | 4 | import "@chainlink/contracts/src/v0.6/LinkTokenReceiver.sol"; 5 | import "@chainlink/contracts/src/v0.6/interfaces/ChainlinkRequestInterface.sol"; 6 | import "@chainlink/contracts/src/v0.6/interfaces/LinkTokenInterface.sol"; 7 | import "@chainlink/contracts/src/v0.6/vendor/SafeMathChainlink.sol"; 8 | 9 | /** 10 | * @title The Chainlink Mock Oracle contract 11 | * @notice Chainlink smart contract developers can use this to test their contracts 12 | */ 13 | contract MockOracle is ChainlinkRequestInterface, LinkTokenReceiver { 14 | using SafeMathChainlink for uint256; 15 | 16 | uint256 public constant EXPIRY_TIME = 5 minutes; 17 | uint256 private constant MINIMUM_CONSUMER_GAS_LIMIT = 400000; 18 | 19 | struct Request { 20 | address callbackAddr; 21 | bytes4 callbackFunctionId; 22 | } 23 | 24 | LinkTokenInterface internal LinkToken; 25 | mapping(bytes32 => Request) private commitments; 26 | 27 | event OracleRequest( 28 | bytes32 indexed specId, 29 | address requester, 30 | bytes32 requestId, 31 | uint256 payment, 32 | address callbackAddr, 33 | bytes4 callbackFunctionId, 34 | uint256 cancelExpiration, 35 | uint256 dataVersion, 36 | bytes data 37 | ); 38 | 39 | event CancelOracleRequest(bytes32 indexed requestId); 40 | 41 | /** 42 | * @notice Deploy with the address of the LINK token 43 | * @dev Sets the LinkToken address for the imported LinkTokenInterface 44 | * @param _link The address of the LINK token 45 | */ 46 | constructor(address _link) public { 47 | LinkToken = LinkTokenInterface(_link); // external but already deployed and unalterable 48 | } 49 | 50 | /** 51 | * @notice Creates the Chainlink request 52 | * @dev Stores the hash of the params as the on-chain commitment for the request. 53 | * Emits OracleRequest event for the Chainlink node to detect. 54 | * @param _sender The sender of the request 55 | * @param _payment The amount of payment given (specified in wei) 56 | * @param _specId The Job Specification ID 57 | * @param _callbackAddress The callback address for the response 58 | * @param _callbackFunctionId The callback function ID for the response 59 | * @param _nonce The nonce sent by the requester 60 | * @param _dataVersion The specified data version 61 | * @param _data The CBOR payload of the request 62 | */ 63 | function oracleRequest( 64 | address _sender, 65 | uint256 _payment, 66 | bytes32 _specId, 67 | address _callbackAddress, 68 | bytes4 _callbackFunctionId, 69 | uint256 _nonce, 70 | uint256 _dataVersion, 71 | bytes calldata _data 72 | ) external override onlyLINK checkCallbackAddress(_callbackAddress) { 73 | bytes32 requestId = keccak256(abi.encodePacked(_sender, _nonce)); 74 | require(commitments[requestId].callbackAddr == address(0), "Must use a unique ID"); 75 | // solhint-disable-next-line not-rely-on-time 76 | uint256 expiration = now.add(EXPIRY_TIME); 77 | 78 | commitments[requestId] = Request(_callbackAddress, _callbackFunctionId); 79 | 80 | emit OracleRequest( 81 | _specId, 82 | _sender, 83 | requestId, 84 | _payment, 85 | _callbackAddress, 86 | _callbackFunctionId, 87 | expiration, 88 | _dataVersion, 89 | _data 90 | ); 91 | } 92 | 93 | /** 94 | * @notice Called by the Chainlink node to fulfill requests 95 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 96 | * Will call the callback address' callback function without bubbling up error 97 | * checking in a `require` so that the node can get paid. 98 | * @param _requestId The fulfillment request ID that must match the requester's 99 | * @param _data The data to return to the consuming contract 100 | * @return Status if the external call was successful 101 | */ 102 | function fulfillOracleRequest(bytes32 _requestId, bytes32 _data) external isValidRequest(_requestId) returns (bool) { 103 | Request memory req = commitments[_requestId]; 104 | delete commitments[_requestId]; 105 | require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); 106 | // All updates to the oracle's fulfillment should come before calling the 107 | // callback(addr+functionId) as it is untrusted. 108 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 109 | (bool success, ) = req.callbackAddr.call(abi.encodeWithSelector(req.callbackFunctionId, _requestId, _data)); // solhint-disable-line avoid-low-level-calls 110 | return success; 111 | } 112 | 113 | /** 114 | * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK 115 | * sent for the request back to the requester's address. 116 | * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid 117 | * Emits CancelOracleRequest event. 118 | * @param _requestId The request ID 119 | * @param _payment The amount of payment given (specified in wei) 120 | * @param _expiration The time of the expiration for the request 121 | */ 122 | function cancelOracleRequest( 123 | bytes32 _requestId, 124 | uint256 _payment, 125 | bytes4, 126 | uint256 _expiration 127 | ) external override { 128 | require(commitments[_requestId].callbackAddr != address(0), "Must use a unique ID"); 129 | // solhint-disable-next-line not-rely-on-time 130 | require(_expiration <= now, "Request is not expired"); 131 | 132 | delete commitments[_requestId]; 133 | emit CancelOracleRequest(_requestId); 134 | 135 | assert(LinkToken.transfer(msg.sender, _payment)); 136 | } 137 | 138 | /** 139 | * @notice Returns the address of the LINK token 140 | * @dev This is the public implementation for chainlinkTokenAddress, which is 141 | * an internal method of the ChainlinkClient contract 142 | */ 143 | function getChainlinkToken() public view override returns (address) { 144 | return address(LinkToken); 145 | } 146 | 147 | // MODIFIERS 148 | 149 | /** 150 | * @dev Reverts if request ID does not exist 151 | * @param _requestId The given request ID to check in stored `commitments` 152 | */ 153 | modifier isValidRequest(bytes32 _requestId) { 154 | require(commitments[_requestId].callbackAddr != address(0), "Must have a valid requestId"); 155 | _; 156 | } 157 | 158 | /** 159 | * @dev Reverts if the callback address is the LINK token 160 | * @param _to The callback address 161 | */ 162 | modifier checkCallbackAddress(address _to) { 163 | require(_to != address(LinkToken), "Cannot callback to LINK"); 164 | _; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.6.6; 3 | 4 | import "@chainlink/contracts/src/v0.6/tests/MockV3Aggregator.sol"; 5 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/VRFCoordinatorV2Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; 5 | -------------------------------------------------------------------------------- /packages/hardhat/contracts/mocks/VRFV2Wrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.6; 3 | 4 | import "@chainlink/contracts/src/v0.8/VRFV2Wrapper.sol"; 5 | -------------------------------------------------------------------------------- /packages/hardhat/decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mocha-skip-if' 2 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/00_Deploy_Multicall.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | 4 | const func: DeployFunction = async function ({ 5 | deployments, 6 | getNamedAccounts, 7 | getChainId, 8 | }: HardhatRuntimeEnvironment) { 9 | const { deploy } = deployments 10 | const { deployer } = await getNamedAccounts() 11 | const chainId = await getChainId() 12 | 13 | if (chainId === '31337') { 14 | await deploy('Multicall', { from: deployer }) 15 | } 16 | } 17 | 18 | func.tags = ['all', 'main'] 19 | 20 | export default func 21 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/01_Deploy_Mocks.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { BigNumber } from 'ethers' 3 | import { formatBytes32String } from 'ethers/lib/utils' 4 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 5 | import { DeployFunction } from 'hardhat-deploy/types' 6 | import { VRFV2Wrapper } from 'types/typechain' 7 | 8 | const DECIMALS = '18' 9 | const INITIAL_PRICE = BigNumber.from('3000000000000000') 10 | const BASE_FEE = '100000000000000000' 11 | const GAS_PRICE_LINK = '1000000000' // 0.000000001 LINK per gas 12 | 13 | const func: DeployFunction = async function ({ 14 | deployments, 15 | getNamedAccounts, 16 | getChainId, 17 | }: HardhatRuntimeEnvironment) { 18 | const { deploy } = deployments 19 | const { deployer } = await getNamedAccounts() 20 | const chainId = await getChainId() 21 | 22 | if (chainId === '31337') { 23 | await deploy('MockFeedRegistry', { 24 | from: deployer, 25 | log: true, 26 | }) 27 | 28 | const aggregator = await deploy('EthUsdAggregator', { 29 | contract: 'MockV3Aggregator', 30 | from: deployer, 31 | log: true, 32 | args: [DECIMALS, INITIAL_PRICE], 33 | }) 34 | 35 | const linkToken = await deploy('LinkToken', { from: deployer, log: true }) 36 | 37 | const coordinator = await deploy('VRFCoordinatorV2Mock', { 38 | from: deployer, 39 | log: true, 40 | args: [BASE_FEE, GAS_PRICE_LINK], 41 | }) 42 | 43 | const vrfV2Wrapper = await deploy('VRFV2Wrapper', { 44 | from: deployer, 45 | log: true, 46 | args: [linkToken.address, aggregator.address, coordinator.address], 47 | }) 48 | await configureWrapper(vrfV2Wrapper.address) 49 | 50 | await deploy('MockOracle', { 51 | from: deployer, 52 | log: true, 53 | args: [linkToken.address], 54 | }) 55 | } 56 | } 57 | 58 | const WRAPPER_GAS_OVERHEAD = BigNumber.from(60000) 59 | const COORDINATOR_GAS_OVERHEAD = BigNumber.from(52000) 60 | const WRAPPER_PREMIUM_PERCENTAGE = 10 61 | const MAX_NUM_WORDS = 5 62 | 63 | async function configureWrapper(wrapperAddress: string) { 64 | const wrapper = (await ethers.getContractAt( 65 | 'VRFV2Wrapper', 66 | wrapperAddress 67 | )) as VRFV2Wrapper 68 | 69 | await wrapper.setConfig( 70 | WRAPPER_GAS_OVERHEAD, 71 | COORDINATOR_GAS_OVERHEAD, 72 | WRAPPER_PREMIUM_PERCENTAGE, 73 | formatBytes32String('keyHash'), 74 | MAX_NUM_WORDS 75 | ) 76 | } 77 | 78 | func.tags = ['all', 'mocks', 'main'] 79 | 80 | export default func 81 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/02_Deploy_FeedRegistryConsumer.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | import { networkConfig } from '../helper-hardhat-config' 4 | 5 | const func: DeployFunction = async function ({ 6 | deployments, 7 | getNamedAccounts, 8 | getChainId, 9 | }: HardhatRuntimeEnvironment) { 10 | const { deploy } = deployments 11 | const { deployer } = await getNamedAccounts() 12 | const chainId = await getChainId() 13 | 14 | let feedRegistryAddress: string 15 | if (chainId === '31337') { 16 | const MockFeedRegistry = await deployments.get('MockFeedRegistry') 17 | feedRegistryAddress = MockFeedRegistry.address 18 | } else { 19 | feedRegistryAddress = networkConfig[chainId].feedRegistry as string 20 | } 21 | 22 | if (!feedRegistryAddress) return 23 | 24 | await deploy('FeedRegistryConsumer', { 25 | from: deployer, 26 | args: [feedRegistryAddress], 27 | log: true, 28 | }) 29 | } 30 | 31 | func.tags = ['all', 'feed', 'main'] 32 | 33 | export default func 34 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/03_Deploy_PriceConsumerV3.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | import { networkConfig } from '../helper-hardhat-config' 4 | 5 | const func: DeployFunction = async function ({ 6 | deployments, 7 | getNamedAccounts, 8 | getChainId, 9 | }: HardhatRuntimeEnvironment) { 10 | const { deploy } = deployments 11 | const { deployer } = await getNamedAccounts() 12 | const chainId = await getChainId() 13 | 14 | let ethUsdPriceFeedAddress: string 15 | if (chainId === '31337') { 16 | const EthUsdAggregator = await deployments.get('EthUsdAggregator') 17 | ethUsdPriceFeedAddress = EthUsdAggregator.address 18 | } else { 19 | ethUsdPriceFeedAddress = networkConfig[chainId].ethUsdPriceFeed as string 20 | } 21 | 22 | await deploy('PriceConsumerV3', { 23 | from: deployer, 24 | args: [ethUsdPriceFeedAddress], 25 | log: true, 26 | }) 27 | } 28 | 29 | func.tags = ['all', 'feed', 'main'] 30 | 31 | export default func 32 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/04_Deploy_RandomNumberConsumer.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | import { networkConfig } from '../helper-hardhat-config' 4 | 5 | const func: DeployFunction = async function ({ 6 | deployments, 7 | getNamedAccounts, 8 | getChainId, 9 | }: HardhatRuntimeEnvironment) { 10 | const { deploy, get } = deployments 11 | const { deployer } = await getNamedAccounts() 12 | const chainId = await getChainId() 13 | let linkTokenAddress: string 14 | let wrapperAddress: string 15 | 16 | if (chainId === '31337') { 17 | const LinkToken = await get('LinkToken') 18 | linkTokenAddress = LinkToken.address 19 | const VRFV2Wrapper = await get('VRFV2Wrapper') 20 | wrapperAddress = VRFV2Wrapper.address 21 | } else { 22 | linkTokenAddress = networkConfig[chainId].linkToken as string 23 | wrapperAddress = networkConfig[chainId].wrapperAddress as string 24 | } 25 | const { vrfCallbackGasLimit } = networkConfig[chainId] 26 | 27 | await deploy('RandomNumberConsumer', { 28 | from: deployer, 29 | args: [ 30 | wrapperAddress, 31 | linkTokenAddress, 32 | vrfCallbackGasLimit?.randomNumberConsumer, 33 | ], 34 | log: true, 35 | }) 36 | } 37 | 38 | func.tags = ['all', 'vrf', 'main'] 39 | 40 | export default func 41 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/05_Deploy_RandomSVG.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { networkConfig } from '../helper-hardhat-config' 3 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 4 | import { DeployFunction, DeploymentsExtension } from 'hardhat-deploy/types' 5 | import { VRFCoordinatorV2Mock } from 'types/typechain' 6 | 7 | const func: DeployFunction = async function ({ 8 | deployments, 9 | getNamedAccounts, 10 | getChainId, 11 | }: HardhatRuntimeEnvironment) { 12 | const { deployer } = await getNamedAccounts() 13 | const chainId = await getChainId() 14 | 15 | if (chainId === '31337') { 16 | await deployToLocalNetwork(deployments, deployer, chainId) 17 | } else { 18 | await deployToPublicNetwork(deployments, deployer, chainId) 19 | } 20 | } 21 | 22 | async function deployToLocalNetwork( 23 | deployments: DeploymentsExtension, 24 | deployer: string, 25 | chainId: string 26 | ) { 27 | const { deploy, get } = deployments 28 | const { vrfGasLane, vrfCallbackGasLimit } = networkConfig[chainId] 29 | 30 | const VRFCoordinatorMockV2 = await get('VRFCoordinatorV2Mock') 31 | const vrfCoordinatorV2Mock = (await ethers.getContractAt( 32 | 'VRFCoordinatorV2Mock', 33 | VRFCoordinatorMockV2.address 34 | )) as VRFCoordinatorV2Mock 35 | 36 | const vrfCoordinatorV2 = vrfCoordinatorV2Mock.address 37 | const vrfSubscriptionId = await createMockSubscription( 38 | vrfCoordinatorV2Mock, 39 | chainId 40 | ) 41 | 42 | await deploy('RandomSVG', { 43 | from: deployer, 44 | args: [ 45 | vrfCoordinatorV2, 46 | vrfSubscriptionId, 47 | vrfGasLane, 48 | vrfCallbackGasLimit?.randomSVG, 49 | ], 50 | log: true, 51 | }) 52 | 53 | const RandomSVG = await get('RandomSVG') 54 | 55 | const consumerIsAdded = await vrfCoordinatorV2Mock.consumerIsAdded( 56 | vrfSubscriptionId, 57 | RandomSVG.address 58 | ) 59 | if (!consumerIsAdded) { 60 | await vrfCoordinatorV2Mock?.addConsumer( 61 | vrfSubscriptionId, 62 | RandomSVG.address 63 | ) 64 | } 65 | } 66 | 67 | async function deployToPublicNetwork( 68 | deployments: DeploymentsExtension, 69 | deployer: string, 70 | chainId: string 71 | ) { 72 | const { deploy } = deployments 73 | const { 74 | vrfCoordinatorV2, 75 | vrfSubscriptionId, 76 | vrfGasLane, 77 | vrfCallbackGasLimit, 78 | } = networkConfig[chainId] 79 | 80 | await deploy('RandomSVG', { 81 | from: deployer, 82 | args: [ 83 | vrfCoordinatorV2, 84 | vrfSubscriptionId, 85 | vrfGasLane, 86 | vrfCallbackGasLimit?.randomSVG, 87 | ], 88 | log: true, 89 | }) 90 | } 91 | 92 | async function createMockSubscription( 93 | vrfCoordinatorV2Mock: VRFCoordinatorV2Mock, 94 | chainId: string 95 | ): Promise { 96 | const { fundAmount } = networkConfig[chainId] 97 | 98 | const transaction = await vrfCoordinatorV2Mock.createSubscription() 99 | const transactionReceipt = await transaction.wait() 100 | const vrfSubscriptionId = ethers.BigNumber.from( 101 | transactionReceipt!.events![0].topics[1] 102 | ).toString() 103 | 104 | await vrfCoordinatorV2Mock.fundSubscription(vrfSubscriptionId, fundAmount) 105 | 106 | return vrfSubscriptionId 107 | } 108 | 109 | func.tags = ['all', 'vrf', 'nft', 'main'] 110 | 111 | export default func 112 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/06_Deploy_APIConsumer.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | import { networkConfig } from '../helper-hardhat-config' 4 | 5 | const func: DeployFunction = async function ({ 6 | deployments, 7 | getNamedAccounts, 8 | getChainId, 9 | }: HardhatRuntimeEnvironment) { 10 | const { deploy, get } = deployments 11 | const { deployer } = await getNamedAccounts() 12 | const chainId = await getChainId() 13 | let linkTokenAddress: string 14 | let oracleAddress: string 15 | 16 | if (chainId === '31337') { 17 | const LinkToken = await get('LinkToken') 18 | linkTokenAddress = LinkToken.address 19 | const MockOracle = await get('MockOracle') 20 | oracleAddress = MockOracle.address 21 | } else { 22 | linkTokenAddress = networkConfig[chainId].linkToken as string 23 | oracleAddress = networkConfig[chainId].oracle as string 24 | } 25 | const { jobId, fee } = networkConfig[chainId] 26 | 27 | await deploy('APIConsumer', { 28 | from: deployer, 29 | args: [oracleAddress, jobId, fee, linkTokenAddress], 30 | log: true, 31 | }) 32 | } 33 | 34 | func.tags = ['all', 'api', 'main'] 35 | 36 | export default func 37 | -------------------------------------------------------------------------------- /packages/hardhat/deploy/07_Setup_Contracts.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { DeployFunction } from 'hardhat-deploy/types' 3 | import { ethers } from 'hardhat' 4 | import { networkConfig } from '../helper-hardhat-config' 5 | import { autoFundCheck } from '../utils' 6 | import { LinkToken } from 'types/typechain' 7 | 8 | const func: DeployFunction = async function ({ 9 | deployments, 10 | getChainId, 11 | }: HardhatRuntimeEnvironment) { 12 | const { get } = deployments 13 | const chainId = await getChainId() 14 | let linkToken: LinkToken 15 | let linkTokenAddress: string 16 | const fundAmount = networkConfig[chainId]['fundAmount'] 17 | 18 | if (chainId == '31337') { 19 | linkTokenAddress = (await get('LinkToken')).address 20 | linkToken = (await ethers.getContractAt( 21 | 'LinkToken', 22 | linkTokenAddress 23 | )) as LinkToken 24 | } else { 25 | linkTokenAddress = networkConfig[chainId].linkToken as string 26 | linkToken = (await ethers.getContractAt( 27 | 'LinkToken', 28 | linkTokenAddress 29 | )) as LinkToken 30 | } 31 | 32 | // Try Auto-fund RandomNumberConsumer contract 33 | const RandomNumberConsumer = await deployments.get('RandomNumberConsumer') 34 | const randomNumberConsumer = await ethers.getContractAt( 35 | 'RandomNumberConsumer', 36 | RandomNumberConsumer.address 37 | ) 38 | if ( 39 | await autoFundCheck(randomNumberConsumer.address, chainId, linkTokenAddress) 40 | ) { 41 | await linkToken.transfer(randomNumberConsumer.address, fundAmount) 42 | } 43 | 44 | // Fund RandomSVG instructions 45 | if (chainId !== '31337') { 46 | console.log( 47 | `Please add RandomSVG address in your prefunded Chainlink VRF sucscription at https://vrf.chain.link` 48 | ) 49 | } 50 | 51 | // Try Auto-fund APIConsumer contract with LINK 52 | const APIConsumer = await deployments.get('APIConsumer') 53 | const apiConsumer = await ethers.getContractAt( 54 | 'APIConsumer', 55 | APIConsumer.address 56 | ) 57 | if (await autoFundCheck(apiConsumer.address, chainId, linkTokenAddress)) { 58 | await linkToken.transfer(apiConsumer.address, fundAmount) 59 | } 60 | } 61 | 62 | func.tags = ['all'] 63 | 64 | export default func 65 | -------------------------------------------------------------------------------- /packages/hardhat/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | import '@nomiclabs/hardhat-waffle' 3 | import '@nomiclabs/hardhat-etherscan' 4 | import '@typechain/hardhat' 5 | import 'hardhat-deploy' 6 | import 'dotenv/config' 7 | import { task } from 'hardhat/config' 8 | import './tasks/withdraw-link' 9 | import './tasks/accounts' 10 | import './tasks/fund-link' 11 | import { HardhatUserConfig } from 'hardhat/types' 12 | import 'solidity-coverage' 13 | 14 | // This is a sample Hardhat task. To learn how to create your own go to 15 | // https://hardhat.org/guides/create-task.html 16 | task('accounts', 'Prints the list of accounts', async (_args, hre) => { 17 | const accounts = await hre.ethers.getSigners() 18 | 19 | for (const account of accounts) { 20 | console.log(await account.address) 21 | } 22 | }) 23 | 24 | // You need to export an object to set up your config 25 | // Go to https://hardhat.org/config/ to learn more 26 | 27 | const GOERLI_RPC_URL = 28 | process.env.GOERLI_RPC_URL || 'https://goerli.infura.io/v3/your-api-key' 29 | 30 | const SEPOLIA_RPC_URL = 31 | process.env.SEPOLIA_RPC_URL || 'https://sepolia.infura.io/v3/your-api-key' 32 | 33 | const MNEMONIC = process.env.MNEMONIC || 'your mnemonic' 34 | const ETHERSCAN_API_KEY = 35 | process.env.ETHERSCAN_API_KEY || 'Your etherscan API key' 36 | // const PRIVATE_KEY = process.env.PRIVATE_KEY || 'your private key' 37 | 38 | /** 39 | * @type import('hardhat/config').HardhatUserConfig 40 | */ 41 | const config: HardhatUserConfig = { 42 | defaultNetwork: 'localhost', 43 | networks: { 44 | hardhat: {}, 45 | localhost: { 46 | chainId: 31337, 47 | url: 'http://127.0.0.1:8545/', 48 | }, 49 | goerli: { 50 | chainId: 5, 51 | url: GOERLI_RPC_URL, 52 | // accounts: [PRIVATE_KEY], 53 | accounts: { 54 | mnemonic: MNEMONIC, 55 | }, 56 | saveDeployments: true, 57 | }, 58 | sepolia: { 59 | chainId: 11155111, 60 | url: SEPOLIA_RPC_URL, 61 | // accounts: [PRIVATE_KEY], 62 | accounts: { 63 | mnemonic: MNEMONIC, 64 | }, 65 | saveDeployments: true, 66 | }, 67 | }, 68 | etherscan: { 69 | // Your API key for Etherscan 70 | // Obtain one at https://etherscan.io/ 71 | apiKey: ETHERSCAN_API_KEY, 72 | }, 73 | namedAccounts: { 74 | deployer: { 75 | default: 0, // here this will by default take the first account as deployer 76 | 1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another 77 | }, 78 | feeCollector: { 79 | default: 1, 80 | }, 81 | }, 82 | typechain: { 83 | outDir: '../types/typechain', 84 | }, 85 | solidity: { 86 | compilers: [ 87 | { 88 | version: '0.8.6', 89 | }, 90 | { 91 | version: '0.8.4', 92 | }, 93 | { 94 | version: '0.8.3', 95 | }, 96 | { 97 | version: '0.6.6', 98 | }, 99 | ], 100 | }, 101 | mocha: { 102 | timeout: 100000, 103 | }, 104 | } 105 | 106 | export default config 107 | -------------------------------------------------------------------------------- /packages/hardhat/helper-hardhat-config.ts: -------------------------------------------------------------------------------- 1 | export const networkConfig: Record< 2 | string, 3 | { 4 | name: string 5 | linkToken?: string 6 | feedRegistry?: string 7 | ethUsdPriceFeed?: string 8 | wrapperAddress?: string 9 | vrfCoordinatorV2?: string 10 | vrfSubscriptionId?: string 11 | vrfGasLane?: string 12 | vrfCallbackGasLimit?: { 13 | randomNumberConsumer: string 14 | randomSVG: string 15 | } 16 | keyHash?: string 17 | oracle?: string 18 | jobId: string 19 | fee: string 20 | fundAmount: string 21 | } 22 | > = { 23 | '31337': { 24 | name: 'hardhat', 25 | keyHash: 26 | '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4', 27 | jobId: '29fa9aa13bf1468788b7cc4a500a45b8', 28 | vrfGasLane: 29 | '0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15', 30 | vrfCallbackGasLimit: { 31 | randomNumberConsumer: '500000', 32 | randomSVG: '500000', 33 | }, 34 | fee: '100000000000000000', 35 | fundAmount: '50000000000000000000', 36 | }, 37 | '5': { 38 | name: 'goerli', 39 | linkToken: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB', 40 | ethUsdPriceFeed: '0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e', 41 | wrapperAddress: '0x708701a1DfF4f478de54383E49a627eD4852C816', 42 | vrfCoordinatorV2: '0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D', 43 | vrfSubscriptionId: '0', 44 | vrfGasLane: 45 | '0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15', 46 | vrfCallbackGasLimit: { 47 | randomNumberConsumer: '150000', 48 | randomSVG: '200000', 49 | }, 50 | keyHash: 51 | '0x0476f9a745b61ea5c0ab224d3a6e4c99f0b02fce4da01143a4f70aa80ae76e8a', 52 | oracle: '0xCC79157eb46F5624204f47AB42b3906cAA40eaB7', 53 | jobId: 'ca98366cc7314957b8c012c72f05aeeb', 54 | fee: '100000000000000000', 55 | fundAmount: '5000000000000000000', 56 | }, 57 | '11155111': { 58 | name: 'sepolia', 59 | linkToken: '0x779877A7B0D9E8603169DdbD7836e478b4624789', 60 | ethUsdPriceFeed: '0x694AA1769357215DE4FAC081bf1f309aDC325306', 61 | wrapperAddress: '0xab18414CD93297B0d12ac29E63Ca20f515b3DB46', 62 | vrfCoordinatorV2: '0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625', 63 | vrfSubscriptionId: '0', 64 | vrfGasLane: 65 | '0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c', 66 | vrfCallbackGasLimit: { 67 | randomNumberConsumer: '150000', 68 | randomSVG: '200000', 69 | }, 70 | oracle: '0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD', 71 | jobId: 'ca98366cc7314957b8c012c72f05aeeb', 72 | fee: '100000000000000000', 73 | fundAmount: '5000000000000000000', 74 | }, 75 | } 76 | 77 | export const developmentChains = ['hardhat', 'localhost'] 78 | -------------------------------------------------------------------------------- /packages/hardhat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainlink-fullstack-hardhat", 3 | "author": "@hackbg", 4 | "license": "MIT", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "chain": "npx hardhat node --network hardhat", 8 | "compile": "npx hardhat compile", 9 | "deploy": "npx hardhat deploy --export-all ../frontend/contracts/hardhat_contracts.json", 10 | "test": "npx hardhat test", 11 | "coverage": "npx hardhat coverage --network hardhat", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@appliedblockchain/chainlink-contracts": "^0.0.4", 16 | "@chainlink/contracts": "^0.4.2", 17 | "@chainlink/token": "^1.1.0", 18 | "@nomiclabs/hardhat-ethers": "^2.1.0", 19 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 20 | "@nomiclabs/hardhat-waffle": "^2.0.3", 21 | "@typechain/ethers-v5": "^10.1.0", 22 | "@typechain/hardhat": "^6.1.2", 23 | "@types/chai": "^4.3.1", 24 | "@types/mocha": "^9.1.1", 25 | "@types/node": "^18.6.2", 26 | "base64-sol": "^1.1.0", 27 | "chai": "^4.3.6", 28 | "dotenv": "^16.0.1", 29 | "ethereum-waffle": "^3.4.4", 30 | "ethers": "5.6.9", 31 | "hardhat": "^2.10.1", 32 | "hardhat-deploy": "^0.11.12", 33 | "mocha-skip-if": "^0.0.3", 34 | "prettier": "^2.7.1", 35 | "prettier-plugin-solidity": "^1.0.0-dev.23", 36 | "solidity-coverage": "^0.7.21", 37 | "ts-node": "^10.9.1", 38 | "typechain": "^8.1.0", 39 | "types": "0.1.1", 40 | "typescript": "^4.7.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/hardhat/tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | 3 | // This is a sample Hardhat task. To learn how to create your own go to 4 | // https://hardhat.org/guides/create-task.html 5 | task('accounts', 'Prints the list of accounts', async (_args, hre) => { 6 | const accounts = await hre.ethers.getSigners() 7 | 8 | for (const account of accounts) { 9 | console.log(account.address) 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /packages/hardhat/tasks/fund-link.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | import { networkConfig } from '../helper-hardhat-config' 3 | import { task } from 'hardhat/config' 4 | import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' 5 | 6 | task('fund-link', 'Transfer LINK tokens to a recipient') 7 | .addParam( 8 | 'contract', 9 | 'The address of the EOA or contract account that will receive your LINK tokens' 10 | ) 11 | .addParam('amount', 'Amount in Juels. 1LINK=10**18 JUELS') 12 | .addOptionalParam('linkAddress', 'Set the LINK token address') 13 | .setAction( 14 | async (taskArgs: TaskArguments, hre: HardhatRuntimeEnvironment) => { 15 | const { contract: recipientAddress, amount } = taskArgs 16 | const networkId = hre.network.config.chainId 17 | 18 | if (!networkId) return 19 | 20 | //Get signer information 21 | const accounts = await hre.ethers.getSigners() 22 | const signer = accounts[0] 23 | 24 | const linkTokenAddress = 25 | networkConfig[networkId]['linkToken'] || taskArgs.linkAddress 26 | const LinkToken = await hre.ethers.getContractFactory('LinkToken') 27 | const linkTokenContract = await LinkToken.attach(linkTokenAddress) 28 | 29 | const balance = await linkTokenContract.balanceOf(signer.address) 30 | console.log( 31 | `LINK balance of sender ${ 32 | signer.address 33 | } is + ${hre.ethers.utils.formatEther(balance)}` 34 | ) 35 | const amountBN = BigNumber.from(amount) 36 | if (balance.gte(amountBN)) { 37 | const result = await linkTokenContract.transfer( 38 | recipientAddress, 39 | amount 40 | ) 41 | await result.wait() 42 | console.log( 43 | `${hre.ethers.utils.formatEther( 44 | amountBN 45 | )} LINK were sent from sender ${ 46 | signer.address 47 | } to ${recipientAddress}.Transaction Hash: ${result.hash}` 48 | ) 49 | } else { 50 | console.log( 51 | `Sender doesn't have enough LINK. Current balance is ${hre.ethers.utils.formatEther( 52 | balance 53 | )} LINK and transfer amount is ${hre.ethers.utils.formatEther( 54 | amount 55 | )} LINK` 56 | ) 57 | } 58 | } 59 | ) 60 | -------------------------------------------------------------------------------- /packages/hardhat/tasks/withdraw-link.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ContractTransaction } from 'ethers' 2 | import { task } from 'hardhat/config' 3 | import { networkConfig } from '../helper-hardhat-config' 4 | 5 | task('withdraw-link', 'Returns any LINK left in deployed contract') 6 | .addParam('contract', 'The address of the contract') 7 | .addOptionalParam('linkaddress', 'Set the LINK token address') 8 | .setAction(async (taskArgs, hre) => { 9 | const contractAddr: string = taskArgs.contract 10 | const chainId = await hre.getChainId() 11 | 12 | //Get signer information 13 | const accounts = await hre.ethers.getSigners() 14 | const signer = accounts[0] 15 | 16 | //First, lets see if there is any LINK to withdraw 17 | const linkTokenAddress: string = 18 | networkConfig[chainId]['linkToken'] || taskArgs.linkaddress 19 | const LinkToken = await hre.ethers.getContractFactory('LinkToken') 20 | const linkTokenContract = new hre.ethers.Contract( 21 | linkTokenAddress, 22 | LinkToken.interface, 23 | signer 24 | ) 25 | const balance: BigNumber = await linkTokenContract.balanceOf(contractAddr) 26 | console.log( 27 | 'LINK balance of contract: ' + 28 | contractAddr + 29 | ' is ' + 30 | hre.ethers.utils.formatEther(balance) 31 | ) 32 | 33 | if (balance.gt(hre.ethers.BigNumber.from(0))) { 34 | //Could also be Any-API contract, but in either case the function signature is the same, so we just need to use one 35 | const RandomNumberConsumer = await hre.ethers.getContractFactory( 36 | 'RandomNumberConsumer' 37 | ) 38 | 39 | //Create connection to Consumer Contract and call the withdraw function 40 | const ConsumerContract = new hre.ethers.Contract( 41 | contractAddr, 42 | RandomNumberConsumer.interface, 43 | signer 44 | ) 45 | const result: ContractTransaction = await ConsumerContract.withdrawLink() 46 | console.log( 47 | 'All LINK withdrew from contract ' + contractAddr, 48 | '. Transaction Hash: ', 49 | result.hash 50 | ) 51 | } else { 52 | console.log("Contract doesn't have any LINK to withdraw") 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /packages/hardhat/test/data/randomSVG.txt: -------------------------------------------------------------------------------- 1 | data:application/json;base64,eyJuYW1lIjoiU1ZHIE5GVCIsICJkZXNjcmlwdGlvbiI6IkFuIE5GVCBiYXNlZCBvbiBTVkchIiwgImF0dHJpYnV0ZXMiOiIiLCAiaW1hZ2UiOiJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MG5hSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY25JR2hsYVdkb2REMG5OVEF3SnlCM2FXUjBhRDBuTlRBd0p6NDhjR0YwYUNCa1BTZE5JREl3TVNBME56Qk1JREV3TlNBME9UaE5JRE01T0NBME5qQk1JRE14TlNBek1ERk1JREV4TWlBek1qSW5JR1pwYkd3OUozUnlZVzV6Y0dGeVpXNTBKeUJ6ZEhKdmEyVTlKMkpzZFdVbkx6NDhjR0YwYUNCa1BTZE1JRFF6TnlBME5qTW5JR1pwYkd3OUozUnlZVzV6Y0dGeVpXNTBKeUJ6ZEhKdmEyVTlKMmR5WldWdUp5OCtQSEJoZEdnZ1pEMG5UQ0F5T1RRZ016RTNKeUJtYVd4c1BTZDBjbUZ1YzNCaGNtVnVkQ2NnYzNSeWIydGxQU2RpYkdGamF5Y3ZQanh3WVhSb0lHUTlKMDBnTXpBZ01URXhUQ0EwT0RNZ09UTk1JREkyTkNBeU9UZE5JRFV6SURReE4wd2dNell6SURZMkp5Qm1hV3hzUFNkMGNtRnVjM0JoY21WdWRDY2djM1J5YjJ0bFBTZGliSFZsSnk4K1BIQmhkR2dnWkQwblRDQXpPVGtnTkRJNFRDQTBOREVnTWpBeUp5Qm1hV3hzUFNkMGNtRnVjM0JoY21WdWRDY2djM1J5YjJ0bFBTZG5jbVZsYmljdlBqeHdZWFJvSUdROUowMGdNakVnTXprd1RDQXpPVE1nTkRReUp5Qm1hV3hzUFNkMGNtRnVjM0JoY21WdWRDY2djM1J5YjJ0bFBTZG5jbVZsYmljdlBqeHdZWFJvSUdROUowMGdNVE01SURFM05Vd2dNalE1SURJeE1VMGdNVEkzSURRMk1VMGdNaklnTVRVekp5Qm1hV3hzUFNkMGNtRnVjM0JoY21WdWRDY2djM1J5YjJ0bFBTZGliSFZsSnk4K1BIQmhkR2dnWkQwblRTQXhPVGNnTVRrNEp5Qm1hV3hzUFNkMGNtRnVjM0JoY21WdWRDY2djM1J5YjJ0bFBTZDVaV3hzYjNjbkx6NDhMM04yWno0PSJ9 -------------------------------------------------------------------------------- /packages/hardhat/test/integration/APIConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network } from 'hardhat' 2 | import { expect } from 'chai' 3 | import skip from 'mocha-skip-if' 4 | import { developmentChains } from '../../helper-hardhat-config' 5 | import { APIConsumer } from 'types/typechain' 6 | 7 | skip 8 | .if(developmentChains.includes(network.name)) 9 | .describe('APIConsumer Integration Tests', () => { 10 | let apiConsumer: APIConsumer 11 | 12 | beforeEach(async () => { 13 | const APIConsumer = await deployments.get('APIConsumer') 14 | apiConsumer = (await ethers.getContractAt( 15 | 'APIConsumer', 16 | APIConsumer.address 17 | )) as APIConsumer 18 | }) 19 | 20 | it('should successfully make an external API request and get a result', async () => { 21 | const transaction = await apiConsumer.requestData( 22 | 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD', 23 | 'RAW,ETH,USD,VOLUME24HOUR', 24 | '1000000000000000000' 25 | ) 26 | await transaction.wait() 27 | 28 | //wait 7 min for oracle to callback 29 | await new Promise((resolve) => setTimeout(resolve, 420000)) 30 | 31 | //Now check the result 32 | const result = await apiConsumer.data() 33 | expect(result).to.be.gt(0) 34 | }).timeout(520000) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/hardhat/test/integration/PriceConsumerV3.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network } from 'hardhat' 2 | import { expect } from 'chai' 3 | import skip from 'mocha-skip-if' 4 | import { developmentChains } from '../../helper-hardhat-config' 5 | import { PriceConsumerV3 } from 'types/typechain' 6 | 7 | skip 8 | .if(developmentChains.includes(network.name)) 9 | .describe('PriceConsumerV3 Integration Tests', () => { 10 | let priceConsumerV3: PriceConsumerV3 11 | 12 | beforeEach(async () => { 13 | const PriceConsumerV3 = await deployments.get('PriceConsumerV3') 14 | priceConsumerV3 = (await ethers.getContractAt( 15 | 'PriceConsumerV3', 16 | PriceConsumerV3.address 17 | )) as PriceConsumerV3 18 | }) 19 | 20 | it('should return a positive value', async () => { 21 | let result = await priceConsumerV3.getLatestPrice() 22 | expect(result).to.be.gt(0) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/hardhat/test/integration/RandomNumberConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network } from 'hardhat' 2 | import { expect } from 'chai' 3 | import skip from 'mocha-skip-if' 4 | import { developmentChains } from '../../helper-hardhat-config' 5 | import { RandomNumberConsumer } from 'types/typechain' 6 | 7 | skip 8 | .if(developmentChains.includes(network.name)) 9 | .describe('RandomNumberConsumer Integration Tests', async function () { 10 | let randomNumberConsumer: RandomNumberConsumer 11 | 12 | beforeEach(async () => { 13 | const RandomNumberConsumer = await deployments.get('RandomNumberConsumer') 14 | randomNumberConsumer = (await ethers.getContractAt( 15 | 'RandomNumberConsumer', 16 | RandomNumberConsumer.address 17 | )) as RandomNumberConsumer 18 | }) 19 | 20 | it('should successfully make a VRF request and get a result', async () => { 21 | const transaction = await randomNumberConsumer.getRandomNumber() 22 | await transaction.wait() 23 | 24 | //wait 7 min for oracle to callback 25 | await new Promise((resolve) => setTimeout(resolve, 420000)) 26 | 27 | //Now check the result 28 | const result = await randomNumberConsumer.randomResult() 29 | //console.log("VRF Result: ", result.toString()); 30 | expect(result).to.be.gt(0) 31 | }).timeout(520000) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/hardhat/test/unit/APIConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network, getChainId, run } from 'hardhat' 2 | import { BigNumber } from 'ethers' 3 | import { expect } from 'chai' 4 | import skip from 'mocha-skip-if' 5 | import { developmentChains, networkConfig } from '../../helper-hardhat-config' 6 | import { autoFundCheck } from '../../utils' 7 | import { APIConsumer, LinkToken } from 'types/typechain' 8 | 9 | skip 10 | .if(!developmentChains.includes(network.name)) 11 | .describe('APIConsumer Unit Tests', () => { 12 | let apiConsumer: APIConsumer, linkToken: LinkToken 13 | 14 | beforeEach(async () => { 15 | const chainId = await getChainId() 16 | await deployments.fixture(['mocks', 'api']) 17 | const LinkToken = await deployments.get('LinkToken') 18 | linkToken = (await ethers.getContractAt( 19 | 'LinkToken', 20 | LinkToken.address 21 | )) as LinkToken 22 | 23 | const linkTokenAddress = linkToken.address 24 | 25 | const APIConsumer = await deployments.get('APIConsumer') 26 | apiConsumer = (await ethers.getContractAt( 27 | 'APIConsumer', 28 | APIConsumer.address 29 | )) as APIConsumer 30 | 31 | if (await autoFundCheck(apiConsumer.address, chainId, linkTokenAddress)) { 32 | const fundAmount = networkConfig[chainId]['fundAmount'] 33 | await linkToken.transfer(apiConsumer.address, fundAmount) 34 | } 35 | }) 36 | 37 | it('should successfully make an API request', async () => { 38 | const transaction = await apiConsumer.requestData( 39 | 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD', 40 | 'RAW,ETH,USD,VOLUME24HOUR', 41 | '1000000000000000000' 42 | ) 43 | const tx_receipt = await transaction.wait() 44 | const requestId = BigNumber.from( 45 | tx_receipt.events && tx_receipt.events[0].topics[1] 46 | ) 47 | expect(requestId).to.be.gt(0) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /packages/hardhat/test/unit/FeedRegistryConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network } from 'hardhat' 2 | import { BigNumber } from 'ethers' 3 | import { expect } from 'chai' 4 | import skip from 'mocha-skip-if' 5 | import { developmentChains } from '../../helper-hardhat-config' 6 | import { FeedRegistryConsumer, MockFeedRegistry } from 'types/typechain' 7 | 8 | skip 9 | .if(!developmentChains.includes(network.name)) 10 | .describe('FeedRegistryConsumer Unit Tests', () => { 11 | let feedRegistryConsumer: FeedRegistryConsumer 12 | let mockFeedRegistry: MockFeedRegistry 13 | 14 | beforeEach(async () => { 15 | await deployments.fixture(['mocks', 'feed']) 16 | const FeedRegistryConsumer = await deployments.get('FeedRegistryConsumer') 17 | feedRegistryConsumer = (await ethers.getContractAt( 18 | 'FeedRegistryConsumer', 19 | FeedRegistryConsumer.address 20 | )) as FeedRegistryConsumer 21 | 22 | const MockFeedRegistry = await deployments.get('MockFeedRegistry') 23 | mockFeedRegistry = (await ethers.getContractAt( 24 | 'MockFeedRegistry', 25 | MockFeedRegistry.address 26 | )) as MockFeedRegistry 27 | }) 28 | 29 | it('should return the expected value', async () => { 30 | const mockEthPrice = BigNumber.from(1000) 31 | const mockEthAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' 32 | const mockUSDAddress = '0x0000000000000000000000000000000000000348' 33 | 34 | await mockFeedRegistry.updateAnswer(mockEthPrice) 35 | let result = await feedRegistryConsumer.getPrice( 36 | mockEthAddress, 37 | mockUSDAddress 38 | ) 39 | expect(result).to.be.equal(mockEthPrice) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/hardhat/test/unit/PriceConsumerV3.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network } from 'hardhat' 2 | import { expect } from 'chai' 3 | import skip from 'mocha-skip-if' 4 | import { developmentChains } from '../../helper-hardhat-config' 5 | import { PriceConsumerV3 } from 'types/typechain' 6 | 7 | skip 8 | .if(!developmentChains.includes(network.name)) 9 | .describe('PriceConsumerV3 Unit Tests', () => { 10 | let priceConsumerV3: PriceConsumerV3 11 | 12 | beforeEach(async () => { 13 | await deployments.fixture(['mocks', 'feed']) 14 | const PriceConsumerV3 = await deployments.get('PriceConsumerV3') 15 | priceConsumerV3 = (await ethers.getContractAt( 16 | 'PriceConsumerV3', 17 | PriceConsumerV3.address 18 | )) as PriceConsumerV3 19 | }) 20 | 21 | it('should return a positive value', async () => { 22 | let result = await priceConsumerV3.getLatestPrice() 23 | expect(result).to.be.gt(0) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/hardhat/test/unit/RandomNumberConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers, deployments, network, getChainId, run } from 'hardhat' 2 | import { BigNumber } from 'ethers' 3 | import { expect } from 'chai' 4 | import skip from 'mocha-skip-if' 5 | import { developmentChains, networkConfig } from '../../helper-hardhat-config' 6 | import { autoFundCheck } from '../../utils' 7 | import { RandomNumberConsumer, LinkToken } from 'types/typechain' 8 | 9 | skip 10 | .if(!developmentChains.includes(network.name)) 11 | .describe('RandomNumberConsumer Unit Tests', () => { 12 | let randomNumberConsumer: RandomNumberConsumer, linkToken: LinkToken 13 | 14 | beforeEach(async () => { 15 | const chainId = await getChainId() 16 | await deployments.fixture(['mocks', 'vrf']) 17 | const LinkToken = await deployments.get('LinkToken') 18 | linkToken = (await ethers.getContractAt( 19 | 'LinkToken', 20 | LinkToken.address 21 | )) as LinkToken 22 | 23 | const linkTokenAddress = linkToken.address 24 | 25 | const RandomNumberConsumer = await deployments.get('RandomNumberConsumer') 26 | randomNumberConsumer = (await ethers.getContractAt( 27 | 'RandomNumberConsumer', 28 | RandomNumberConsumer.address 29 | )) as RandomNumberConsumer 30 | 31 | if ( 32 | await autoFundCheck( 33 | randomNumberConsumer.address, 34 | chainId, 35 | linkTokenAddress 36 | ) 37 | ) { 38 | const fundAmount = networkConfig[chainId]['fundAmount'] 39 | await linkToken.transfer(randomNumberConsumer.address, fundAmount) 40 | } 41 | }) 42 | 43 | it('should successfully make an external random number request', async () => { 44 | const transaction = await randomNumberConsumer.getRandomNumber() 45 | const tx_receipt = await transaction.wait() 46 | const requestId = BigNumber.from( 47 | tx_receipt.events && tx_receipt.events[2].topics[1] 48 | ) 49 | expect(requestId).to.not.be.null 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/hardhat/test/unit/RandomSVG.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { ethers, deployments, network } from 'hardhat' 3 | import { expect } from 'chai' 4 | import skip from 'mocha-skip-if' 5 | import { developmentChains } from '../../helper-hardhat-config' 6 | import { RandomSVG, VRFCoordinatorV2Mock } from 'types/typechain' 7 | 8 | skip 9 | .if(!developmentChains.includes(network.name)) 10 | .describe('RandomSVG Unit Tests', () => { 11 | let randomSvg: RandomSVG 12 | let vrfCoordinatorV2: VRFCoordinatorV2Mock 13 | 14 | beforeEach(async () => { 15 | await deployments.fixture(['mocks', 'vrf', 'nft']) 16 | 17 | const VRFCoordinatorMockV2 = await deployments.get('VRFCoordinatorV2Mock') 18 | vrfCoordinatorV2 = (await ethers.getContractAt( 19 | 'VRFCoordinatorV2Mock', 20 | VRFCoordinatorMockV2.address 21 | )) as VRFCoordinatorV2Mock 22 | 23 | const RandomSVG = await deployments.get('RandomSVG') 24 | randomSvg = (await ethers.getContractAt( 25 | 'RandomSVG', 26 | RandomSVG.address 27 | )) as RandomSVG 28 | }) 29 | 30 | it('should return the correct URI', async () => { 31 | const transactionCreate = await randomSvg.create() 32 | const receipt = await transactionCreate.wait() 33 | const [, requestId, tokenId] = 34 | (receipt.events && receipt.events[1].topics) || [] 35 | const fakeRandomNumber = 77777 36 | 37 | const transactionResponse = 38 | await vrfCoordinatorV2.fulfillRandomWordsWithOverride( 39 | requestId, 40 | randomSvg.address, 41 | [fakeRandomNumber] 42 | ) 43 | 44 | await transactionResponse.wait() 45 | const transactionMint = await randomSvg.finishMint(tokenId) 46 | await transactionMint.wait() 47 | 48 | const expectedURI = fs.readFileSync('./test/data/randomSVG.txt', 'utf8') 49 | const uri = await randomSvg.tokenURI(0) 50 | expect(uri).to.equal(expectedURI) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/hardhat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true 9 | }, 10 | "include": ["./scripts", "./test", "./deploy"], 11 | "files": ["./hardhat.config.ts", "./helper-hardhat-config.ts", "./decs.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/hardhat/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | import { BigNumber } from 'ethers' 3 | import { networkConfig } from './helper-hardhat-config' 4 | 5 | export const autoFundCheck = async ( 6 | contractAddr: string, 7 | chainId: string, 8 | linkTokenAddress: string 9 | ) => { 10 | console.log('Checking to see if contract can be auto-funded with LINK:') 11 | const accounts = await ethers.getSigners() 12 | const signer = accounts[0] 13 | const LinkToken = await ethers.getContractFactory('LinkToken') 14 | const linkTokenContract = new ethers.Contract( 15 | linkTokenAddress, 16 | LinkToken.interface, 17 | signer 18 | ) 19 | const accountBalance: BigNumber = await linkTokenContract.balanceOf( 20 | signer.address 21 | ) 22 | const contractBalance: BigNumber = await linkTokenContract.balanceOf( 23 | contractAddr 24 | ) 25 | const fundAmount = BigNumber.from(networkConfig[chainId]['fundAmount']) 26 | if ( 27 | accountBalance.gt(fundAmount) && 28 | fundAmount.gt(0) && 29 | contractBalance.lt(fundAmount) 30 | ) { 31 | //user has enough LINK to auto-fund 32 | //and the contract isn't already funded 33 | return true 34 | } else { 35 | //user doesn't have enough LINK, print a warning 36 | console.log( 37 | "Account doesn't have enough LINK to fund contracts, or you're deploying to a network where auto funding is not done by default" 38 | ) 39 | console.log( 40 | 'Please obtain LINK via the faucet at https://' + 41 | networkConfig[chainId].name + 42 | '.chain.link/, then run the following command to fund contract with LINK:' 43 | ) 44 | console.log( 45 | 'npx hardhat fund-link --contract ' + 46 | contractAddr + 47 | ' --network ' + 48 | networkConfig[chainId].name + 49 | chainId === 50 | '31337' 51 | ? ' --linkaddress ' + linkTokenAddress 52 | : '' 53 | ) 54 | return false 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "types", 3 | "author": "@hackbg", 4 | "license": "MIT", 5 | "version": "0.0.1", 6 | "dependencies": { 7 | "ethers": "5.6.9" 8 | } 9 | } 10 | --------------------------------------------------------------------------------