├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── checks.yml │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── .releaserc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-typescript.cjs │ │ └── plugin-workspace-tools.cjs ├── releases │ └── yarn-3.8.5.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── index.js │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── LICENSE.txt ├── README.md ├── babel.config.js ├── commitlint.config.js ├── jest.config.js ├── package.json ├── packages ├── constants │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── aggregator.ts │ │ ├── chains.ts │ │ ├── index.ts │ │ ├── tokens.ts │ │ └── withdrawal_queue.ts │ ├── test │ │ ├── aggregator.test.ts │ │ ├── chains.test.ts │ │ ├── tokens.test.ts │ │ └── withdrawal_queue.test.ts │ └── tsconfig.json ├── contracts │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── abi │ │ │ ├── aggregator.abi.json │ │ │ ├── erc20.abi.json │ │ │ ├── ldo.abi.json │ │ │ ├── steth.abi.json │ │ │ ├── withdrawal_queue.abi.json │ │ │ └── wsteth.abi.json │ │ ├── contracts.ts │ │ ├── factories.ts │ │ ├── index.ts │ │ └── types.ts │ ├── test │ │ └── contracts.test.ts │ └── tsconfig.json ├── fetch │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── fetch.ts │ │ ├── fetchRPC.ts │ │ ├── fetchWithFallbacks.ts │ │ ├── index.ts │ │ └── providersUrls.ts │ ├── test │ │ ├── fetchRPC.test.ts │ │ ├── fetchWithFallbacks.test.ts │ │ └── providersUrls.test.ts │ └── tsconfig.json ├── helpers │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── divide.ts │ │ ├── etherscan.ts │ │ ├── index.ts │ │ └── openWindow.ts │ ├── test │ │ ├── divide.test.ts │ │ ├── etherscan.test.ts │ │ └── openWindow.test.ts │ └── tsconfig.json ├── providers │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── providersRPC.ts │ │ └── staticJsonRpcBatchProvider.ts │ ├── test │ │ ├── providersRPC.test.ts │ │ └── staticJsonRpcBatchProvider.test.ts │ └── tsconfig.json └── react │ ├── .npmignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ ├── context │ │ ├── SDK.tsx │ │ └── index.ts │ ├── factories │ │ ├── contracts.ts │ │ ├── index.ts │ │ └── tokens.ts │ ├── hooks │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useAllowance.ts │ │ ├── useApprove.ts │ │ ├── useContractEstimateGasSWR.ts │ │ ├── useContractSWR.ts │ │ ├── useDebounceCallback.ts │ │ ├── useDecimals.ts │ │ ├── useEthPrice.ts │ │ ├── useEthereumBalance.ts │ │ ├── useEthereumSWR.ts │ │ ├── useEtherscanOpen.ts │ │ ├── useFeeAnalytics.ts │ │ ├── useFeeHistory.ts │ │ ├── useLidoSWR.ts │ │ ├── useLidoSWRImmutable.ts │ │ ├── useLocalStorage.ts │ │ ├── useMountedState.ts │ │ ├── useSDK.ts │ │ ├── useTokenAddress.ts │ │ ├── useTokenBalance.ts │ │ ├── useTokenToWallet.ts │ │ ├── useTotalSupply.ts │ │ └── useTxPrice.ts │ └── index.ts │ ├── test │ ├── factories │ │ ├── contracts.test.tsx │ │ └── tokens.test.ts │ └── hooks │ │ ├── testUtils.tsx │ │ ├── useAllowance.test.tsx │ │ ├── useApprove.test.tsx │ │ ├── useContractEstimateGasSWR.test.ts │ │ ├── useContractSWR.test.ts │ │ ├── useDebounceCallback.test.ts │ │ ├── useDecimals.test.ts │ │ ├── useEthPrice.test.ts │ │ ├── useEthereumBalance.test.tsx │ │ ├── useEthereumSWR.test.tsx │ │ ├── useEtherscanOpen.test.ts │ │ ├── useFeeAnalytics.test.tsx │ │ ├── useFeeHistory.test.tsx │ │ ├── useLidoSWR.test.ts │ │ ├── useLidoSWRImmutable.test.ts │ │ ├── useLocalStorage.test.ts │ │ ├── useMountedState.test.ts │ │ ├── useTokenAddress.test.ts │ │ ├── useTokenBalance.test.tsx │ │ ├── useTokenToWallet.test.tsx │ │ ├── useTotalSupply.test.tsx │ │ └── useTxPrice.test.ts │ └── tsconfig.json ├── rollup.config.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,.yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.jest 4 | /packages/**/dist/ 5 | 6 | /.yarn/* 7 | /.pnp.* 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:jsx-a11y/recommended", 13 | "plugin:prettier/recommended" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 12, 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["@typescript-eslint", "react"], 24 | "rules": { 25 | "prettier/prettier": ["error", {}, { "usePrettierrc": true }], 26 | "react/react-in-jsx-scope": "off", 27 | "react/prop-types": "off", 28 | "@typescript-eslint/no-empty-interface": "off", 29 | "@typescript-eslint/no-unused-vars": [ 30 | "error", 31 | { "ignoreRestSiblings": true } 32 | ] 33 | }, 34 | "overrides": [ 35 | { 36 | "files": ["*.test.ts", "*.test.tsx"], 37 | "rules": { 38 | "@typescript-eslint/no-explicit-any": "off" 39 | } 40 | }, 41 | { 42 | "files": ["*.js"], 43 | "rules": { 44 | "@typescript-eslint/explicit-module-boundary-types": "off" 45 | } 46 | } 47 | ], 48 | "settings": { 49 | "react": { 50 | "version": "detect" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lidofinance/lido-si 2 | .github @lidofinance/review-gh-workflows 3 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Tests and Checks 2 | 3 | on: push 4 | 5 | jobs: 6 | security: 7 | uses: lidofinance/linters/.github/workflows/security.yml@master 8 | permissions: 9 | security-events: write 10 | contents: read 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | environment: production 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '20' 23 | cache: 'yarn' 24 | - name: Install dependencies 25 | run: yarn install --immutable 26 | - name: Generate types 27 | run: yarn typechain 28 | - name: Run lint 29 | run: yarn lint 30 | - name: Build Components 31 | run: yarn build 32 | - name: Run tests 33 | run: yarn test 34 | - name: Setup .npmrc 35 | run: | 36 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc 37 | echo "workspaces-update=false" >> .npmrc 38 | env: 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | - name: Publish to NPM 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: yarn multi-semantic-release --deps.bump=override --deps.release=patch --sequential-init 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: pull_request 3 | jobs: 4 | test-components: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout repo 8 | uses: actions/checkout@v3 9 | - name: Setup Node 10 | uses: actions/setup-node@v3 11 | with: 12 | node-version: '20' 13 | cache: 'yarn' 14 | - name: Install dependencies 15 | run: yarn install --immutable 16 | - name: Generate types 17 | run: yarn typechain 18 | - name: Run lint 19 | run: yarn lint 20 | - name: Build Components 21 | run: yarn build 22 | - name: Run tests 23 | run: yarn test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # JS/TS 2 | node_modules 3 | /.yarn/* 4 | !/.yarn/releases 5 | !/.yarn/plugins 6 | !/.yarn/sdks 7 | /.pnp.* 8 | /.jest 9 | dist 10 | **/src/generated 11 | 12 | # Editors 13 | .idea 14 | .vscode 15 | 16 | # Other 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main'], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/npm', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/lib/api.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/lib/api.js your application uses 20 | module.exports = absRequire(`eslint/lib/api.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "7.29.0-pnpify", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by PnPify. 2 | # Manual changes will be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/index.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/index.js your application uses 20 | module.exports = absRequire(`prettier/index.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.3.1-pnpify", 4 | "main": "./index.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.js"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.js"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | const moduleWrapper = (tsserver) => { 13 | const { isAbsolute } = require(`path`); 14 | const pnpApi = require(`pnpapi`); 15 | 16 | const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//); 17 | const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 18 | 19 | const dependencyTreeRoots = new Set( 20 | pnpApi.getDependencyTreeRoots().map((locator) => { 21 | return `${locator.name}@${locator.reference}`; 22 | }), 23 | ); 24 | 25 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 26 | // doesn't understand. This layer makes sure to remove the protocol 27 | // before forwarding it to TS, and to add it back on all returned paths. 28 | 29 | function toEditorPath(str) { 30 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 31 | if ( 32 | isAbsolute(str) && 33 | !str.match(/^\^zip:/) && 34 | (str.match(/\.zip\//) || isVirtual(str)) 35 | ) { 36 | // We also take the opportunity to turn virtual paths into physical ones; 37 | // this makes it much easier to work with workspaces that list peer 38 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 39 | // file instances instead of the real ones. 40 | // 41 | // We only do this to modules owned by the the dependency tree roots. 42 | // This avoids breaking the resolution when jumping inside a vendor 43 | // with peer dep (otherwise jumping into react-dom would show resolution 44 | // errors on react). 45 | // 46 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 47 | if (resolved) { 48 | const locator = pnpApi.findPackageLocator(resolved); 49 | if ( 50 | locator && 51 | dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) 52 | ) { 53 | str = resolved; 54 | } 55 | } 56 | 57 | str = normalize(str); 58 | 59 | if (str.match(/\.zip\//)) { 60 | switch (hostInfo) { 61 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 62 | // VSCode only adds it automatically for supported schemes, 63 | // so we have to do it manually for the `zip` scheme. 64 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 65 | // 66 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 67 | // 68 | case `vscode`: 69 | { 70 | str = `^zip:${str}`; 71 | } 72 | break; 73 | 74 | // To make "go to definition" work, 75 | // We have to resolve the actual file system path from virtual path 76 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 77 | case `coc-nvim`: 78 | { 79 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 80 | str = resolve(`zipfile:${str}`); 81 | } 82 | break; 83 | 84 | default: 85 | { 86 | str = `zip:${str}`; 87 | } 88 | break; 89 | } 90 | } 91 | } 92 | 93 | return str; 94 | } 95 | 96 | function fromEditorPath(str) { 97 | return process.platform === `win32` 98 | ? str.replace(/^\^?zip:\//, ``) 99 | : str.replace(/^\^?zip:/, ``); 100 | } 101 | 102 | // Force enable 'allowLocalPluginLoads' 103 | // TypeScript tries to resolve plugins using a path relative to itself 104 | // which doesn't work when using the global cache 105 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 106 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 107 | // TypeScript already does local loads and if this code is running the user trusts the workspace 108 | // https://github.com/microsoft/vscode/issues/45856 109 | const ConfiguredProject = tsserver.server.ConfiguredProject; 110 | const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = 111 | ConfiguredProject.prototype; 112 | ConfiguredProject.prototype.enablePluginsWithOptions = function () { 113 | this.projectService.allowLocalPluginLoads = true; 114 | return originalEnablePluginsWithOptions.apply(this, arguments); 115 | }; 116 | 117 | // And here is the point where we hijack the VSCode <-> TS communications 118 | // by adding ourselves in the middle. We locate everything that looks 119 | // like an absolute path of ours and normalize it. 120 | 121 | const Session = tsserver.server.Session; 122 | const { onMessage: originalOnMessage, send: originalSend } = 123 | Session.prototype; 124 | let hostInfo = `unknown`; 125 | 126 | return Object.assign(Session.prototype, { 127 | onMessage(/** @type {string} */ message) { 128 | const parsedMessage = JSON.parse(message); 129 | 130 | if ( 131 | parsedMessage != null && 132 | typeof parsedMessage === `object` && 133 | parsedMessage.arguments && 134 | typeof parsedMessage.arguments.hostInfo === `string` 135 | ) { 136 | hostInfo = parsedMessage.arguments.hostInfo; 137 | } 138 | 139 | return originalOnMessage.call( 140 | this, 141 | JSON.stringify(parsedMessage, (key, value) => { 142 | return typeof value === `string` ? fromEditorPath(value) : value; 143 | }), 144 | ); 145 | }, 146 | 147 | send(/** @type {any} */ msg) { 148 | return originalSend.call( 149 | this, 150 | JSON.parse( 151 | JSON.stringify(msg, (key, value) => { 152 | return typeof value === `string` ? toEditorPath(value) : value; 153 | }), 154 | ), 155 | ); 156 | }, 157 | }); 158 | }; 159 | 160 | if (existsSync(absPnpApiPath)) { 161 | if (!process.versions.pnp) { 162 | // Setup the environment to be able to require typescript/lib/tsserver.js 163 | require(absPnpApiPath).setup(); 164 | } 165 | } 166 | 167 | // Defer to the real typescript/lib/tsserver.js your application uses 168 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); 169 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require(`fs`); 4 | const { createRequire, createRequireFromPath } = require(`module`); 5 | const { resolve } = require(`path`); 6 | 7 | const relPnpApiPath = '../../../../.pnp.js'; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.3.4-pnpify", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 5 | spec: "@yarnpkg/plugin-typescript" 6 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 7 | spec: "@yarnpkg/plugin-workspace-tools" 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: "@yarnpkg/plugin-interactive-tools" 10 | 11 | yarnPath: .yarn/releases/yarn-3.8.5.cjs 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Lido JS SDK 7 | 8 | JS SDK for Lido Finance projects. 9 | 10 | ## Packages 11 | 12 | - [@lido-sdk/constants](/packages/constants/README.md). Chain ids, Lido tokens 13 | - [@lido-sdk/contracts](/packages/contracts/README.md). Typed contracts for Lido tokens, ERC20 contract factory 14 | - [@lido-sdk/fetch](/packages/fetch/README.md). Ethereum data fetcher with fallbacks 15 | - [@lido-sdk/helpers](/packages/helpers/README.md) 16 | - [@lido-sdk/providers](/packages/providers/README.md). RPC provider getters with cache 17 | - [@lido-sdk/react](/packages/react/README.md). React hooks and providers. SSR ready 18 | 19 | ## Install 20 | 21 | 1. `yarn && yarn postinstall` 22 | 2. `yarn build` 23 | 24 | ## Usage 25 | 26 | - `yarn build` — Build all packages 27 | - `yarn lint` — Run eslint across packages 28 | - `yarn test` — Run tests across packages 29 | - `yarn test:watch` — Run tests in watch mode 30 | - `yarn typechain` — Generate types 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { esmodules: true } }], 4 | ['@babel/preset-react', { runtime: 'automatic' }], 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [0, 'always', 'Infinity'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: '.jest/cache', 3 | coverageDirectory: '.jest/coverage', 4 | coverageThreshold: { 5 | global: { 6 | branches: 95, 7 | functions: 95, 8 | lines: 95, 9 | statements: 95, 10 | }, 11 | }, 12 | collectCoverageFrom: ['packages/**/src/**/*.{ts,tsx}'], 13 | coveragePathIgnorePatterns: ['src/generated'], 14 | transform: { 15 | '^.+\\.[t|j]sx?$': 'babel-jest', 16 | }, 17 | testEnvironment: 'jest-environment-jsdom', 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lido-js-sdk", 3 | "version": "0.0.0-semantic-release", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "rollup -c", 11 | "test": "jest --coverage", 12 | "test:watch": "jest --watch --coverage", 13 | "lint": "eslint --ext ts,tsx .", 14 | "postinstall": "husky install && yarn typechain", 15 | "typechain": "yarn workspace @lido-sdk/contracts run typechain" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.25.2", 19 | "@babel/preset-env": "^7.25.4", 20 | "@babel/preset-react": "^7.24.7", 21 | "@babel/preset-typescript": "^7.24.7", 22 | "@commitlint/cli": "^13.1.0", 23 | "@commitlint/config-conventional": "^13.1.0", 24 | "@qiwi/multi-semantic-release": "^3.16.0", 25 | "@rollup/plugin-node-resolve": "^13.0.5", 26 | "@types/babel__core": "^7.1.19", 27 | "@types/babel__preset-env": "^7.9.2", 28 | "@types/eslint-plugin-prettier": "^3.1.0", 29 | "@types/jest": "^27.0.2", 30 | "@types/react": "^17.0.45", 31 | "@types/react-dom": "^17.0.17", 32 | "@typescript-eslint/eslint-plugin": "^4.31.2", 33 | "@typescript-eslint/parser": "^4.31.2", 34 | "babel-jest": "^27.2.1", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^8.3.0", 37 | "eslint-plugin-jsx-a11y": "^6.4.1", 38 | "eslint-plugin-prettier": "^4.0.0", 39 | "eslint-plugin-react": "^7.26.0", 40 | "eslint-plugin-react-hooks": "^4.2.0", 41 | "husky": "^7.0.4", 42 | "jest": "^27.2.1", 43 | "lint-staged": "^11.1.2", 44 | "prettier": "^2.4.1", 45 | "react": "^17.0.2", 46 | "react-dom": "^17.0.2", 47 | "rollup": "^2.57.0", 48 | "rollup-plugin-copy": "^3.4.0", 49 | "rollup-plugin-delete": "^2.0.0", 50 | "rollup-plugin-typescript2": "0.32.1", 51 | "tslib": "2.4.1", 52 | "typescript": "^4.4.3", 53 | "yarn-workspaces-list": "^0.2.0" 54 | }, 55 | "lint-staged": { 56 | "./**/src/**/*.{ts,tsx}": [ 57 | "eslint --max-warnings=0", 58 | "jest --bail --findRelatedTests" 59 | ], 60 | "./**/*.{ts,tsx,js,jsx,md,json}": [ 61 | "prettier --write" 62 | ] 63 | }, 64 | "packageManager": "yarn@3.8.5" 65 | } 66 | -------------------------------------------------------------------------------- /packages/constants/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | !node_modules/ 7 | -------------------------------------------------------------------------------- /packages/constants/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/constants/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Constants 7 | 8 | Constants for Lido Finance projects. 9 | Part of [Lido JS SDK](https://github.com/lidofinance/lido-js-sdk/#readme) 10 | 11 | - [Install](#install) 12 | - [Chains](#chains) 13 | - [Chains enum](#chains-enum) 14 | - [Array of chains ids](#array-of-chains-ids) 15 | - [getChainColor](#getchaincolor) 16 | - [Tokens](#tokens) 17 | - [Lido tokens enum](#lido-tokens-enum) 18 | - [getTokenAddress](#gettokenaddress) 19 | - [Aggregator](#aggregator) 20 | - [getAggregatorAddress](#getaggregatoraddress) 21 | - [WithdrawalQueue](#withdrawalqueue) 22 | - [getWithdrawalQueueAddress](#getWithdrawalQueueAddress) 23 | 24 | ## Install 25 | 26 | ```bash 27 | yarn add @lido-sdk/constants 28 | ``` 29 | 30 | ## Chains 31 | 32 | [Source](src/chains.ts) 33 | 34 | ### Chains enum 35 | 36 | ```ts 37 | import { CHAINS } from '@lido-sdk/constants'; 38 | 39 | console.log(CHAINS.Mainnet, CHAINS.Hoodi); // 1, 560048 40 | ``` 41 | 42 | ### Array of chains ids 43 | 44 | ```ts 45 | import { CHAINS_IDS } from '@lido-sdk/constants'; 46 | 47 | console.log(CHAINS_IDS); // [1, 3, 4, 5, 42] 48 | ``` 49 | 50 | ### getChainColor 51 | 52 | Color getter by chain id 53 | 54 | ```ts 55 | import { CHAINS, getChainColor } from '@lido-sdk/constants'; 56 | 57 | const hoodiChainColor = getChainColor(CHAINS.Hoodi); 58 | console.log(hoodiChainColor); // #AA346A 59 | ``` 60 | 61 | ## Tokens 62 | 63 | [Source](src/tokens.ts) 64 | 65 | ### Lido tokens enum 66 | 67 | ```ts 68 | import { TOKENS } from '@lido-sdk/constants'; 69 | 70 | console.log(TOKENS.WSTETH); // WSTETH 71 | console.log(TOKENS.STETH); // STETH 72 | console.log(TOKENS.LDO); // LDO 73 | ``` 74 | 75 | ### getTokenAddress 76 | 77 | Getter for Lido token addresses. Returns a contract address or throws an error if the contract is not deployed in the chain. 78 | 79 | ```ts 80 | import { CHAINS, TOKENS, getTokenAddress } from '@lido-sdk/constants'; 81 | 82 | const stethAddress = getTokenAddress(CHAINS.Mainnet, TOKENS.STETH); 83 | console.log(stethAddress); // 0xae7ab96520de3a18e5e111b5eaab095312d7fe84 84 | ``` 85 | 86 | ## Aggregator 87 | 88 | [Source](src/aggregator.ts) 89 | 90 | EACAggregatorProxy https://etherscan.io/address/0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 91 | It’s used to get the ETH price 92 | 93 | ### getAggregatorAddress 94 | 95 | ```ts 96 | import { CHAINS, getAggregatorAddress } from '@lido-sdk/constants'; 97 | 98 | const aggregatorAddress = getAggregatorAddress(CHAINS.Mainnet); 99 | console.log(aggregatorAddress); // 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 100 | ``` 101 | 102 | ## WithdrawalQueue 103 | 104 | WithdrawalQueue contract for LIDO protocol 105 | 106 | ### getWithdrawalQueueAddress 107 | 108 | ```ts 109 | import { CHAINS, getWithdrawalQueueAddress } from '@lido-sdk/constants'; 110 | 111 | const withdrawalQueueAddress = getWithdrawalQueueAddress(CHAINS.Mainnet); 112 | console.log(withdrawalQueueAddress); // 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1 113 | ``` 114 | -------------------------------------------------------------------------------- /packages/constants/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/constants", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/constants" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^27.0.2" 30 | }, 31 | "dependencies": { 32 | "tiny-invariant": "^1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/constants/src/aggregator.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from './chains'; 3 | 4 | export const AGGREGATOR_BY_NETWORK: { 5 | [key in CHAINS]?: string; 6 | } = { 7 | [CHAINS.Mainnet]: '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', 8 | }; 9 | 10 | export const getAggregatorAddress = (chainId: CHAINS): string => { 11 | const address = AGGREGATOR_BY_NETWORK[chainId]; 12 | invariant(address != null, 'Chain is not supported'); 13 | 14 | return address; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/constants/src/chains.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | 3 | export enum CHAINS { 4 | Mainnet = 1, 5 | Ropsten = 3, // decommissioned 6 | Rinkeby = 4, // decommissioned 7 | Goerli = 5, // deprecated 8 | Kovan = 42, // decommissioned 9 | Kintsugi = 1337702, // decommissioned 10 | Kiln = 1337802, // decommissioned 11 | Holesky = 17000, 12 | Moonbeam = 1284, 13 | Moonriver = 1285, 14 | Moonbase = 1287, 15 | Arbitrum = 42161, 16 | Optimism = 10, 17 | Fuji = 43113, 18 | Avalanche = 43114, 19 | Sepolia = 11155111, 20 | Hoodi = 560048, 21 | } 22 | 23 | export const CHAINS_IDS = [ 24 | CHAINS.Mainnet, 25 | CHAINS.Ropsten, 26 | CHAINS.Holesky, 27 | CHAINS.Rinkeby, 28 | CHAINS.Goerli, 29 | CHAINS.Kovan, 30 | CHAINS.Sepolia, 31 | CHAINS.Hoodi, 32 | ]; 33 | 34 | export const CHAINS_COLORS: { 35 | [key in CHAINS]?: string; 36 | } = { 37 | [CHAINS.Mainnet]: '#29b6af', 38 | [CHAINS.Ropsten]: '#ff4a8d', 39 | [CHAINS.Rinkeby]: '#f6c343', 40 | [CHAINS.Goerli]: '#3099f2', 41 | [CHAINS.Holesky]: '#AA346A', 42 | [CHAINS.Hoodi]: '#AA346A', 43 | [CHAINS.Kovan]: '#9064ff', 44 | [CHAINS.Sepolia]: '#FFD700', 45 | }; 46 | 47 | export const getChainColor = (chainId: CHAINS): string => { 48 | const color = CHAINS_COLORS[chainId]; 49 | invariant(color != null, 'Chain is not supported'); 50 | 51 | return color; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/constants/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregator'; 2 | export * from './withdrawal_queue'; 3 | export * from './chains'; 4 | export * from './tokens'; 5 | -------------------------------------------------------------------------------- /packages/constants/src/tokens.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from './chains'; 3 | 4 | export enum TOKENS { 5 | WSTETH = 'WSTETH', 6 | STETH = 'STETH', 7 | LDO = 'LDO', 8 | } 9 | 10 | export const TOKENS_BY_NETWORK: { 11 | [key in CHAINS]?: { [key in TOKENS]?: string }; 12 | } = { 13 | [CHAINS.Mainnet]: { 14 | [TOKENS.WSTETH]: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', 15 | [TOKENS.STETH]: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', 16 | [TOKENS.LDO]: '0x5a98fcbea516cf06857215779fd812ca3bef1b32', 17 | }, 18 | [CHAINS.Ropsten]: { 19 | [TOKENS.STETH]: '0xd40EefCFaB888C9159a61221def03bF77773FC19', 20 | }, 21 | [CHAINS.Rinkeby]: { 22 | [TOKENS.WSTETH]: '0x2Ca788280fB10384946D3ECC838D94DeCa505CF4', 23 | [TOKENS.STETH]: '0xbA453033d328bFdd7799a4643611b616D80ddd97', 24 | [TOKENS.LDO]: '0xbfcb02cf3df4f36ab8185469834e0e00a5fc6053', 25 | }, 26 | [CHAINS.Goerli]: { 27 | [TOKENS.WSTETH]: '0x6320cD32aA674d2898A68ec82e869385Fc5f7E2f', 28 | [TOKENS.STETH]: '0x1643e812ae58766192cf7d2cf9567df2c37e9b7f', 29 | [TOKENS.LDO]: '0x56340274fB5a72af1A3C6609061c451De7961Bd4', 30 | }, 31 | [CHAINS.Holesky]: { 32 | [TOKENS.WSTETH]: '0x8d09a4502Cc8Cf1547aD300E066060D043f6982D', 33 | [TOKENS.STETH]: '0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034', 34 | [TOKENS.LDO]: '0x14ae7daeecdf57034f3E9db8564e46Dba8D97344', 35 | }, 36 | [CHAINS.Sepolia]: { 37 | [TOKENS.WSTETH]: '0xB82381A3fBD3FaFA77B3a7bE693342618240067b', 38 | [TOKENS.STETH]: '0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af', 39 | [TOKENS.LDO]: '0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB', 40 | }, 41 | [CHAINS.Hoodi]: { 42 | [TOKENS.WSTETH]: '0x7E99eE3C66636DE415D2d7C880938F2f40f94De4', 43 | [TOKENS.STETH]: '0x3508A952176b3c15387C97BE809eaffB1982176a', 44 | [TOKENS.LDO]: '0xEf2573966D009CcEA0Fc74451dee2193564198dc', 45 | }, 46 | }; 47 | 48 | export const getTokenAddress = (chainId: CHAINS, token: TOKENS): string => { 49 | const tokens = TOKENS_BY_NETWORK[chainId]; 50 | invariant(tokens, 'Chain is not supported'); 51 | 52 | const address = tokens[token]; 53 | invariant(address, 'Token is not supported'); 54 | 55 | return address; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/constants/src/withdrawal_queue.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from './chains'; 3 | 4 | export const WITHDRAWAL_QUEUE_BY_NETWORK: { 5 | [key in CHAINS]?: string; 6 | } = { 7 | [CHAINS.Mainnet]: '0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1', 8 | [CHAINS.Goerli]: '0xCF117961421cA9e546cD7f50bC73abCdB3039533', 9 | [CHAINS.Holesky]: '0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50', 10 | [CHAINS.Sepolia]: '0x1583C7b3f4C3B008720E6BcE5726336b0aB25fdd', 11 | [CHAINS.Hoodi]: '0xfe56573178f1bcdf53F01A6E9977670dcBBD9186', 12 | }; 13 | 14 | export const getWithdrawalQueueAddress = (chainId: CHAINS): string => { 15 | const address = WITHDRAWAL_QUEUE_BY_NETWORK[chainId]; 16 | invariant(address, 'Chain is not supported'); 17 | return address; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/constants/test/aggregator.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '../src/chains'; 2 | import { getAggregatorAddress } from '../src/aggregator'; 3 | 4 | describe('getAggregatorAddress', () => { 5 | test('should work if chain is correct', () => { 6 | expect(typeof getAggregatorAddress(CHAINS.Mainnet)).toBe('string'); 7 | }); 8 | test('should throw if chain is incorrect', () => { 9 | expect(() => getAggregatorAddress(-1)).toThrowError(); 10 | expect(() => getAggregatorAddress('' as any)).toThrowError(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/constants/test/chains.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS, getChainColor } from '../src/chains'; 2 | 3 | describe('getChainColor', () => { 4 | test('should work if chain is correct', () => { 5 | expect(typeof getChainColor(CHAINS.Mainnet)).toBe('string'); 6 | expect(typeof getChainColor(CHAINS.Rinkeby)).toBe('string'); 7 | }); 8 | test('should throw if chain is incorrect', () => { 9 | expect(() => getChainColor(-1)).toThrowError(); 10 | expect(() => getChainColor('' as any)).toThrowError(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/constants/test/tokens.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '../src/chains'; 2 | import { getTokenAddress, TOKENS } from '../src/tokens'; 3 | 4 | describe('getTokenAddress', () => { 5 | test('should work if chain is correct', () => { 6 | expect(typeof getTokenAddress(CHAINS.Mainnet, TOKENS.STETH)).toBe('string'); 7 | expect(typeof getTokenAddress(CHAINS.Rinkeby, TOKENS.LDO)).toBe('string'); 8 | }); 9 | test('should throw if chain is incorrect', () => { 10 | expect(() => getTokenAddress(-1, TOKENS.LDO)).toThrowError(); 11 | expect(() => getTokenAddress('' as any, TOKENS.LDO)).toThrowError(); 12 | }); 13 | test('should throw if token is incorrect', () => { 14 | expect(() => getTokenAddress(CHAINS.Mainnet, 'weth' as any)).toThrowError(); 15 | expect(() => getTokenAddress(CHAINS.Mainnet, '' as any)).toThrowError(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/constants/test/withdrawal_queue.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '../src/chains'; 2 | import { getWithdrawalQueueAddress } from '../src/withdrawal_queue'; 3 | 4 | describe('getWithdrawalQueueAddress', () => { 5 | test('should work if chain is correct', () => { 6 | expect(typeof getWithdrawalQueueAddress(CHAINS.Mainnet)).toBe('string'); 7 | }); 8 | test('should throw if chain is incorrect', () => { 9 | expect(() => getWithdrawalQueueAddress(-1)).toThrowError(); 10 | expect(() => getWithdrawalQueueAddress('' as any)).toThrowError(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/constants/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/contracts/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | !node_modules/ 7 | -------------------------------------------------------------------------------- /packages/contracts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/contracts/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Contracts 7 | 8 | Contracts for Lido Finance projects. 9 | Part of [Lido JS SDK](https://github.com/lidofinance/lido-js-sdk/#readme) 10 | 11 | A Contract is an abstraction of code that has been deployed to the blockchain. A Contract may be sent transactions, which will trigger its code to be run with the input of the transaction data. More details in the [ethers docs](https://docs.ethers.io/v5/api/contract/contract/). 12 | 13 | It uses [TypeChain](https://github.com/ethereum-ts/TypeChain) under the hood to generate TypeScript typings for contacts. 14 | 15 | - [Install](#install) 16 | - [Getters](#getters) 17 | - [getERC20Contract](#geterc20contract) 18 | - [getWSTETHContract](#getwstethcontract) 19 | - [getSTETHContract](#getstethcontract) 20 | - [getLDOContract](#getldocontract) 21 | - [getWithdrawalQueueContract](#getwithdrawalqueuecontract) 22 | - [getAggregatorContract](#getaggregatorcontract) 23 | - [Cache](#cache) 24 | 25 | ## Install 26 | 27 | ```bash 28 | yarn add @lido-sdk/contracts 29 | ``` 30 | 31 | ## Getters 32 | 33 | [Source](src/contracts.ts) 34 | 35 | Each getter returns a cached [Contract](https://docs.ethers.io/v5/api/contract/contract/#Contract--creating) instance with an attached [Provider](https://docs.ethers.io/v5/api/providers/) and an [ABI](https://docs.ethers.io/v5/api/utils/abi/). The Provider is required to work with the network and sign transactions and the ABI contains information about methods of the contract on the ethereum side. So, the resulting instance contains all the methods supported by the contract and allows you to call them. 36 | 37 | _If a contract method requires signing a transaction, then you need a provider with [Signer](https://docs.ethers.io/v5/api/signer/)_ 38 | 39 | ### getERC20Contract 40 | 41 | Returns an instance of `Contract` based on [ERC20](https://eips.ethereum.org/EIPS/eip-20) standard contract ABI. 42 | 43 | ```ts 44 | import { getERC20Contract } from '@lido-sdk/contracts'; 45 | import { JsonRpcProvider } from '@ethersproject/providers'; 46 | 47 | const provider = new JsonRpcProvider('http://localhost:8545'); 48 | const contract = getERC20Contract( 49 | '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', 50 | provider, 51 | ); 52 | 53 | const symbol = await contract.symbol(); 54 | const decimals = await contract.decimals(); 55 | ``` 56 | 57 | ### getWSTETHContract 58 | 59 | Returns an instance of `Contract` based on wstETH contract [ABI](https://docs.ethers.io/v5/api/utils/abi/). Available contract methods and detailed documentation can be found here: https://docs.lido.fi/contracts/wsteth 60 | 61 | ### getSTETHContract 62 | 63 | Returns an instance of `Contract` based on stETH contract [ABI](https://docs.ethers.io/v5/api/utils/abi/). Available contract methods and detailed documentation can be found here: https://docs.lido.fi/contracts/lido 64 | 65 | ### getLDOContract 66 | 67 | Returns an instance of `Contract` based on LDO token [ABI](https://docs.ethers.io/v5/api/utils/abi/). LDO Token docs can be found here: https://docs.lido.fi/lido-dao/#ldo-token 68 | 69 | ### getWithdrawalQueueContract 70 | 71 | Returns an instance of `Contract` based on WithdrawalQueue contract [ABI](https://docs.ethers.io/v5/api/utils/abi/). Contract docs here: https://docs.lido.fi/contracts/withdrawal-queue-erc721 72 | 73 | ### getAggregatorContract 74 | 75 | Returns an instance of `Contract` based on ChainLink USD/ETH price oracle [ABI](https://docs.ethers.io/v5/api/utils/abi/). EACAggregatorProxy https://etherscan.io/address/0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 76 | 77 | ## Cache 78 | 79 | To get another contract instance, getters have a third optional parameter `cacheSeed`. 80 | 81 | Calls without `cacheSeed` or with the same `cacheSeed` return the same contracts: 82 | 83 | ```ts 84 | const contractFirst = getERC20Contract('0x0...', provider, 1); 85 | const contractSecond = getERC20Contract('0x0...', provider, 1); 86 | 87 | contractFirst === contractSecond; // true 88 | ``` 89 | 90 | Calls with different `cacheSeed` return different contracts: 91 | 92 | ```ts 93 | const contractFirst = getERC20Contract('0x0...', provider, 1); 94 | const contractSecond = getERC20Contract('0x0...', provider, 2); 95 | 96 | contractFirst !== contractSecond; // true 97 | ``` 98 | 99 | Of course, if the `cacheSeed` is the same, but `address` or `provider` are different the result contracts will also be different: 100 | 101 | ```ts 102 | const contractFirst = getERC20Contract('0x1...', provider, 1); 103 | const contractSecond = getERC20Contract('0x0...', provider, 1); 104 | 105 | contractFirst !== contractSecond; // true, because the addresses are different 106 | ``` 107 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/contracts", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/contracts" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "scripts": { 29 | "typechain": "typechain --target=ethers-v5 --out-dir ./src/generated './src/abi/*.json'" 30 | }, 31 | "devDependencies": { 32 | "@ethersproject/abi": "^5.4.1", 33 | "@ethersproject/abstract-signer": "^5.4.0", 34 | "@ethersproject/bytes": "^5.4.0", 35 | "@ethersproject/contracts": "^5.4.1", 36 | "@ethersproject/providers": "^5.4.5", 37 | "@lido-sdk/providers": "workspace:*", 38 | "@typechain/ethers-v5": "^7.1.0", 39 | "@types/jest": "^27.0.2", 40 | "ethers": "^5.4.7", 41 | "typechain": "^5.1.2" 42 | }, 43 | "peerDependencies": { 44 | "@ethersproject/abstract-signer": "5", 45 | "@ethersproject/contracts": "5", 46 | "@ethersproject/providers": "5", 47 | "ethers": "5" 48 | }, 49 | "dependencies": { 50 | "@lido-sdk/constants": "workspace:*", 51 | "tiny-invariant": "^1.1.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/contracts/src/abi/erc20.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "constant": true, 175 | "inputs": [], 176 | "name": "DOMAIN_SEPARATOR", 177 | "outputs": [{ "name": "", "type": "bytes32" }], 178 | "payable": false, 179 | "stateMutability": "view", 180 | "type": "function" 181 | }, 182 | { 183 | "constant": true, 184 | "inputs": [{ "name": "owner", "type": "address" }], 185 | "name": "nonces", 186 | "outputs": [{ "name": "", "type": "uint256" }], 187 | "payable": false, 188 | "stateMutability": "view", 189 | "type": "function" 190 | }, 191 | { 192 | "constant": false, 193 | "inputs": [ 194 | { "name": "_owner", "type": "address" }, 195 | { "name": "_spender", "type": "address" }, 196 | { "name": "_value", "type": "uint256" }, 197 | { "name": "_deadline", "type": "uint256" }, 198 | { "name": "_v", "type": "uint8" }, 199 | { "name": "_r", "type": "bytes32" }, 200 | { "name": "_s", "type": "bytes32" } 201 | ], 202 | "name": "permit", 203 | "outputs": [], 204 | "payable": false, 205 | "stateMutability": "nonpayable", 206 | "type": "function" 207 | }, 208 | { 209 | "payable": true, 210 | "stateMutability": "payable", 211 | "type": "fallback" 212 | }, 213 | { 214 | "anonymous": false, 215 | "inputs": [ 216 | { 217 | "indexed": true, 218 | "name": "owner", 219 | "type": "address" 220 | }, 221 | { 222 | "indexed": true, 223 | "name": "spender", 224 | "type": "address" 225 | }, 226 | { 227 | "indexed": false, 228 | "name": "value", 229 | "type": "uint256" 230 | } 231 | ], 232 | "name": "Approval", 233 | "type": "event" 234 | }, 235 | { 236 | "anonymous": false, 237 | "inputs": [ 238 | { 239 | "indexed": true, 240 | "name": "from", 241 | "type": "address" 242 | }, 243 | { 244 | "indexed": true, 245 | "name": "to", 246 | "type": "address" 247 | }, 248 | { 249 | "indexed": false, 250 | "name": "value", 251 | "type": "uint256" 252 | } 253 | ], 254 | "name": "Transfer", 255 | "type": "event" 256 | } 257 | ] 258 | -------------------------------------------------------------------------------- /packages/contracts/src/contracts.ts: -------------------------------------------------------------------------------- 1 | import { BaseContract } from '@ethersproject/contracts'; 2 | import { Provider } from '@ethersproject/providers'; 3 | import { Signer } from '@ethersproject/abstract-signer'; 4 | import { 5 | AggregatorAbiFactory, 6 | Erc20AbiFactory, 7 | StethAbiFactory, 8 | WstethAbiFactory, 9 | LdoAbiFactory, 10 | WithdrawalQueueAbiFactory, 11 | } from './factories'; 12 | 13 | export interface Factory { 14 | connect(address: string, signerOrProvider: Signer | Provider): C; 15 | } 16 | 17 | export const createContractGetter = ( 18 | factory: Factory, 19 | ): (( 20 | address: string, 21 | signerOrProvider: Signer | Provider, 22 | cacheSeed?: number, 23 | ) => C) => { 24 | const providerCache = new WeakMap>(); 25 | 26 | return (address, signerOrProvider, cacheSeed = 0) => { 27 | const cacheByAddressKey = `${address}-${cacheSeed}`; 28 | let cacheByAddress = providerCache.get(signerOrProvider); 29 | let contract = cacheByAddress?.[cacheByAddressKey]; 30 | 31 | if (!cacheByAddress) { 32 | cacheByAddress = {}; 33 | providerCache.set(signerOrProvider, cacheByAddress); 34 | } 35 | 36 | if (!contract) { 37 | contract = factory.connect(address, signerOrProvider); 38 | cacheByAddress[cacheByAddressKey] = contract; 39 | } 40 | 41 | return contract; 42 | }; 43 | }; 44 | 45 | export const getAggregatorContract = createContractGetter(AggregatorAbiFactory); 46 | export const getERC20Contract = createContractGetter(Erc20AbiFactory); 47 | export const getSTETHContract = createContractGetter(StethAbiFactory); 48 | export const getWSTETHContract = createContractGetter(WstethAbiFactory); 49 | export const getLDOContract = createContractGetter(LdoAbiFactory); 50 | export const getWithdrawalQueueContract = createContractGetter( 51 | WithdrawalQueueAbiFactory, 52 | ); 53 | -------------------------------------------------------------------------------- /packages/contracts/src/factories.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AggregatorAbi__factory as AggregatorAbiFactory, 3 | Erc20Abi__factory as Erc20AbiFactory, 4 | StethAbi__factory as StethAbiFactory, 5 | WstethAbi__factory as WstethAbiFactory, 6 | LdoAbi__factory as LdoAbiFactory, 7 | WithdrawalQueueAbi__factory as WithdrawalQueueAbiFactory, 8 | } from './generated'; 9 | -------------------------------------------------------------------------------- /packages/contracts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contracts'; 2 | export * from './factories'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/contracts/src/types.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AggregatorAbi, 3 | Erc20Abi, 4 | StethAbi, 5 | WstethAbi, 6 | WithdrawalQueueAbi, 7 | } from './generated'; 8 | -------------------------------------------------------------------------------- /packages/contracts/test/contracts.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHAINS, 3 | getTokenAddress, 4 | getAggregatorAddress, 5 | getWithdrawalQueueAddress, 6 | TOKENS, 7 | } from '@lido-sdk/constants'; 8 | import { getRpcProvider } from '@lido-sdk/providers'; 9 | import { 10 | getAggregatorContract, 11 | getERC20Contract, 12 | getSTETHContract, 13 | getWSTETHContract, 14 | getWithdrawalQueueContract, 15 | } from '../src/contracts'; 16 | 17 | describe('getAggregatorContract', () => { 18 | test('should create a contract', () => { 19 | const address = getAggregatorAddress(CHAINS.Mainnet); 20 | const provider = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 21 | const contract = getAggregatorContract(address, provider); 22 | 23 | expect(contract).toBeInstanceOf(Object); 24 | expect(contract).toEqual(expect.objectContaining({ address })); 25 | }); 26 | }); 27 | 28 | describe('getWithdrawalQueueContract', () => { 29 | test('should create a contract', () => { 30 | const address = getWithdrawalQueueAddress(CHAINS.Mainnet); 31 | const provider = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 32 | const contract = getWithdrawalQueueContract(address, provider); 33 | 34 | expect(contract).toBeInstanceOf(Object); 35 | expect(contract).toEqual(expect.objectContaining({ address })); 36 | }); 37 | }); 38 | 39 | describe('getERC20Contract', () => { 40 | test('should create a contract', () => { 41 | const address = getTokenAddress(CHAINS.Mainnet, TOKENS.STETH); 42 | const provider = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 43 | const contract = getERC20Contract(address, provider); 44 | 45 | expect(contract).toBeInstanceOf(Object); 46 | expect(contract).toEqual(expect.objectContaining({ address })); 47 | }); 48 | }); 49 | 50 | describe('getSTETHContract', () => { 51 | test('should create a contract', () => { 52 | const address = getTokenAddress(CHAINS.Mainnet, TOKENS.STETH); 53 | const provider = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 54 | const contract = getSTETHContract(address, provider); 55 | 56 | expect(contract).toBeInstanceOf(Object); 57 | expect(contract).toEqual(expect.objectContaining({ address })); 58 | }); 59 | }); 60 | 61 | describe('getWSTETHContract', () => { 62 | test('should create a contract', () => { 63 | const address = getTokenAddress(CHAINS.Mainnet, TOKENS.WSTETH); 64 | const provider = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 65 | const contract = getWSTETHContract(address, provider); 66 | 67 | expect(contract).toBeInstanceOf(Object); 68 | expect(contract).toEqual(expect.objectContaining({ address })); 69 | }); 70 | }); 71 | 72 | describe('cache', () => { 73 | const addressFirst = getTokenAddress(CHAINS.Mainnet, TOKENS.STETH); 74 | const addressSecond = getTokenAddress(CHAINS.Rinkeby, TOKENS.WSTETH); 75 | 76 | const providerFirst = getRpcProvider(CHAINS.Mainnet, '/api/rpc'); 77 | const providerSecond = getRpcProvider(CHAINS.Rinkeby, '/api/rpc'); 78 | 79 | test('should use cache if args are equal', () => { 80 | const contractFirst = getERC20Contract(addressFirst, providerFirst); 81 | const contractSecond = getERC20Contract(addressFirst, providerFirst); 82 | 83 | expect(contractFirst).toBe(contractSecond); 84 | }); 85 | 86 | test('should be different if addresses are different', () => { 87 | const contractFirst = getERC20Contract(addressFirst, providerFirst); 88 | const contractSecond = getERC20Contract(addressSecond, providerFirst); 89 | 90 | expect(addressFirst).not.toBe(addressSecond); 91 | expect(contractFirst).not.toBe(contractSecond); 92 | }); 93 | 94 | test('should be different if providers are different', () => { 95 | const contractFirst = getERC20Contract(addressFirst, providerFirst); 96 | const contractSecond = getERC20Contract(addressSecond, providerSecond); 97 | 98 | expect(providerFirst).not.toBe(providerSecond); 99 | expect(contractFirst).not.toBe(contractSecond); 100 | }); 101 | 102 | test('should use cache if seeds are equal', () => { 103 | const contractFirst = getERC20Contract(addressFirst, providerFirst, 1); 104 | const contractSecond = getERC20Contract(addressFirst, providerFirst, 1); 105 | 106 | expect(contractFirst).toBe(contractSecond); 107 | }); 108 | 109 | test('should be different if seeds are different', () => { 110 | const contractFirst = getERC20Contract(addressFirst, providerFirst, 1); 111 | const contractSecond = getERC20Contract(addressFirst, providerFirst, 2); 112 | 113 | expect(contractFirst).not.toBe(contractSecond); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/fetch/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | !node_modules/ 7 | -------------------------------------------------------------------------------- /packages/fetch/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/fetch/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Fetchers 7 | 8 | Fetchers for Lido Finance projects. 9 | Part of [Lido JS SDK](https://github.com/lidofinance/lido-js-sdk/#readme) 10 | 11 | - [Install](#install) 12 | - [Fetch](#fetch) 13 | - [Fetch With Fallbacks](#fetch-with-fallbacks) 14 | - [Fetch RPC](#fetch-rpc) 15 | - [Options](#options) 16 | - [Helpers](#helpers) 17 | - [getInfuraRPCUrl](#getinfurarpcurl) 18 | - [getAlchemyRPCUrl](#getalchemyrpcurl) 19 | 20 | ## Install 21 | 22 | ```bash 23 | yarn add @lido-sdk/fetch 24 | ``` 25 | 26 | ## Fetch 27 | 28 | node-fetch package [NPM](https://www.npmjs.com/package/node-fetch), [Github](https://github.com/node-fetch/node-fetch) 29 | 30 | ```ts 31 | import { fetch } from '@lido-sdk/fetch'; 32 | 33 | const response = await fetch('https://example.com'); 34 | const result = await response.json(); 35 | ``` 36 | 37 | ## Fetch With Fallbacks 38 | 39 | [Source](src/fetchWithFallbacks.ts) 40 | 41 | A wrapper over `fetch` which takes an array of URLs instead of a single URL. If a request throws an exception, it takes the following URL from the array and repeats the request to it. 42 | 43 | ```ts 44 | import { fetchWithFallbacks } from '@lido-sdk/fetch'; 45 | 46 | const urls = ['https://example.com', 'https://fallback.com']; 47 | 48 | const response = await fetchWithFallbacks(urls); 49 | const result = await response.json(); 50 | ``` 51 | 52 | ## Fetch RPC 53 | 54 | [Source](src/fetchRPC.ts) 55 | 56 | A wrapper over `fetchWithFallbacks`, which is useful as a backend part of proxying RPC requests from frontend to API provider. 57 | 58 | ```ts 59 | import { fetchRPC } from '@lido-sdk/fetch'; 60 | 61 | const options = { 62 | urls: [ 63 | 'http://your_rpc_server.url', 64 | (chainId) => `http://your_rpc_server.url/?chainId=${chainId}`, 65 | ], 66 | providers: { 67 | infura: 'INFURA_API_KEY', 68 | alchemy: 'ALCHEMY_API_KEY', 69 | }, 70 | }; 71 | 72 | const rpc = async (req, res) => { 73 | // chainId and body from request 74 | const chainId = Number(req.query.chainId); 75 | const body = JSON.stringify(req.body); 76 | 77 | const response = await fetchRPC(chainId, { body, ...options }); 78 | const result = await response.json(); 79 | 80 | res.json(result); 81 | }; 82 | ``` 83 | 84 | ### Options 85 | 86 | Options extend `RequestInit` interface with the `urls` and `providers`. `urls` have priority over `providers`. 87 | 88 | ```tsx 89 | import { CHAINS } from '@lido-sdk/constants'; 90 | import { RequestInit } from 'node-fetch'; 91 | 92 | interface FetchRPCOptions extends RequestInit { 93 | providers?: { 94 | infura?: string; 95 | alchemy?: string; 96 | }; 97 | urls?: Array string>; 98 | } 99 | ``` 100 | 101 | ## Helpers 102 | 103 | [Source](src/providersUrls.ts) 104 | 105 | ### getInfuraRPCUrl 106 | 107 | Returns [infura](https://infura.io/) endpoint for API key and chainId 108 | 109 | ```ts 110 | import { getInfuraRPCUrl } from '@lido-sdk/fetch'; 111 | import { CHAINS } from '@lido-sdk/constants'; 112 | 113 | const url = getInfuraRPCUrl(CHAINS.Mainnet, 'YOUR_API_KEY'); 114 | console.log(url); // https://mainnet.infura.io/v3/YOUR_API_KEY 115 | ``` 116 | 117 | ### getAlchemyRPCUrl 118 | 119 | Returns [alchemy](https://www.alchemy.com/) endpoint for API key and chainId 120 | 121 | ```ts 122 | import { getAlchemyRPCUrl } from '@lido-sdk/fetch'; 123 | import { CHAINS } from '@lido-sdk/constants'; 124 | 125 | const url = getAlchemyRPCUrl(CHAINS.Mainnet, 'YOUR_API_KEY'); 126 | console.log(url); // https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY 127 | ``` 128 | -------------------------------------------------------------------------------- /packages/fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/fetch", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/fetch" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^27.0.2", 30 | "@types/node-fetch": "^2.5.12" 31 | }, 32 | "dependencies": { 33 | "@lido-sdk/constants": "workspace:*", 34 | "node-fetch": "^2.6.7", 35 | "tiny-invariant": "^1.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/fetch/src/fetch.ts: -------------------------------------------------------------------------------- 1 | export { default as fetch } from 'node-fetch'; 2 | export { default as default } from 'node-fetch'; 3 | export * from 'node-fetch'; 4 | -------------------------------------------------------------------------------- /packages/fetch/src/fetchRPC.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from '@lido-sdk/constants'; 3 | import { fetchWithFallbacks } from './fetchWithFallbacks'; 4 | import { getRPCUrls, RPCProvidersKeys } from './providersUrls'; 5 | import { RequestInit, Response } from './fetch'; 6 | 7 | export type FetRPCUrl = (chainId: CHAINS) => string; 8 | 9 | export interface FetchRPCOptions extends RequestInit { 10 | providers?: RPCProvidersKeys; 11 | urls?: (string | FetRPCUrl)[]; 12 | } 13 | 14 | export type FetchRPC = ( 15 | chainId: CHAINS, 16 | options: FetchRPCOptions, 17 | ) => Promise; 18 | 19 | export type CreateRPCFetcher = (options: FetchRPCOptions) => FetchRPC; 20 | 21 | export const fetchRPC: FetchRPC = (chainId, options) => { 22 | const { providers = {}, urls = [], ...init } = options; 23 | 24 | const customUrls = urls.map((value) => { 25 | let url = value; 26 | if (typeof value === 'function') url = value(chainId); 27 | invariant(typeof url === 'string', 'URL should be a string'); 28 | 29 | return url; 30 | }); 31 | const providersUrls = getRPCUrls(chainId, providers); 32 | const combinedUrls = [...customUrls, ...providersUrls]; 33 | 34 | invariant(combinedUrls.length > 0, 'There are no API keys or URLs provided'); 35 | 36 | return fetchWithFallbacks(combinedUrls, { method: 'POST', ...init }); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/fetch/src/fetchWithFallbacks.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import nodeFetch, { RequestInfo, RequestInit, Response } from './fetch'; 3 | 4 | export interface FetchWithFallbacksOptions extends RequestInit { 5 | fetch?: (url: RequestInfo, init?: RequestInit) => Promise; 6 | } 7 | 8 | export type FetchWithFallbacks = ( 9 | inputs: RequestInfo[], 10 | options?: FetchWithFallbacksOptions, 11 | ) => Promise; 12 | 13 | export const fetchWithFallbacks: FetchWithFallbacks = async ( 14 | inputs, 15 | options = {}, 16 | ) => { 17 | invariant(inputs.length > 0, 'Inputs are required'); 18 | 19 | const { fetch = nodeFetch, ...init } = options; 20 | const [input, ...restInputs] = inputs; 21 | 22 | try { 23 | const response = await fetch(input, init); 24 | 25 | invariant(response?.ok, 'Request failed'); 26 | 27 | return response; 28 | } catch (error) { 29 | if (!restInputs.length) throw error; 30 | return await fetchWithFallbacks(restInputs, options); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetch'; 2 | export * from './fetchRPC'; 3 | export * from './fetchWithFallbacks'; 4 | export * from './providersUrls'; 5 | -------------------------------------------------------------------------------- /packages/fetch/src/providersUrls.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '@lido-sdk/constants'; 2 | import invariant from 'tiny-invariant'; 3 | 4 | export const getInfuraRPCUrl = (chainId: CHAINS, apiKey: string): string => { 5 | invariant(apiKey && typeof apiKey === 'string', 'API key should be a string'); 6 | 7 | switch (chainId) { 8 | case CHAINS.Mainnet: 9 | return `https://mainnet.infura.io/v3/${apiKey}`; 10 | case CHAINS.Ropsten: 11 | return `https://ropsten.infura.io/v3/${apiKey}`; 12 | case CHAINS.Rinkeby: 13 | return `https://rinkeby.infura.io/v3/${apiKey}`; 14 | case CHAINS.Goerli: 15 | return `https://goerli.infura.io/v3/${apiKey}`; 16 | case CHAINS.Kovan: 17 | return `https://kovan.infura.io/v3/${apiKey}`; 18 | case CHAINS.Holesky: 19 | return `https://holesky.infura.io/v3/${apiKey}`; 20 | case CHAINS.Sepolia: 21 | return `https://sepolia.infura.io/v3/${apiKey}`; 22 | case CHAINS.Hoodi: 23 | return `https://hoodi.infura.io/v3/${apiKey}`; 24 | default: 25 | invariant(false, 'Chain is not supported'); 26 | } 27 | }; 28 | 29 | export const getAlchemyRPCUrl = (chainId: CHAINS, apiKey: string): string => { 30 | invariant(apiKey && typeof apiKey === 'string', 'API key should be a string'); 31 | 32 | switch (chainId) { 33 | case CHAINS.Mainnet: 34 | return `https://eth-mainnet.alchemyapi.io/v2/${apiKey}`; 35 | case CHAINS.Ropsten: 36 | return `https://eth-ropsten.alchemyapi.io/v2/${apiKey}`; 37 | case CHAINS.Rinkeby: 38 | return `https://eth-rinkeby.alchemyapi.io/v2/${apiKey}`; 39 | case CHAINS.Goerli: 40 | return `https://eth-goerli.alchemyapi.io/v2/${apiKey}`; 41 | case CHAINS.Kovan: 42 | return `https://eth-kovan.alchemyapi.io/v2/${apiKey}`; 43 | case CHAINS.Holesky: 44 | return `https://eth-holesky.alchemyapi.io/v2/${apiKey}`; 45 | case CHAINS.Sepolia: 46 | return `https://eth-sepolia.g.alchemy.com/v2/${apiKey}`; 47 | case CHAINS.Hoodi: 48 | return `https://eth-hoodi.g.alchemy.com/v2/${apiKey}`; 49 | default: 50 | invariant(false, 'Chain is not supported'); 51 | } 52 | }; 53 | 54 | export interface RPCProvidersKeys { 55 | infura?: string; 56 | alchemy?: string; 57 | } 58 | 59 | export const getRPCUrls = ( 60 | chainId: CHAINS, 61 | keys: RPCProvidersKeys, 62 | ): string[] => { 63 | const urls = []; 64 | 65 | if (keys.alchemy) urls.push(getAlchemyRPCUrl(chainId, keys.alchemy)); 66 | if (keys.infura) urls.push(getInfuraRPCUrl(chainId, keys.infura)); 67 | 68 | return urls; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/fetch/test/fetchRPC.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('node-fetch'); 2 | 3 | import fetch from 'node-fetch'; 4 | import { CHAINS } from '@lido-sdk/constants'; 5 | import { fetchRPC } from '../src/fetchRPC'; 6 | 7 | const { Response } = jest.requireActual('node-fetch'); 8 | const mockFetch = fetch as jest.MockedFunction; 9 | 10 | describe('fetchRPC', () => { 11 | const params = { method: 'POST' }; 12 | const expected = 42; 13 | 14 | afterEach(() => { 15 | mockFetch.mockReset(); 16 | }); 17 | 18 | test('should throw if providers or urls are not passed', () => { 19 | expect(() => fetchRPC(CHAINS.Mainnet, {})).toThrowError(); 20 | }); 21 | 22 | test('should fetch correctly if providers are passed', async () => { 23 | const providers = { infura: 'API_KEY' }; 24 | mockFetch.mockReturnValue(Promise.resolve(new Response(expected))); 25 | 26 | const response = await fetchRPC(CHAINS.Mainnet, { providers }); 27 | const result = await response.json(); 28 | 29 | expect(fetch).toHaveBeenCalledTimes(1); 30 | expect(fetch).toHaveBeenCalledWith( 31 | expect.stringContaining('infura'), 32 | params, 33 | ); 34 | expect(result).toBe(expected); 35 | }); 36 | 37 | test('should fetch correctly if urls are passed', async () => { 38 | const url = 'https://example.com'; 39 | mockFetch.mockReturnValue(Promise.resolve(new Response(expected))); 40 | 41 | const response = await fetchRPC(CHAINS.Mainnet, { urls: [url] }); 42 | const result = await response.json(); 43 | 44 | expect(fetch).toHaveBeenCalledTimes(1); 45 | expect(fetch).toHaveBeenCalledWith(url, params); 46 | expect(result).toBe(expected); 47 | }); 48 | 49 | test('should combine an url string correctly', async () => { 50 | const url = (chainId: CHAINS) => `https://example.com?chainId=${chainId}`; 51 | mockFetch.mockReturnValue(Promise.resolve(new Response(expected))); 52 | 53 | await fetchRPC(CHAINS.Mainnet, { urls: [url] }); 54 | 55 | expect(fetch).toHaveBeenCalledTimes(1); 56 | expect(fetch).toHaveBeenCalledWith(url(CHAINS.Mainnet), params); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/fetch/test/fetchWithFallbacks.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('node-fetch'); 2 | 3 | import fetch from 'node-fetch'; 4 | import { fetchWithFallbacks } from '../src/fetchWithFallbacks'; 5 | 6 | const { Response } = jest.requireActual('node-fetch'); 7 | const mockFetch = fetch as jest.MockedFunction; 8 | 9 | describe('fetchWithFallbacks', () => { 10 | const params = { method: 'POST' }; 11 | const urlFirst = 'https://first.com'; 12 | const urlSecond = 'https://second.com'; 13 | const expected = 42; 14 | 15 | afterEach(() => { 16 | mockFetch.mockReset(); 17 | }); 18 | 19 | test('should throw if inputs are not passed', async () => { 20 | await expect(fetchWithFallbacks([])).rejects.toThrowError(); 21 | }); 22 | 23 | test('should fetch correctly', async () => { 24 | mockFetch.mockReturnValue(Promise.resolve(new Response(expected))); 25 | 26 | const response = await fetchWithFallbacks([urlFirst], params); 27 | const result = await response.json(); 28 | 29 | expect(fetch).toHaveBeenCalledTimes(1); 30 | expect(fetch).toHaveBeenCalledWith(urlFirst, params); 31 | expect(result).toBe(expected); 32 | }); 33 | 34 | test('should be rejected if both urls reject', async () => { 35 | mockFetch.mockReturnValue(Promise.reject(new Error())); 36 | 37 | await expect( 38 | fetchWithFallbacks([urlFirst, urlSecond], params), 39 | ).rejects.toThrowError(); 40 | 41 | expect(fetch).toHaveBeenCalledTimes(2); 42 | expect(fetch).toHaveBeenNthCalledWith(1, urlFirst, params); 43 | expect(fetch).toHaveBeenNthCalledWith(2, urlSecond, params); 44 | }); 45 | 46 | test('should use second url', async () => { 47 | mockFetch.mockReturnValue(Promise.resolve(new Response(expected))); 48 | mockFetch.mockReturnValueOnce(Promise.reject(new Error())); 49 | 50 | const response = await fetchWithFallbacks([urlFirst, urlSecond], params); 51 | const result = await response.json(); 52 | 53 | expect(fetch).toHaveBeenCalledTimes(2); 54 | expect(fetch).toHaveBeenNthCalledWith(1, urlFirst, params); 55 | expect(fetch).toHaveBeenNthCalledWith(2, urlSecond, params); 56 | expect(result).toBe(expected); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/fetch/test/providersUrls.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS, CHAINS_IDS } from '@lido-sdk/constants'; 2 | import { 3 | getInfuraRPCUrl, 4 | getAlchemyRPCUrl, 5 | getRPCUrls, 6 | } from '../src/providersUrls'; 7 | 8 | describe('getInfuraRPCUrl', () => { 9 | test('should work if chain is correct', () => { 10 | CHAINS_IDS.forEach((chainId) => { 11 | expect(typeof getInfuraRPCUrl(chainId, 'API_KEY')).toBe('string'); 12 | }); 13 | }); 14 | test('should throw if chain is incorrect', () => { 15 | expect(() => getInfuraRPCUrl(-1, 'API_KEY')).toThrowError(); 16 | expect(() => getInfuraRPCUrl('' as any, 'API_KEY')).toThrowError(); 17 | }); 18 | test('should throw if API key is incorrect', () => { 19 | expect(() => getInfuraRPCUrl(CHAINS.Mainnet, '')).toThrowError(); 20 | expect(() => getInfuraRPCUrl(CHAINS.Mainnet, null as any)).toThrowError(); 21 | }); 22 | }); 23 | 24 | describe('getAlchemyRPCUrl', () => { 25 | CHAINS_IDS.forEach((chainId) => { 26 | expect(typeof getAlchemyRPCUrl(chainId, 'API_KEY')).toBe('string'); 27 | }); 28 | test('should throw if chain is incorrect', () => { 29 | expect(() => getAlchemyRPCUrl(-1, 'API_KEY')).toThrowError(); 30 | expect(() => getAlchemyRPCUrl('' as any, 'API_KEY')).toThrowError(); 31 | }); 32 | test('should throw if API key is incorrect', () => { 33 | expect(() => getAlchemyRPCUrl(CHAINS.Mainnet, '')).toThrowError(); 34 | expect(() => getAlchemyRPCUrl(CHAINS.Mainnet, null as any)).toThrowError(); 35 | }); 36 | }); 37 | 38 | describe('getRPCUrls', () => { 39 | test('should work correctly', () => { 40 | expect( 41 | getRPCUrls(CHAINS.Mainnet, { 42 | alchemy: 'API_KEY', 43 | infura: 'API_KEY', 44 | }), 45 | ).toEqual([ 46 | expect.stringContaining('alchemy'), 47 | expect.stringContaining('infura'), 48 | ]); 49 | }); 50 | 51 | test('should return empty array', () => { 52 | expect(getRPCUrls(CHAINS.Mainnet, {})).toEqual([]); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/fetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/helpers/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | -------------------------------------------------------------------------------- /packages/helpers/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/helpers/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Helpers 7 | 8 | Helpers for Lido Finance projects. 9 | Part of [Lido JS SDK](https://github.com/lidofinance/lido-js-sdk/#readme) 10 | 11 | - [Install](#install) 12 | - [Etherscan](#etherscan) 13 | - [getEtherscanTxLink](#getetherscantxlink) 14 | - [getEtherscanTokenLink](#getetherscantokenlink) 15 | - [getEtherscanAddressLink](#getetherscanaddresslink) 16 | - [Open window](#open-window) 17 | 18 | ## Install 19 | 20 | ```bash 21 | yarn add @lido-sdk/helpers 22 | ``` 23 | 24 | ## Etherscan 25 | 26 | A set of functions for generating links to [etherscan](https://etherscan.io/) 27 | 28 | ### getEtherscanTxLink 29 | 30 | ```ts 31 | import { getEtherscanTxLink } from '@lido-sdk/helpers'; 32 | import { CHAINS } from '@lido-sdk/constants'; 33 | 34 | const link = getEtherscanTxLink( 35 | CHAINS.Mainnet, 36 | '0x0000000000000000000000000000000000000000000000000000000000000000', 37 | ); 38 | console.log(link); // https://etherscan.io/tx/0x0000000000000000000000000000000000000000000000000000000000000000 39 | ``` 40 | 41 | ### getEtherscanTokenLink 42 | 43 | ```ts 44 | import { getEtherscanTokenLink } from '@lido-sdk/helpers'; 45 | import { CHAINS } from '@lido-sdk/constants'; 46 | 47 | const link = getEtherscanTokenLink( 48 | CHAINS.Mainnet, 49 | '0x0000000000000000000000000000000000000000', 50 | ); 51 | console.log(link); // https://etherscan.io/address/0x0000000000000000000000000000000000000000 52 | ``` 53 | 54 | ### getEtherscanAddressLink 55 | 56 | ```ts 57 | import { getEtherscanAddressLink } from '@lido-sdk/helpers'; 58 | import { CHAINS } from '@lido-sdk/constants'; 59 | 60 | const link = getEtherscanAddressLink( 61 | CHAINS.Mainnet, 62 | '0x0000000000000000000000000000000000000000', 63 | ); 64 | console.log(link); // https://etherscan.io/address/0x0000000000000000000000000000000000000000 65 | ``` 66 | 67 | ## Open window 68 | 69 | ```ts 70 | import { openWindow } from '@lido-sdk/helpers'; 71 | 72 | openWindow('https://lido.fi'); 73 | ``` 74 | -------------------------------------------------------------------------------- /packages/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/helpers", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/helpers" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "devDependencies": { 29 | "@ethersproject/bignumber": "^5.4.2", 30 | "@types/jest": "^27.0.2" 31 | }, 32 | "dependencies": { 33 | "@lido-sdk/constants": "workspace:*", 34 | "tiny-invariant": "^1.1.0" 35 | }, 36 | "peerDependencies": { 37 | "@ethersproject/bignumber": "5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/helpers/src/divide.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import invariant from 'tiny-invariant'; 3 | 4 | const PRECISION = 6; 5 | 6 | export const divide = ( 7 | number: BigNumber, 8 | divider: BigNumber, 9 | precision = PRECISION, 10 | ): number => { 11 | invariant(number != null, 'Number is required'); 12 | invariant(divider != null, 'Divider is required'); 13 | 14 | const multiplier = 10 ** precision; 15 | return number.mul(multiplier).div(divider).toNumber() / multiplier; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/helpers/src/etherscan.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from '@lido-sdk/constants'; 3 | 4 | export enum ETHERSCAN_ENTITIES { 5 | Tx = 'tx', 6 | Token = 'token', 7 | Address = 'address', 8 | } 9 | 10 | export type EtherscanEntities = `${ETHERSCAN_ENTITIES}`; 11 | 12 | export const ETHERSCAN_PREFIX_BY_NETWORK: { 13 | [key in CHAINS]?: string; 14 | } = { 15 | [CHAINS.Mainnet]: '', 16 | [CHAINS.Ropsten]: 'ropsten.', 17 | [CHAINS.Rinkeby]: 'rinkeby.', 18 | [CHAINS.Goerli]: 'goerli.', 19 | [CHAINS.Kovan]: 'kovan.', 20 | [CHAINS.Holesky]: 'holesky.', 21 | [CHAINS.Sepolia]: 'sepolia.', 22 | [CHAINS.Hoodi]: 'hoodi.', 23 | }; 24 | 25 | export const getEtherscanPrefix = (chainId: CHAINS): string => { 26 | const prefix = ETHERSCAN_PREFIX_BY_NETWORK[chainId]; 27 | invariant(prefix != null, 'Chain is not supported'); 28 | 29 | return prefix; 30 | }; 31 | 32 | export const getEtherscanLink = ( 33 | chainId: CHAINS, 34 | hash: string, 35 | entity: EtherscanEntities, 36 | ): string => { 37 | const prefix = getEtherscanPrefix(chainId); 38 | invariant(hash && typeof hash === 'string', 'Hash should be a string'); 39 | invariant(entity && typeof entity === 'string', 'Entity should be a string'); 40 | 41 | return `https://${prefix}etherscan.io/${entity}/${hash}`; 42 | }; 43 | 44 | export const getEtherscanTxLink = (chainId: CHAINS, hash: string): string => { 45 | return getEtherscanLink(chainId, hash, ETHERSCAN_ENTITIES.Tx); 46 | }; 47 | 48 | export const getEtherscanTokenLink = ( 49 | chainId: CHAINS, 50 | hash: string, 51 | ): string => { 52 | return getEtherscanLink(chainId, hash, ETHERSCAN_ENTITIES.Token); 53 | }; 54 | 55 | export const getEtherscanAddressLink = ( 56 | chainId: CHAINS, 57 | hash: string, 58 | ): string => { 59 | return getEtherscanLink(chainId, hash, ETHERSCAN_ENTITIES.Address); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './divide'; 2 | export * from './etherscan'; 3 | export * from './openWindow'; 4 | -------------------------------------------------------------------------------- /packages/helpers/src/openWindow.ts: -------------------------------------------------------------------------------- 1 | export const openWindow = (url: string): void => { 2 | if (typeof window === 'undefined') return; 3 | 4 | window.open(url, '_blank', 'noopener,noreferrer'); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/helpers/test/divide.test.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import { divide } from '../src/divide'; 3 | 4 | describe('divide', () => { 5 | test('should divide correctly', () => { 6 | expect(divide(BigNumber.from(10000), BigNumber.from(100))).toBe(100); 7 | }); 8 | 9 | test('should divide with precision', () => { 10 | const precision = 3; 11 | const result = divide(BigNumber.from(1), BigNumber.from(17), precision); 12 | 13 | expect(result).toBe(0.058); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/helpers/test/etherscan.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '@lido-sdk/constants'; 2 | import { 3 | getEtherscanPrefix, 4 | getEtherscanLink, 5 | getEtherscanTxLink, 6 | getEtherscanTokenLink, 7 | getEtherscanAddressLink, 8 | } from '../src/etherscan'; 9 | 10 | describe('getEtherscanPrefix', () => { 11 | test('should work if chain is correct', () => { 12 | expect(typeof getEtherscanPrefix(CHAINS.Mainnet)).toBe('string'); 13 | expect(typeof getEtherscanPrefix(CHAINS.Rinkeby)).toBe('string'); 14 | }); 15 | test('should throw if chain is incorrect', () => { 16 | expect(() => getEtherscanPrefix(-1)).toThrowError(); 17 | expect(() => getEtherscanPrefix('' as any)).toThrowError(); 18 | }); 19 | }); 20 | 21 | describe('getEtherscanLink', () => { 22 | test('should work if params are correct', () => { 23 | expect(typeof getEtherscanLink(CHAINS.Mainnet, '0', 'tx')).toBe('string'); 24 | expect(typeof getEtherscanLink(CHAINS.Kovan, '0', 'token')).toBe('string'); 25 | }); 26 | test('should throw if chain is incorrect', () => { 27 | expect(() => getEtherscanLink(-1, '0', 'tx')).toThrowError(); 28 | }); 29 | test('should throw if hash is incorrect', () => { 30 | expect(() => getEtherscanLink(CHAINS.Mainnet, '', 'tx')).toThrowError(); 31 | }); 32 | test('should throw if entity is incorrect', () => { 33 | expect(() => getEtherscanLink(CHAINS.Kovan, '0', '' as any)).toThrowError(); 34 | }); 35 | }); 36 | 37 | describe('getEtherscanTxLink', () => { 38 | test('should work correctly', () => { 39 | expect(typeof getEtherscanTxLink(CHAINS.Mainnet, '0')).toBe('string'); 40 | }); 41 | }); 42 | 43 | describe('getEtherscanTokenLink', () => { 44 | test('should work correctly', () => { 45 | expect(typeof getEtherscanTokenLink(CHAINS.Mainnet, '0')).toBe('string'); 46 | }); 47 | }); 48 | 49 | describe('getEtherscanAddressLink', () => { 50 | test('should work correctly', () => { 51 | expect(typeof getEtherscanAddressLink(CHAINS.Mainnet, '0')).toBe('string'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/helpers/test/openWindow.test.ts: -------------------------------------------------------------------------------- 1 | import { openWindow } from '../src/openWindow'; 2 | 3 | describe('openWindow', () => { 4 | test('should open url', () => { 5 | const url = 'http://foo.bar'; 6 | const spy = jest.spyOn(window, 'open').mockImplementation(() => null); 7 | 8 | openWindow(url); 9 | expect(spy).toHaveBeenCalledWith(url, '_blank', 'noopener,noreferrer'); 10 | 11 | spy.mockRestore(); 12 | }); 13 | 14 | test('should not throw an error on server side', () => { 15 | const url = 'http://foo.bar'; 16 | const spy = jest 17 | .spyOn(global, 'window', 'get') 18 | .mockReturnValue(undefined as any); 19 | 20 | expect(() => openWindow(url)).not.toThrow(); 21 | 22 | spy.mockRestore(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/providers/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | !node_modules/ 7 | -------------------------------------------------------------------------------- /packages/providers/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/providers/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️DEPRECATION WARNING⚠️ 2 | 3 | This project is being slowly deprecated and may not receive further updates. 4 | Check out [modern Lido SDK](https://github.com/lidofinance/lido-ethereum-sdk/pulls) to access latest functionality. It is actively maintained and is built for interacting with Lido Protocol. 5 | 6 | # Providers 7 | 8 | Providers for Lido Finance projects. 9 | Part of [Lido JS SDK](https://github.com/lidofinance/lido-js-sdk/#readme) 10 | 11 | A Provider is an abstraction of a connection to the Ethereum network, providing a concise, consistent interface to standard Ethereum node functionality. More details in the [ethers docs](https://docs.ethers.io/v5/api/providers/). 12 | 13 | - [Install](#install) 14 | - [RPC providers](#rpc-providers) 15 | - [getRpcProvider](#getrpcprovider) 16 | - [getRpcBatchProvider](#getrpcbatchprovider) 17 | - [getStaticRpcProvider](#getstaticrpcprovider) 18 | - [getStaticRpcBatchProvider](#getstaticrpcbatchprovider) 19 | - [Cache](#cache) 20 | 21 | ## Install 22 | 23 | ```bash 24 | yarn add @lido-sdk/providers 25 | ``` 26 | 27 | ## RPC providers 28 | 29 | [Source](src/providersRPC.ts) 30 | 31 | Each getter returns a cached [Provider](https://docs.ethers.io/v5/api/providers/provider/) instance. 32 | 33 | ### getRpcProvider 34 | 35 | Returns a [JsonRpcProvider](https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#JsonRpcProvider) instance. 36 | 37 | ```ts 38 | import { CHAINS } from '@lido-sdk/constants'; 39 | import { getRpcProvider } from '@lido-sdk/providers'; 40 | 41 | const provider = getRpcProvider(CHAINS.Mainnet, '/rpc/url'); 42 | const hoodiProvider = getRpcProvider(CHAINS.Hoodi, '/rpc/hoodi-url'); 43 | ``` 44 | 45 | ### getRpcBatchProvider 46 | 47 | Returns an instance of batch version of [JsonRpcProvider](https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#JsonRpcProvider). 48 | 49 | ```ts 50 | import { CHAINS } from '@lido-sdk/constants'; 51 | import { getRpcBatchProvider } from '@lido-sdk/providers'; 52 | 53 | const batchProvider = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url'); 54 | ``` 55 | 56 | ### getStaticRpcProvider 57 | 58 | Returns a [StaticJsonRpcProvider](https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#StaticJsonRpcProvider) instance. 59 | 60 | ```ts 61 | import { CHAINS } from '@lido-sdk/constants'; 62 | import { getStaticRpcProvider } from '@lido-sdk/providers'; 63 | 64 | const staticProvider = getStaticRpcProvider(CHAINS.Mainnet, '/rpc/url'); 65 | ``` 66 | 67 | ### getStaticRpcBatchProvider 68 | 69 | Returns an instance of batch version of [StaticJsonRpcProvider](https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#StaticJsonRpcProvider). 70 | 71 | ```ts 72 | import { CHAINS } from '@lido-sdk/constants'; 73 | import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; 74 | 75 | const staticProvider = getStaticRpcBatchProvider(CHAINS.Mainnet, '/rpc/url'); 76 | ``` 77 | 78 | ## Cache 79 | 80 | To get another provider instance, getters have a third optional parameter `cacheSeed`. 81 | 82 | Calls without `cacheSeed` or with the same `cacheSeed` return the same providers: 83 | 84 | ```ts 85 | const providerFirst = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url', 1); 86 | const providerSecond = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url', 1); 87 | 88 | providerFirst === providerSecond; // true 89 | ``` 90 | 91 | Calls with different `cacheSeed` return different providers: 92 | 93 | ```ts 94 | const providerFirst = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url', 1); 95 | const providerSecond = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url', 2); 96 | 97 | providerFirst !== providerSecond; // true 98 | ``` 99 | 100 | Of course, if the `cacheSeed` is the same, but `chainId` or `url` are different the result providers will also be different: 101 | 102 | ```ts 103 | const providerFirst = getRpcBatchProvider(CHAINS.Mainnet, '/rpc/url', 1); 104 | const providerSecond = getRpcBatchProvider( 105 | CHAINS.Mainnet, 106 | '/another/rpc/url', 107 | 1, 108 | ); 109 | 110 | providerFirst !== providerSecond; // true, because the urls are different 111 | ``` 112 | -------------------------------------------------------------------------------- /packages/providers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/providers", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/providers" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "devDependencies": { 29 | "@ethersproject/logger": "^5.4.0", 30 | "@ethersproject/networks": "^5.4.0", 31 | "@ethersproject/properties": "^5.4.0", 32 | "@ethersproject/providers": "^5.4.5", 33 | "@types/jest": "^27.0.2" 34 | }, 35 | "dependencies": { 36 | "@lido-sdk/constants": "workspace:*" 37 | }, 38 | "peerDependencies": { 39 | "@ethersproject/logger": "5", 40 | "@ethersproject/networks": "5", 41 | "@ethersproject/properties": "5", 42 | "@ethersproject/providers": "5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/providers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './providersRPC'; 2 | -------------------------------------------------------------------------------- /packages/providers/src/providersRPC.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '@lido-sdk/constants'; 2 | import { 3 | JsonRpcProvider, 4 | JsonRpcBatchProvider, 5 | StaticJsonRpcProvider, 6 | } from '@ethersproject/providers'; 7 | import { StaticJsonRpcBatchProvider } from './staticJsonRpcBatchProvider'; 8 | 9 | // function factory for creating a provider getter 10 | const createProviderGetter =

( 11 | Provider: P, 12 | ) => { 13 | const cache = new Map>(); 14 | 15 | return ( 16 | chainId: CHAINS, 17 | url: string, 18 | cacheSeed = 0, 19 | pollingInterval: number | null = null, 20 | ): InstanceType

=> { 21 | const cacheKey = `${chainId}-${cacheSeed}-${url}`; 22 | let provider = cache.get(cacheKey); 23 | 24 | if (!provider) { 25 | provider = new Provider(url, chainId) as InstanceType

; 26 | cache.set(cacheKey, provider); 27 | } 28 | 29 | if (pollingInterval) { 30 | provider.pollingInterval = pollingInterval; 31 | } 32 | 33 | return provider; 34 | }; 35 | }; 36 | 37 | export const getRpcProvider = createProviderGetter(JsonRpcProvider); 38 | export const getRpcBatchProvider = createProviderGetter(JsonRpcBatchProvider); 39 | 40 | export const getStaticRpcProvider = createProviderGetter(StaticJsonRpcProvider); 41 | export const getStaticRpcBatchProvider = createProviderGetter( 42 | StaticJsonRpcBatchProvider, 43 | ); 44 | -------------------------------------------------------------------------------- /packages/providers/src/staticJsonRpcBatchProvider.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcBatchProvider } from '@ethersproject/providers'; 2 | import { Network } from '@ethersproject/networks'; 3 | import { defineReadOnly } from '@ethersproject/properties'; 4 | import { Logger } from '@ethersproject/logger'; 5 | 6 | /* 7 | * is based on 8 | * https://github.com/ethers-io/ethers.js/blob/master/packages/providers/src.ts/url-json-rpc-provider.ts#L28 9 | */ 10 | 11 | const logger = new Logger('StaticJsonRpcBatchProvider/1.0'); 12 | 13 | export class StaticJsonRpcBatchProvider extends JsonRpcBatchProvider { 14 | async detectNetwork(): Promise { 15 | let network = this.network; 16 | 17 | if (network == null) { 18 | network = await super.detectNetwork(); 19 | 20 | if (!network) { 21 | logger.throwError( 22 | 'no network detected', 23 | Logger.errors.UNKNOWN_ERROR, 24 | {}, 25 | ); 26 | } 27 | 28 | // If still not set, set it 29 | if (this._network == null) { 30 | // A static network does not support "any" 31 | defineReadOnly(this, '_network', network); 32 | 33 | this.emit('network', network, null); 34 | } 35 | } 36 | 37 | return network; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/providers/test/providersRPC.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '@lido-sdk/constants'; 2 | import { 3 | getRpcProvider, 4 | getRpcBatchProvider, 5 | getStaticRpcProvider, 6 | getStaticRpcBatchProvider, 7 | } from '../src/providersRPC'; 8 | 9 | describe('getRpcProvider', () => { 10 | test('should return a provider instance', () => { 11 | const url = '/api/rpc'; 12 | const provider = getRpcProvider(CHAINS.Mainnet, url); 13 | 14 | expect(provider).toBeInstanceOf(Object); 15 | expect(provider.connection.url).toBe(url); 16 | }); 17 | 18 | test('should use cache if url and chain are the same', () => { 19 | const url = '/api/rpc'; 20 | const providerFirst = getRpcProvider(CHAINS.Mainnet, url); 21 | const providerSecond = getRpcProvider(CHAINS.Mainnet, url); 22 | 23 | expect(providerFirst).toBe(providerSecond); 24 | }); 25 | 26 | test('should be different if urls are different', () => { 27 | const providerFirst = getRpcProvider(CHAINS.Mainnet, '/foo-url'); 28 | const providerSecond = getRpcProvider(CHAINS.Mainnet, '/bar-url'); 29 | 30 | expect(providerFirst).not.toBe(providerSecond); 31 | }); 32 | 33 | test('should be different if chains are different', () => { 34 | const url = '/api/rpc'; 35 | const providerFirst = getRpcProvider(CHAINS.Mainnet, url); 36 | const providerSecond = getRpcProvider(CHAINS.Rinkeby, url); 37 | 38 | expect(providerFirst).not.toBe(providerSecond); 39 | }); 40 | 41 | test('should use cache if seeds are the same', () => { 42 | const url = '/api/rpc'; 43 | const providerFirst = getRpcProvider(CHAINS.Mainnet, url, 1); 44 | const providerSecond = getRpcProvider(CHAINS.Mainnet, url, 1); 45 | 46 | expect(providerFirst).toBe(providerSecond); 47 | }); 48 | 49 | test('should be different if seeds are different', () => { 50 | const url = '/api/rpc'; 51 | const providerFirst = getRpcProvider(CHAINS.Mainnet, url, 1); 52 | const providerSecond = getRpcProvider(CHAINS.Mainnet, url, 2); 53 | 54 | expect(providerFirst).not.toBe(providerSecond); 55 | }); 56 | 57 | test('should use pollingInterval', () => { 58 | const url = '/api/rpc'; 59 | const pollingInterval = 1000; 60 | const provider = getRpcProvider(CHAINS.Mainnet, url, 0, pollingInterval); 61 | 62 | expect(provider.pollingInterval).toBe(pollingInterval); 63 | }); 64 | }); 65 | 66 | describe('getRpcBatchProvider', () => { 67 | test('should return a provider instance', () => { 68 | const url = '/api/rpc'; 69 | const provider = getRpcBatchProvider(CHAINS.Mainnet, url); 70 | 71 | expect(provider).toBeInstanceOf(Object); 72 | expect(provider.connection.url).toBe(url); 73 | }); 74 | }); 75 | 76 | describe('getStaticRpcProvider', () => { 77 | test('should return a provider instance', () => { 78 | const url = '/api/rpc'; 79 | const provider = getStaticRpcProvider(CHAINS.Mainnet, url); 80 | 81 | expect(provider).toBeInstanceOf(Object); 82 | expect(provider.connection.url).toBe(url); 83 | }); 84 | }); 85 | 86 | describe('getStaticRpcBatchProvider', () => { 87 | test('should return a provider instance', () => { 88 | const url = '/api/rpc'; 89 | const provider = getStaticRpcBatchProvider(CHAINS.Mainnet, url); 90 | 91 | expect(provider).toBeInstanceOf(Object); 92 | expect(provider.connection.url).toBe(url); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/providers/test/staticJsonRpcBatchProvider.test.ts: -------------------------------------------------------------------------------- 1 | import { CHAINS } from '@lido-sdk/constants'; 2 | import { StaticJsonRpcBatchProvider } from '../src/staticJsonRpcBatchProvider'; 3 | 4 | describe('StaticJsonRpcBatchProvider', () => { 5 | test('should return a provider instance', () => { 6 | const url = '/api/rpc'; 7 | const provider = new StaticJsonRpcBatchProvider(url, CHAINS.Mainnet); 8 | 9 | expect(provider).toBeInstanceOf(Object); 10 | expect(provider.connection.url).toBe(url); 11 | }); 12 | 13 | test('should detect network', async () => { 14 | const url = '/api/rpc'; 15 | const chainId = CHAINS.Mainnet; 16 | const provider = new StaticJsonRpcBatchProvider(url, chainId); 17 | 18 | const network = await provider.detectNetwork(); 19 | 20 | expect(network).toBeInstanceOf(Object); 21 | expect(network.chainId).toBe(chainId); 22 | }); 23 | 24 | test('should cache network', async () => { 25 | const url = '/api/rpc'; 26 | const chainId = CHAINS.Mainnet; 27 | const provider = new StaticJsonRpcBatchProvider(url, chainId); 28 | 29 | jest.spyOn(provider, 'network', 'get').mockReturnValue(null as any); 30 | 31 | const sendMock = jest 32 | .spyOn(provider, 'send') 33 | .mockReturnValue(chainId as any); 34 | 35 | expect(sendMock).toHaveBeenCalledTimes(0); 36 | 37 | await provider.detectNetwork(); 38 | expect(sendMock).toHaveBeenCalledTimes(1); 39 | 40 | await provider.detectNetwork(); 41 | expect(sendMock).toHaveBeenCalledTimes(1); 42 | }); 43 | 44 | test('should throw an error if network cannot be detected', async () => { 45 | const url = '/api/rpc'; 46 | const provider = new StaticJsonRpcBatchProvider(url); 47 | 48 | jest.spyOn(provider, 'network', 'get').mockReturnValue(null as any); 49 | jest.spyOn(provider, '_uncachedDetectNetwork').mockReturnValue(null as any); 50 | 51 | await expect(provider.detectNetwork()).rejects.toThrowError(); 52 | }); 53 | 54 | test('should detect network if chainId is not passed', async () => { 55 | const url = '/api/rpc'; 56 | const chainId = CHAINS.Mainnet; 57 | const provider = new StaticJsonRpcBatchProvider(url); 58 | 59 | jest.spyOn(provider, 'network', 'get').mockReturnValue(null as any); 60 | jest.spyOn(provider, 'send').mockReturnValue(chainId as any); 61 | 62 | const network = await provider.detectNetwork(); 63 | expect(network.chainId).toBe(chainId); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/providers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | *.test.js 3 | *.test.ts 4 | test 5 | src 6 | !node_modules/ 7 | -------------------------------------------------------------------------------- /packages/react/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lido-sdk/react", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/cjs/index.js", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "license": "MIT", 8 | "homepage": "https://github.com/lidofinance/lido-js-sdk", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lidofinance/lido-js-sdk.git", 12 | "directory": "packages/react" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/lidofinance/lido-js-sdk/issues" 16 | }, 17 | "sideEffects": false, 18 | "keywords": [ 19 | "lido", 20 | "lido-sdk", 21 | "lido-js-sdk", 22 | "lidofinance" 23 | ], 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/", 26 | "access": "public" 27 | }, 28 | "devDependencies": { 29 | "@ethersproject/bignumber": "^5.4.2", 30 | "@ethersproject/bytes": "^5.4.0", 31 | "@ethersproject/constants": "^5.4.0", 32 | "@ethersproject/contracts": "^5.4.1", 33 | "@ethersproject/providers": "^5.4.5", 34 | "@testing-library/react": "^12.1.5", 35 | "@testing-library/react-hooks": "^7.0.2", 36 | "@types/jest": "^27.0.2", 37 | "@types/react": "^17.0.45", 38 | "@types/react-dom": "^17.0.17", 39 | "react": "^17.0.2", 40 | "react-dom": "^17.0.2" 41 | }, 42 | "dependencies": { 43 | "@lido-sdk/constants": "workspace:*", 44 | "@lido-sdk/contracts": "workspace:*", 45 | "@lido-sdk/helpers": "workspace:*", 46 | "swr": "^1.0.1", 47 | "tiny-invariant": "^1.1.0", 48 | "tiny-warning": "^1.0.3" 49 | }, 50 | "peerDependencies": { 51 | "@ethersproject/bignumber": "5", 52 | "@ethersproject/bytes": "5", 53 | "@ethersproject/constants": "5", 54 | "@ethersproject/contracts": "5", 55 | "@ethersproject/providers": "5", 56 | "react": ">=16", 57 | "react-dom": ">=16" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/react/src/context/SDK.tsx: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { CHAINS } from '@lido-sdk/constants'; 3 | import { 4 | BaseProvider, 5 | Web3Provider, 6 | getDefaultProvider, 7 | getNetwork, 8 | } from '@ethersproject/providers'; 9 | import { createContext, memo, useMemo, FC } from 'react'; 10 | import { SWRConfiguration } from 'swr'; 11 | 12 | export interface SDKContextProps { 13 | chainId: CHAINS; 14 | supportedChainIds: CHAINS[]; 15 | providerMainnetRpc?: BaseProvider; 16 | providerRpc?: BaseProvider; 17 | providerWeb3?: Web3Provider; 18 | swrConfig?: SWRConfiguration; 19 | account?: string; 20 | onError?: (error: unknown) => void; 21 | } 22 | 23 | export interface SDKContextValue { 24 | chainId: CHAINS; 25 | supportedChainIds: CHAINS[]; 26 | providerMainnetRpc: BaseProvider; 27 | providerRpc: BaseProvider; 28 | providerWeb3?: Web3Provider; 29 | swrConfig?: SWRConfiguration; 30 | account?: string; 31 | onError: (error: unknown) => void; 32 | } 33 | 34 | export const SDKContext = createContext(null); 35 | SDKContext.displayName = 'LidoSDKContext'; 36 | 37 | const ProviderSDK: FC = (props) => { 38 | const { 39 | children, 40 | account, 41 | chainId, 42 | supportedChainIds, 43 | providerWeb3, 44 | swrConfig, 45 | } = props; 46 | 47 | invariant(chainId, 'invalid chainId'); 48 | invariant(supportedChainIds?.length, 'Supported chains are required'); 49 | 50 | const providerRpc = useMemo(() => { 51 | return props.providerRpc ?? getDefaultProvider(getNetwork(chainId)); 52 | }, [props.providerRpc, chainId]); 53 | 54 | const providerMainnetRpc = useMemo(() => { 55 | return props.providerMainnetRpc ?? getDefaultProvider('mainnet'); 56 | }, [props.providerMainnetRpc]); 57 | 58 | const onError = useMemo(() => { 59 | return props.onError ?? console.error; 60 | }, [props.onError]); 61 | 62 | const value = useMemo( 63 | () => ({ 64 | account, 65 | chainId, 66 | supportedChainIds, 67 | providerMainnetRpc, 68 | providerRpc, 69 | providerWeb3, 70 | swrConfig, 71 | onError, 72 | }), 73 | [ 74 | account, 75 | chainId, 76 | supportedChainIds, 77 | providerMainnetRpc, 78 | providerRpc, 79 | providerWeb3, 80 | swrConfig, 81 | onError, 82 | ], 83 | ); 84 | 85 | return {children}; 86 | }; 87 | 88 | export default memo>(ProviderSDK); 89 | -------------------------------------------------------------------------------- /packages/react/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SDK'; 2 | export { default as ProviderSDK } from './SDK'; 3 | -------------------------------------------------------------------------------- /packages/react/src/factories/contracts.ts: -------------------------------------------------------------------------------- 1 | import { BaseContract } from '@ethersproject/contracts'; 2 | import { 3 | TOKENS, 4 | CHAINS, 5 | getTokenAddress, 6 | getWithdrawalQueueAddress, 7 | } from '@lido-sdk/constants'; 8 | import { 9 | WstethAbiFactory, 10 | StethAbiFactory, 11 | LdoAbiFactory, 12 | Factory, 13 | createContractGetter, 14 | WithdrawalQueueAbiFactory, 15 | } from '@lido-sdk/contracts'; 16 | import { useMemo } from 'react'; 17 | import { useSDK } from '../hooks'; 18 | 19 | export const contractHooksFactory = ( 20 | factory: Factory, 21 | getTokenAddress: (chainId: CHAINS) => string, 22 | ): { 23 | useContractRPC: () => C; 24 | useContractWeb3: () => C | null; 25 | } => { 26 | const getContract = createContractGetter(factory); 27 | 28 | return { 29 | useContractRPC: () => { 30 | const { chainId, providerRpc } = useSDK(); 31 | const tokenAddress = getTokenAddress(chainId); 32 | 33 | return getContract(tokenAddress, providerRpc); 34 | }, 35 | useContractWeb3: () => { 36 | const { chainId, providerWeb3 } = useSDK(); 37 | const tokenAddress = getTokenAddress(chainId); 38 | 39 | const signer = useMemo(() => { 40 | return providerWeb3?.getSigner(); 41 | }, [providerWeb3]); 42 | 43 | if (!signer) return null; 44 | return getContract(tokenAddress, signer); 45 | }, 46 | }; 47 | }; 48 | 49 | const wsteth = contractHooksFactory(WstethAbiFactory, (chainId) => 50 | getTokenAddress(chainId, TOKENS.WSTETH), 51 | ); 52 | export const useWSTETHContractRPC = wsteth.useContractRPC; 53 | export const useWSTETHContractWeb3 = wsteth.useContractWeb3; 54 | 55 | const steth = contractHooksFactory(StethAbiFactory, (chainId) => 56 | getTokenAddress(chainId, TOKENS.STETH), 57 | ); 58 | export const useSTETHContractRPC = steth.useContractRPC; 59 | export const useSTETHContractWeb3 = steth.useContractWeb3; 60 | 61 | const ldo = contractHooksFactory(LdoAbiFactory, (chainId) => 62 | getTokenAddress(chainId, TOKENS.LDO), 63 | ); 64 | export const useLDOContractRPC = ldo.useContractRPC; 65 | export const useLDOContractWeb3 = ldo.useContractWeb3; 66 | 67 | const withdrawalQueue = contractHooksFactory( 68 | WithdrawalQueueAbiFactory, 69 | (chainId) => getWithdrawalQueueAddress(chainId), 70 | ); 71 | export const useWithdrawalQueueContractRPC = withdrawalQueue.useContractRPC; 72 | export const useWithdrawalQueueContractWeb3 = withdrawalQueue.useContractWeb3; 73 | -------------------------------------------------------------------------------- /packages/react/src/factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokens'; 2 | export * from './contracts'; 3 | -------------------------------------------------------------------------------- /packages/react/src/factories/tokens.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import { TOKENS, CHAINS, getTokenAddress } from '@lido-sdk/constants'; 3 | import { 4 | SWRResponse, 5 | UseApproveResponse, 6 | UseApproveWrapper, 7 | useAllowance, 8 | useDecimals, 9 | useSDK, 10 | useTokenBalance, 11 | useTotalSupply, 12 | useApprove, 13 | } from '../hooks'; 14 | import { SWRConfiguration } from 'swr'; 15 | 16 | export const hooksFactory = ( 17 | getTokenAddress: (chainId: CHAINS) => string, 18 | ): { 19 | useTokenBalance: ( 20 | config?: SWRConfiguration, 21 | ) => SWRResponse; 22 | useTotalSupply: ( 23 | config?: SWRConfiguration, 24 | ) => SWRResponse; 25 | useDecimals: (config?: SWRConfiguration) => SWRResponse; 26 | useAllowance: ( 27 | spender: string, 28 | config?: SWRConfiguration, 29 | ) => SWRResponse; 30 | useApprove: ( 31 | amount: BigNumber, 32 | spender: string, 33 | wrapper: UseApproveWrapper, 34 | ) => UseApproveResponse; 35 | } => { 36 | return { 37 | useTokenBalance: (config) => { 38 | const { chainId } = useSDK(); 39 | const tokenAddress = getTokenAddress(chainId); 40 | return useTokenBalance(tokenAddress, undefined, config); 41 | }, 42 | useTotalSupply: (config) => { 43 | const { chainId } = useSDK(); 44 | const tokenAddress = getTokenAddress(chainId); 45 | return useTotalSupply(tokenAddress, config); 46 | }, 47 | useDecimals: (config) => { 48 | const { chainId } = useSDK(); 49 | const tokenAddress = getTokenAddress(chainId); 50 | return useDecimals(tokenAddress, config); 51 | }, 52 | useAllowance: (spender, config) => { 53 | const { chainId } = useSDK(); 54 | const tokenAddress = getTokenAddress(chainId); 55 | return useAllowance(tokenAddress, spender, undefined, config); 56 | }, 57 | useApprove: (amount, spender, wrapper) => { 58 | const { chainId, account } = useSDK(); 59 | const tokenAddress = getTokenAddress(chainId); 60 | return useApprove(amount, tokenAddress, spender, account, wrapper); 61 | }, 62 | }; 63 | }; 64 | 65 | const wsteth = hooksFactory((chainId) => 66 | getTokenAddress(chainId, TOKENS.WSTETH), 67 | ); 68 | export const useWSTETHBalance = wsteth.useTokenBalance; 69 | export const useWSTETHTotalSupply = wsteth.useTotalSupply; 70 | export const useWSTETHDecimals = wsteth.useDecimals; 71 | export const useWSTETHAllowance = wsteth.useAllowance; 72 | export const useWSTETHApprove = wsteth.useApprove; 73 | 74 | const steth = hooksFactory((chainId) => getTokenAddress(chainId, TOKENS.STETH)); 75 | export const useSTETHBalance = steth.useTokenBalance; 76 | export const useSTETHTotalSupply = steth.useTotalSupply; 77 | export const useSTETHDecimals = steth.useDecimals; 78 | export const useSTETHAllowance = steth.useAllowance; 79 | export const useSTETHApprove = steth.useApprove; 80 | 81 | const ldo = hooksFactory((chainId) => getTokenAddress(chainId, TOKENS.LDO)); 82 | export const useLDOBalance = ldo.useTokenBalance; 83 | export const useLDOTotalSupply = ldo.useTotalSupply; 84 | export const useLDODecimals = ldo.useDecimals; 85 | export const useLDOAllowance = ldo.useAllowance; 86 | export const useLDOApprove = ldo.useApprove; 87 | -------------------------------------------------------------------------------- /packages/react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAllowance'; 2 | export * from './useApprove'; 3 | export * from './useContractEstimateGasSWR'; 4 | export * from './useContractSWR'; 5 | export * from './useDebounceCallback'; 6 | export * from './useDecimals'; 7 | export * from './useEthereumBalance'; 8 | export * from './useEthereumSWR'; 9 | export * from './useEtherscanOpen'; 10 | export * from './useEthPrice'; 11 | export * from './useFeeAnalytics'; 12 | export * from './useFeeHistory'; 13 | export * from './useLidoSWR'; 14 | export * from './useLidoSWRImmutable'; 15 | export * from './useLocalStorage'; 16 | export * from './useMountedState'; 17 | export * from './useSDK'; 18 | export * from './useTokenAddress'; 19 | export * from './useTokenBalance'; 20 | export * from './useTokenToWallet'; 21 | export * from './useTotalSupply'; 22 | export * from './useTxPrice'; 23 | -------------------------------------------------------------------------------- /packages/react/src/hooks/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-types */ 3 | 4 | export type FilterAsyncMethods = { 5 | [K in keyof T]: T[K] extends (...args: any[]) => Promise ? K : never; 6 | }[keyof T]; 7 | 8 | export type UnpackedPromise = T extends Promise ? U : T; 9 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useAllowance.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import warning from 'tiny-warning'; 3 | import { useEffect } from 'react'; 4 | import { BigNumber } from '@ethersproject/bignumber'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { useContractSWR } from './useContractSWR'; 7 | import { SWRResponse } from './useLidoSWR'; 8 | import { useSDK } from './useSDK'; 9 | import { useDebounceCallback } from './useDebounceCallback'; 10 | import type { SWRConfiguration } from 'swr'; 11 | 12 | export const useAllowance = ( 13 | token: string, 14 | spender: string, 15 | owner?: string, 16 | config?: SWRConfiguration, 17 | ): SWRResponse => { 18 | const { providerRpc, providerWeb3, account } = useSDK(); 19 | const mergedOwner = owner ?? account; 20 | 21 | invariant(token != null, 'Token is required'); 22 | invariant(spender != null, 'Spender is required'); 23 | 24 | const contractRpc = getERC20Contract(token, providerRpc); 25 | const contractWeb3 = providerWeb3 26 | ? getERC20Contract(token, providerWeb3) 27 | : null; 28 | 29 | const result = useContractSWR({ 30 | shouldFetch: !!mergedOwner, 31 | contract: contractRpc, 32 | method: 'allowance', 33 | params: [mergedOwner, spender], 34 | config, 35 | }); 36 | 37 | const updateAllowanceDebounced = useDebounceCallback(result.update, 1000); 38 | 39 | useEffect(() => { 40 | if (!mergedOwner || !providerWeb3 || !contractWeb3) return; 41 | 42 | try { 43 | const transfer = contractWeb3.filters.Transfer(mergedOwner, spender); 44 | const approve = contractWeb3.filters.Approval(mergedOwner, spender); 45 | 46 | providerWeb3.on(transfer, updateAllowanceDebounced); 47 | providerWeb3.on(approve, updateAllowanceDebounced); 48 | 49 | return () => { 50 | providerWeb3.off(transfer, updateAllowanceDebounced); 51 | providerWeb3.off(approve, updateAllowanceDebounced); 52 | }; 53 | } catch (error) { 54 | return warning(false, 'Cannot subscribe to event'); 55 | } 56 | }, [ 57 | contractWeb3, 58 | mergedOwner, 59 | providerWeb3, 60 | updateAllowanceDebounced, 61 | spender, 62 | ]); 63 | 64 | return result; 65 | }; 66 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useApprove.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { useCallback } from 'react'; 3 | import { ContractTransaction, ContractReceipt } from '@ethersproject/contracts'; 4 | import { BigNumber } from '@ethersproject/bignumber'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { useSDK } from './useSDK'; 7 | import { useMountedState } from './useMountedState'; 8 | import { useAllowance } from './useAllowance'; 9 | import { Zero } from '@ethersproject/constants'; 10 | 11 | type TransactionCallback = () => Promise; 12 | 13 | const defaultWrapper = async (callback: TransactionCallback) => { 14 | const transaction = await callback(); 15 | return await transaction.wait(); 16 | }; 17 | 18 | export type UseApproveResponse = { 19 | approve: () => Promise; 20 | approving: boolean; 21 | needsApprove: boolean; 22 | initialLoading: boolean; 23 | allowance: BigNumber; 24 | loading: boolean; 25 | error: unknown; 26 | }; 27 | 28 | export type UseApproveWrapper = ( 29 | callback: TransactionCallback, 30 | ) => Promise; 31 | 32 | export const useApprove = ( 33 | amount: BigNumber, 34 | token: string, 35 | spender: string, 36 | owner?: string, 37 | wrapper: UseApproveWrapper = defaultWrapper, 38 | ): UseApproveResponse => { 39 | const { providerWeb3, account, onError } = useSDK(); 40 | const mergedOwner = owner ?? account; 41 | 42 | invariant(token != null, 'Token is required'); 43 | invariant(spender != null, 'Spender is required'); 44 | 45 | const [approving, setApproving] = useMountedState(false); 46 | const result = useAllowance(token, spender, mergedOwner); 47 | const { 48 | data: allowance = Zero, 49 | initialLoading, 50 | update: updateAllowance, 51 | } = result; 52 | 53 | const needsApprove = 54 | !initialLoading && !amount.isZero() && amount.gt(allowance); 55 | 56 | const approve = useCallback(async () => { 57 | invariant(providerWeb3 != null, 'Web3 provider is required'); 58 | const contractWeb3 = getERC20Contract(token, providerWeb3.getSigner()); 59 | 60 | setApproving(true); 61 | 62 | try { 63 | await wrapper(() => contractWeb3.approve(spender, amount)); 64 | await updateAllowance(); 65 | } catch (error) { 66 | onError(error); 67 | } finally { 68 | setApproving(false); 69 | } 70 | }, [ 71 | providerWeb3, 72 | token, 73 | spender, 74 | amount, 75 | wrapper, 76 | setApproving, 77 | updateAllowance, 78 | onError, 79 | ]); 80 | 81 | return { 82 | approve, 83 | approving, 84 | needsApprove, 85 | 86 | allowance, 87 | initialLoading, 88 | 89 | /* 90 | * support dependency collection 91 | * https://swr.vercel.app/advanced/performance#dependency-collection 92 | */ 93 | 94 | get loading() { 95 | return result.loading; 96 | }, 97 | get error() { 98 | return result.error; 99 | }, 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useContractEstimateGasSWR.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { BaseContract } from '@ethersproject/contracts'; 3 | import { BigNumber } from '@ethersproject/bignumber'; 4 | import { useLidoSWR, SWRResponse } from './useLidoSWR'; 5 | import { FilterAsyncMethods } from './types'; 6 | import { SWRConfiguration } from 'swr'; 7 | 8 | export const useContractEstimateGasSWR = < 9 | C extends BaseContract, 10 | M extends FilterAsyncMethods, 11 | F extends boolean, 12 | >(props: { 13 | contract?: C; 14 | method: M; 15 | shouldFetch?: F; 16 | params?: F extends false ? unknown[] : Parameters; 17 | config?: SWRConfiguration; 18 | }): SWRResponse => { 19 | const { shouldFetch = true, params = [], contract, method, config } = props; 20 | 21 | invariant(method != null, 'Method is required'); 22 | 23 | return useLidoSWR( 24 | shouldFetch && contract ? [contract, method, ...params] : null, 25 | (contract: C, method: string, ...params: unknown[]) => { 26 | return contract.estimateGas[method](...params); 27 | }, 28 | config, 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useContractSWR.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { BaseContract } from '@ethersproject/contracts'; 3 | import { useLidoSWR, SWRResponse } from './useLidoSWR'; 4 | import { FilterAsyncMethods, UnpackedPromise } from './types'; 5 | import { SWRConfiguration } from 'swr'; 6 | 7 | export const useContractSWR = < 8 | C extends BaseContract, 9 | M extends FilterAsyncMethods, 10 | R extends UnpackedPromise>, 11 | F extends boolean, 12 | >(props: { 13 | contract: C; 14 | method: M; 15 | shouldFetch?: F; 16 | params?: F extends false ? unknown[] : Parameters; 17 | config?: SWRConfiguration; 18 | }): SWRResponse => { 19 | const { shouldFetch = true, params = [], contract, method, config } = props; 20 | 21 | invariant(contract != null, 'Contract is required'); 22 | invariant(method != null, 'Method is required'); 23 | 24 | return useLidoSWR( 25 | shouldFetch ? [contract, method, ...params] : null, 26 | (contract: C, method: M, ...params: Parameters) => { 27 | return contract[method](...params); 28 | }, 29 | config, 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useDebounceCallback.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export const useDebounceCallback = ( 4 | callback: () => unknown, 5 | timeout = 0, 6 | ): (() => void) => { 7 | const timer = useRef(null); 8 | 9 | const clearTimer = useCallback(() => { 10 | if (timer.current != null) { 11 | clearTimeout(timer.current); 12 | } 13 | }, []); 14 | 15 | useEffect(() => { 16 | return clearTimer; 17 | }, [clearTimer]); 18 | 19 | return useCallback(() => { 20 | clearTimer(); 21 | timer.current = setTimeout(callback, timeout); 22 | }, [callback, timeout, clearTimer]); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useDecimals.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { getERC20Contract } from '@lido-sdk/contracts'; 3 | import { useContractSWR } from './useContractSWR'; 4 | import { SWRResponse } from './useLidoSWR'; 5 | import { useSDK } from './useSDK'; 6 | import { SWRConfiguration } from 'swr'; 7 | 8 | export const useDecimals = ( 9 | token: string, 10 | config?: SWRConfiguration, 11 | ): SWRResponse => { 12 | const { providerRpc } = useSDK(); 13 | 14 | invariant(token != null, 'Token address is required'); 15 | 16 | const contract = getERC20Contract(token, providerRpc); 17 | const result = useContractSWR({ contract, method: 'decimals', config }); 18 | 19 | return result; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useEthPrice.ts: -------------------------------------------------------------------------------- 1 | import { SWRConfiguration } from 'swr'; 2 | import { BigNumber } from '@ethersproject/bignumber'; 3 | import { getAggregatorContract } from '@lido-sdk/contracts'; 4 | import { getAggregatorAddress, CHAINS } from '@lido-sdk/constants'; 5 | import { divide } from '@lido-sdk/helpers'; 6 | import { useSDK } from './useSDK'; 7 | import { SWRResponse, useLidoSWR } from './useLidoSWR'; 8 | 9 | type useEthPriceResult = number; 10 | 11 | export const useEthPrice = ( 12 | config?: SWRConfiguration, 13 | ): SWRResponse => { 14 | const { providerMainnetRpc } = useSDK(); 15 | const address = getAggregatorAddress(CHAINS.Mainnet); 16 | const aggregatorContract = getAggregatorContract(address, providerMainnetRpc); 17 | 18 | return useLidoSWR( 19 | ['lido-swr:eth-price', aggregatorContract], 20 | async () => { 21 | const [decimals, latestAnswer] = await Promise.all([ 22 | aggregatorContract.decimals(), 23 | aggregatorContract.latestAnswer(), 24 | ]); 25 | return divide(latestAnswer, BigNumber.from(10).pow(decimals)); 26 | }, 27 | config, 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useEthereumBalance.ts: -------------------------------------------------------------------------------- 1 | import warning from 'tiny-warning'; 2 | import { useEffect } from 'react'; 3 | import { BigNumber } from '@ethersproject/bignumber'; 4 | import { useSDK } from './useSDK'; 5 | import { useEthereumSWR } from './useEthereumSWR'; 6 | import { SWRResponse } from './useLidoSWR'; 7 | import { SWRConfiguration } from 'swr'; 8 | import { useDebounceCallback } from './useDebounceCallback'; 9 | 10 | export const useEthereumBalance = ( 11 | account?: string, 12 | config?: SWRConfiguration, 13 | ): SWRResponse => { 14 | const { providerWeb3, account: sdkAccount } = useSDK(); 15 | const mergedAccount = account ?? sdkAccount; 16 | 17 | const result = useEthereumSWR({ 18 | shouldFetch: !!mergedAccount, 19 | method: 'getBalance', 20 | params: [mergedAccount, 'latest'], 21 | config, 22 | }); 23 | 24 | const updateBalanceDebounced = useDebounceCallback(result.update, 1000); 25 | 26 | useEffect(() => { 27 | if (!mergedAccount || !providerWeb3) return; 28 | 29 | try { 30 | providerWeb3.on('block', updateBalanceDebounced); 31 | 32 | return () => { 33 | providerWeb3.off('block', updateBalanceDebounced); 34 | }; 35 | } catch (error) { 36 | return warning(false, 'Cannot subscribe to Block event'); 37 | } 38 | }, [providerWeb3, mergedAccount, updateBalanceDebounced]); 39 | 40 | return result; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useEthereumSWR.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import { BaseProvider } from '@ethersproject/providers'; 3 | import { useLidoSWR, SWRResponse } from './useLidoSWR'; 4 | import { FilterAsyncMethods, UnpackedPromise } from './types'; 5 | import { SWRConfiguration } from 'swr'; 6 | import { useSDK } from './useSDK'; 7 | 8 | export const useEthereumSWR = < 9 | P extends BaseProvider, 10 | M extends FilterAsyncMethods

, 11 | R extends UnpackedPromise>, 12 | F extends boolean, 13 | >(props: { 14 | method: M; 15 | shouldFetch?: F; 16 | providerRpc?: P; 17 | params?: F extends false ? unknown[] : Parameters; 18 | config?: SWRConfiguration; 19 | }): SWRResponse => { 20 | const { shouldFetch = true, params = [], method, config } = props; 21 | const providerRpcFromSdk = useSDK().providerRpc as P; 22 | const providerRpc = props.providerRpc ?? providerRpcFromSdk; 23 | 24 | invariant(providerRpc != null, 'RPC Provider is not provided'); 25 | invariant(method != null, 'Method is required'); 26 | 27 | return useLidoSWR( 28 | shouldFetch ? [providerRpc, method, ...params] : null, 29 | (providerRpc: P, method: M, ...params: Parameters) => { 30 | return providerRpc[method](...params); 31 | }, 32 | config, 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useEtherscanOpen.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getEtherscanLink, 3 | EtherscanEntities, 4 | openWindow, 5 | } from '@lido-sdk/helpers'; 6 | import { useCallback } from 'react'; 7 | import { useSDK } from './useSDK'; 8 | 9 | export const useEtherscanOpen = ( 10 | hash: string, 11 | entity: EtherscanEntities, 12 | ): (() => void) => { 13 | const { chainId } = useSDK(); 14 | 15 | return useCallback(() => { 16 | const link = getEtherscanLink(chainId, hash, entity); 17 | openWindow(link); 18 | }, [chainId, entity, hash]); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useFeeAnalytics.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import { Zero } from '@ethersproject/constants'; 3 | import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers'; 4 | import { SWRConfiguration } from 'swr'; 5 | import { FeeHistory, useFeeHistory } from './useFeeHistory'; 6 | import { SWRResponse } from './useLidoSWR'; 7 | 8 | export type FeeAnalytics = SWRResponse & { 9 | percentile: number; 10 | baseFee: BigNumber; 11 | }; 12 | 13 | export const calculatePercentile = ( 14 | array: BigNumber[], 15 | target: BigNumber, 16 | ): number => { 17 | const lessThenTarget = array.reduce( 18 | (counter, current) => (current.lt(target) ? counter + 1 : counter), 19 | 0, 20 | ); 21 | 22 | return array.length ? lessThenTarget / array.length : 1; 23 | }; 24 | 25 | export const useFeeAnalytics = (props?: { 26 | shouldFetch?: boolean; 27 | providerRpc?: JsonRpcProvider; 28 | providerWeb3?: Web3Provider; 29 | blocks?: number; 30 | config?: SWRConfiguration; 31 | }): FeeAnalytics => { 32 | const history = useFeeHistory(props); 33 | const { data, mutate, update } = history; 34 | 35 | const feeHistory = data?.baseFeePerGas || []; 36 | const baseFee = feeHistory[feeHistory.length - 1] ?? Zero; 37 | 38 | const percentile = calculatePercentile([...feeHistory], baseFee); 39 | 40 | return { 41 | data, 42 | percentile, 43 | baseFee, 44 | 45 | mutate, 46 | update, 47 | 48 | /* 49 | * support dependency collection 50 | * https://swr.vercel.app/advanced/performance#dependency-collection 51 | */ 52 | 53 | get loading() { 54 | return history.loading; 55 | }, 56 | get initialLoading() { 57 | return history.initialLoading; 58 | }, 59 | get error() { 60 | return history.error; 61 | }, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useFeeHistory.ts: -------------------------------------------------------------------------------- 1 | import warning from 'tiny-warning'; 2 | import invariant from 'tiny-invariant'; 3 | import { useCallback, useEffect } from 'react'; 4 | import { SWRConfiguration } from 'swr'; 5 | import { CHAINS } from '@lido-sdk/constants'; 6 | import { 7 | BaseProvider, 8 | JsonRpcProvider, 9 | Web3Provider, 10 | } from '@ethersproject/providers'; 11 | import { BigNumber } from '@ethersproject/bignumber'; 12 | import { hexValue } from '@ethersproject/bytes'; 13 | import { useSDK } from './useSDK'; 14 | import { useLidoSWR, SWRResponse } from './useLidoSWR'; 15 | import { useDebounceCallback } from './useDebounceCallback'; 16 | 17 | export type SourceFeeHistory = { 18 | oldestBlock: number; 19 | baseFeePerGas: readonly string[]; 20 | gasUsedRatio: readonly number[]; 21 | }; 22 | 23 | export type FeeHistory = { 24 | oldestBlock: number; 25 | baseFeePerGas: readonly BigNumber[]; 26 | gasUsedRatio: readonly number[]; 27 | }; 28 | 29 | const MAX_BLOCKS_PER_REQUEST = 1024; 30 | const DEFAULT_HISTORY_BLOCKS = MAX_BLOCKS_PER_REQUEST; 31 | const DEFAULT_CACHE_DATA = Object.freeze({ 32 | oldestBlock: -1, 33 | baseFeePerGas: Object.freeze([]), 34 | gasUsedRatio: Object.freeze([]), 35 | }); 36 | 37 | export const historyCache = new Map(); 38 | 39 | export const getBlockNumber = async ( 40 | provider: BaseProvider, 41 | ): Promise => { 42 | const cachedNumber = provider.blockNumber; 43 | return cachedNumber === -1 ? await provider.getBlockNumber() : cachedNumber; 44 | }; 45 | 46 | export const getChunksArguments = ( 47 | fromBlock: number, 48 | toBlock: number, 49 | chunkSize = MAX_BLOCKS_PER_REQUEST, 50 | ): T[] => { 51 | invariant( 52 | fromBlock <= toBlock, 53 | 'fromBlock should be less than or equal to toBlock', 54 | ); 55 | invariant(chunkSize > 0, 'chunkSize should be greater than 0'); 56 | 57 | const totalBlocks = toBlock - fromBlock + 1; 58 | const totalChunks = Math.ceil(totalBlocks / chunkSize); 59 | 60 | return Array.from({ length: totalChunks }, (_value, index) => { 61 | const newestBlock = toBlock - chunkSize * index; 62 | const blocks = Math.min(1 + newestBlock - fromBlock, chunkSize); 63 | 64 | return [blocks, hexValue(BigNumber.from(newestBlock)), []]; 65 | }).reverse() as T[]; 66 | }; 67 | 68 | export const combineHistory = (...histories: FeeHistory[]): FeeHistory => { 69 | histories.forEach((currentHistory, index) => { 70 | if (index === 0) return; 71 | const previousHistory = histories[index - 1]; 72 | 73 | invariant( 74 | currentHistory.oldestBlock === 75 | previousHistory.oldestBlock + previousHistory.baseFeePerGas.length - 1, 76 | 'Histories cannot be merged', 77 | ); 78 | }, []); 79 | 80 | const lastHistory = histories[histories.length - 1]; 81 | const lastHistoryFees = lastHistory.baseFeePerGas; 82 | const lastFeePerGas = lastHistoryFees[lastHistoryFees.length - 1]; 83 | 84 | const oldestBlock = histories[0].oldestBlock; 85 | const baseFeePerGas = histories 86 | .flatMap(({ baseFeePerGas }) => baseFeePerGas.slice(0, -1)) 87 | .concat(lastFeePerGas); 88 | 89 | const gasUsedRatio = histories.flatMap(({ gasUsedRatio }) => gasUsedRatio); 90 | 91 | return { 92 | oldestBlock, 93 | baseFeePerGas, 94 | gasUsedRatio, 95 | }; 96 | }; 97 | 98 | export const trimHistory = ( 99 | history: FeeHistory, 100 | blocks: number, 101 | ): FeeHistory => { 102 | invariant(blocks > 0, 'blocks number should be greater than 0'); 103 | 104 | const currentBlocks = history.gasUsedRatio.length; 105 | const trimmedBlocks = Math.max(0, currentBlocks - blocks); 106 | const oldestBlock = history.oldestBlock + trimmedBlocks; 107 | 108 | const baseFeePerGas = history.baseFeePerGas.slice(-(blocks + 1)); 109 | const gasUsedRatio = history.gasUsedRatio.slice(-blocks); 110 | 111 | return { 112 | oldestBlock, 113 | baseFeePerGas, 114 | gasUsedRatio, 115 | }; 116 | }; 117 | 118 | export const getFeeHistory = async ( 119 | provider: JsonRpcProvider, 120 | fromBlock: number, 121 | toBlock: number, 122 | chunkSize?: number, 123 | ): Promise => { 124 | const chunksArgs = getChunksArguments(fromBlock, toBlock, chunkSize); 125 | 126 | const histories = await Promise.all( 127 | chunksArgs.map((args) => { 128 | return provider.send('eth_feeHistory', args) as Promise; 129 | }), 130 | ); 131 | 132 | const convertedHistories = histories.map((history) => ({ 133 | ...history, 134 | oldestBlock: BigNumber.from(history.oldestBlock).toNumber(), 135 | baseFeePerGas: history.baseFeePerGas.map((fee) => BigNumber.from(fee)), 136 | })); 137 | 138 | return combineHistory(...convertedHistories); 139 | }; 140 | 141 | export const useFeeHistory = < 142 | P extends JsonRpcProvider, 143 | W extends Web3Provider, 144 | >(props?: { 145 | shouldFetch?: boolean; 146 | providerRpc?: P; 147 | providerWeb3?: W; 148 | blocks?: number; 149 | config?: SWRConfiguration; 150 | }): SWRResponse => { 151 | const { 152 | shouldFetch = true, 153 | blocks = DEFAULT_HISTORY_BLOCKS, 154 | config, 155 | } = props || {}; 156 | const providerRpcFromSdk = useSDK().providerRpc as P; 157 | const providerRpc = props?.providerRpc ?? providerRpcFromSdk; 158 | 159 | const providerWeb3FromSdk = useSDK().providerWeb3 as W; 160 | const providerWeb3 = props?.providerWeb3 ?? providerWeb3FromSdk; 161 | 162 | const { chainId } = useSDK(); 163 | 164 | invariant(providerRpc != null, 'RPC Provider is not provided'); 165 | invariant(blocks > 0, 'blocks number should be greater than 0'); 166 | 167 | const result = useLidoSWR( 168 | shouldFetch ? [providerRpc, chainId, blocks] : null, 169 | async ( 170 | providerRpc: P, 171 | chainId: CHAINS, 172 | blocks: number, 173 | ): Promise => { 174 | const currentBlock = await getBlockNumber(providerRpc); 175 | 176 | const cachedHistory = historyCache.get(chainId) ?? DEFAULT_CACHE_DATA; 177 | const oldestCachedBlock = cachedHistory.oldestBlock; 178 | const blocksInCache = cachedHistory.gasUsedRatio.length; 179 | const newestCachedBlock = blocksInCache 180 | ? oldestCachedBlock + blocksInCache - 1 181 | : -1; 182 | const firstRequiredBlock = currentBlock - blocks + 1; 183 | 184 | if (blocksInCache && newestCachedBlock >= currentBlock) { 185 | return cachedHistory; 186 | } 187 | 188 | const fromBlock = Math.max(newestCachedBlock + 1, firstRequiredBlock); 189 | const toBlock = currentBlock; 190 | 191 | const newHistory = await getFeeHistory(providerRpc, fromBlock, toBlock); 192 | 193 | const shouldCombine = blocksInCache 194 | ? newestCachedBlock < newHistory.oldestBlock 195 | : false; 196 | 197 | const combinedHistory = shouldCombine 198 | ? combineHistory(cachedHistory, newHistory) 199 | : newHistory; 200 | 201 | const trimmedHistory = trimHistory(combinedHistory, blocks); 202 | 203 | historyCache.set(chainId, trimmedHistory); 204 | return trimmedHistory; 205 | }, 206 | config, 207 | ); 208 | 209 | const updateHistory = useDebounceCallback(result.update); 210 | 211 | const subscribeToUpdates = useCallback(() => { 212 | const provider = providerWeb3 || providerRpc; 213 | 214 | try { 215 | provider.on('block', updateHistory); 216 | 217 | return () => { 218 | provider.off('block', updateHistory); 219 | }; 220 | } catch (error) { 221 | return warning(false, 'Cannot subscribe to Block event'); 222 | } 223 | }, [providerRpc, providerWeb3, updateHistory]); 224 | 225 | useEffect(subscribeToUpdates, [subscribeToUpdates]); 226 | 227 | return result; 228 | }; 229 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useLidoSWR.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { default as useSWRSource, SWRConfiguration } from 'swr'; 3 | import { Key, Fetcher, MutatorCallback } from 'swr/dist/types'; 4 | import { useSDK } from './useSDK'; 5 | 6 | const LIDO_SWR_DEFAULT_CONFIG = { 7 | errorRetryInterval: 10_000, 8 | focusThrottleInterval: 10_000, 9 | }; 10 | 11 | export type SWRResponse = { 12 | data?: Data; 13 | error?: Error; 14 | mutate: ( 15 | data?: Data | Promise | MutatorCallback, 16 | shouldRevalidate?: boolean, 17 | ) => Promise; 18 | update: () => Promise; 19 | loading: boolean; 20 | initialLoading: boolean; 21 | }; 22 | 23 | export const useLidoSWR = ( 24 | key: Key | null, 25 | fetcher: Fetcher | null, 26 | config?: SWRConfiguration, 27 | ): SWRResponse => { 28 | const { swrConfig } = useSDK(); 29 | 30 | const result = useSWRSource(key, fetcher, { 31 | ...LIDO_SWR_DEFAULT_CONFIG, 32 | ...swrConfig, 33 | ...config, 34 | }); 35 | 36 | const mutate = result.mutate; 37 | 38 | const update = useCallback(() => { 39 | return mutate(undefined, true); 40 | }, [mutate]); 41 | 42 | return { 43 | mutate, 44 | update, 45 | 46 | /* 47 | * support dependency collection 48 | * https://swr.vercel.app/advanced/performance#dependency-collection 49 | */ 50 | 51 | get data() { 52 | return result.data; 53 | }, 54 | get loading() { 55 | return result.isValidating; 56 | }, 57 | get initialLoading() { 58 | return result.data == null && result.isValidating; 59 | }, 60 | get error() { 61 | return result.error; 62 | }, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useLidoSWRImmutable.ts: -------------------------------------------------------------------------------- 1 | import { useLidoSWR } from './useLidoSWR'; 2 | 3 | export const useLidoSWRImmutable: typeof useLidoSWR = ( 4 | key, 5 | fetcher, 6 | config, 7 | ) => { 8 | return useLidoSWR(key, fetcher, { 9 | revalidateIfStale: false, 10 | revalidateOnFocus: false, 11 | revalidateOnReconnect: false, 12 | ...config, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useCallback, 3 | useEffect, 4 | useState, 5 | Dispatch, 6 | SetStateAction, 7 | } from 'react'; 8 | import warning from 'tiny-warning'; 9 | 10 | export const useLocalStorage = ( 11 | key: string, 12 | initialValue: T, 13 | ): [storedValue: T, setValue: Dispatch>] => { 14 | const readValue = useCallback(() => { 15 | try { 16 | const item = window.localStorage.getItem(key); 17 | return item ? (JSON.parse(item) as T) : initialValue; 18 | } catch (error) { 19 | warning( 20 | typeof window === 'undefined', 21 | `Error reading localStorage key "${key}"`, 22 | ); 23 | return initialValue; 24 | } 25 | }, [initialValue, key]); 26 | 27 | const [storedValue, setStoredValue] = useState(readValue); 28 | 29 | const saveToStorage = useCallback( 30 | (newValue) => { 31 | try { 32 | window.localStorage.setItem(key, JSON.stringify(newValue)); 33 | window.dispatchEvent(new Event('local-storage')); 34 | } catch (error) { 35 | warning( 36 | typeof window === 'undefined', 37 | `Error setting localStorage key "${key}"`, 38 | ); 39 | } 40 | }, 41 | [key], 42 | ); 43 | 44 | const setValue = useCallback( 45 | (value) => { 46 | if (value instanceof Function) { 47 | setStoredValue((current) => { 48 | const newValue = value(current); 49 | saveToStorage(newValue); 50 | return newValue; 51 | }); 52 | } else { 53 | saveToStorage(value); 54 | setStoredValue(value); 55 | } 56 | }, 57 | [saveToStorage], 58 | ); 59 | 60 | useEffect(() => { 61 | setStoredValue(readValue()); 62 | }, [readValue]); 63 | 64 | useEffect(() => { 65 | const handleStorageChange = () => { 66 | setStoredValue(readValue()); 67 | }; 68 | window.addEventListener('storage', handleStorageChange); 69 | window.addEventListener('local-storage', handleStorageChange); 70 | 71 | return () => { 72 | window.removeEventListener('storage', handleStorageChange); 73 | window.removeEventListener('local-storage', handleStorageChange); 74 | }; 75 | }, [readValue]); 76 | 77 | return [storedValue, setValue]; 78 | }; 79 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useMountedState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatch, 3 | SetStateAction, 4 | useCallback, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react'; 9 | 10 | export const useMountedState = ( 11 | initialState: S | (() => S), 12 | ): [S, Dispatch>] => { 13 | const mountedRef = useRef(false); 14 | const [state, setState] = useState(initialState); 15 | 16 | useEffect(() => { 17 | mountedRef.current = true; 18 | 19 | return () => { 20 | mountedRef.current = false; 21 | }; 22 | }, []); 23 | 24 | useEffect(() => { 25 | setState(initialState); 26 | }, [initialState]); 27 | 28 | const setMountedState: Dispatch> = useCallback( 29 | (...args) => { 30 | if (!mountedRef.current) return; 31 | setState(...args); 32 | }, 33 | [], 34 | ); 35 | 36 | return [state, setMountedState]; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useSDK.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SDKContext, SDKContextValue } from '../context'; 3 | import invariant from 'tiny-invariant'; 4 | 5 | export const useSDK = (): SDKContextValue => { 6 | const contextValue = useContext(SDKContext); 7 | invariant(contextValue, 'useSDK was used outside of SDKContext'); 8 | return contextValue; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useTokenAddress.ts: -------------------------------------------------------------------------------- 1 | import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; 2 | import { useSDK } from './useSDK'; 3 | 4 | export const useTokenAddress = (token: TOKENS): string => { 5 | const { chainId } = useSDK(); 6 | return getTokenAddress(chainId, token); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useTokenBalance.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import warning from 'tiny-warning'; 3 | import { useEffect } from 'react'; 4 | import { BigNumber } from '@ethersproject/bignumber'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { useContractSWR } from './useContractSWR'; 7 | import { SWRResponse } from './useLidoSWR'; 8 | import { useSDK } from './useSDK'; 9 | import { SWRConfiguration } from 'swr'; 10 | import { useDebounceCallback } from './useDebounceCallback'; 11 | 12 | export const useTokenBalance = ( 13 | token: string, 14 | account?: string, 15 | config?: SWRConfiguration, 16 | ): SWRResponse => { 17 | const { providerRpc, providerWeb3, account: sdkAccount } = useSDK(); 18 | const mergedAccount = account ?? sdkAccount; 19 | 20 | invariant(token != null, 'Token is required'); 21 | 22 | const contractRpc = getERC20Contract(token, providerRpc); 23 | const contractWeb3 = providerWeb3 24 | ? getERC20Contract(token, providerWeb3) 25 | : null; 26 | 27 | const result = useContractSWR({ 28 | shouldFetch: !!mergedAccount, 29 | contract: contractRpc, 30 | method: 'balanceOf', 31 | params: [mergedAccount], 32 | config, 33 | }); 34 | 35 | const updateBalanceDebounced = useDebounceCallback(result.update, 1000); 36 | 37 | useEffect(() => { 38 | if (!mergedAccount || !providerWeb3 || !contractWeb3) return; 39 | 40 | try { 41 | const fromMe = contractWeb3.filters.Transfer(mergedAccount, null); 42 | const toMe = contractWeb3.filters.Transfer(null, mergedAccount); 43 | 44 | providerWeb3.on(fromMe, updateBalanceDebounced); 45 | providerWeb3.on(toMe, updateBalanceDebounced); 46 | 47 | return () => { 48 | providerWeb3.off(fromMe, updateBalanceDebounced); 49 | providerWeb3.off(toMe, updateBalanceDebounced); 50 | }; 51 | } catch (error) { 52 | return warning(false, 'Cannot subscribe to events'); 53 | } 54 | }, [providerWeb3, contractWeb3, mergedAccount, updateBalanceDebounced]); 55 | 56 | return result; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useTokenToWallet.ts: -------------------------------------------------------------------------------- 1 | import { getERC20Contract } from '@lido-sdk/contracts'; 2 | import { useCallback } from 'react'; 3 | import { useSDK } from './useSDK'; 4 | import { useMountedState } from './useMountedState'; 5 | 6 | export const useTokenToWallet = ( 7 | address: string, 8 | image?: string, 9 | ): { 10 | addToken?: () => Promise; 11 | loading: boolean; 12 | } => { 13 | const [loading, setLoading] = useMountedState(false); 14 | const { providerRpc, providerWeb3, onError } = useSDK(); 15 | 16 | const handleAdd = useCallback(async () => { 17 | const provider = providerWeb3?.provider; 18 | if (!provider?.request) return false; 19 | 20 | try { 21 | setLoading(true); 22 | const contract = getERC20Contract(address, providerRpc); 23 | 24 | const [symbol, decimals] = await Promise.all([ 25 | contract.symbol(), 26 | contract.decimals(), 27 | ]); 28 | 29 | const result = await provider.request({ 30 | method: 'wallet_watchAsset', 31 | params: { 32 | type: 'ERC20', 33 | options: { 34 | address, 35 | symbol, 36 | decimals, 37 | image, 38 | }, 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | } as any, 41 | }); 42 | 43 | return !!result; 44 | } catch (error) { 45 | onError(error); 46 | return false; 47 | } finally { 48 | setLoading(false); 49 | } 50 | }, [address, image, providerWeb3, providerRpc, setLoading, onError]); 51 | 52 | const canAdd = !!providerWeb3?.provider.isMetaMask; 53 | const addToken = canAdd ? handleAdd : undefined; 54 | 55 | return { 56 | addToken, 57 | loading, 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useTotalSupply.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import warning from 'tiny-warning'; 3 | import { useEffect } from 'react'; 4 | import { BigNumber } from '@ethersproject/bignumber'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { useContractSWR } from './useContractSWR'; 7 | import { SWRResponse } from './useLidoSWR'; 8 | import { useSDK } from './useSDK'; 9 | import { SWRConfiguration } from 'swr'; 10 | import { useDebounceCallback } from './useDebounceCallback'; 11 | 12 | export const useTotalSupply = ( 13 | token: string, 14 | config?: SWRConfiguration, 15 | ): SWRResponse => { 16 | const { providerRpc, providerWeb3 } = useSDK(); 17 | 18 | invariant(token != null, 'Token is required'); 19 | 20 | const contractRpc = getERC20Contract(token, providerRpc); 21 | const contractWeb3 = providerWeb3 22 | ? getERC20Contract(token, providerWeb3) 23 | : null; 24 | 25 | const result = useContractSWR({ 26 | contract: contractRpc, 27 | method: 'totalSupply', 28 | config, 29 | }); 30 | 31 | const updateSupplyDebounced = useDebounceCallback(result.update, 1000); 32 | 33 | useEffect(() => { 34 | if (!providerWeb3 || !contractWeb3) return; 35 | try { 36 | const transfer = contractWeb3.filters.Transfer(); 37 | providerWeb3.on(transfer, updateSupplyDebounced); 38 | 39 | return () => { 40 | providerWeb3.off(transfer, updateSupplyDebounced); 41 | }; 42 | } catch (error) { 43 | return warning(false, 'Cannot subscribe to events'); 44 | } 45 | }, [providerWeb3, contractWeb3, updateSupplyDebounced]); 46 | 47 | return result; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useTxPrice.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; 2 | import { WeiPerEther } from '@ethersproject/constants'; 3 | import { divide } from '@lido-sdk/helpers'; 4 | import { useCallback, useMemo } from 'react'; 5 | import { useEthereumSWR } from './useEthereumSWR'; 6 | import { useEthPrice } from './useEthPrice'; 7 | import { SWRResponse } from './useLidoSWR'; 8 | 9 | const getTxPrice = ( 10 | gasLimit: BigNumberish, 11 | ethPrice?: number, 12 | gasPrice?: BigNumber, 13 | ) => { 14 | if (!gasLimit || ethPrice == null || gasPrice == null) { 15 | return undefined; 16 | } 17 | 18 | const txCostInWei = gasPrice.mul(BigNumber.from(gasLimit)); 19 | const txCostInEth = divide(txCostInWei, WeiPerEther); 20 | 21 | return ethPrice * txCostInEth; 22 | }; 23 | 24 | export const useTxPrice = ( 25 | gasLimit: BigNumberish, 26 | ): Omit, 'mutate'> => { 27 | const eth = useEthPrice(); 28 | const gas = useEthereumSWR({ method: 'getGasPrice' }); 29 | 30 | const ethPrice = eth.data; 31 | const gasPrice = gas.data; 32 | 33 | const data = useMemo(() => { 34 | return getTxPrice(gasLimit, ethPrice, gasPrice); 35 | }, [gasLimit, ethPrice, gasPrice]); 36 | 37 | const updateEth = eth.update; 38 | const updateGas = gas.update; 39 | 40 | const update = useCallback(async () => { 41 | const [ethPrice, gasPrice] = await Promise.all([updateEth(), updateGas()]); 42 | return getTxPrice(gasLimit, ethPrice, gasPrice); 43 | }, [gasLimit, updateEth, updateGas]); 44 | 45 | return { 46 | update, 47 | data, 48 | 49 | /* 50 | * support dependency collection 51 | * https://swr.vercel.app/advanced/performance#dependency-collection 52 | */ 53 | 54 | get loading() { 55 | return eth.loading || gas.loading; 56 | }, 57 | get initialLoading() { 58 | return eth.initialLoading || gas.initialLoading; 59 | }, 60 | get error() { 61 | return eth.error || gas.error; 62 | }, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './context'; 3 | export * from './factories'; 4 | -------------------------------------------------------------------------------- /packages/react/test/factories/contracts.test.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { renderHook, act } from '@testing-library/react-hooks'; 3 | import { Web3Provider } from '@ethersproject/providers'; 4 | import { Contract } from '@ethersproject/contracts'; 5 | import { ProviderWrapper } from '../hooks/testUtils'; 6 | import { 7 | useSTETHContractRPC, 8 | useWSTETHContractRPC, 9 | useLDOContractRPC, 10 | useWithdrawalQueueContractRPC, 11 | useSTETHContractWeb3, 12 | useWSTETHContractWeb3, 13 | useLDOContractWeb3, 14 | useWithdrawalQueueContractWeb3, 15 | } from '../../src/factories/contracts'; 16 | 17 | const hooksRpc = { 18 | useSTETHContractRPC, 19 | useWSTETHContractRPC, 20 | useLDOContractRPC, 21 | useWithdrawalQueueContractRPC, 22 | }; 23 | 24 | const hooksWeb3 = { 25 | useSTETHContractWeb3, 26 | useWSTETHContractWeb3, 27 | useLDOContractWeb3, 28 | useWithdrawalQueueContractWeb3, 29 | }; 30 | 31 | describe('web3 contracts', () => { 32 | const ProviderWeb3 = new Web3Provider(async () => void 0); 33 | 34 | Object.entries(hooksWeb3).map(([name, hook]) => { 35 | test(`${name} should be a function`, async () => { 36 | expect(hook).toBeInstanceOf(Function); 37 | }); 38 | 39 | test(`${name} should work`, async () => { 40 | const wrapper: FC = (props) => ( 41 | 42 | ); 43 | 44 | const { result } = renderHook(() => hook(), { wrapper }); 45 | expect(result.current).toBeInstanceOf(Contract); 46 | }); 47 | 48 | test(`${name} should return null if providerWeb3 is not passed`, async () => { 49 | const wrapper = ProviderWrapper; 50 | 51 | const { result } = renderHook(() => hook(), { wrapper }); 52 | expect(result.current).toBeNull(); 53 | }); 54 | 55 | test(`${name} should return the same contract`, async () => { 56 | const wrapper: FC = (props) => ( 57 | 58 | ); 59 | 60 | const { result, rerender } = renderHook(() => hook(), { wrapper }); 61 | const firstResult = result.current; 62 | act(() => rerender()); 63 | const secondResult = result.current; 64 | 65 | expect(firstResult).toBe(secondResult); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('RPC contracts', () => { 71 | Object.entries(hooksRpc).map(([name, hook]) => { 72 | test(`${name} should be a function`, async () => { 73 | expect(hook).toBeInstanceOf(Function); 74 | }); 75 | 76 | test(`${name} should return a contract`, async () => { 77 | const wrapper = ProviderWrapper; 78 | 79 | const { result } = renderHook(() => hook(), { wrapper }); 80 | expect(result.current).toBeInstanceOf(Contract); 81 | }); 82 | 83 | test(`${name} should return the same contract`, async () => { 84 | const wrapper = ProviderWrapper; 85 | 86 | const { result, rerender } = renderHook(() => hook(), { wrapper }); 87 | const firstResult = result.current; 88 | act(() => rerender()); 89 | const secondResult = result.current; 90 | 91 | expect(firstResult).toBe(secondResult); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/react/test/factories/tokens.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | 3 | import { renderHook } from '@testing-library/react-hooks'; 4 | import { Zero } from '@ethersproject/constants'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { ProviderWrapper } from '../hooks/testUtils'; 7 | import * as tokensExport from '../../src/factories/tokens'; 8 | 9 | const { hooksFactory, ...hooks } = tokensExport; 10 | 11 | const mockGetter = getERC20Contract as jest.MockedFunction< 12 | typeof getERC20Contract 13 | >; 14 | 15 | describe('tokens', () => { 16 | afterEach(() => { 17 | mockGetter.mockReset(); 18 | }); 19 | 20 | Object.entries(hooks).map(([name, hook]) => { 21 | test(`${name} should be a function`, async () => { 22 | expect(hook).toBeInstanceOf(Function); 23 | }); 24 | 25 | test(`${name} should wrap hook correctly`, async () => { 26 | const wrapper = ProviderWrapper; 27 | 28 | mockGetter.mockReturnValue({} as any); 29 | 30 | const getArguments = (name: string) => { 31 | if (name.endsWith('Allowance')) return ['spender']; 32 | if (name.endsWith('Approve')) return [Zero, 'spender']; 33 | return []; 34 | }; 35 | 36 | const { result } = renderHook(() => hook(...getArguments(name)), { 37 | wrapper, 38 | }); 39 | expect(result.error).toBeUndefined(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/react/test/hooks/testUtils.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { CHAINS } from '@lido-sdk/constants'; 3 | import { SDKContextProps, ProviderSDK } from '../../src/context'; 4 | import { SWRConfig } from 'swr'; 5 | 6 | const supportedChainIds = [CHAINS.Goerli, CHAINS.Mainnet]; 7 | const chainId = CHAINS.Goerli; 8 | const providerProps = { supportedChainIds, chainId }; 9 | 10 | export const ProviderWrapper: FC> = (props) => ( 11 | new Map() }}> 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useAllowance.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | jest.mock('tiny-warning'); 3 | 4 | import { FC } from 'react'; 5 | import warning from 'tiny-warning'; 6 | import { renderHook, act } from '@testing-library/react-hooks'; 7 | import { getERC20Contract } from '@lido-sdk/contracts'; 8 | import { ProviderWrapper } from './testUtils'; 9 | import { useAllowance } from '../../src/hooks/useAllowance'; 10 | 11 | const mockGetter = getERC20Contract as jest.MockedFunction< 12 | typeof getERC20Contract 13 | >; 14 | const mockWarning = warning as jest.MockedFunction; 15 | 16 | describe('useAllowance', () => { 17 | afterEach(() => { 18 | mockGetter.mockReset(); 19 | mockWarning.mockReset(); 20 | }); 21 | 22 | test('should fetch data', async () => { 23 | const expected = 1; 24 | const wrapper = ProviderWrapper; 25 | 26 | mockGetter.mockReturnValue({ allowance: async () => expected } as any); 27 | const { result, waitForNextUpdate } = renderHook( 28 | () => useAllowance('token', 'spender', 'owner'), 29 | { wrapper }, 30 | ); 31 | 32 | expect(result.current.data).toBeUndefined(); 33 | await waitForNextUpdate(); 34 | expect(result.current.data).toBe(expected); 35 | }); 36 | 37 | test('should use account from provider if it’s not passed', async () => { 38 | const expected = 'sdk account'; 39 | const mockAllowance = jest.fn(() => void 0); 40 | mockGetter.mockReturnValue({ allowance: mockAllowance } as any); 41 | const wrapper: FC = (props) => ( 42 | 43 | ); 44 | renderHook(() => useAllowance('token', 'spender'), { wrapper }); 45 | 46 | expect(mockAllowance).toHaveBeenCalledTimes(1); 47 | expect(mockAllowance).toHaveBeenCalledWith(expected, 'spender'); 48 | }); 49 | 50 | test('should subscribe on events data', async () => { 51 | const expected = 1; 52 | const mockOn = jest.fn(() => void 0); 53 | const mockOff = jest.fn(() => void 0); 54 | const providerWeb3 = { on: mockOn, off: mockOff } as any; 55 | 56 | mockGetter.mockReturnValue({ 57 | allowance: async () => expected, 58 | filters: { 59 | Transfer: () => void 0, 60 | Approval: () => void 0, 61 | }, 62 | } as any); 63 | const wrapper: FC = (props) => ( 64 | 65 | ); 66 | const { unmount } = renderHook( 67 | () => useAllowance('token', 'spender', 'owner'), 68 | { wrapper }, 69 | ); 70 | 71 | expect(mockOn).toHaveBeenCalledTimes(2); 72 | act(() => unmount()); 73 | expect(mockOff).toHaveBeenCalledTimes(2); 74 | }); 75 | 76 | test('should catch a subscribe error', async () => { 77 | const expected = 1; 78 | const wrapper: FC = (props) => ( 79 | 80 | ); 81 | mockGetter.mockReturnValue({ allowance: async () => expected } as any); 82 | renderHook(() => useAllowance('token', 'spender', 'owner'), { wrapper }); 83 | 84 | expect(mockWarning).toHaveBeenCalledTimes(1); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useApprove.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | jest.mock('../../src/hooks/useAllowance'); 3 | 4 | import { FC } from 'react'; 5 | import { act, renderHook } from '@testing-library/react-hooks'; 6 | import { getERC20Contract } from '@lido-sdk/contracts'; 7 | import { BigNumber } from '@ethersproject/bignumber'; 8 | import { Zero } from '@ethersproject/constants'; 9 | import { ProviderWrapper } from './testUtils'; 10 | import { useApprove } from '../../src/hooks/useApprove'; 11 | import { useAllowance } from '../../src/hooks/useAllowance'; 12 | 13 | const mockUseAllowance = useAllowance as jest.MockedFunction< 14 | typeof useAllowance 15 | >; 16 | const mockGetter = getERC20Contract as jest.MockedFunction< 17 | typeof getERC20Contract 18 | >; 19 | 20 | const common = { 21 | loading: false, 22 | initialLoading: false, 23 | error: undefined, 24 | mutate: async () => void 0, 25 | update: async () => void 0, 26 | }; 27 | 28 | describe('useApprove', () => { 29 | const allowance = BigNumber.from(1); 30 | const providerWeb3 = { getSigner: () => void 0 } as any; 31 | 32 | beforeEach(() => { 33 | mockUseAllowance.mockReturnValue({ data: allowance, ...common }); 34 | mockGetter.mockReturnValue({ 35 | approve: async () => ({ 36 | wait: async () => void 0, 37 | }), 38 | } as any); 39 | }); 40 | 41 | afterEach(() => { 42 | mockUseAllowance.mockReset(); 43 | mockGetter.mockReset(); 44 | }); 45 | 46 | test('should return allowance', async () => { 47 | const wrapper: FC = (props) => ( 48 | 49 | ); 50 | const { result } = renderHook(() => useApprove(Zero, 'token', 'spender'), { 51 | wrapper, 52 | }); 53 | expect(result.current.allowance).toBe(allowance); 54 | }); 55 | 56 | test('should need approve', async () => { 57 | const wrapper: FC = (props) => ( 58 | 59 | ); 60 | const { result } = renderHook( 61 | () => useApprove(allowance.add(1), 'token', 'spender'), 62 | { wrapper }, 63 | ); 64 | 65 | expect(result.current.needsApprove).toBe(true); 66 | }); 67 | 68 | test('should not need approve', async () => { 69 | const wrapper: FC = (props) => ( 70 | 71 | ); 72 | [Zero, allowance.sub(1), allowance].forEach((amount) => { 73 | const { result } = renderHook( 74 | () => useApprove(amount, 'token', 'spender'), 75 | { wrapper }, 76 | ); 77 | expect(result.current.needsApprove).toBe(false); 78 | }); 79 | }); 80 | 81 | test('should set approving', async () => { 82 | const wrapper: FC = (props) => ( 83 | 84 | ); 85 | 86 | const { result } = renderHook(() => useApprove(Zero, 'token', 'spender'), { 87 | wrapper, 88 | }); 89 | 90 | expect(result.current.approving).toBe(false); 91 | const approve = act(() => result.current.approve()); 92 | expect(result.current.approving).toBe(true); 93 | await approve; 94 | expect(result.current.approving).toBe(false); 95 | }); 96 | 97 | test('should catch an error', async () => { 98 | mockGetter.mockReturnValue({ approve: async () => void 0 } as any); 99 | 100 | const mockOnError = jest.fn(() => void 0); 101 | const wrapper: FC = (props) => ( 102 | 107 | ); 108 | 109 | const { result } = renderHook(() => useApprove(Zero, 'token', 'spender'), { 110 | wrapper, 111 | }); 112 | 113 | await act(() => result.current.approve()); 114 | expect(mockOnError).toHaveBeenCalledTimes(1); 115 | }); 116 | 117 | test('should inherit allowance errors', async () => { 118 | const wrapper: FC = (props) => ( 119 | 120 | ); 121 | mockUseAllowance.mockReturnValue({ error: new Error() } as any); 122 | const { result } = renderHook(() => useApprove(Zero, 'token', 'spender'), { 123 | wrapper, 124 | }); 125 | expect(result.current.error).toBeInstanceOf(Error); 126 | }); 127 | 128 | test('should inherit allowance loading', async () => { 129 | const wrapper: FC = (props) => ( 130 | 131 | ); 132 | mockUseAllowance.mockReturnValue({ loading: true } as any); 133 | const { result } = renderHook(() => useApprove(Zero, 'token', 'spender'), { 134 | wrapper, 135 | }); 136 | expect(result.current.loading).toBe(true); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useContractEstimateGasSWR.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useContractEstimateGasSWR } from '../../src/hooks/useContractEstimateGasSWR'; 3 | import { ProviderWrapper as wrapper } from './testUtils'; 4 | describe('useContractEstimateGasSWR', () => { 5 | test('should fetch data', async () => { 6 | const expected = 1; 7 | const contract = { estimateGas: { test: async () => expected } } as any; 8 | 9 | const { result, waitForNextUpdate } = renderHook( 10 | () => useContractEstimateGasSWR({ method: 'test', contract }), 11 | { wrapper }, 12 | ); 13 | 14 | expect(result.current.data).toBeUndefined(); 15 | await waitForNextUpdate(); 16 | expect(result.current.data).toBe(expected); 17 | }); 18 | 19 | test('should not fetch', async () => { 20 | const expected = 1; 21 | const mockMethod = jest.fn(() => expected); 22 | const contract = { estimateGas: { test: mockMethod } } as any; 23 | 24 | const { result } = renderHook( 25 | () => 26 | useContractEstimateGasSWR({ 27 | shouldFetch: false, 28 | method: 'test', 29 | contract, 30 | }), 31 | { wrapper }, 32 | ); 33 | 34 | expect(result.current.data).toBeUndefined(); 35 | expect(mockMethod).toHaveBeenCalledTimes(0); 36 | }); 37 | 38 | test('should not throw an error if contract is undefined', async () => { 39 | const { result } = renderHook( 40 | () => useContractEstimateGasSWR({ method: 'test' }), 41 | { wrapper }, 42 | ); 43 | 44 | expect(result.current.data).toBeUndefined(); 45 | expect(result.error).toBeUndefined(); 46 | }); 47 | 48 | test('should update if contract is changed', async () => { 49 | const contractFirst = { estimateGas: { test: async () => 1 } } as any; 50 | const contractSecond = { estimateGas: { test: async () => 2 } } as any; 51 | 52 | const { result, rerender, waitForNextUpdate } = renderHook( 53 | ({ contract }) => useContractEstimateGasSWR({ method: 'test', contract }), 54 | { initialProps: { contract: contractFirst }, wrapper }, 55 | ); 56 | 57 | expect(result.current.data).toBeUndefined(); 58 | await waitForNextUpdate(); 59 | expect(result.current.data).toBe(1); 60 | 61 | act(() => rerender({ contract: contractSecond })); 62 | 63 | expect(result.current.data).toBeUndefined(); 64 | await waitForNextUpdate(); 65 | expect(result.current.data).toBe(2); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useContractSWR.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useContractSWR } from '../../src/hooks/useContractSWR'; 3 | 4 | import { ProviderWrapper as wrapper } from './testUtils'; 5 | 6 | describe('useContractSWR', () => { 7 | test('should fetch data', async () => { 8 | const expected = 1; 9 | const contract = { test: async () => expected } as any; 10 | 11 | const { result, waitForNextUpdate } = renderHook( 12 | () => { 13 | const { data } = useContractSWR({ method: 'test', contract }); 14 | return data; 15 | }, 16 | { wrapper }, 17 | ); 18 | 19 | expect(result.current).toBeUndefined(); 20 | await waitForNextUpdate({ timeout: 10_000 }); 21 | expect(result.current).toBe(expected); 22 | }); 23 | 24 | test('should not fetch', async () => { 25 | const expected = 1; 26 | const mockMethod = jest.fn(() => expected); 27 | const contract = { test: mockMethod } as any; 28 | 29 | const { result } = renderHook( 30 | () => 31 | useContractSWR({ 32 | shouldFetch: false, 33 | method: 'test', 34 | contract, 35 | }), 36 | { wrapper }, 37 | ); 38 | 39 | expect(result.current.data).toBeUndefined(); 40 | expect(mockMethod).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | test('should update if contract is changed', async () => { 44 | const contractFirst = { test: async () => 1 } as any; 45 | const contractSecond = { test: async () => 2 } as any; 46 | 47 | const { result, rerender, waitForNextUpdate } = renderHook( 48 | ({ contract }) => useContractSWR({ method: 'test', contract }), 49 | { initialProps: { contract: contractFirst }, wrapper }, 50 | ); 51 | 52 | expect(result.current.data).toBeUndefined(); 53 | await waitForNextUpdate(); 54 | expect(result.current.data).toBe(1); 55 | 56 | act(() => rerender({ contract: contractSecond })); 57 | 58 | expect(result.current.data).toBeUndefined(); 59 | await waitForNextUpdate(); 60 | expect(result.current.data).toBe(2); 61 | }); 62 | 63 | test('should not update on rerender', async () => { 64 | const expected = 1; 65 | const mockMethod = jest.fn(() => expected); 66 | const contract = { test: mockMethod } as any; 67 | 68 | const { result, rerender, waitForNextUpdate } = renderHook( 69 | () => useContractSWR({ method: 'test', contract }), 70 | { wrapper }, 71 | ); 72 | 73 | expect(result.current.data).toBeUndefined(); 74 | await waitForNextUpdate(); 75 | expect(result.current.data).toBe(expected); 76 | 77 | act(() => rerender()); 78 | 79 | expect(result.current.data).toBe(expected); 80 | expect(mockMethod).toHaveBeenCalledTimes(1); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useDebounceCallback.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useDebounceCallback } from '../../src/hooks/useDebounceCallback'; 3 | 4 | describe('useDebounceCallback', () => { 5 | test('should return a function', async () => { 6 | const { result } = renderHook(() => useDebounceCallback(() => 1)); 7 | expect(result.current).toBeInstanceOf(Function); 8 | }); 9 | 10 | test('should be called', async () => { 11 | const expected = 1; 12 | const callback = jest.fn(() => expected); 13 | const { result, waitFor } = renderHook(() => useDebounceCallback(callback)); 14 | 15 | result.current(); 16 | await waitFor(() => expect(callback).toHaveBeenCalledTimes(1)); 17 | }); 18 | 19 | test('should group calls', async () => { 20 | const expected = 1; 21 | const callback = jest.fn(() => expected); 22 | const { result, waitFor } = renderHook(() => useDebounceCallback(callback)); 23 | 24 | result.current(); 25 | result.current(); 26 | 27 | await expect(async () => { 28 | await waitFor(() => expect(callback).toHaveBeenCalledTimes(2), { 29 | timeout: 50, 30 | }); 31 | }).rejects.toThrowError(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useDecimals.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | 3 | import { renderHook } from '@testing-library/react-hooks'; 4 | import { getERC20Contract } from '@lido-sdk/contracts'; 5 | import { ProviderWrapper } from './testUtils'; 6 | import { useDecimals } from '../../src/hooks/useDecimals'; 7 | 8 | const mockGetter = getERC20Contract as jest.MockedFunction< 9 | typeof getERC20Contract 10 | >; 11 | 12 | describe('useDecimals', () => { 13 | afterEach(() => { 14 | mockGetter.mockReset(); 15 | }); 16 | 17 | test('should fetch data', async () => { 18 | const expected = 1; 19 | const wrapper = ProviderWrapper; 20 | 21 | mockGetter.mockReturnValue({ decimals: async () => expected } as any); 22 | const { result, waitForNextUpdate } = renderHook( 23 | () => useDecimals('token'), 24 | { wrapper }, 25 | ); 26 | 27 | expect(result.current.data).toBeUndefined(); 28 | await waitForNextUpdate(); 29 | expect(result.current.data).toBe(expected); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useEthPrice.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | 3 | import { renderHook, act } from '@testing-library/react-hooks'; 4 | import { BigNumber } from '@ethersproject/bignumber'; 5 | import { getAggregatorContract } from '@lido-sdk/contracts'; 6 | import { useEthPrice } from '../../src/hooks/useEthPrice'; 7 | import { ProviderWrapper as wrapper } from './testUtils'; 8 | 9 | const mockGetter = getAggregatorContract as jest.MockedFunction< 10 | typeof getAggregatorContract 11 | >; 12 | 13 | const convertToBig = (number: number, decimals: number) => { 14 | const precision = 6; 15 | const int = Math.floor(number * 10 ** precision); 16 | 17 | return BigNumber.from(int) 18 | .mul(BigNumber.from(10).pow(decimals)) 19 | .div(BigNumber.from(10).pow(precision)); 20 | }; 21 | 22 | describe('useEthPrice', () => { 23 | const expected = 1000; 24 | const decimals = 18; 25 | const latestAnswer = convertToBig(expected, decimals); 26 | 27 | const mockDecimals = jest.fn(async () => decimals); 28 | const mockLatestAnswer = jest.fn(async () => latestAnswer); 29 | 30 | beforeEach(() => { 31 | mockGetter.mockReturnValue({ 32 | decimals: mockDecimals, 33 | latestAnswer: mockLatestAnswer, 34 | } as any); 35 | }); 36 | 37 | afterEach(() => { 38 | mockGetter.mockReset(); 39 | }); 40 | 41 | test('should fetch data', async () => { 42 | const { result, waitForNextUpdate } = renderHook(() => useEthPrice().data, { 43 | wrapper, 44 | }); 45 | 46 | expect(result.current).toBeUndefined(); 47 | await waitForNextUpdate(); 48 | expect(result.current).toBe(expected); 49 | }); 50 | 51 | test('should update', async () => { 52 | const expectedFirst = 1234.56; 53 | const expectedSecond = 234.567; 54 | 55 | mockLatestAnswer.mockReturnValue( 56 | Promise.resolve(convertToBig(expectedFirst, decimals)), 57 | ); 58 | 59 | const { result, waitForNextUpdate } = renderHook( 60 | () => { 61 | const { data, update } = useEthPrice(); 62 | return { data, update }; 63 | }, 64 | { 65 | wrapper, 66 | }, 67 | ); 68 | 69 | expect(result.current.data).toBeUndefined(); 70 | await waitForNextUpdate(); 71 | expect(result.current.data).toBe(expectedFirst); 72 | 73 | mockLatestAnswer.mockReturnValue( 74 | Promise.resolve(convertToBig(expectedSecond, decimals)), 75 | ); 76 | 77 | await act(async () => { 78 | await expect(result.current.update()).resolves.toBe(expectedSecond); 79 | }); 80 | expect(result.current.data).toBe(expectedSecond); 81 | }); 82 | 83 | test('should set loading', async () => { 84 | const { result, waitForNextUpdate } = renderHook( 85 | () => useEthPrice().loading, 86 | { 87 | wrapper, 88 | }, 89 | ); 90 | 91 | expect(result.current).toBe(true); 92 | await waitForNextUpdate(); 93 | expect(result.current).toBe(false); 94 | }); 95 | 96 | test('should set initial loading', async () => { 97 | const { result, waitForNextUpdate } = renderHook( 98 | () => useEthPrice().initialLoading, 99 | { 100 | wrapper, 101 | }, 102 | ); 103 | 104 | expect(result.current).toBe(true); 105 | await waitForNextUpdate(); 106 | expect(result.current).toBe(false); 107 | }); 108 | 109 | test('should catch an error', async () => { 110 | mockGetter.mockReturnValue({ 111 | decimals: async () => { 112 | throw new Error(); 113 | }, 114 | latestAnswer: mockLatestAnswer, 115 | } as any); 116 | 117 | const { result, waitForNextUpdate } = renderHook(() => useEthPrice(), { 118 | wrapper, 119 | }); 120 | 121 | expect(result.current.error).toBeUndefined(); 122 | await waitForNextUpdate(); 123 | expect(result.current.error).toBeInstanceOf(Error); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useEthereumBalance.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('tiny-warning'); 2 | 3 | import warning from 'tiny-warning'; 4 | import { FC } from 'react'; 5 | import { renderHook } from '@testing-library/react-hooks'; 6 | import { ProviderWrapper } from './testUtils'; 7 | import { useEthereumBalance } from '../../src/hooks/useEthereumBalance'; 8 | 9 | const mockWarning = warning as jest.MockedFunction; 10 | 11 | describe('useEthereumBalance', () => { 12 | afterEach(() => { 13 | mockWarning.mockReset(); 14 | }); 15 | 16 | test('should fetch data', async () => { 17 | const expected = 1; 18 | const mockGetBalance = jest.fn(() => expected); 19 | const mockOn = jest.fn(() => void 0); 20 | const mockOff = jest.fn(() => void 0); 21 | const providerRpc = { getBalance: mockGetBalance } as any; 22 | const providerWeb3 = { on: mockOn, off: mockOff } as any; 23 | 24 | const wrapper: FC = (props) => ( 25 | 30 | ); 31 | const { result, waitForNextUpdate } = renderHook( 32 | () => useEthereumBalance('account'), 33 | { 34 | wrapper, 35 | }, 36 | ); 37 | 38 | expect(result.current.data).toBeUndefined(); 39 | await waitForNextUpdate(); 40 | expect(result.current.data).toBe(expected); 41 | }); 42 | 43 | test('should use account from provider if it’s not passed', async () => { 44 | const expected = 'sdk account'; 45 | const mockGetBalance = jest.fn(() => expected); 46 | const providerRpc = { getBalance: mockGetBalance } as any; 47 | const wrapper: FC = (props) => ( 48 | 53 | ); 54 | renderHook(() => useEthereumBalance(), { wrapper }); 55 | 56 | expect(mockGetBalance).toHaveBeenCalledTimes(1); 57 | expect(mockGetBalance).toHaveBeenCalledWith(expected, 'latest'); 58 | }); 59 | 60 | test('should catch a subscribe error', async () => { 61 | const wrapper: FC = (props) => ( 62 | 67 | ); 68 | renderHook(() => useEthereumBalance('account'), { wrapper }); 69 | 70 | expect(mockWarning).toHaveBeenCalledTimes(1); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useEthereumSWR.test.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { renderHook, act } from '@testing-library/react-hooks'; 3 | import { ProviderWrapper } from './testUtils'; 4 | import { useEthereumSWR } from '../../src/hooks/useEthereumSWR'; 5 | 6 | describe('useEthereumSWR', () => { 7 | test('should fetch data', async () => { 8 | const expected = 1; 9 | const providerRpc = { getGasPrice: async () => expected } as any; 10 | 11 | const { result, waitForNextUpdate } = renderHook( 12 | () => useEthereumSWR({ method: 'getGasPrice', providerRpc }), 13 | { wrapper: ProviderWrapper }, 14 | ); 15 | 16 | expect(result.current.data).toBeUndefined(); 17 | await waitForNextUpdate(); 18 | expect(result.current.data).toBe(expected); 19 | }); 20 | 21 | test('should not fetch', async () => { 22 | const expected = 1; 23 | const mockMethod = jest.fn(() => expected); 24 | const providerRpc = { getGasPrice: mockMethod } as any; 25 | 26 | const { result } = renderHook( 27 | () => 28 | useEthereumSWR({ 29 | shouldFetch: false, 30 | method: 'getGasPrice', 31 | providerRpc, 32 | }), 33 | { wrapper: ProviderWrapper }, 34 | ); 35 | 36 | expect(result.current.data).toBeUndefined(); 37 | expect(mockMethod).toHaveBeenCalledTimes(0); 38 | }); 39 | 40 | test('should update if provider is changed', async () => { 41 | const providerFirst = { getGasPrice: async () => 1 } as any; 42 | const providerSecond = { getGasPrice: async () => 2 } as any; 43 | 44 | const { result, rerender, waitForNextUpdate } = renderHook( 45 | ({ providerRpc }) => 46 | useEthereumSWR({ method: 'getGasPrice', providerRpc }), 47 | { 48 | initialProps: { providerRpc: providerFirst }, 49 | wrapper: ProviderWrapper, 50 | }, 51 | ); 52 | 53 | expect(result.current.data).toBeUndefined(); 54 | await waitForNextUpdate(); 55 | expect(result.current.data).toBe(1); 56 | 57 | act(() => rerender({ providerRpc: providerSecond })); 58 | 59 | expect(result.current.data).toBeUndefined(); 60 | await waitForNextUpdate(); 61 | expect(result.current.data).toBe(2); 62 | }); 63 | 64 | test('should not update on rerender', async () => { 65 | const expected = 1; 66 | const mockMethod = jest.fn(() => expected); 67 | const providerRpc = { getGasPrice: mockMethod } as any; 68 | 69 | const { result, rerender, waitForNextUpdate } = renderHook( 70 | () => useEthereumSWR({ method: 'getGasPrice', providerRpc }), 71 | { wrapper: ProviderWrapper }, 72 | ); 73 | 74 | expect(result.current.data).toBeUndefined(); 75 | await waitForNextUpdate(); 76 | expect(result.current.data).toBe(expected); 77 | 78 | act(() => rerender()); 79 | 80 | expect(result.current.data).toBe(expected); 81 | expect(mockMethod).toHaveBeenCalledTimes(1); 82 | }); 83 | 84 | test('should use providerRpc from SDK', async () => { 85 | const mockMethod = jest.fn(() => void 0); 86 | const providerRpc = { getGasPrice: mockMethod } as any; 87 | const wrapper: FC = (props) => ( 88 | 89 | ); 90 | renderHook(() => useEthereumSWR({ method: 'getGasPrice' }), { wrapper }); 91 | 92 | expect(mockMethod).toHaveBeenCalledTimes(1); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useEtherscanOpen.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { ProviderWrapper } from './testUtils'; 3 | import { useEtherscanOpen } from '../../src/hooks/useEtherscanOpen'; 4 | 5 | describe('useEtherscanOpen', () => { 6 | test('should fetch data', async () => { 7 | const wrapper = ProviderWrapper; 8 | const spy = jest.spyOn(window, 'open').mockImplementation(() => null); 9 | 10 | const { result } = renderHook( 11 | () => useEtherscanOpen('http://foo.bar', 'token'), 12 | { wrapper }, 13 | ); 14 | 15 | expect(result.current).toBeInstanceOf(Function); 16 | expect(() => result.current()).not.toThrow(); 17 | expect(spy).toHaveBeenCalledTimes(1); 18 | 19 | spy.mockRestore(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useFeeAnalytics.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/hooks/useFeeHistory'); 2 | 3 | import { BigNumber } from '@ethersproject/bignumber'; 4 | import { renderHook } from '@testing-library/react-hooks'; 5 | import { useFeeHistory } from '../../src/hooks/useFeeHistory'; 6 | import { 7 | calculatePercentile, 8 | useFeeAnalytics, 9 | } from '../../src/hooks/useFeeAnalytics'; 10 | 11 | const mockUseFeeHistory = useFeeHistory as jest.MockedFunction< 12 | typeof useFeeHistory 13 | >; 14 | 15 | describe('calculatePercentile', () => { 16 | test('should work correctly', () => { 17 | const percentile = calculatePercentile( 18 | [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3)], 19 | BigNumber.from(2), 20 | ); 21 | expect(percentile).toBe(1 / 3); 22 | }); 23 | 24 | test('should work if target is not in array', () => { 25 | const percentile = calculatePercentile( 26 | [BigNumber.from(1), BigNumber.from(2)], 27 | BigNumber.from(3), 28 | ); 29 | expect(percentile).toBe(1); 30 | }); 31 | 32 | test('should work if array is empty', () => { 33 | const percentile = calculatePercentile([], BigNumber.from(3)); 34 | expect(percentile).toBe(1); 35 | }); 36 | }); 37 | 38 | const common = { 39 | loading: false, 40 | initialLoading: false, 41 | error: undefined, 42 | mutate: async () => void [], 43 | update: async () => void [], 44 | }; 45 | 46 | describe('useFeeAnalytics', () => { 47 | beforeEach(() => { 48 | const feeHistory = { 49 | oldestBlock: 1, 50 | baseFeePerGas: [BigNumber.from(1), BigNumber.from(2), BigNumber.from(3)], 51 | gasUsedRatio: [1, 2], 52 | }; 53 | mockUseFeeHistory.mockReturnValue({ data: feeHistory, ...common }); 54 | }); 55 | 56 | afterEach(() => { 57 | mockUseFeeHistory.mockReset(); 58 | }); 59 | 60 | test('should return percentile', async () => { 61 | const { result } = renderHook(() => useFeeAnalytics()); 62 | expect(result.current.percentile).toBe(2 / 3); 63 | }); 64 | 65 | test('should return last baseFee', async () => { 66 | const { result } = renderHook(() => useFeeAnalytics()); 67 | expect(result.current.baseFee).toEqual(BigNumber.from(3)); 68 | }); 69 | 70 | test('should inherit useFeeHistory errors', async () => { 71 | mockUseFeeHistory.mockReturnValue({ error: new Error() } as any); 72 | const { result } = renderHook(() => useFeeAnalytics()); 73 | expect(result.current.error).toBeInstanceOf(Error); 74 | }); 75 | 76 | test('should inherit useFeeHistory initialLoading', async () => { 77 | mockUseFeeHistory.mockReturnValue({ initialLoading: true } as any); 78 | const { result } = renderHook(() => useFeeAnalytics()); 79 | expect(result.current.initialLoading).toBe(true); 80 | }); 81 | 82 | test('should inherit useFeeHistory loading', async () => { 83 | mockUseFeeHistory.mockReturnValue({ loading: true } as any); 84 | const { result } = renderHook(() => useFeeAnalytics()); 85 | expect(result.current.loading).toBe(true); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useLidoSWR.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useLidoSWR } from '../../src/hooks/useLidoSWR'; 3 | 4 | import { ProviderWrapper } from './testUtils'; 5 | 6 | describe('useLidoSWR', () => { 7 | const wrapper = ProviderWrapper; 8 | test('should fetch data', async () => { 9 | const expected = 1; 10 | const { result, waitForNextUpdate } = renderHook( 11 | () => useLidoSWR('/data', () => expected), 12 | { wrapper }, 13 | ); 14 | 15 | expect(result.current.data).toBeUndefined(); 16 | await waitForNextUpdate(); 17 | expect(result.current.data).toBe(expected); 18 | }); 19 | 20 | test('should update data correctly', async () => { 21 | const expected = 1; 22 | const mockFetcher = jest.fn(() => expected); 23 | const { result, waitForNextUpdate } = renderHook( 24 | () => useLidoSWR('/update', mockFetcher), 25 | { wrapper }, 26 | ); 27 | 28 | expect(result.current.data).toBeUndefined(); 29 | await waitForNextUpdate(); 30 | expect(result.current.data).toBe(expected); 31 | expect(mockFetcher).toHaveBeenCalledTimes(1); 32 | 33 | await act(async () => { 34 | await expect(result.current.update()).resolves.toBe(expected); 35 | }); 36 | expect(result.current.data).toBe(expected); 37 | expect(mockFetcher).toHaveBeenCalledTimes(2); 38 | }); 39 | 40 | test('should set loading', async () => { 41 | const { result, waitForNextUpdate } = renderHook( 42 | () => useLidoSWR('/loading', () => 1), 43 | { wrapper }, 44 | ); 45 | 46 | expect(result.current.loading).toBe(true); 47 | await waitForNextUpdate(); 48 | expect(result.current.loading).toBe(false); 49 | }); 50 | 51 | test('should set initial loading', async () => { 52 | const { result, waitForNextUpdate } = renderHook( 53 | () => useLidoSWR('/initial', () => 1), 54 | { wrapper }, 55 | ); 56 | 57 | expect(result.current.initialLoading).toBe(true); 58 | await waitForNextUpdate(); 59 | expect(result.current.initialLoading).toBe(false); 60 | }); 61 | 62 | test('should catch an error', async () => { 63 | const { result, waitForNextUpdate } = renderHook( 64 | () => 65 | useLidoSWR('/error', async () => { 66 | throw new Error(); 67 | }), 68 | { wrapper }, 69 | ); 70 | 71 | expect(result.current.error).toBeUndefined(); 72 | await waitForNextUpdate(); 73 | expect(result.current.error).toBeInstanceOf(Error); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useLidoSWRImmutable.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { act, fireEvent } from '@testing-library/react'; 3 | import { useLidoSWRImmutable } from '../../src/hooks/useLidoSWRImmutable'; 4 | 5 | import { ProviderWrapper as wrapper } from './testUtils'; 6 | 7 | describe('useLidoSWRImmutable', () => { 8 | test('should fetch data', async () => { 9 | const expected = 1; 10 | const { result, waitForNextUpdate } = renderHook( 11 | () => useLidoSWRImmutable('/data', () => expected), 12 | { wrapper }, 13 | ); 14 | 15 | expect(result.current.data).toBeUndefined(); 16 | await waitForNextUpdate(); 17 | expect(result.current.data).toBe(expected); 18 | }); 19 | 20 | test('should not update data on focus', async () => { 21 | const fetcher = jest.fn(() => 1); 22 | const { result, waitForNextUpdate } = renderHook( 23 | () => useLidoSWRImmutable('/focus', fetcher, { dedupingInterval: 0 }), 24 | { wrapper }, 25 | ); 26 | 27 | expect(result.current.data).toBeUndefined(); 28 | await waitForNextUpdate(); 29 | expect(fetcher).toBeCalledTimes(1); 30 | 31 | act(() => { 32 | fireEvent.focus(window); 33 | }); 34 | 35 | expect(fetcher).toBeCalledTimes(1); 36 | }); 37 | 38 | test('should not update data on reconnect', async () => { 39 | const fetcher = jest.fn(() => 1); 40 | const { result, waitForNextUpdate } = renderHook( 41 | () => useLidoSWRImmutable('/reconnect', fetcher, { dedupingInterval: 0 }), 42 | { wrapper }, 43 | ); 44 | 45 | expect(result.current.data).toBeUndefined(); 46 | await waitForNextUpdate(); 47 | expect(fetcher).toBeCalledTimes(1); 48 | 49 | act(() => { 50 | window.dispatchEvent(new Event('offline')); 51 | }); 52 | 53 | act(() => { 54 | window.dispatchEvent(new Event('online')); 55 | }); 56 | 57 | expect(fetcher).toBeCalledTimes(1); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useLocalStorage.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('tiny-warning'); 2 | 3 | import warning from 'tiny-warning'; 4 | import { renderHook, act } from '@testing-library/react-hooks'; 5 | import { renderHook as renderHookOnServer } from '@testing-library/react-hooks/server'; 6 | import { useLocalStorage } from '../../src/hooks/useLocalStorage'; 7 | 8 | const mockWarning = warning as jest.MockedFunction; 9 | const lsSpy = jest.spyOn(window, 'localStorage', 'get'); 10 | 11 | beforeEach(() => { 12 | lsSpy.mockReturnValue({ 13 | store: {}, 14 | getItem(key: string) { 15 | return this.store[key] || null; 16 | }, 17 | setItem(key: string, value: any) { 18 | this.store[key] = String(value); 19 | }, 20 | } as any); 21 | }); 22 | 23 | afterEach(() => { 24 | mockWarning.mockReset(); 25 | lsSpy.mockReset(); 26 | }); 27 | 28 | describe('useLocalStorage', () => { 29 | test('should use initial state', async () => { 30 | const expected = 1; 31 | const { result } = renderHook(() => useLocalStorage('key', expected)); 32 | const [state] = result.current; 33 | 34 | expect(state).toBe(expected); 35 | }); 36 | 37 | test('should use state from LS', async () => { 38 | const initial = 1; 39 | const another = 2; 40 | 41 | const first = renderHook(() => useLocalStorage('key', initial)); 42 | act(() => first.result.current[1](another)); 43 | 44 | const second = renderHook(() => useLocalStorage('key', initial)); 45 | 46 | expect(initial).not.toBe(another); 47 | expect(second.result.current[0]).toBe(another); 48 | }); 49 | 50 | test('should update if initial state is changed', async () => { 51 | let initialState = 1; 52 | const { result, rerender } = renderHook( 53 | ({ initialState }) => useLocalStorage('key', initialState), 54 | { initialProps: { initialState } }, 55 | ); 56 | 57 | expect(result.current[0]).toBe(initialState); 58 | 59 | initialState = 2; 60 | rerender({ initialState }); 61 | 62 | expect(result.current[0]).toBe(initialState); 63 | }); 64 | 65 | test('should change state', async () => { 66 | const initialState = 1; 67 | const changedState = 2; 68 | const { result } = renderHook(() => useLocalStorage('key', initialState)); 69 | 70 | const [, setState] = result.current; 71 | 72 | expect(result.current[0]).toBe(initialState); 73 | act(() => setState(changedState)); 74 | expect(result.current[0]).toBe(changedState); 75 | }); 76 | 77 | test('should change state with passed function', async () => { 78 | const initialState = 1; 79 | const { result } = renderHook(() => useLocalStorage('key', initialState)); 80 | 81 | const [, setState] = result.current; 82 | 83 | expect(result.current[0]).toBe(initialState); 84 | act(() => setState((initialState) => initialState + 1)); 85 | expect(result.current[0]).toBe(initialState + 1); 86 | }); 87 | 88 | test('should work with SSR', async () => { 89 | const expected = 1; 90 | const { result } = renderHookOnServer(() => 91 | useLocalStorage('key', expected), 92 | ); 93 | const [state] = result.current; 94 | 95 | expect(state).toBe(expected); 96 | expect(result.error).toBeUndefined(); 97 | }); 98 | 99 | test('should work without LS', async () => { 100 | lsSpy.mockReturnValue(undefined as any); 101 | 102 | const expected = 1; 103 | const changedState = 2; 104 | 105 | const { result } = renderHook(() => useLocalStorage('key', expected)); 106 | const [state, setState] = result.current; 107 | 108 | expect(state).toBe(expected); 109 | act(() => setState(changedState)); 110 | expect(result.current[0]).toBe(changedState); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useMountedState.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useMountedState } from '../../src/hooks/useMountedState'; 3 | 4 | describe('useMountedState', () => { 5 | test('should use initial state', async () => { 6 | const expected = 1; 7 | const { result } = renderHook(() => useMountedState(expected)); 8 | const [state] = result.current; 9 | 10 | expect(state).toBe(expected); 11 | }); 12 | 13 | test('should update if initial state is changed', async () => { 14 | let initialState = 1; 15 | const { result, rerender } = renderHook( 16 | ({ initialState }) => useMountedState(initialState), 17 | { initialProps: { initialState } }, 18 | ); 19 | 20 | expect(result.current[0]).toBe(initialState); 21 | 22 | initialState = 2; 23 | rerender({ initialState }); 24 | 25 | expect(result.current[0]).toBe(initialState); 26 | }); 27 | 28 | test('should change state', async () => { 29 | const initialState = 1; 30 | const changedState = 2; 31 | const { result } = renderHook(() => useMountedState(initialState)); 32 | 33 | const [, setState] = result.current; 34 | 35 | expect(result.current[0]).toBe(initialState); 36 | act(() => setState(changedState)); 37 | expect(result.current[0]).toBe(changedState); 38 | }); 39 | 40 | test('should not throw an error when calling setState on an unmounted component', async () => { 41 | const initialState = 1; 42 | const changedState = 2; 43 | const { result, unmount } = renderHook(() => useMountedState(initialState)); 44 | 45 | const [, setState] = result.current; 46 | 47 | act(() => unmount()); 48 | act(() => setState(changedState)); 49 | 50 | expect(result.current[0]).toBe(initialState); 51 | expect(result.error).toBeUndefined(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useTokenAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { TOKENS } from '@lido-sdk/constants'; 3 | import { ProviderWrapper } from './testUtils'; 4 | import { useTokenAddress } from '../../src/hooks/useTokenAddress'; 5 | 6 | describe('useTokenAddress', () => { 7 | test('should return address', async () => { 8 | const wrapper = ProviderWrapper; 9 | const { result } = renderHook(() => useTokenAddress(TOKENS.STETH), { 10 | wrapper, 11 | }); 12 | 13 | expect(typeof result.current).toBe('string'); 14 | expect(result.current).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useTokenBalance.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | jest.mock('tiny-warning'); 3 | 4 | import warning from 'tiny-warning'; 5 | import { FC } from 'react'; 6 | import { renderHook, act } from '@testing-library/react-hooks'; 7 | import { getERC20Contract } from '@lido-sdk/contracts'; 8 | import { ProviderWrapper } from './testUtils'; 9 | import { useTokenBalance } from '../../src/hooks/useTokenBalance'; 10 | 11 | const mockGetter = getERC20Contract as jest.MockedFunction< 12 | typeof getERC20Contract 13 | >; 14 | const mockWarning = warning as jest.MockedFunction; 15 | 16 | describe('useTokenBalance', () => { 17 | afterEach(() => { 18 | mockGetter.mockReset(); 19 | mockWarning.mockReset(); 20 | }); 21 | 22 | test('should fetch data', async () => { 23 | const expected = 1; 24 | const wrapper = ProviderWrapper; 25 | 26 | mockGetter.mockReturnValue({ balanceOf: async () => expected } as any); 27 | const { result, waitForNextUpdate } = renderHook( 28 | () => useTokenBalance('token', 'account'), 29 | { 30 | wrapper, 31 | }, 32 | ); 33 | 34 | expect(result.current.data).toBeUndefined(); 35 | await waitForNextUpdate(); 36 | expect(result.current.data).toBe(expected); 37 | }); 38 | 39 | test('should use account from provider if it’s not passed', async () => { 40 | const expected = 'sdk account'; 41 | const mockBalanceOf = jest.fn(() => void 0); 42 | mockGetter.mockReturnValue({ balanceOf: mockBalanceOf } as any); 43 | const wrapper: FC = (props) => ( 44 | 45 | ); 46 | renderHook(() => useTokenBalance('token'), { wrapper }); 47 | 48 | expect(mockBalanceOf).toHaveBeenCalledTimes(1); 49 | expect(mockBalanceOf).toHaveBeenCalledWith(expected); 50 | }); 51 | 52 | test('should subscribe on events data', async () => { 53 | const expected = 1; 54 | const mockOn = jest.fn(() => void 0); 55 | const mockOff = jest.fn(() => void 0); 56 | const providerWeb3 = { on: mockOn, off: mockOff } as any; 57 | 58 | mockGetter.mockReturnValue({ 59 | balanceOf: async () => expected, 60 | filters: { Transfer: () => void 0 }, 61 | } as any); 62 | const wrapper: FC = (props) => ( 63 | 64 | ); 65 | const { unmount } = renderHook(() => useTokenBalance('token', 'account'), { 66 | wrapper, 67 | }); 68 | 69 | expect(mockOn).toHaveBeenCalledTimes(2); 70 | act(() => unmount()); 71 | expect(mockOff).toHaveBeenCalledTimes(2); 72 | }); 73 | 74 | test('should catch a subscribe error', async () => { 75 | const expected = 1; 76 | const wrapper: FC = (props) => ( 77 | 78 | ); 79 | mockGetter.mockReturnValue({ balanceOf: async () => expected } as any); 80 | renderHook(() => useTokenBalance('token', 'account'), { wrapper }); 81 | 82 | expect(mockWarning).toHaveBeenCalledTimes(1); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useTokenToWallet.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | 3 | import { FC } from 'react'; 4 | import { act, renderHook } from '@testing-library/react-hooks'; 5 | import { getERC20Contract } from '@lido-sdk/contracts'; 6 | import { ProviderWrapper } from './testUtils'; 7 | import { useTokenToWallet } from '../../src/hooks/useTokenToWallet'; 8 | 9 | const mockGetter = getERC20Contract as jest.MockedFunction< 10 | typeof getERC20Contract 11 | >; 12 | 13 | describe('useTokenToWallet', () => { 14 | afterEach(() => { 15 | mockGetter.mockReset(); 16 | }); 17 | 18 | test('should work', async () => { 19 | const mockRequest = jest.fn(async () => true); 20 | const providerWeb3 = { 21 | provider: { 22 | request: mockRequest, 23 | isMetaMask: true, 24 | }, 25 | } as any; 26 | 27 | mockGetter.mockReturnValue({ 28 | symbol: async () => 'STETH', 29 | decimals: async () => 18, 30 | } as any); 31 | 32 | const wrapper: FC = (props) => ( 33 | 34 | ); 35 | const { result } = renderHook(() => useTokenToWallet('address'), { 36 | wrapper, 37 | }); 38 | 39 | expect(result.current.addToken).toBeInstanceOf(Function); 40 | await act(async () => { 41 | const added = await result.current.addToken?.(); 42 | expect(added).toBe(true); 43 | }); 44 | }); 45 | 46 | test('should call onError', async () => { 47 | const mockError = jest.fn(() => void 0); 48 | const providerWeb3 = { 49 | provider: { 50 | request: async () => true, 51 | isMetaMask: true, 52 | }, 53 | } as any; 54 | 55 | const wrapper: FC = (props) => ( 56 | 61 | ); 62 | const { result } = renderHook(() => useTokenToWallet('address'), { 63 | wrapper, 64 | }); 65 | 66 | await act(async () => { 67 | await result.current.addToken?.(); 68 | }); 69 | 70 | expect(mockError).toHaveBeenCalledTimes(1); 71 | }); 72 | 73 | test('should return false in provider is not exist', async () => { 74 | const providerWeb3 = { 75 | provider: { 76 | isMetaMask: true, 77 | }, 78 | } as any; 79 | 80 | const wrapper: FC = (props) => ( 81 | 82 | ); 83 | const { result } = renderHook(() => useTokenToWallet('address'), { 84 | wrapper, 85 | }); 86 | 87 | await act(async () => { 88 | const added = await result.current.addToken?.(); 89 | expect(added).toBe(false); 90 | }); 91 | }); 92 | 93 | test('should not return callback if it’s not metamask', async () => { 94 | const wrapper = ProviderWrapper; 95 | const { result } = renderHook(() => useTokenToWallet('address'), { 96 | wrapper, 97 | }); 98 | 99 | expect(result.current.addToken).toBeUndefined(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useTotalSupply.test.tsx: -------------------------------------------------------------------------------- 1 | jest.mock('@lido-sdk/contracts'); 2 | jest.mock('tiny-warning'); 3 | 4 | import warning from 'tiny-warning'; 5 | import { FC } from 'react'; 6 | import { renderHook, act } from '@testing-library/react-hooks'; 7 | import { getERC20Contract } from '@lido-sdk/contracts'; 8 | import { ProviderWrapper } from './testUtils'; 9 | import { useTotalSupply } from '../../src/hooks/useTotalSupply'; 10 | 11 | const mockGetter = getERC20Contract as jest.MockedFunction< 12 | typeof getERC20Contract 13 | >; 14 | const mockWarning = warning as jest.MockedFunction; 15 | 16 | describe('useTotalSupply', () => { 17 | afterEach(() => { 18 | mockGetter.mockReset(); 19 | mockWarning.mockReset(); 20 | }); 21 | 22 | test('should fetch data', async () => { 23 | const expected = 1; 24 | const wrapper = ProviderWrapper; 25 | 26 | mockGetter.mockReturnValue({ totalSupply: async () => expected } as any); 27 | const { result, waitForNextUpdate } = renderHook( 28 | () => useTotalSupply('token'), 29 | { wrapper }, 30 | ); 31 | 32 | expect(result.current.data).toBeUndefined(); 33 | await waitForNextUpdate(); 34 | expect(result.current.data).toBe(expected); 35 | }); 36 | 37 | test('should subscribe on events data', async () => { 38 | const expected = 1; 39 | const mockOn = jest.fn(() => void 0); 40 | const mockOff = jest.fn(() => void 0); 41 | const providerWeb3 = { on: mockOn, off: mockOff } as any; 42 | 43 | mockGetter.mockReturnValue({ 44 | totalSupply: async () => expected, 45 | filters: { Transfer: () => void 0 }, 46 | } as any); 47 | const wrapper: FC = (props) => ( 48 | 49 | ); 50 | const { unmount } = renderHook(() => useTotalSupply('token'), { wrapper }); 51 | 52 | expect(mockOn).toHaveBeenCalledTimes(1); 53 | act(() => unmount()); 54 | expect(mockOff).toHaveBeenCalledTimes(1); 55 | }); 56 | 57 | test('should catch a subscribe error', async () => { 58 | const expected = 1; 59 | const wrapper: FC = (props) => ( 60 | 61 | ); 62 | mockGetter.mockReturnValue({ totalSupply: async () => expected } as any); 63 | renderHook(() => useTotalSupply('token'), { wrapper }); 64 | 65 | expect(mockWarning).toHaveBeenCalledTimes(1); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/react/test/hooks/useTxPrice.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/hooks/useEthPrice'); 2 | jest.mock('../../src/hooks/useEthereumSWR'); 3 | 4 | import { renderHook, act } from '@testing-library/react-hooks'; 5 | import { WeiPerEther } from '@ethersproject/constants'; 6 | import { useEthPrice } from '../../src/hooks/useEthPrice'; 7 | import { useEthereumSWR } from '../../src/hooks/useEthereumSWR'; 8 | import { useTxPrice } from '../../src/hooks/useTxPrice'; 9 | 10 | const mockUseEthPrice = useEthPrice as jest.MockedFunction; 11 | 12 | const mockUseEthereumSWR = useEthereumSWR as jest.MockedFunction< 13 | typeof useEthereumSWR 14 | >; 15 | 16 | const common = { 17 | loading: false, 18 | initialLoading: false, 19 | error: undefined, 20 | mutate: async () => void 0, 21 | update: async () => void 0, 22 | }; 23 | 24 | describe('useTxPrice', () => { 25 | const ethPrice = 1000; 26 | const gasLimit = 10; 27 | 28 | beforeEach(() => { 29 | mockUseEthPrice.mockReturnValue({ data: ethPrice, ...common }); 30 | mockUseEthereumSWR.mockReturnValue({ data: WeiPerEther, ...common }); 31 | }); 32 | 33 | afterAll(() => { 34 | mockUseEthPrice.mockReset(); 35 | mockUseEthereumSWR.mockReset(); 36 | }); 37 | 38 | test('should multiply correct', async () => { 39 | const { result } = renderHook(() => useTxPrice(gasLimit)); 40 | expect(result.current.data).toBe(ethPrice * gasLimit); 41 | }); 42 | 43 | test('should update', async () => { 44 | const mockUpdate = jest.fn(async () => void 0); 45 | 46 | mockUseEthPrice.mockReturnValue({ 47 | ...common, 48 | data: ethPrice, 49 | update: mockUpdate, 50 | }); 51 | 52 | mockUseEthereumSWR.mockReturnValue({ 53 | ...common, 54 | data: WeiPerEther, 55 | update: mockUpdate, 56 | }); 57 | 58 | const { result } = renderHook(() => useTxPrice(gasLimit)); 59 | 60 | expect(mockUpdate).toHaveBeenCalledTimes(0); 61 | await act(async () => { 62 | await result.current.update(); 63 | }); 64 | expect(mockUpdate).toHaveBeenCalledTimes(2); 65 | }); 66 | 67 | test('should inherit eth loading', async () => { 68 | mockUseEthPrice.mockReturnValue({ loading: true } as any); 69 | const { result } = renderHook(() => useTxPrice(gasLimit)); 70 | expect(result.current.loading).toBe(true); 71 | }); 72 | 73 | test('should inherit gas loading', async () => { 74 | mockUseEthereumSWR.mockReturnValue({ loading: true } as any); 75 | const { result } = renderHook(() => useTxPrice(gasLimit)); 76 | expect(result.current.loading).toBe(true); 77 | }); 78 | 79 | test('should inherit eth initial loading', async () => { 80 | mockUseEthPrice.mockReturnValue({ initialLoading: true } as any); 81 | const { result } = renderHook(() => useTxPrice(gasLimit)); 82 | expect(result.current.initialLoading).toBe(true); 83 | }); 84 | 85 | test('should inherit gas initial loading', async () => { 86 | mockUseEthereumSWR.mockReturnValue({ initialLoading: true } as any); 87 | const { result } = renderHook(() => useTxPrice(gasLimit)); 88 | expect(result.current.initialLoading).toBe(true); 89 | }); 90 | 91 | test('should inherit eth errors', async () => { 92 | mockUseEthPrice.mockReturnValue({ error: new Error() } as any); 93 | const { result } = renderHook(() => useTxPrice(gasLimit)); 94 | expect(result.current.error).toBeInstanceOf(Error); 95 | }); 96 | 97 | test('should inherit gas errors', async () => { 98 | mockUseEthereumSWR.mockReturnValue({ error: new Error() } as any); 99 | const { result } = renderHook(() => useTxPrice(gasLimit)); 100 | expect(result.current.error).toBeInstanceOf(Error); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import tslib from 'tslib'; 4 | import ts from 'typescript'; 5 | import { topologicallySort, listWorkspaces } from 'yarn-workspaces-list'; 6 | import typescript from 'rollup-plugin-typescript2'; 7 | import del from 'rollup-plugin-delete'; 8 | import copy from 'rollup-plugin-copy'; 9 | import resolve from '@rollup/plugin-node-resolve'; 10 | 11 | const excludedWorkspaces = ['.']; 12 | const extensions = ['.ts', '.tsx', '.d.ts']; 13 | const commonExternal = ['react/jsx-runtime']; 14 | 15 | export default async () => { 16 | const packages = await listWorkspaces(); 17 | const filteredPackages = packages.filter( 18 | ({ location }) => !excludedWorkspaces.includes(location), 19 | ); 20 | const sortedPackages = topologicallySort(filteredPackages); 21 | 22 | const config = sortedPackages.map((packageData) => { 23 | const packageDir = packageData.location; 24 | const packageJson = JSON.parse( 25 | fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'), 26 | ); 27 | const { dependencies, peerDependencies } = packageJson; 28 | const external = [ 29 | ...commonExternal, 30 | ...Object.keys({ ...dependencies, ...peerDependencies }), 31 | ]; 32 | 33 | const cjsDir = path.join(packageDir, path.dirname(packageJson.main)); 34 | const esmDir = path.join(packageDir, path.dirname(packageJson.module)); 35 | 36 | return { 37 | input: path.join(packageDir, 'src/index'), 38 | output: [ 39 | { 40 | dir: cjsDir, 41 | preserveModulesRoot: path.join(packageDir, 'src'), 42 | preserveModules: true, 43 | format: 'cjs', 44 | exports: 'named', 45 | }, 46 | { 47 | dir: esmDir, 48 | preserveModulesRoot: path.join(packageDir, 'src'), 49 | preserveModules: true, 50 | format: 'es', 51 | exports: 'named', 52 | }, 53 | ], 54 | plugins: [ 55 | del({ targets: path.join(packageDir, 'dist/*'), runOnce: true }), 56 | copy({ 57 | targets: [ 58 | { 59 | src: path.join(packageDir, 'src/generated/*.d.ts'), 60 | dest: [ 61 | path.join(cjsDir, 'generated'), 62 | path.join(esmDir, 'generated'), 63 | ], 64 | }, 65 | ], 66 | copyOnce: true, 67 | }), 68 | resolve({ extensions }), 69 | typescript({ 70 | tslib, 71 | typescript: ts, 72 | tsconfig: path.join(packageDir, 'tsconfig.json'), 73 | tsconfigOverride: { 74 | compilerOptions: { 75 | paths: { tslib: [require.resolve('tslib/tslib.d.ts')] }, 76 | }, 77 | exclude: ['node_modules', 'dist', '**/*.test.*'], 78 | include: ['src/**/*'], 79 | }, 80 | }), 81 | ], 82 | external, 83 | }; 84 | }); 85 | 86 | return config; 87 | }; 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "esnext"], 4 | "strict": true, 5 | "strictNullChecks": true, 6 | "noImplicitReturns": true, 7 | "noImplicitThis": true, 8 | "noImplicitAny": true, 9 | "baseUrl": ".", 10 | "declaration": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "module": "esnext", 14 | "jsx": "react-jsx", 15 | "target": "es6" 16 | }, 17 | "include": ["**/src/**/*"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------